Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 33e9b8c394 | |||
| 5bb0f00c0e | |||
| 670b636c47 | |||
| e48460c8cc | |||
| ad5bc5b5ca | |||
| 013cb2fb64 | |||
| c4df60bb8d | |||
| 786a4956dd | |||
| 323e272c85 | |||
| 6e769797fd | |||
| 0a48ddad8f | |||
| c939cd4b6d | |||
| fd27acfcb1 | |||
| 9ae0e4a58b | |||
| 874472a85d | |||
| 5f893f890f | |||
| 69e03eacba | |||
| d369112db4 | |||
| 62d603a054 | |||
| 4cb39164fa | |||
| d5e8561011 | |||
| 944455bcce | |||
| ee888615ea | |||
| 3f58d35337 | |||
| 6983a17fc0 | |||
| ce2d88a951 | |||
| 5a4f73d144 | |||
| 1c8396e2b5 | |||
| 5668055015 | |||
| 1dfd13841a | |||
| 405cd4fb35 | |||
| 2f4b6bd2d7 | |||
| acf70635c6 | |||
| 5d15e96a98 | |||
| e3b1e103bd | |||
| 8d1867cdcc | |||
| 2cfa9817b1 | |||
| 641b8c31ee | |||
| 6ce1d39f4e | |||
| 9d4fb0a3ad | |||
| 4110054b51 | |||
| dd4d110c9b | |||
| 017f8ff6bf | |||
| 1ff7beef8f | |||
| 35783784b3 | |||
| 194ca4970a | |||
| d4f3852dd2 | |||
| 3fbbc7a86b | |||
| 7cff4e8bc1 | |||
| 827803ea3d | |||
| ccb639002f | |||
| df62f09a4c | |||
| 7c0a982958 | |||
| 940e4da0fa | |||
| ae531ae852 | |||
| 84d3ae1b08 | |||
| d51f307ffa | |||
| a4018fbcdd | |||
| 2dd34c5ae4 | |||
| 71f83af13c | |||
| d587d8b242 | |||
| f1b1570b02 | |||
| f9ca67a78d | |||
| c8f9140e70 | |||
| 47c211ba0e | |||
| 9f270d956e | |||
| 32ee8e3d34 | |||
| 77a1413fa0 | |||
| 14760d9787 | |||
| 03b6b93589 | |||
| 47402416fd | |||
| 0087cede97 | |||
| 264c405192 | |||
| 1742e0c27c | |||
| 7371cb4838 | |||
| c1512e8d89 | |||
| e5aaeed609 | |||
| a950ed04a7 | |||
| 964c98c8d9 | |||
| 635389b235 | |||
| 1385e3bc09 | |||
| 2dc581f717 | |||
| 4f8369c3f1 | |||
| 730b405d68 | |||
| 87c04f1e0b | |||
| f4df13fb6c | |||
| 4a8abf77ac | |||
| 7e48ec1820 | |||
| 05087afd40 | |||
| 56afed9678 | |||
| af8c168f78 | |||
| 4c52746b41 | |||
| 7768a35366 | |||
| 05847affc1 | |||
| f652a2b613 | |||
| 04d4cbde78 |
+2
-2
@@ -36,7 +36,7 @@ jobs:
|
||||
- php: 7.2
|
||||
env: DB=none TASK=CITEST
|
||||
- php: 7.2
|
||||
env: DB=none TASK=GRUNT NVM_VERSION='lts/carbon'
|
||||
env: DB=none TASK=GRUNT NVM_VERSION='lts/gallium'
|
||||
|
||||
- if: env(MOODLE_DATABASE) = "pgsql" OR env(MOODLE_DATABASE) = "all" OR env(MOODLE_DATABASE) IS NOT present
|
||||
php: 7.2
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
env: DB=none TASK=CITEST
|
||||
- if: env(MOODLE_PHP) = "all"
|
||||
php: 7.4
|
||||
env: DB=none TASK=GRUNT NVM_VERSION='lts/carbon'
|
||||
env: DB=none TASK=GRUNT NVM_VERSION='lts/gallium'
|
||||
|
||||
- if: env(MOODLE_PHP) = "all" AND (env(MOODLE_DATABASE) = "pgsql" OR env(MOODLE_DATABASE) = "all" OR env(MOODLE_DATABASE) IS NOT present)
|
||||
php: 7.4
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -20,9 +20,10 @@ Feature: Forms manipulation
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Quiz" to section "1"
|
||||
And the following "activities" exist:
|
||||
| activity | course | section | name |
|
||||
| quiz | C1 | 1 | Quiz 1 |
|
||||
And I am on the "Quiz 1" "quiz activity editing" page logged in as admin
|
||||
When I expand all fieldsets
|
||||
Then I should see "Close the quiz"
|
||||
And I should see "Group mode"
|
||||
|
||||
@@ -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"
|
||||
@@ -27,7 +27,8 @@ require_once($CFG->libdir . '/externallib.php');
|
||||
|
||||
$userid = required_param('userid', PARAM_INT); // The user id the key belongs to (for double-checking).
|
||||
$key = required_param('key', PARAM_ALPHANUMEXT); // The key generated by the tool_mobile_external::get_autologin_key() external function.
|
||||
$urltogo = optional_param('urltogo', $CFG->wwwroot, PARAM_URL); // URL to redirect.
|
||||
$urltogo = optional_param('urltogo', $CFG->wwwroot, PARAM_LOCALURL); // URL to redirect.
|
||||
$urltogo = $urltogo ?: $CFG->wwwroot;
|
||||
|
||||
$context = context_system::instance();
|
||||
$PAGE->set_context($context);
|
||||
|
||||
@@ -19,14 +19,13 @@ Feature: Backup user data
|
||||
And the following config values are set as admin:
|
||||
| coursebinenable | 1 | tool_recyclebin |
|
||||
| autohide | 0 | tool_recyclebin |
|
||||
And the following "activities" exist:
|
||||
| activity | course | section | name | intro |
|
||||
| quiz | C1 | 1 | Quiz 1 | Test quiz description |
|
||||
|
||||
@javascript
|
||||
Scenario: Delete and restore a quiz with user data
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Quiz" to section "1" and I fill the form with:
|
||||
| Name | Quiz 1 |
|
||||
| Description | Test quiz description |
|
||||
Given I am on the "Quiz 1" "quiz activity" page logged in as teacher1
|
||||
And I add a "True/False" question to the "Quiz 1" quiz with:
|
||||
| Question name | TF1 |
|
||||
| Question text | First question |
|
||||
@@ -41,6 +40,8 @@ Feature: Backup user data
|
||||
| Correct answer | False |
|
||||
| Feedback for the response 'True'. | So you think it is true |
|
||||
| Feedback for the response 'False'. | So you think it is false |
|
||||
And I set the field "maxgrade" to "10.0"
|
||||
And I press "savechanges"
|
||||
And I log out
|
||||
When I am on the "Quiz 1" "quiz activity" page logged in as student1
|
||||
And I press "Attempt quiz now"
|
||||
|
||||
@@ -77,6 +77,7 @@ abstract class backup_cron_automated_helper {
|
||||
* First backup courses that do not have an entry in backup_courses first,
|
||||
* as they are likely new and never been backed up. Do the oldest modified courses first.
|
||||
* Then backup courses that have previously been backed up starting with the oldest next start time.
|
||||
* Finally, all else being equal, defer to the sortorder of the courses.
|
||||
*
|
||||
* @param null|int $now timestamp to use in course selection.
|
||||
* @return moodle_recordset The recordset of matching courses.
|
||||
@@ -93,7 +94,8 @@ abstract class backup_cron_automated_helper {
|
||||
LEFT JOIN {backup_courses} bc ON bc.courseid = c.id
|
||||
WHERE bc.nextstarttime IS NULL OR bc.nextstarttime < ?
|
||||
ORDER BY nextstarttime ASC,
|
||||
c.timemodified DESC';
|
||||
c.timemodified DESC,
|
||||
c.sortorder';
|
||||
|
||||
$params = array(
|
||||
$now, // Only get courses where the backup start time is in the past.
|
||||
|
||||
@@ -383,7 +383,7 @@ class backup_cron_helper_testcase extends advanced_testcase {
|
||||
$backupcourse3 = new stdClass;
|
||||
$backupcourse3->courseid = $course3->id;
|
||||
$backupcourse3->laststatus = testable_backup_cron_automated_helper::BACKUP_STATUS_OK;
|
||||
$backupcourse3->nextstarttime = 1554858160;
|
||||
$backupcourse3->nextstarttime = 1554822160;
|
||||
$DB->insert_record('backup_courses', $backupcourse3);
|
||||
|
||||
$backupcourse4 = new stdClass;
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -36,7 +36,7 @@ class moodle1_block_html_handler extends moodle1_block_handler {
|
||||
$contextid = $this->converter->get_contextid(CONTEXT_BLOCK, $olddata['id']);
|
||||
$decodeddata = base64_decode($olddata['configdata']);
|
||||
list($updated, $configdata) = upgrade_fix_serialized_objects($decodeddata);
|
||||
$configdata = unserialize($configdata);
|
||||
$configdata = unserialize_object($configdata);
|
||||
|
||||
// get a fresh new file manager for this instance
|
||||
$this->fileman = $this->converter->get_file_manager($contextid, 'block_html');
|
||||
@@ -44,7 +44,7 @@ class moodle1_block_html_handler extends moodle1_block_handler {
|
||||
// convert course files embedded in the block content
|
||||
$this->fileman->filearea = 'content';
|
||||
$this->fileman->itemid = 0;
|
||||
$configdata->text = moodle1_converter::migrate_referenced_files($configdata->text, $this->fileman);
|
||||
$configdata->text = moodle1_converter::migrate_referenced_files($configdata->text ?? '', $this->fileman);
|
||||
$configdata->format = FORMAT_HTML;
|
||||
|
||||
return base64_encode(serialize($configdata));
|
||||
|
||||
@@ -10,10 +10,18 @@ Feature: Add URL to main menu block
|
||||
And I am on site homepage
|
||||
And I navigate to "Turn editing on" in current page administration
|
||||
And I add the "Main menu" block
|
||||
When I add a "URL" to section "0" and I fill the form with:
|
||||
| Name | google |
|
||||
| Description | gooooooooogle |
|
||||
| External URL | http://www.google.com |
|
||||
And the following "activity" exists:
|
||||
| activity | url |
|
||||
| course | Acceptance test site |
|
||||
| name | google |
|
||||
| intro | gooooooooogle |
|
||||
| externalurl | http://www.google.com |
|
||||
| section | 0 |
|
||||
When I am on the "google" "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 "google" "link" should exist in the "Main menu" "block"
|
||||
And "Add an activity" "button" should exist in the "Main menu" "block"
|
||||
And "Add an activity or resource" "button" should exist in the "Main menu" "block"
|
||||
|
||||
@@ -9,6 +9,9 @@ Feature: Comment on a blog entry
|
||||
| username | firstname | lastname | email |
|
||||
| testuser | Test | User | moodle@example.com |
|
||||
| testuser2 | Test2 | User2 | moodle2@example.com |
|
||||
And the following "core_blog > entries" exist:
|
||||
| subject | body | user |
|
||||
| Blog post from user 1 | User 1 blog post content | testuser |
|
||||
And I log in as "admin"
|
||||
And I am on site homepage
|
||||
And I turn editing mode on
|
||||
@@ -19,15 +22,6 @@ Feature: Comment on a blog entry
|
||||
| Page contexts | Display throughout the entire site |
|
||||
And I press "Save changes"
|
||||
And I log out
|
||||
And I log in as "testuser"
|
||||
And I click on "Site pages" "list_item" in the "Navigation" "block"
|
||||
And I click on "Site blogs" "link" in the "Navigation" "block"
|
||||
And I follow "Add a new entry"
|
||||
And I set the following fields to these values:
|
||||
| Entry title | Blog post from user 1 |
|
||||
| Blog entry body | User 1 blog post content |
|
||||
And I press "Save changes"
|
||||
And I log out
|
||||
|
||||
@javascript
|
||||
Scenario: Commenting on my own blog entry
|
||||
|
||||
@@ -8,6 +8,10 @@ Feature: Delete a blog entry
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| testuser | Test | User | moodle@example.com |
|
||||
And the following "core_blog > entries" exist:
|
||||
| subject | body | user |
|
||||
| Blog post one | User 1 blog post content | testuser |
|
||||
| Blog post two | User 1 blog post content | testuser |
|
||||
And I log in as "admin"
|
||||
And I am on site homepage
|
||||
And I turn editing mode on
|
||||
@@ -19,17 +23,6 @@ Feature: Delete a blog entry
|
||||
And I press "Save changes"
|
||||
And I log out
|
||||
And I log in as "testuser"
|
||||
And I click on "Site blogs" "link" in the "Navigation" "block"
|
||||
And I follow "Add a new entry"
|
||||
And I set the following fields to these values:
|
||||
| Entry title | Blog post one |
|
||||
| Blog entry body | User 1 blog post content |
|
||||
And I press "Save changes"
|
||||
And I follow "Add a new entry"
|
||||
And I set the following fields to these values:
|
||||
| Entry title | Blog post two |
|
||||
| Blog entry body | User 1 blog post content |
|
||||
And I press "Save changes"
|
||||
And I am on site homepage
|
||||
And I click on "Site blogs" "link" in the "Navigation" "block"
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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 core_blog.
|
||||
*
|
||||
* @package core_blog
|
||||
* @category test
|
||||
* @copyright 2022 Noel De Martin
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* Behat data generator for core_blog.
|
||||
*
|
||||
* @package core_blog
|
||||
* @category test
|
||||
* @copyright 2022 Noel De Martin
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class behat_core_blog_generator extends behat_generator_base {
|
||||
|
||||
/**
|
||||
* Get a list of the entities that can be created.
|
||||
*
|
||||
* @return array entity name => information about how to generate.
|
||||
*/
|
||||
protected function get_creatable_entities(): array {
|
||||
return [
|
||||
'entries' => [
|
||||
'singular' => 'entry',
|
||||
'datagenerator' => 'entry',
|
||||
'required' => ['subject', 'body'],
|
||||
'switchids' => ['user' => 'userid'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Generator for blog area.
|
||||
*
|
||||
* @package core_blog
|
||||
* @category test
|
||||
* @copyright 2022 Noel De Martin
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/blog/locallib.php');
|
||||
|
||||
/**
|
||||
* Blog module test data generator class
|
||||
*
|
||||
* @package core_blog
|
||||
* @category test
|
||||
* @copyright 2022 Noel De Martin
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class core_blog_generator extends component_generator_base {
|
||||
|
||||
/**
|
||||
* Create a blog entry
|
||||
*
|
||||
* @param array $data Entry data.
|
||||
* @return blog_entry Entry instance.
|
||||
*/
|
||||
public function create_entry(array $data = []): blog_entry {
|
||||
$data['publishstate'] = $data['publishstate'] ?? 'site';
|
||||
$data['summary'] = $data['summary'] ?? $data['body'];
|
||||
|
||||
$entry = new blog_entry(null, $data);
|
||||
$entry->add();
|
||||
|
||||
return $entry;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -34,9 +34,9 @@ Feature: Restrict sections availability through completion or grade conditions
|
||||
And I set the following fields to these values:
|
||||
| Enable completion tracking | Yes |
|
||||
And I press "Save and display"
|
||||
And I add a "Label" to section "1" and I fill the form with:
|
||||
| Label text | Test label |
|
||||
| Completion tracking | Students can manually mark the activity as completed |
|
||||
And the following "activities" exist:
|
||||
| activity | course | section | intro | completion | idnumber |
|
||||
| label | C1 | 1 | Test label | 1 | 1 |
|
||||
And I add a "Page" to section "2" and I fill the form with:
|
||||
| Name | Test page name |
|
||||
| Description | Test page description |
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -14,13 +14,10 @@ Feature: View subfolders in a course in-line
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Folder" to section "3" and I fill the form with:
|
||||
| Name | Test folder |
|
||||
| Display folder contents | On a separate page |
|
||||
| Show subfolders expanded | |
|
||||
And I am on the "Test folder" "folder activity" page
|
||||
And the following "activities" exist:
|
||||
| activity | course | name | display | showexpanded |
|
||||
| folder | C1 | Test folder | 0 | 0 |
|
||||
And I am on the "Test folder" "folder activity" page logged in as "teacher1"
|
||||
And I press "Edit"
|
||||
And I press "Create folder"
|
||||
And I set the field "New folder name" to "Test subfolder 1"
|
||||
|
||||
@@ -12,7 +12,7 @@ Feature: We can change what we are viewing on the grader report
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
| student2 | Student | 1 | student2@example.com |
|
||||
| student2 | Student | 2 | student2@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
@@ -26,19 +26,15 @@ Feature: We can change what we are viewing on the grader report
|
||||
| assign | user | onlinetext |
|
||||
| Test assignment name 1 | student1 | This is a submission for assignment 1 |
|
||||
| Test assignment name 2 | student1 | This is a submission for assignment 2 |
|
||||
And I am on the "Test assignment name 1" "assign activity" page logged in as student1
|
||||
Then I should see "Submitted for grading"
|
||||
And I am on the "Test assignment name 2" "assign activity" page
|
||||
Then I should see "Submitted for grading"
|
||||
And I log out
|
||||
And the following "grade items" exist:
|
||||
| itemname | grademin | grademax | course |
|
||||
| Manual grade | 20 | 40 | C1 |
|
||||
And the following "grade grades" exist:
|
||||
| gradeitem | user | grade |
|
||||
| Test assignment name 1 | student1 | 80 |
|
||||
| Test assignment name 2 | student1 | 90 |
|
||||
| Manual grade | student1 | 30 |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "View > Grader report" in the course gradebook
|
||||
And I turn editing mode on
|
||||
And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment name 1"
|
||||
And I give the grade "90.00" to the user "Student 1" for the grade item "Test assignment name 2"
|
||||
And I press "Save changes"
|
||||
And I turn editing mode off
|
||||
|
||||
@javascript
|
||||
Scenario: View and minimise the grader report containing hidden activities
|
||||
@@ -49,13 +45,15 @@ Feature: We can change what we are viewing on the grader report
|
||||
And I navigate to "View > Grader report" in the course gradebook
|
||||
And I should see "Test assignment name 1"
|
||||
And I should see "Test assignment name 2"
|
||||
And I should see "Manual grade"
|
||||
And I should see "Course total"
|
||||
And the following should exist in the "user-grades" table:
|
||||
| -1- | -4- | -5- | -6- |
|
||||
| Student 1 | 80 | 90 | 170 |
|
||||
| -1- | -4- | -5- | -6- | -7- |
|
||||
| Student 1 | 80 | 90 | 30 | 170 |
|
||||
And I click on "Change to aggregates only" "link"
|
||||
And I should not see "Test assignment name 1"
|
||||
And I should not see "Test assignment name 2"
|
||||
And I should not see "Manual grade"
|
||||
And I should see "Course total"
|
||||
And the following should exist in the "user-grades" table:
|
||||
| -1- | -4- |
|
||||
@@ -63,10 +61,11 @@ Feature: We can change what we are viewing on the grader report
|
||||
And I click on "Change to grades only" "link"
|
||||
And I should see "Test assignment name 1"
|
||||
And I should see "Test assignment name 2"
|
||||
And I should see "Manual grade"
|
||||
And I should not see "Course total"
|
||||
And the following should exist in the "user-grades" table:
|
||||
| -1- | -4- | -5- |
|
||||
| Student 1 | 80 | 90 |
|
||||
| -1- | -4- | -5- | -6- |
|
||||
| Student 1 | 80 | 90 | 30 |
|
||||
|
||||
@javascript @skip_chrome_zerosize
|
||||
Scenario: View and minimise the grader report containing hidden activities without the 'moodle/grade:viewhidden' capability
|
||||
@@ -84,21 +83,24 @@ Feature: We can change what we are viewing on the grader report
|
||||
And I navigate to "View > Grader report" in the course gradebook
|
||||
And I should see "Test assignment name 1"
|
||||
And I should see "Test assignment name 2"
|
||||
And I should see "Manual grade"
|
||||
And I should see "Course total"
|
||||
And the following should exist in the "user-grades" table:
|
||||
| -1- | -4- | -5- | -6- |
|
||||
| Student 1 | 80 | - | 80 |
|
||||
| -1- | -4- | -5- | -6- | -7- |
|
||||
| Student 1 | 80 | - | 30 | 105.71 |
|
||||
And I click on "Change to aggregates only" "link"
|
||||
And I should not see "Test assignment name 1"
|
||||
And I should not see "Test assignment name 2"
|
||||
And I should not see "Manual grade"
|
||||
And I should see "Course total"
|
||||
And the following should exist in the "user-grades" table:
|
||||
| -1- | -4- |
|
||||
| Student 1 | 80 |
|
||||
| Student 1 | 105.71 |
|
||||
And I click on "Change to grades only" "link"
|
||||
And I should see "Test assignment name 1"
|
||||
And I should see "Test assignment name 2"
|
||||
And I should see "Manual grade"
|
||||
And I should not see "Course total"
|
||||
And the following should exist in the "user-grades" table:
|
||||
| -1- | -4- | -5- |
|
||||
| Student 1 | 80 | - |
|
||||
| -1- | -4- | -5- | -6- |
|
||||
| Student 1 | 80 | - | 30 |
|
||||
|
||||
@@ -1453,6 +1453,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.';
|
||||
|
||||
|
||||
@@ -363,6 +363,7 @@ $string['europe/jersey'] = 'Europe/Jersey';
|
||||
$string['europe/kaliningrad'] = 'Europe/Kaliningrad';
|
||||
$string['europe/kiev'] = 'Europe/Kiev';
|
||||
$string['europe/kirov'] = 'Europe/Kirov';
|
||||
$string['europe/kyiv'] = 'Europe/Kyiv';
|
||||
$string['europe/lisbon'] = 'Europe/Lisbon';
|
||||
$string['europe/ljubljana'] = 'Europe/Ljubljana';
|
||||
$string['europe/london'] = 'Europe/London';
|
||||
|
||||
@@ -150,6 +150,12 @@ class behat_core_generator extends behat_generator_base {
|
||||
'required' => ['fullname', 'course'],
|
||||
'switchids' => ['course' => 'courseid', 'gradecategory' => 'parent'],
|
||||
],
|
||||
'grade grades' => [
|
||||
'singular' => 'grade grade',
|
||||
'datagenerator' => 'grade_grade',
|
||||
'required' => ['gradeitem'],
|
||||
'switchids' => ['user' => 'userid', 'gradeitem' => 'itemid'],
|
||||
],
|
||||
'grade items' => [
|
||||
'singular' => 'grade item',
|
||||
'datagenerator' => 'grade_item',
|
||||
@@ -275,11 +281,33 @@ class behat_core_generator extends behat_generator_base {
|
||||
'required' => ['user', 'course', 'lastaccess'],
|
||||
'switchids' => ['user' => 'userid', 'course' => 'courseid'],
|
||||
],
|
||||
'notifications' => [
|
||||
'singular' => 'notification',
|
||||
'datagenerator' => 'notification',
|
||||
'required' => ['subject', 'userfrom', 'userto'],
|
||||
'switchids' => ['userfrom' => 'userfromid', 'userto' => 'usertoid'],
|
||||
],
|
||||
];
|
||||
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the grade item id using a name.
|
||||
*
|
||||
* @param string $name
|
||||
* @return int The grade item id
|
||||
*/
|
||||
protected function get_gradeitem_id(string $name): int {
|
||||
global $DB;
|
||||
|
||||
if (!$id = $DB->get_field('grade_items', 'id', ['itemname' => $name])) {
|
||||
throw new Exception('The specified grade item with name "' . $name . '" could not be found.');
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any empty custom fields, to avoid errors when creating the course.
|
||||
*
|
||||
@@ -1009,6 +1037,39 @@ class behat_core_generator extends behat_generator_base {
|
||||
$DB->insert_record('badge_backpack', $backpack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates notifications to specific user.
|
||||
*
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
protected function process_notification(array $data) {
|
||||
global $DB;
|
||||
|
||||
$notification = new stdClass();
|
||||
$notification->useridfrom = $data['userfromid'];
|
||||
$notification->useridto = $data['usertoid'];
|
||||
$notification->subject = $data['subject'];
|
||||
$notification->fullmessage = $data['subject'] . ' description';
|
||||
$notification->smallmessage = $data['subject'] . ' description';
|
||||
$notification->fullmessagehtml = $data['subject'] . ' description';
|
||||
|
||||
if ($data['timecreated'] !== 'null') {
|
||||
$notification->timecreated = $data['timecreated'];
|
||||
}
|
||||
|
||||
if ($data['timeread'] !== 'null') {
|
||||
$notification->timeread = $data['timeread'];
|
||||
}
|
||||
|
||||
if (!empty($data)) {
|
||||
$popupnotification = new stdClass();
|
||||
$popupnotification->notificationid = $DB->insert_record('notifications', $notification);
|
||||
$DB->insert_record('message_popup_notifications', $popupnotification);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates user last access data within given courses.
|
||||
*
|
||||
|
||||
@@ -316,6 +316,36 @@ abstract class behat_generator_base {
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user id from it's username.
|
||||
* @throws Exception
|
||||
* @param string $username
|
||||
* @return int
|
||||
*/
|
||||
protected function get_userfrom_id(string $username) {
|
||||
global $DB;
|
||||
|
||||
if (!$id = $DB->get_field('user', 'id', ['username' => $username])) {
|
||||
throw new Exception('The specified user with username "' . $username . '" does not exist');
|
||||
}
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user id from it's username.
|
||||
* @throws Exception
|
||||
* @param string $username
|
||||
* @return int
|
||||
*/
|
||||
protected function get_userto_id(string $username) {
|
||||
global $DB;
|
||||
|
||||
if (!$id = $DB->get_field('user', 'id', ['username' => $username])) {
|
||||
throw new Exception('The specified user with username "' . $username . '" does not exist');
|
||||
}
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the role id from it's shortname.
|
||||
* @throws Exception
|
||||
|
||||
@@ -111,7 +111,7 @@ abstract class scheduled_task extends task_base {
|
||||
*/
|
||||
public function set_minute($minute, $expandr = true) {
|
||||
if ($minute === 'R' && $expandr) {
|
||||
$minute = mt_rand(self::HOURMIN, self::HOURMAX);
|
||||
$minute = mt_rand(self::MINUTEMIN, self::MINUTEMAX);
|
||||
}
|
||||
$this->minute = $minute;
|
||||
}
|
||||
|
||||
+13
-7
@@ -506,21 +506,27 @@ function get_users_listing($sort='lastaccess', $dir='ASC', $page=0, $recordsperp
|
||||
$params = $params + (array)$extraparams;
|
||||
}
|
||||
|
||||
if ($sort) {
|
||||
$sort = " ORDER BY $sort $dir";
|
||||
}
|
||||
|
||||
// If a context is specified, get extra user fields that the current user
|
||||
// is supposed to see.
|
||||
$extrafields = '';
|
||||
$includedfields = ['id', 'username', 'email', 'firstname', 'lastname', 'city', 'country',
|
||||
'lastaccess', 'confirmed', 'mnethostid', 'suspended'];
|
||||
if ($extracontext) {
|
||||
$extrafields = get_extra_user_fields_sql($extracontext, '', '',
|
||||
array('id', 'username', 'email', 'firstname', 'lastname', 'city', 'country',
|
||||
'lastaccess', 'confirmed', 'mnethostid'));
|
||||
$extrafields = get_extra_user_fields_sql($extracontext, '', '', $includedfields);
|
||||
}
|
||||
$namefields = get_all_user_name_fields(true);
|
||||
$extrafields = "$extrafields, $namefields";
|
||||
|
||||
if ($sort) {
|
||||
$orderbymap = trim($extrafields, ', ');
|
||||
$orderbymap = array_merge(explode(',', $orderbymap), $includedfields);
|
||||
$neworderbymap = ['default' => 'lastaccess'];
|
||||
foreach($orderbymap as $value) {
|
||||
$neworderbymap[$value] = $value;
|
||||
}
|
||||
$sort = get_safe_orderby($neworderbymap, $sort, $dir);
|
||||
}
|
||||
|
||||
// warning: will return UNCONFIRMED USERS
|
||||
return $DB->get_records_sql("SELECT id, username, email, city, country, lastaccess, confirmed, mnethostid, suspended $extrafields
|
||||
FROM {user}
|
||||
|
||||
+2
-2
@@ -242,8 +242,8 @@ $tasks = array(
|
||||
array(
|
||||
'classname' => 'core\task\check_for_updates_task',
|
||||
'blocking' => 0,
|
||||
'minute' => '0',
|
||||
'hour' => '*/2',
|
||||
'minute' => 'R',
|
||||
'hour' => 'R',
|
||||
'day' => '*',
|
||||
'dayofweek' => '*',
|
||||
'month' => '*'
|
||||
|
||||
@@ -38,7 +38,7 @@ class processor implements \core_analytics\classifier, \core_analytics\regresso
|
||||
/**
|
||||
* The required version of the python package that performs all calculations.
|
||||
*/
|
||||
const REQUIRED_PIP_PACKAGE_VERSION = '2.6.5';
|
||||
const REQUIRED_PIP_PACKAGE_VERSION = '2.6.6';
|
||||
|
||||
/**
|
||||
* The python package is installed in a server.
|
||||
|
||||
+4
-1
@@ -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');
|
||||
|
||||
|
||||
@@ -18,5 +18,5 @@ 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
|
||||
1) If the relevant pull request has not been accepted yet, apply the following commit, so we are able to disable unnecessary rendering:
|
||||
https://github.com/bobthecow/mustache.php/pull/402/commits/db771014c7e346438f68077813ebdda3fdae12df#
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -26,31 +26,34 @@ class Mustache_Compiler
|
||||
private $entityFlags;
|
||||
private $charset;
|
||||
private $strictCallables;
|
||||
private $disableLambdaRendering;
|
||||
|
||||
/**
|
||||
* Compile a Mustache token parse tree into PHP source code.
|
||||
*
|
||||
* @param string $source Mustache Template source code
|
||||
* @param string $tree Parse tree of Mustache tokens
|
||||
* @param string $name Mustache Template class name
|
||||
* @param bool $customEscape (default: false)
|
||||
* @param string $charset (default: 'UTF-8')
|
||||
* @param bool $strictCallables (default: false)
|
||||
* @param int $entityFlags (default: ENT_COMPAT)
|
||||
* @param string $source Mustache Template source code
|
||||
* @param array $tree Parse tree of Mustache tokens
|
||||
* @param string $name Mustache Template class name
|
||||
* @param bool $customEscape (default: false)
|
||||
* @param string $charset (default: 'UTF-8')
|
||||
* @param bool $strictCallables (default: false)
|
||||
* @param int $entityFlags (default: ENT_COMPAT)
|
||||
* @param bool $disableLambdaRendering (default: false)
|
||||
*
|
||||
* @return string Generated PHP source code
|
||||
*/
|
||||
public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT)
|
||||
public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT, $disableLambdaRendering = false)
|
||||
{
|
||||
$this->pragmas = $this->defaultPragmas;
|
||||
$this->sections = array();
|
||||
$this->blocks = array();
|
||||
$this->source = $source;
|
||||
$this->indentNextLine = true;
|
||||
$this->customEscape = $customEscape;
|
||||
$this->entityFlags = $entityFlags;
|
||||
$this->charset = $charset;
|
||||
$this->strictCallables = $strictCallables;
|
||||
$this->pragmas = $this->defaultPragmas;
|
||||
$this->sections = array();
|
||||
$this->blocks = array();
|
||||
$this->source = $source;
|
||||
$this->indentNextLine = true;
|
||||
$this->customEscape = $customEscape;
|
||||
$this->entityFlags = $entityFlags;
|
||||
$this->charset = $charset;
|
||||
$this->strictCallables = $strictCallables;
|
||||
$this->disableLambdaRendering = $disableLambdaRendering;
|
||||
|
||||
return $this->writeCode($tree, $name);
|
||||
}
|
||||
@@ -286,7 +289,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 +323,6 @@ class Mustache_Compiler
|
||||
}
|
||||
|
||||
const SECTION_CALL = '
|
||||
// %s section
|
||||
$value = $context->%s(%s);%s
|
||||
$buffer .= $this->section%s($context, $indent, $value);
|
||||
';
|
||||
@@ -333,14 +334,8 @@ class Mustache_Compiler
|
||||
|
||||
if (%s) {
|
||||
$source = %s;
|
||||
$result = call_user_func($value, $source, %s);
|
||||
if (strpos($result, \'{{\') === false) {
|
||||
$buffer .= $result;
|
||||
} else {
|
||||
$buffer .= $this->mustache
|
||||
->loadLambda((string) $result%s)
|
||||
->renderInternal($context);
|
||||
}
|
||||
$result = (string) call_user_func($value, $source, %s);%s
|
||||
$buffer .= $result;
|
||||
} elseif (!empty($value)) {
|
||||
$values = $this->isIterable($value) ? $value : array($value);
|
||||
foreach ($values as $value) {
|
||||
@@ -354,6 +349,36 @@ class Mustache_Compiler
|
||||
}
|
||||
';
|
||||
|
||||
const SECTION_RENDER_LAMBDA = '
|
||||
if (strpos($result, \'{{\') !== false) {
|
||||
$result = $this->mustache
|
||||
->loadLambda($result%s)
|
||||
->renderInternal($context);
|
||||
}
|
||||
';
|
||||
|
||||
/**
|
||||
* Helper function to compile section with and without lambda rendering.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $callable
|
||||
* @param string $source
|
||||
* @param string $helper
|
||||
* @param string $delims
|
||||
* @param string $content
|
||||
*
|
||||
* @return string section code
|
||||
*/
|
||||
private function getSection($key, $callable, $source, $helper, $delims, $content)
|
||||
{
|
||||
$render = '';
|
||||
if (!$this->disableLambdaRendering) {
|
||||
$render = sprintf($this->prepare(self::SECTION_RENDER_LAMBDA, 2), $delims);
|
||||
}
|
||||
|
||||
return sprintf($this->prepare(self::SECTION), $key, $callable, $source, $helper, $render, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Mustache Template section PHP source.
|
||||
*
|
||||
@@ -385,18 +410,17 @@ class Mustache_Compiler
|
||||
$key = ucfirst(md5($delims . "\n" . $source));
|
||||
|
||||
if (!isset($this->sections[$key])) {
|
||||
$this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $helper, $delims, $this->walk($nodes, 2));
|
||||
$this->sections[$key] = $this->getSection($key, $callable, $source, $helper, $delims, $this->walk($nodes, 2));
|
||||
}
|
||||
|
||||
$method = $this->getFindMethod($id);
|
||||
$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 +443,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 +531,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.1';
|
||||
const SPEC_VERSION = '1.2.2';
|
||||
|
||||
const PRAGMA_FILTERS = 'FILTERS';
|
||||
const PRAGMA_BLOCKS = 'BLOCKS';
|
||||
@@ -53,6 +53,7 @@ class Mustache_Engine
|
||||
private $charset = 'UTF-8';
|
||||
private $logger;
|
||||
private $strictCallables = false;
|
||||
private $disableLambdaRendering = false;
|
||||
private $pragmas = array();
|
||||
private $delimiters;
|
||||
|
||||
@@ -130,6 +131,11 @@ class Mustache_Engine
|
||||
* // This currently defaults to false, but will default to true in v3.0.
|
||||
* 'strict_callables' => true,
|
||||
*
|
||||
* // Do not render the output of lambdas. Use this to prevent repeated rendering if the lambda already
|
||||
* // takes care of rendering its content. This helps protect against mustache code injection when user
|
||||
* // input is passed directly into the template. Defaults to false.
|
||||
* 'disable_lambda_rendering' => true,
|
||||
*
|
||||
* // Enable pragmas across all templates, regardless of the presence of pragma tags in the individual
|
||||
* // templates.
|
||||
* 'pragmas' => [Mustache_Engine::PRAGMA_FILTERS],
|
||||
@@ -204,6 +210,10 @@ class Mustache_Engine
|
||||
$this->strictCallables = $options['strict_callables'];
|
||||
}
|
||||
|
||||
if (isset($options['disable_lambda_rendering'])) {
|
||||
$this->disableLambdaRendering = $options['disable_lambda_rendering'];
|
||||
}
|
||||
|
||||
if (isset($options['delimiters'])) {
|
||||
$this->delimiters = $options['delimiters'];
|
||||
}
|
||||
@@ -624,14 +634,15 @@ class Mustache_Engine
|
||||
//
|
||||
// Keep this list in alphabetical order :)
|
||||
$chunks = array(
|
||||
'charset' => $this->charset,
|
||||
'delimiters' => $this->delimiters ? $this->delimiters : '{{ }}',
|
||||
'entityFlags' => $this->entityFlags,
|
||||
'escape' => isset($this->escape) ? 'custom' : 'default',
|
||||
'key' => ($source instanceof Mustache_Source) ? $source->getKey() : 'source',
|
||||
'pragmas' => $this->getPragmas(),
|
||||
'strictCallables' => $this->strictCallables,
|
||||
'version' => self::VERSION,
|
||||
'charset' => $this->charset,
|
||||
'delimiters' => $this->delimiters ? $this->delimiters : '{{ }}',
|
||||
'entityFlags' => $this->entityFlags,
|
||||
'escape' => isset($this->escape) ? 'custom' : 'default',
|
||||
'key' => ($source instanceof Mustache_Source) ? $source->getKey() : 'source',
|
||||
'pragmas' => $this->getPragmas(),
|
||||
'strictCallables' => $this->strictCallables,
|
||||
'disableLambdaRendering' => $this->disableLambdaRendering,
|
||||
'version' => self::VERSION,
|
||||
);
|
||||
|
||||
$key = json_encode($chunks);
|
||||
@@ -810,7 +821,7 @@ class Mustache_Engine
|
||||
$compiler = $this->getCompiler();
|
||||
$compiler->setPragmas($this->getPragmas());
|
||||
|
||||
return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags);
|
||||
return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags, $this->disableLambdaRendering);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
@@ -107,7 +107,7 @@ class Mustache_Tokenizer
|
||||
|
||||
$this->reset();
|
||||
|
||||
if ($delimiters = trim($delimiters)) {
|
||||
if (is_string($delimiters) && $delimiters = trim($delimiters)) {
|
||||
$this->setDelimiters($delimiters);
|
||||
}
|
||||
|
||||
|
||||
@@ -129,7 +129,10 @@ class renderer_base {
|
||||
// Don't allow the JavaScript helper to be executed from within another
|
||||
// helper. If it's allowed it can be used by users to inject malicious
|
||||
// JS into the page.
|
||||
'blacklistednestedhelpers' => ['js']));
|
||||
'blacklistednestedhelpers' => ['js'],
|
||||
// Disable lambda rendering - content in helpers is already rendered, no need to render it again.
|
||||
'disable_lambda_rendering' => true,
|
||||
));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1002,6 +1002,57 @@ EOD;
|
||||
return $gradecategory->get_record_data();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a grade_grade.
|
||||
*
|
||||
* @param array $record
|
||||
* @return grade_grade the grade record
|
||||
*/
|
||||
public function create_grade_grade(?array $record = null): grade_grade {
|
||||
global $DB, $USER;
|
||||
|
||||
$item = $DB->get_record('grade_items', ['id' => $record['itemid']]);
|
||||
$userid = $record['userid'] ?? $USER->id;
|
||||
|
||||
unset($record['itemid']);
|
||||
unset($record['userid']);
|
||||
|
||||
if ($item->itemtype === 'mod') {
|
||||
$cm = get_coursemodule_from_instance($item->itemmodule, $item->iteminstance);
|
||||
$module = new $item->itemmodule(context_module::instance($cm->id), $cm, false);
|
||||
$record['attemptnumber'] = $record['attemptnumber'] ?? 0;
|
||||
|
||||
$module->save_grade($userid, (object) $record);
|
||||
|
||||
$grade = grade_grade::fetch(['userid' => $userid, 'itemid' => $item->id]);
|
||||
} else {
|
||||
$grade = grade_grade::fetch(['userid' => $userid, 'itemid' => $item->id]);
|
||||
$record['rawgrade'] = $record['rawgrade'] ?? $record['grade'] ?? null;
|
||||
$record['finalgrade'] = $record['finalgrade'] ?? $record['grade'] ?? null;
|
||||
|
||||
unset($record['grade']);
|
||||
|
||||
if ($grade) {
|
||||
$fields = $grade->required_fields + array_keys($grade->optional_fields);
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$grade->{$field} = $record[$field] ?? $grade->{$field};
|
||||
}
|
||||
|
||||
$grade->update();
|
||||
} else {
|
||||
$record['userid'] = $userid;
|
||||
$record['itemid'] = $item->id;
|
||||
|
||||
$grade = new grade_grade($record, false);
|
||||
|
||||
$grade->insert();
|
||||
}
|
||||
}
|
||||
|
||||
return $grade;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a grade_item.
|
||||
*
|
||||
@@ -1256,4 +1307,120 @@ EOD;
|
||||
"data generators yet. Class {$classname} not found.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new category for custom profile fields.
|
||||
*
|
||||
* @param array $data Array with 'name' and optionally 'sortorder'
|
||||
* @return \stdClass New category object
|
||||
*/
|
||||
public function create_custom_profile_field_category(array $data): \stdClass {
|
||||
global $DB;
|
||||
|
||||
// Pick next sortorder if not defined.
|
||||
if (!array_key_exists('sortorder', $data)) {
|
||||
$data['sortorder'] = (int)$DB->get_field_sql('SELECT MAX(sortorder) FROM {user_info_category}') + 1;
|
||||
}
|
||||
|
||||
$category = (object)[
|
||||
'name' => $data['name'],
|
||||
'sortorder' => $data['sortorder']
|
||||
];
|
||||
$category->id = $DB->insert_record('user_info_category', $category);
|
||||
|
||||
return $category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new custom profile field.
|
||||
*
|
||||
* Optional fields are:
|
||||
*
|
||||
* categoryid (or use 'category' to specify by name). If you don't specify
|
||||
* either, it will add the field to a 'Testing' category, which will be created for you if
|
||||
* necessary.
|
||||
*
|
||||
* sortorder (if you don't specify this, it will pick the next one in the category).
|
||||
*
|
||||
* all the other database fields (if you don't specify this, it will pick sensible defaults
|
||||
* based on the data type).
|
||||
*
|
||||
* @param array $data Array with 'datatype', 'shortname', and 'name'
|
||||
* @return \stdClass Database object from the user_info_field table
|
||||
*/
|
||||
public function create_custom_profile_field(array $data): \stdClass {
|
||||
global $DB, $CFG;
|
||||
require_once($CFG->dirroot . '/user/profile/lib.php');
|
||||
|
||||
// Set up category if necessary.
|
||||
if (!array_key_exists('categoryid', $data)) {
|
||||
if (array_key_exists('category', $data)) {
|
||||
$data['categoryid'] = $DB->get_field('user_info_category', 'id',
|
||||
['name' => $data['category']], MUST_EXIST);
|
||||
} else {
|
||||
// Make up a 'Testing' category or use existing.
|
||||
$data['categoryid'] = $DB->get_field('user_info_category', 'id', ['name' => 'Testing']);
|
||||
if (!$data['categoryid']) {
|
||||
$created = $this->create_custom_profile_field_category(['name' => 'Testing']);
|
||||
$data['categoryid'] = $created->id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pick sort order if necessary.
|
||||
if (!array_key_exists('sortorder', $data)) {
|
||||
$data['sortorder'] = (int)$DB->get_field_sql(
|
||||
'SELECT MAX(sortorder) FROM {user_info_field} WHERE categoryid = ?',
|
||||
[$data['categoryid']]) + 1;
|
||||
}
|
||||
|
||||
// Defaults for other values.
|
||||
$defaults = [
|
||||
'description' => '',
|
||||
'descriptionformat' => 0,
|
||||
'required' => 0,
|
||||
'locked' => 0,
|
||||
'visible' => PROFILE_VISIBLE_ALL,
|
||||
'forceunique' => 0,
|
||||
'signup' => 0,
|
||||
'defaultdata' => '',
|
||||
'defaultdataformat' => 0,
|
||||
'param1' => '',
|
||||
'param2' => '',
|
||||
'param3' => '',
|
||||
'param4' => '',
|
||||
'param5' => ''
|
||||
];
|
||||
|
||||
// Type-specific defaults for other values.
|
||||
$typedefaults = [
|
||||
'text' => [
|
||||
'param1' => 30,
|
||||
'param2' => 2048
|
||||
],
|
||||
'menu' => [
|
||||
'param1' => "Yes\nNo",
|
||||
'defaultdata' => 'No'
|
||||
],
|
||||
'datetime' => [
|
||||
'param1' => '2010',
|
||||
'param2' => '2015',
|
||||
'param3' => 1
|
||||
],
|
||||
'checkbox' => [
|
||||
'defaultdata' => 0
|
||||
]
|
||||
];
|
||||
foreach ($typedefaults[$data['datatype']] ?? [] as $field => $value) {
|
||||
$defaults[$field] = $value;
|
||||
}
|
||||
|
||||
foreach ($defaults as $field => $value) {
|
||||
if (!array_key_exists($field, $data)) {
|
||||
$data[$field] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$data['id'] = $DB->insert_record('user_info_field', $data);
|
||||
return (object)$data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -90,7 +90,7 @@ class core_renderer_template_exploit_testcase extends advanced_testcase {
|
||||
'test1' => $norender,
|
||||
],
|
||||
'js' => 'some nasty JS',
|
||||
'expected' => 'core, move, some text',
|
||||
'expected' => 'core, move, some text {{}}',
|
||||
'include' => false
|
||||
],
|
||||
'js helper not nested' => [
|
||||
@@ -133,7 +133,7 @@ class core_renderer_template_exploit_testcase extends advanced_testcase {
|
||||
'testpix' => $singlerender
|
||||
],
|
||||
'js' => 'some nasty JS',
|
||||
'expected' => 'core, move,',
|
||||
'expected' => 'core, move, {{}}',
|
||||
'include' => false
|
||||
],
|
||||
'js in context double depth with single render' => [
|
||||
@@ -149,7 +149,7 @@ class core_renderer_template_exploit_testcase extends advanced_testcase {
|
||||
'testpix' => $singlerender
|
||||
],
|
||||
'js' => 'some nasty JS',
|
||||
'expected' => 'core, move, {{#js}} some nasty JS {{/js}}',
|
||||
'expected' => 'core, move, {{second}}',
|
||||
'include' => false
|
||||
],
|
||||
'js in context double depth with recursive render' => [
|
||||
@@ -269,24 +269,7 @@ class core_renderer_template_exploit_testcase extends advanced_testcase {
|
||||
'testpix' => $singlerender
|
||||
],
|
||||
'js' => 'some nasty JS',
|
||||
'expected' => 'core, move,',
|
||||
'include' => false
|
||||
],
|
||||
'partial double nested with js from context single render' => [
|
||||
'templates' => [
|
||||
'test' => '{{#testpix}} core, move, {{foo}}{{/testpix}}',
|
||||
'test2' => '{{#js}} some nasty JS {{/js}}',
|
||||
],
|
||||
'torender' => 'test',
|
||||
'context' => [
|
||||
'foo' => '{{{bar}}}',
|
||||
'bar' => '{{> test2}}'
|
||||
],
|
||||
'helpers' => [
|
||||
'testpix' => $singlerender
|
||||
],
|
||||
'js' => 'some nasty JS',
|
||||
'expected' => 'core, move, {{> test2}}',
|
||||
'expected' => 'core, move, {{> test2}}',
|
||||
'include' => false
|
||||
],
|
||||
'partial double nested with js from context recursive render' => [
|
||||
|
||||
@@ -684,6 +684,108 @@ class core_datalib_testcase extends advanced_testcase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the get_users_listing function.
|
||||
*/
|
||||
public function test_get_users_listing(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = $this->getDataGenerator();
|
||||
|
||||
// Set up profile field.
|
||||
$generator->create_custom_profile_field(['datatype' => 'text',
|
||||
'shortname' => 'specialid', 'name' => 'Special user id']);
|
||||
|
||||
// Set up the show user identity option.
|
||||
set_config('showuseridentity', 'department');
|
||||
|
||||
// Get all the existing user ids (we're going to remove these from test results).
|
||||
$existingids = array_fill_keys($DB->get_fieldset_select('user', 'id', '1 = 1'), true);
|
||||
|
||||
// Create some test user accounts.
|
||||
$userids = [];
|
||||
foreach (['a', 'b', 'c', 'd'] as $key) {
|
||||
$record = [
|
||||
'username' => 'user_' . $key,
|
||||
'firstname' => $key . '_first',
|
||||
'lastname' => 'last_' . $key,
|
||||
'department' => 'department_' . $key,
|
||||
'lastaccess' => ord($key)
|
||||
];
|
||||
$user = $generator->create_user($record);
|
||||
$userids[] = $user->id;
|
||||
}
|
||||
|
||||
// Check default result with no parameters.
|
||||
$results = get_users_listing();
|
||||
$results = array_diff_key($results, $existingids);
|
||||
|
||||
// It should return all the results in order.
|
||||
$this->assertEquals($userids, array_keys($results));
|
||||
|
||||
// Results should have some general fields and name fields, check some samples.
|
||||
$this->assertEquals('user_a', $results[$userids[0]]->username);
|
||||
$this->assertEquals('user_a@example.com', $results[$userids[0]]->email);
|
||||
$this->assertEquals(1, $results[$userids[0]]->confirmed);
|
||||
$this->assertEquals('a_first', $results[$userids[0]]->firstname);
|
||||
$this->assertObjectHasAttribute('firstnamephonetic', $results[$userids[0]]);
|
||||
|
||||
// Should not have department because no context specified.
|
||||
$this->assertObjectNotHasAttribute('department', $results[$userids[0]]);
|
||||
|
||||
// Check sorting.
|
||||
$results = get_users_listing('username', 'DESC');
|
||||
$results = array_diff_key($results, $existingids);
|
||||
$this->assertEquals([$userids[3], $userids[2], $userids[1], $userids[0]], array_keys($results));
|
||||
|
||||
// Check default fallback sort field works as expected.
|
||||
$results = get_users_listing('blah2', 'ASC');
|
||||
$results = array_diff_key($results, $existingids);
|
||||
$this->assertEquals([$userids[0], $userids[1], $userids[2], $userids[3]], array_keys($results));
|
||||
|
||||
// Check default fallback sort direction works as expected.
|
||||
$results = get_users_listing('lastaccess', 'blah2');
|
||||
$results = array_diff_key($results, $existingids);
|
||||
$this->assertEquals([$userids[0], $userids[1], $userids[2], $userids[3]], array_keys($results));
|
||||
|
||||
// Add the options to showuseridentity and check it returns those fields but only if you
|
||||
// specify a context AND have permissions.
|
||||
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', '', null,
|
||||
\context_system::instance());
|
||||
$this->assertObjectNotHasAttribute('department', $results[$userids[0]]);
|
||||
$this->setAdminUser();
|
||||
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', '', null,
|
||||
\context_system::instance());
|
||||
$this->assertEquals('department_a', $results[$userids[0]]->department);
|
||||
|
||||
// Check search (full name, email, username).
|
||||
$results = get_users_listing('lastaccess', 'asc', 0, 0, 'b_first last_b');
|
||||
$this->assertEquals([$userids[1]], array_keys($results));
|
||||
$results = get_users_listing('lastaccess', 'asc', 0, 0, 'c@example');
|
||||
$this->assertEquals([$userids[2]], array_keys($results));
|
||||
$results = get_users_listing('lastaccess', 'asc', 0, 0, 'user_d');
|
||||
$this->assertEquals([$userids[3]], array_keys($results));
|
||||
|
||||
// Check first and last initial restriction (all the test ones have same last initial).
|
||||
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', 'C');
|
||||
$this->assertEquals([$userids[2]], array_keys($results));
|
||||
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', 'L');
|
||||
$results = array_diff_key($results, $existingids);
|
||||
$this->assertEquals($userids, array_keys($results));
|
||||
|
||||
// Check the extra where clause, either with the 'u.' prefix or not.
|
||||
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', 'id IN (:x,:y)',
|
||||
['x' => $userids[1], 'y' => $userids[3]]);
|
||||
$results = array_diff_key($results, $existingids);
|
||||
$this->assertEquals([$userids[1], $userids[3]], array_keys($results));
|
||||
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', 'id IN (:x,:y)',
|
||||
['x' => $userids[1], 'y' => $userids[3]]);
|
||||
$results = array_diff_key($results, $existingids);
|
||||
$this->assertEquals([$userids[1], $userids[3]], array_keys($results));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_get_safe_orderby().
|
||||
*
|
||||
|
||||
@@ -1679,6 +1679,7 @@ EOF;
|
||||
}
|
||||
|
||||
// Burst up to the capacity and make sure that the bucket allows it.
|
||||
$burststart = microtime();
|
||||
for ($i = 0; $i < $capacity; $i++) {
|
||||
if ($i) {
|
||||
sleep(1); // A little delay so we have different timemodified value for files.
|
||||
@@ -1693,9 +1694,16 @@ EOF;
|
||||
// The bucket should be full after bursting.
|
||||
$this->assertTrue(file_is_draft_areas_limit_reached($user->id));
|
||||
|
||||
// Calculate the time taken to burst up the bucket capacity.
|
||||
$timetaken = microtime_diff($burststart, microtime());
|
||||
|
||||
// The bucket leaks so it shouldn't be full after a certain time.
|
||||
// Reiterating that this test could have been faster if MDL-37327 was implemented.
|
||||
sleep(ceil(1 / $leak) - ($capacity - 1));
|
||||
// Items are added into the bucket at the rate of 1 item per second.
|
||||
// One item leaks from the bucket every 1/$leak seconds.
|
||||
// So it takes 1/$leak - ($capacity-1) seconds for the bucket to leak one item and not be full anymore.
|
||||
$milliseconds = ceil(1000000 * ((1 / $leak) - ($capacity - 1)) - ($timetaken * 1000));
|
||||
usleep($milliseconds);
|
||||
|
||||
$this->assertFalse(file_is_draft_areas_limit_reached($user->id));
|
||||
|
||||
// Only one item was leaked from the bucket. So the bucket should become full again if we add a single item to it.
|
||||
|
||||
@@ -192,7 +192,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>
|
||||
|
||||
@@ -2714,3 +2714,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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ use \core_privacy\local\metadata\collection;
|
||||
use \core_privacy\local\request\contextlist;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\writer;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
@@ -42,6 +43,7 @@ use \core_privacy\local\request\approved_userlist;
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\user_preference_provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
/**
|
||||
@@ -125,4 +127,21 @@ class provider implements
|
||||
*/
|
||||
public static function delete_data_for_user(approved_contextlist $contextlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user preferences for the plugin
|
||||
*
|
||||
* @param int $userid
|
||||
*/
|
||||
public static function export_user_preferences(int $userid) {
|
||||
$preference = get_user_preferences('message_processor_email_email', null, $userid);
|
||||
if (!empty($preference)) {
|
||||
writer::export_user_preference(
|
||||
'message_email',
|
||||
'email',
|
||||
$preference,
|
||||
get_string('privacy:preference:email', 'message_email')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ $string['privacy:metadata:replyto'] = 'The email address to reply to.';
|
||||
$string['privacy:metadata:replytoname'] = 'Name of reply to recipient.';
|
||||
$string['privacy:metadata:subject'] = 'The subject line of the message.';
|
||||
$string['privacy:metadata:userfrom'] = 'The user sending the message.';
|
||||
$string['privacy:preference:email'] = 'Preferred email notification address';
|
||||
$string['tasksendemail'] = 'Messages digest mailings';
|
||||
|
||||
// Deprecated since Moodle 3.9.
|
||||
|
||||
@@ -13,28 +13,27 @@
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
/**
|
||||
* Base class for unit tests for message_email.
|
||||
*
|
||||
* @package message_email
|
||||
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
namespace message_email\privacy;
|
||||
|
||||
use context_system;
|
||||
use core_message_external;
|
||||
use core_privacy\local\request\writer;
|
||||
use core_privacy\tests\provider_testcase;
|
||||
|
||||
use \core_privacy\tests\provider_testcase;
|
||||
/**
|
||||
* Unit tests for message\output\email\classes\privacy\provider.php
|
||||
*
|
||||
* @package message_email
|
||||
* @covers \message_email\privacy\provider
|
||||
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class message_email_testcase extends provider_testcase {
|
||||
class provider_test extends provider_testcase {
|
||||
/**
|
||||
* Basic setup for these tests.
|
||||
*/
|
||||
public function setUp() {
|
||||
public function setUp(): void {
|
||||
$this->resetAfterTest(true);
|
||||
}
|
||||
|
||||
@@ -56,5 +55,38 @@ class message_email_testcase extends provider_testcase {
|
||||
$contextlist = \message_email\privacy\provider::get_contexts_for_userid($user->id);
|
||||
$this->assertEmpty($contextlist);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test exporting user preferences
|
||||
*/
|
||||
public function test_export_user_preferences(): void {
|
||||
global $CFG;
|
||||
|
||||
require_once("{$CFG->dirroot}/message/externallib.php");
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($user);
|
||||
|
||||
// Submit configuration form, which adds the preferences..
|
||||
core_message_external::message_processor_config_form($user->id, 'email', [
|
||||
[
|
||||
'name' => 'email_email',
|
||||
'value' => 'alternate@example.com',
|
||||
],
|
||||
]);
|
||||
|
||||
// Switch to admin user (so we can validate preferences of the correct user are being exported).
|
||||
$this->setAdminUser();
|
||||
|
||||
provider::export_user_preferences($user->id);
|
||||
|
||||
$writer = writer::with_context(context_system::instance());
|
||||
$this->assertTrue($writer->has_any_data());
|
||||
|
||||
$preferences = $writer->get_user_preferences('message_email');
|
||||
$this->assertNotEmpty($preferences->email);
|
||||
|
||||
$this->assertEquals('alternate@example.com', $preferences->email->value);
|
||||
$this->assertEquals(get_string('privacy:preference:email', 'message_email'), $preferences->email->description);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,61 +5,41 @@ Feature: Notification popover unread notifications
|
||||
I am notified about relevant events in Moodle
|
||||
|
||||
Background:
|
||||
# This will make sure popup notifications are enabled and create
|
||||
# two assignment notifications. One for the student submitting their
|
||||
# assignment and another for the teacher grading it.
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category | groupmode |
|
||||
| Course 1 | C1 | 0 | 1 |
|
||||
# Make sure the popup notifications are enabled for assignments.
|
||||
And the following config values are set as admin:
|
||||
| popup_provider_mod_assign_assign_notification_permitted | permitted | message |
|
||||
| message_provider_mod_assign_assign_notification_loggedin | popup | message |
|
||||
| message_provider_mod_assign_assign_notification_loggedoff | popup | message |
|
||||
And the following "users" exist:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
And the following "activity" exists:
|
||||
| activity | assign |
|
||||
| course | C1 |
|
||||
| name | Test assignment name |
|
||||
| assignsubmission_onlinetext_enabled | 1 |
|
||||
| assignsubmission_file_enabled | 0 |
|
||||
| submissiondrafts | 0 |
|
||||
# This should generate a notification.
|
||||
And the following "mod_assign > submissions" exist:
|
||||
| assign | user | onlinetext |
|
||||
| Test assignment name | student1 | I'm the student1 submission |
|
||||
| student2 | Student | 2 | student2@example.com |
|
||||
# This should generate some notifications
|
||||
And the following "notifications" exist:
|
||||
| subject | userfrom | userto | timecreated | timeread |
|
||||
| Test 01 | student2 | student1 | 1654587996 | null |
|
||||
| Test 02 | student2 | student1 | 1654587997 | null |
|
||||
|
||||
Scenario: Notification popover shows correct unread count
|
||||
When I log in as "student1"
|
||||
Given I log in as "student1"
|
||||
# Confirm the popover is saying 1 unread notifications.
|
||||
Then I should see "1" in the "#nav-notification-popover-container [data-region='count-container']" "css_element"
|
||||
And I should see "2" in the "#nav-notification-popover-container [data-region='count-container']" "css_element"
|
||||
# Open the popover.
|
||||
And I open the notification popover
|
||||
# Confirm the submission notification is visible.
|
||||
And I should see "You have submitted your assignment submission for Test assignment name" in the "#nav-notification-popover-container" "css_element"
|
||||
When I open the notification popover
|
||||
# Confirm the notifications are visible.
|
||||
Then I should see "Test 01" in the "#nav-notification-popover-container" "css_element"
|
||||
And I should see "Test 02" in the "#nav-notification-popover-container" "css_element"
|
||||
|
||||
@_bug_phantomjs
|
||||
Scenario: Clicking a notification marks it as read
|
||||
When I log in as "student1"
|
||||
# Open the popover.
|
||||
Given I log in as "student1"
|
||||
# Open the notifications.
|
||||
When I open the notification popover
|
||||
And I follow "Test 01"
|
||||
And I open the notification popover
|
||||
# Click on the submission notification.
|
||||
And I follow "You have submitted your assignment submission for Test assignment name"
|
||||
And I follow "Test 02"
|
||||
|
||||
# Confirm the count element is hidden (i.e. there are no unread notifications).
|
||||
Then "[data-region='count-container']" "css_element" in the "#nav-notification-popover-container" "css_element" should not be visible
|
||||
|
||||
Scenario: Mark all notifications as read
|
||||
When I log in as "student1"
|
||||
# Open the popover.
|
||||
And I open the notification popover
|
||||
# Click the mark all as read button.
|
||||
Given I log in as "student1"
|
||||
When I open the notification popover
|
||||
And I click on "Mark all as read" "link" in the "#nav-notification-popover-container" "css_element"
|
||||
# Refresh the page to make sure we send a new request for the unread count.
|
||||
And I reload the page
|
||||
|
||||
@@ -632,7 +632,7 @@ class pdf extends TcpdfFpdi {
|
||||
$gsexec = \escapeshellarg($CFG->pathtogs);
|
||||
$tempdstarg = \escapeshellarg($tempdst);
|
||||
$tempsrcarg = \escapeshellarg($tempsrc);
|
||||
$command = "$gsexec -q -sDEVICE=pdfwrite -dBATCH -dNOPAUSE -sOutputFile=$tempdstarg $tempsrcarg";
|
||||
$command = "$gsexec -q -sDEVICE=pdfwrite -dSAFER -dBATCH -dNOPAUSE -sOutputFile=$tempdstarg $tempsrcarg";
|
||||
exec($command);
|
||||
if (!file_exists($tempdst)) {
|
||||
// Something has gone wrong in the conversion.
|
||||
|
||||
@@ -24,32 +24,27 @@ Feature: Users can view and search database entries
|
||||
And the following "activities" exist:
|
||||
| activity | name | intro | course | idnumber |
|
||||
| data | Test database name | Database intro | C1 | data1 |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I add a "Text input" field to "Test database name" database and I fill the form with:
|
||||
| Field name | Test field name |
|
||||
| Field description | Test field description |
|
||||
And I add a "Text input" field to "Test database name" database and I fill the form with:
|
||||
| Field name | Test field 2 name |
|
||||
| Field description | Test field 2 description |
|
||||
# To generate the default templates.
|
||||
And I follow "Templates"
|
||||
And I log out
|
||||
And the following "mod_data > fields" exist:
|
||||
| database | type | name | description |
|
||||
| data1 | text | Test field name | Test field description |
|
||||
| data1 | text | Test field 2 name | Test field 2 description |
|
||||
And the following "mod_data > templates" exist:
|
||||
| database | name |
|
||||
| data1 | singletemplate |
|
||||
| data1 | listtemplate |
|
||||
| data1 | addtemplate |
|
||||
| data1 | asearchtemplate |
|
||||
| data1 | rsstemplate |
|
||||
|
||||
Scenario: Students can add view, list and search entries
|
||||
Given I log in as "student1"
|
||||
Given the following "mod_data > entries" exist:
|
||||
| database | Test field name | Test field 2 name |
|
||||
| data1 | Student entry 1 | |
|
||||
| data1 | Student entry 2 | |
|
||||
| data1 | Student entry 3 | |
|
||||
When I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Test database name"
|
||||
And I add an entry to "Test database name" database with:
|
||||
| Test field name | Student entry 1 |
|
||||
And I press "Save and add another"
|
||||
And I add an entry to "Test database name" database with:
|
||||
| Test field name | Student entry 2 |
|
||||
And I press "Save and add another"
|
||||
And I add an entry to "Test database name" database with:
|
||||
| Test field name | Student entry 3 |
|
||||
And I press "Save and view"
|
||||
And I follow "Test database name"
|
||||
Then I should see "Student entry 1"
|
||||
And I should see "Student entry 2"
|
||||
And I should see "Student entry 3"
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<?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_data.
|
||||
*
|
||||
* @package mod_data
|
||||
* @category test
|
||||
* @copyright 2022 Noel De Martin
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class behat_mod_data_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 [
|
||||
'entries' => [
|
||||
'singular' => 'entry',
|
||||
'datagenerator' => 'entry',
|
||||
'required' => ['database'],
|
||||
'switchids' => ['database' => 'databaseid'],
|
||||
],
|
||||
'fields' => [
|
||||
'singular' => 'field',
|
||||
'datagenerator' => 'field',
|
||||
'required' => ['database', 'type', 'name'],
|
||||
'switchids' => ['database' => 'databaseid'],
|
||||
],
|
||||
'templates' => [
|
||||
'singular' => 'template',
|
||||
'datagenerator' => 'template',
|
||||
'required' => ['database', 'name'],
|
||||
'switchids' => ['database' => 'databaseid'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the database id using an activity idnumber.
|
||||
*
|
||||
* @param string $idnumber
|
||||
* @return int The database id
|
||||
*/
|
||||
protected function get_database_id(string $idnumber): int {
|
||||
$cm = $this->get_cm_by_activity_name('data', $idnumber);
|
||||
|
||||
return $cm->instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an entry.
|
||||
*
|
||||
* @param array $data Entry data.
|
||||
*/
|
||||
public function process_entry(array $data): void {
|
||||
global $DB;
|
||||
|
||||
$database = $DB->get_record('data', ['id' => $data['databaseid']], '*', MUST_EXIST);
|
||||
|
||||
unset($data['databaseid']);
|
||||
|
||||
$data = array_reduce(array_keys($data), function ($fields, $fieldname) use ($data, $database) {
|
||||
global $DB;
|
||||
|
||||
$field = $DB->get_record('data_fields', ['name' => $fieldname, 'dataid' => $database->id], 'id', MUST_EXIST);
|
||||
|
||||
$fields[$field->id] = $data[$fieldname];
|
||||
|
||||
return $fields;
|
||||
}, []);
|
||||
|
||||
$this->get_data_generator()->create_entry($database, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a field.
|
||||
*
|
||||
* @param array $data Field data.
|
||||
*/
|
||||
public function process_field(array $data): void {
|
||||
global $DB;
|
||||
|
||||
$database = $DB->get_record('data', ['id' => $data['databaseid']], '*', MUST_EXIST);
|
||||
|
||||
unset($data['databaseid']);
|
||||
|
||||
$this->get_data_generator()->create_field((object) $data, $database);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a template.
|
||||
*
|
||||
* @param array $data Template data.
|
||||
*/
|
||||
public function process_template(array $data): void {
|
||||
global $DB;
|
||||
|
||||
$database = $DB->get_record('data', ['id' => $data['databaseid']], '*', MUST_EXIST);
|
||||
|
||||
if (empty($data['content'])) {
|
||||
data_generate_default_template($database, $data['name']);
|
||||
} else {
|
||||
$newdata = new stdClass();
|
||||
$newdata->id = $database->id;
|
||||
$newdata->{$data['name']} = $data['content'];
|
||||
$DB->update_record('data', $newdata);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the module data generator.
|
||||
*
|
||||
* @return mod_data_generator Database data generator.
|
||||
*/
|
||||
protected function get_data_generator(): mod_data_generator {
|
||||
return $this->componentdatagenerator;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,15 +42,36 @@ Feature: Posting to all groups in a separate group discussion is restricted to u
|
||||
And the following "activities" exist:
|
||||
| activity | name | intro | course | idnumber | groupmode |
|
||||
| forum | Standard forum name | Standard forum description | C1 | sepgroups | 1 |
|
||||
And the following "mod_forum > discussions" exist:
|
||||
| forum | name | subject | message | group |
|
||||
| sepgroups | Initial Disc ALL | Initial Disc ALL | Disc ALL content | All participants |
|
||||
| sepgroups | Initial Disc G1 | Initial Disc G1 | Disc G1 content | G1 |
|
||||
| sepgroups | Initial Disc G2 | Initial Disc G2 | Disc G2 content | G2 |
|
||||
| sepgroups | Initial Disc G3 | Initial Disc G3 | Disc G3 content | G3 |
|
||||
|
||||
Scenario: Teacher with accessallgroups can view all groups
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
When I follow "Standard forum name"
|
||||
Then the "Separate groups" select box should contain "All participants"
|
||||
Then the "Separate groups" select box should contain "Group A"
|
||||
Then the "Separate groups" select box should contain "Group B"
|
||||
Then the "Separate groups" select box should contain "Group C"
|
||||
And the "Separate groups" select box should contain "Group A"
|
||||
And the "Separate groups" select box should contain "Group B"
|
||||
And the "Separate groups" select box should contain "Group C"
|
||||
And I select "All participants" from the "Separate groups" singleselect
|
||||
And I should see "Initial Disc ALL"
|
||||
And I should see "Initial Disc G1"
|
||||
And I should see "Initial Disc G2"
|
||||
And I should see "Initial Disc G2"
|
||||
And I select "Group A" from the "Separate groups" singleselect
|
||||
And I should see "Initial Disc ALL"
|
||||
And I should see "Initial Disc G1"
|
||||
But I should not see "Initial Disc G2"
|
||||
And I should not see "Initial Disc G3"
|
||||
And I select "Group B" from the "Separate groups" singleselect
|
||||
And I should see "Initial Disc ALL"
|
||||
And I should see "Initial Disc G2"
|
||||
But I should not see "Initial Disc G1"
|
||||
And I should not see "Initial Disc G3"
|
||||
|
||||
Scenario: Teacher with accessallgroups can select any group when posting
|
||||
Given I log in as "teacher1"
|
||||
@@ -249,14 +270,7 @@ Feature: Posting to all groups in a separate group discussion is restricted to u
|
||||
And I should see "Post a copy to all groups"
|
||||
|
||||
Scenario: Students can view all participants discussions in separate groups mode
|
||||
Given I log in as "teacher1"
|
||||
Given I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
When I add a new discussion to "Standard forum name" forum with:
|
||||
| Subject | Forum post to all participants |
|
||||
| Message | This is the body |
|
||||
| Group | All participants |
|
||||
And I log out
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Standard forum name"
|
||||
Then I should see "Forum post to all participants"
|
||||
When I follow "Standard forum name"
|
||||
Then I should see "Initial Disc ALL"
|
||||
|
||||
@@ -33,15 +33,36 @@ Feature: Posting to all groups in a visible group discussion is restricted to us
|
||||
And the following "activities" exist:
|
||||
| activity | name | intro | course | idnumber | groupmode |
|
||||
| forum | Standard forum name | Standard forum description | C1 | groups | 2 |
|
||||
And the following "mod_forum > discussions" exist:
|
||||
| forum | name | subject | message | group |
|
||||
| groups | Initial Disc ALL | Initial Disc ALL | Disc ALL content | All participants |
|
||||
| groups | Initial Disc G1 | Initial Disc G1 | Disc G1 content | G1 |
|
||||
| groups | Initial Disc G2 | Initial Disc G2 | Disc G2 content | G2 |
|
||||
| groups | Initial Disc G3 | Initial Disc G3 | Disc G3 content | G3 |
|
||||
|
||||
Scenario: Teacher with accessallgroups can view all groups
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
When I follow "Standard forum name"
|
||||
Then the "Visible groups" select box should contain "All participants"
|
||||
Then the "Visible groups" select box should contain "Group A"
|
||||
Then the "Visible groups" select box should contain "Group B"
|
||||
Then the "Visible groups" select box should contain "Group C"
|
||||
And the "Visible groups" select box should contain "Group A"
|
||||
And the "Visible groups" select box should contain "Group B"
|
||||
And the "Visible groups" select box should contain "Group C"
|
||||
And I select "All participants" from the "Visible groups" singleselect
|
||||
And I should see "Initial Disc ALL"
|
||||
And I should see "Initial Disc G1"
|
||||
And I should see "Initial Disc G2"
|
||||
And I should see "Initial Disc G2"
|
||||
And I select "Group A" from the "Visible groups" singleselect
|
||||
And I should see "Initial Disc ALL"
|
||||
And I should see "Initial Disc G1"
|
||||
But I should not see "Initial Disc G2"
|
||||
And I should not see "Initial Disc G3"
|
||||
And I select "Group B" from the "Visible groups" singleselect
|
||||
And I should see "Initial Disc ALL"
|
||||
And I should see "Initial Disc G2"
|
||||
But I should not see "Initial Disc G1"
|
||||
And I should not see "Initial Disc G3"
|
||||
|
||||
Scenario: Teacher with accessallgroups can select any group when posting
|
||||
Given I log in as "teacher1"
|
||||
@@ -150,9 +171,24 @@ Feature: Posting to all groups in a visible group discussion is restricted to us
|
||||
And I am on "Course 1" course homepage
|
||||
When I follow "Standard forum name"
|
||||
Then the "Visible groups" select box should contain "All participants"
|
||||
Then the "Visible groups" select box should contain "Group A"
|
||||
Then the "Visible groups" select box should contain "Group B"
|
||||
Then the "Visible groups" select box should contain "Group C"
|
||||
And the "Visible groups" select box should contain "Group A"
|
||||
And the "Visible groups" select box should contain "Group B"
|
||||
And the "Visible groups" select box should contain "Group C"
|
||||
And I select "All participants" from the "Visible groups" singleselect
|
||||
And I should see "Initial Disc ALL"
|
||||
And I should see "Initial Disc G1"
|
||||
And I should see "Initial Disc G2"
|
||||
And I should see "Initial Disc G2"
|
||||
And I select "Group A" from the "Visible groups" singleselect
|
||||
And I should see "Initial Disc ALL"
|
||||
And I should see "Initial Disc G1"
|
||||
But I should not see "Initial Disc G2"
|
||||
And I should not see "Initial Disc G3"
|
||||
And I select "Group B" from the "Visible groups" singleselect
|
||||
And I should see "Initial Disc ALL"
|
||||
And I should see "Initial Disc G2"
|
||||
But I should not see "Initial Disc G1"
|
||||
And I should not see "Initial Disc G3"
|
||||
|
||||
Scenario: Students in one group can only post in their group
|
||||
Given I log in as "student1"
|
||||
|
||||
@@ -35,7 +35,7 @@ class behat_mod_forum_generator extends behat_generator_base {
|
||||
'singular' => 'discussion',
|
||||
'datagenerator' => 'discussion',
|
||||
'required' => ['forum'],
|
||||
'switchids' => ['forum' => 'forumid', 'user' => 'userid'],
|
||||
'switchids' => ['forum' => 'forumid', 'user' => 'userid', 'group' => 'groupid'],
|
||||
],
|
||||
'posts' => [
|
||||
'singular' => 'post',
|
||||
@@ -62,6 +62,21 @@ class behat_mod_forum_generator extends behat_generator_base {
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the group id from it's idnumber. It allows using 'All participants' as idnumber.
|
||||
*
|
||||
* @throws Exception
|
||||
* @param string $idnumber
|
||||
* @return int
|
||||
*/
|
||||
protected function get_group_id($idnumber): int {
|
||||
if ($idnumber === 'All participants') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return parent::get_group_id($idnumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocess discussion data.
|
||||
*
|
||||
|
||||
@@ -332,15 +332,24 @@ class manager {
|
||||
*
|
||||
* @since Moodle 3.9.7
|
||||
* @param bool $allpotentialusers if true, the join will return all active users, not only the ones with attempts.
|
||||
* @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
|
||||
* @return sql_join the active users attempts join
|
||||
*/
|
||||
public function get_active_users_join(bool $allpotentialusers = false): sql_join {
|
||||
public function get_active_users_join(bool $allpotentialusers = false, $currentgroup = false): sql_join {
|
||||
|
||||
// Only valid users counts. By default, all users with submit capability are considered potential ones.
|
||||
$context = $this->get_context();
|
||||
$coursemodule = $this->get_coursemodule();
|
||||
|
||||
// Ensure user can view users from all groups.
|
||||
if ($currentgroup === 0 && $coursemodule->effectivegroupmode == SEPARATEGROUPS
|
||||
&& !has_capability('moodle/site:accessallgroups', $context)) {
|
||||
|
||||
return new sql_join('', '1=2', [], true);
|
||||
}
|
||||
|
||||
// We want to present all potential users.
|
||||
$capjoin = get_enrolled_with_capabilities_join($context, '', 'mod/h5pactivity:view');
|
||||
$capjoin = get_enrolled_with_capabilities_join($context, '', 'mod/h5pactivity:view', $currentgroup);
|
||||
|
||||
if ($capjoin->cannotmatchanyrows) {
|
||||
return $capjoin;
|
||||
@@ -438,10 +447,13 @@ class manager {
|
||||
*
|
||||
* @param int $userid an opional userid to show
|
||||
* @param int $attemptid an optional $attemptid to show
|
||||
* @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
|
||||
* @return report|null available report (or null if no report available)
|
||||
*/
|
||||
public function get_report(int $userid = null, int $attemptid = null): ?report {
|
||||
global $USER;
|
||||
public function get_report(int $userid = null, int $attemptid = null, $currentgroup = false): ?report {
|
||||
global $USER, $CFG;
|
||||
|
||||
require_once("{$CFG->dirroot}/user/lib.php");
|
||||
|
||||
// If tracking is disabled, no reports are available.
|
||||
if (!$this->instance->enabletracking) {
|
||||
@@ -460,6 +472,14 @@ class manager {
|
||||
|
||||
if ($this->can_view_all_attempts()) {
|
||||
$user = core_user::get_user($userid);
|
||||
|
||||
// Ensure user can view the attempt of specific userid, respecting access checks.
|
||||
if ($user && $user->id != $USER->id) {
|
||||
$course = get_course($this->coursemodule->course);
|
||||
if ($this->coursemodule->effectivegroupmode == SEPARATEGROUPS && !user_can_view_profile($user, $course)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else if ($this->can_view_own_attempts()) {
|
||||
$user = core_user::get_user($USER->id);
|
||||
if ($userid && $user->id != $userid) {
|
||||
@@ -480,7 +500,7 @@ class manager {
|
||||
} else if ($user) {
|
||||
return new attempts($this, $user);
|
||||
}
|
||||
return new participants($this);
|
||||
return new participants($this, $currentgroup);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -61,8 +61,9 @@ class participants extends table_sql implements report {
|
||||
* Create a new participants report.
|
||||
*
|
||||
* @param manager $manager h5pactivitymanager object
|
||||
* @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
|
||||
*/
|
||||
public function __construct(manager $manager) {
|
||||
public function __construct(manager $manager, $currentgroup = false) {
|
||||
parent::__construct('mod_h5pactivity-participants');
|
||||
$this->manager = $manager;
|
||||
$this->scores = $manager->get_users_scaled_score();
|
||||
@@ -83,7 +84,7 @@ class participants extends table_sql implements report {
|
||||
$this->no_sorting('attempts');
|
||||
$this->pageable(true);
|
||||
|
||||
$capjoin = $this->manager->get_active_users_join(true);
|
||||
$capjoin = $this->manager->get_active_users_join(true, $currentgroup);
|
||||
|
||||
// Final SQL.
|
||||
$this->set_sql(
|
||||
|
||||
@@ -43,9 +43,11 @@ if (empty($id)) {
|
||||
|
||||
require_login($course, true, $cm);
|
||||
|
||||
$currentgroup = groups_get_activity_group($cm, true);
|
||||
|
||||
$manager = manager::create_from_coursemodule($cm);
|
||||
|
||||
$report = $manager->get_report($userid, $attemptid);
|
||||
$report = $manager->get_report($userid, $attemptid, $currentgroup);
|
||||
if (!$report) {
|
||||
print_error('permissiondenied');
|
||||
}
|
||||
@@ -125,6 +127,8 @@ $PAGE->set_context($context);
|
||||
|
||||
echo $OUTPUT->header();
|
||||
|
||||
groups_print_activity_menu($cm, $PAGE->url);
|
||||
|
||||
echo $report->print();
|
||||
|
||||
echo $OUTPUT->footer();
|
||||
|
||||
@@ -22,13 +22,13 @@ Feature: Add H5P activity
|
||||
|
||||
@javascript
|
||||
Scenario: Add a h5pactivity activity to a course
|
||||
When I add a "H5P" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Awesome H5P package |
|
||||
| Description | H5P activity Description |
|
||||
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
And I wait until the page is ready
|
||||
Given the following "activity" exists:
|
||||
| activity | h5pactivity |
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| intro | H5P activity Description |
|
||||
| packagefilepath | h5p/tests/fixtures/ipsums.h5p |
|
||||
When I am on the "Awesome H5P package" "h5pactivity activity" page
|
||||
Then I should see "H5P activity Description"
|
||||
And I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
@@ -40,13 +40,13 @@ Feature: Add H5P activity
|
||||
|
||||
@javascript
|
||||
Scenario: Add a h5pactivity activity with download
|
||||
When I add a "H5P" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Awesome H5P package |
|
||||
| Description | Description |
|
||||
| Allow download | 1 |
|
||||
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
Given the following "activity" exists:
|
||||
| activity | h5pactivity |
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| displayoptions | 12 |
|
||||
| packagefilepath | h5p/tests/fixtures/ipsums.h5p |
|
||||
When I am on the "Awesome H5P package" "h5pactivity activity" page
|
||||
And I wait until the page is ready
|
||||
Then I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
@@ -57,13 +57,13 @@ Feature: Add H5P activity
|
||||
|
||||
@javascript
|
||||
Scenario: Add a h5pactivity activity with embed
|
||||
When I add a "H5P" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Awesome H5P package |
|
||||
| Description | Description |
|
||||
| Embed button | 1 |
|
||||
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
Given the following "activity" exists:
|
||||
| activity | h5pactivity |
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| displayoptions | 10 |
|
||||
| packagefilepath | h5p/tests/fixtures/ipsums.h5p |
|
||||
When I am on the "Awesome H5P package" "h5pactivity activity" page
|
||||
And I wait until the page is ready
|
||||
Then I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
@@ -74,13 +74,13 @@ Feature: Add H5P activity
|
||||
|
||||
@javascript
|
||||
Scenario: Add a h5pactivity activity with copyright
|
||||
When I add a "H5P" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Awesome H5P package |
|
||||
| Description | Description |
|
||||
| Copyright button | 1 |
|
||||
And I upload "h5p/tests/fixtures/guess-the-answer.h5p" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
Given the following "activity" exists:
|
||||
| activity | h5pactivity |
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| displayoptions | 6 |
|
||||
| packagefilepath | h5p/tests/fixtures/guess-the-answer.h5p |
|
||||
When I am on the "Awesome H5P package" "h5pactivity activity" page
|
||||
And I wait until the page is ready
|
||||
Then I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
@@ -91,13 +91,13 @@ Feature: Add H5P activity
|
||||
|
||||
@javascript
|
||||
Scenario: Add a h5pactivity activity with copyright in a content without copyright
|
||||
When I add a "H5P" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Awesome H5P package |
|
||||
| Description | Description |
|
||||
| Copyright button | 1 |
|
||||
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
Given the following "activity" exists:
|
||||
| activity | h5pactivity |
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| displayoptions | 6 |
|
||||
| packagefilepath | h5p/tests/fixtures/ipsums.h5p |
|
||||
When I am on the "Awesome H5P package" "h5pactivity activity" page
|
||||
And I wait until the page is ready
|
||||
Then I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
@@ -108,15 +108,13 @@ Feature: Add H5P activity
|
||||
|
||||
@javascript
|
||||
Scenario: Add a h5pactivity activity to a course with all display options enabled
|
||||
When I add a "H5P" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Awesome H5P package |
|
||||
| Description | Description |
|
||||
| Allow download | 1 |
|
||||
| Embed button | 1 |
|
||||
| Copyright button | 1 |
|
||||
And I upload "h5p/tests/fixtures/guess-the-answer.h5p" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
Given the following "activity" exists:
|
||||
| activity | h5pactivity |
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| displayoptions | 0 |
|
||||
| packagefilepath | h5p/tests/fixtures/guess-the-answer.h5p |
|
||||
When I am on the "Awesome H5P package" "h5pactivity activity" page
|
||||
And I wait until the page is ready
|
||||
Then I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
|
||||
@@ -19,15 +19,11 @@ Feature: Change grading options in an H5P activity
|
||||
And the following "permission overrides" exist:
|
||||
| capability | permission | role | contextlevel | reference |
|
||||
| moodle/h5p:updatelibraries | Allow | editingteacher | System | |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "H5P" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Awesome H5P package |
|
||||
| Description | Description |
|
||||
And I upload "h5p/tests/fixtures/multiple-choice-2-6.h5p" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
And I log out
|
||||
And the following "activity" exists:
|
||||
| activity | h5pactivity |
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| packagefilepath | h5p/tests/fixtures/multiple-choice-2-6.h5p |
|
||||
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
|
||||
And I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
@@ -44,7 +40,6 @@ Feature: Change grading options in an H5P activity
|
||||
And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element"
|
||||
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
|
||||
And I switch to the main frame
|
||||
And I log out
|
||||
|
||||
@javascript
|
||||
Scenario: Default grading is max attempt grade
|
||||
|
||||
@@ -17,16 +17,12 @@ Feature: View fill the blanks attempt report
|
||||
And the following config values are set as admin:
|
||||
# No HTML should appear even with formatstringstriptags disabled.
|
||||
| formatstringstriptags | 0 |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "H5P" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Awesome H5P package |
|
||||
| Description | Description |
|
||||
| Grading method | Average grade |
|
||||
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
And I log out
|
||||
And the following "activity" exists:
|
||||
| activity | h5pactivity |
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| grademethod | 2 |
|
||||
| packagefilepath | h5p/tests/fixtures/filltheblanks.h5p |
|
||||
|
||||
Scenario: View attempt in a fill the blanks content
|
||||
Given I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
|
||||
|
||||
@@ -17,16 +17,12 @@ Feature: View essay attempt report
|
||||
And the following config values are set as admin:
|
||||
# No HTML should appear even with formatstringstriptags disabled.
|
||||
| formatstringstriptags | 0 |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "H5P" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Awesome H5P package |
|
||||
| Description | Description |
|
||||
| Grading method | Average grade |
|
||||
And I upload "h5p/tests/fixtures/basic_essay.h5p" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
And I log out
|
||||
And the following "activity" exists:
|
||||
| activity | h5pactivity |
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| grademethod | 2 |
|
||||
| packagefilepath | h5p/tests/fixtures/basic_essay.h5p |
|
||||
|
||||
Scenario: View attempt essay content
|
||||
# Do an attempt.
|
||||
|
||||
@@ -19,25 +19,20 @@ Feature: Do a H5P attempt
|
||||
And the following "permission overrides" exist:
|
||||
| capability | permission | role | contextlevel | reference |
|
||||
| moodle/h5p:updatelibraries | Allow | editingteacher | System | |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "H5P" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Awesome H5P package |
|
||||
| Description | Description |
|
||||
And I upload "h5p/tests/fixtures/multiple-choice-2-6.h5p" file to "Package file" filemanager
|
||||
And the following "activity" exists:
|
||||
| activity | h5pactivity |
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| packagefilepath | h5p/tests/fixtures/multiple-choice-2-6.h5p |
|
||||
|
||||
@javascript
|
||||
Scenario: View an H5P as a teacher
|
||||
When I click on "Save and display" "button"
|
||||
When I am on the "Awesome H5P package" "h5pactivity activity" page logged in as teacher1
|
||||
And I wait until the page is ready
|
||||
Then I should see "This content is displayed in preview mode"
|
||||
|
||||
@javascript
|
||||
Scenario: To an attempts and check on course log report
|
||||
When I click on "Save and return to course" "button"
|
||||
And I log out
|
||||
Given I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
|
||||
When I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
|
||||
And I wait until the page is ready
|
||||
And I should not see "This content is displayed in preview mode"
|
||||
And I switch to "h5p-player" class iframe
|
||||
|
||||
@@ -55,6 +55,8 @@ class mod_h5pactivity_generator extends testing_module_generator {
|
||||
// Fill in optional values if not specified.
|
||||
if (!isset($record->packagefilepath)) {
|
||||
$record->packagefilepath = $CFG->dirroot.'/h5p/tests/fixtures/h5ptest.zip';
|
||||
} else if (strpos($record->packagefilepath, $CFG->dirroot) !== 0) {
|
||||
$record->packagefilepath = "{$CFG->dirroot}/{$record->packagefilepath}";
|
||||
}
|
||||
if (!isset($record->grade)) {
|
||||
$record->grade = 100;
|
||||
|
||||
@@ -14,14 +14,6 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* mod_h5pactivity manager tests
|
||||
*
|
||||
* @package mod_h5pactivity
|
||||
* @category test
|
||||
* @copyright 2020 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace mod_h5pactivity\local;
|
||||
use context_module;
|
||||
@@ -31,6 +23,7 @@ use stdClass;
|
||||
* Manager tests class for mod_h5pactivity.
|
||||
*
|
||||
* @package mod_h5pactivity
|
||||
* @covers \mod_h5pactivity\local\manager
|
||||
* @category test
|
||||
* @copyright 2020 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
@@ -627,7 +620,7 @@ class manager_testcase extends \advanced_testcase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Test static count_attempts of all active participants.
|
||||
* Test static test_get_active_users_join of all active participants.
|
||||
*
|
||||
* Most method scenarios are tested in test_count_attempts_all so we only
|
||||
* need to test the with $allpotentialusers true and false.
|
||||
@@ -692,6 +685,58 @@ class manager_testcase extends \advanced_testcase {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test active users joins returns appropriate results for groups
|
||||
*/
|
||||
public function test_get_active_users_join_groupmode(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course(['groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1]);
|
||||
|
||||
// Teacher/user one in group one.
|
||||
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
|
||||
$userone = $this->getDataGenerator()->create_and_enrol($course, 'student');
|
||||
|
||||
$groupone = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
|
||||
$this->getDataGenerator()->create_group_member(['groupid' => $groupone->id, 'userid' => $teacher->id]);
|
||||
$this->getDataGenerator()->create_group_member(['groupid' => $groupone->id, 'userid' => $userone->id]);
|
||||
|
||||
// User two in group two.
|
||||
$usertwo = $this->getDataGenerator()->create_and_enrol($course, 'student');
|
||||
|
||||
$grouptwo = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
|
||||
$this->getDataGenerator()->create_group_member(['groupid' => $grouptwo->id, 'userid' => $usertwo->id]);
|
||||
|
||||
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
|
||||
$manager = manager::create_from_instance($activity);
|
||||
|
||||
// Admin user can view all participants.
|
||||
$usersjoin = $manager->get_active_users_join(true, 0);
|
||||
$users = $DB->get_fieldset_sql("SELECT u.username FROM {user} u {$usersjoin->joins} WHERE {$usersjoin->wheres}",
|
||||
$usersjoin->params);
|
||||
|
||||
$this->assertEqualsCanonicalizing([$teacher->username, $userone->username, $usertwo->username], $users);
|
||||
|
||||
// Switch to teacher, who cannot view all participants.
|
||||
$this->setUser($teacher);
|
||||
|
||||
$usersjoin = $manager->get_active_users_join(true, 0);
|
||||
$users = $DB->get_fieldset_sql("SELECT u.username FROM {user} u {$usersjoin->joins} WHERE {$usersjoin->wheres}",
|
||||
$usersjoin->params);
|
||||
|
||||
$this->assertEmpty($users);
|
||||
|
||||
// Teacher can view participants inside group.
|
||||
$usersjoin = $manager->get_active_users_join(true, $groupone->id);
|
||||
$users = $DB->get_fieldset_sql("SELECT u.username FROM {user} u {$usersjoin->joins} WHERE {$usersjoin->wheres}",
|
||||
$usersjoin->params);
|
||||
|
||||
$this->assertEqualsCanonicalizing([$teacher->username, $userone->username], $users);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test static count_attempts.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
This files describes API changes in /mod/h5pactivity/*,
|
||||
information provided here is intended especially for developers.
|
||||
|
||||
=== 3.9.17 ===
|
||||
|
||||
* The following methods now accept an optional `$currentgroup` parameter in order to better support groups:
|
||||
- `\mod_h5pactivity\local\manager::get_active_users_join`
|
||||
- `\mod_h5pactivity\local\manager::get_report`
|
||||
- `\mod_h5pactivity\local\report\participants` constructor
|
||||
|
||||
=== 3.9.7 ===
|
||||
|
||||
* Added mod_h5pactivity\local\manager::get_active_users_join method to query all active
|
||||
|
||||
@@ -5,7 +5,6 @@ Feature: set label idnumber
|
||||
As a teacher
|
||||
I should create label activity and set an ID number
|
||||
|
||||
@javascript
|
||||
Scenario: label ID number input box should be shown.
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
@@ -18,12 +17,11 @@ Feature: set label idnumber
|
||||
| user | course | role |
|
||||
| teacher | C1 | editingteacher |
|
||||
| student | C1 | student |
|
||||
Given I log in as "teacher"
|
||||
And the following "activities" exist:
|
||||
| activity | course | section | intro | idnumber |
|
||||
| label | C1 | 1 | Label with ID number set | C1LABEL1 |
|
||||
When I log in as "teacher"
|
||||
And I am on "Test" course homepage with editing mode on
|
||||
When I add a "label" to section "1" and I fill the form with:
|
||||
| Label text | Label with ID number set |
|
||||
| Availability | Show on course page |
|
||||
| ID number | C1LABEL1 |
|
||||
Then "Label with ID number set" activity should be visible
|
||||
And I turn editing mode off
|
||||
And "Label with ID number set" activity should be visible
|
||||
@@ -32,11 +30,7 @@ Feature: set label idnumber
|
||||
And I am on "Test" course homepage
|
||||
And I should see "Label with ID number set"
|
||||
And I log out
|
||||
And I log in as "teacher"
|
||||
And I am on "Test" course homepage
|
||||
And I turn editing mode on
|
||||
And I open "Label with ID number set" actions menu
|
||||
And I click on "Edit settings" "link" in the "Label with ID number set" activity
|
||||
And I am on the "Label with ID number set" "label activity editing" page logged in as teacher
|
||||
And I expand all fieldsets
|
||||
And I should see "ID number" in the "Common module settings" "fieldset"
|
||||
And the field "ID number" matches value "C1LABEL1"
|
||||
|
||||
@@ -5,8 +5,7 @@ Feature: Check label visibility works
|
||||
As a teacher
|
||||
I should create label activity
|
||||
|
||||
@javascript
|
||||
Scenario: Hidden label activity should be show as hidden.
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Test | C1 | 0 |
|
||||
@@ -18,38 +17,26 @@ Feature: Check label visibility works
|
||||
| user | course | role |
|
||||
| teacher | C1 | editingteacher |
|
||||
| student | C1 | student |
|
||||
And the following "activities" exist:
|
||||
| activity | course | section | intro | idnumber | visible |
|
||||
| label | C1 | 1 | Swanky label | 1 | 1 |
|
||||
| label | C1 | 1 | Swanky label 2 | 2 | 0 |
|
||||
|
||||
Scenario: Hidden label activity should be show as hidden.
|
||||
Given I log in as "teacher"
|
||||
And I am on "Test" course homepage with editing mode on
|
||||
When I add a "label" to section "1" and I fill the form with:
|
||||
| Label text | Swanky label |
|
||||
| Availability | Hide from students |
|
||||
Then "Swanky label" activity should be hidden
|
||||
When I am on "Test" course homepage with editing mode on
|
||||
Then "Swanky label 2" activity should be hidden
|
||||
And I turn editing mode off
|
||||
And "Swanky label" activity should be hidden
|
||||
Then "Swanky label 2" activity should be hidden
|
||||
And I log out
|
||||
And I log in as "student"
|
||||
And I am on "Test" course homepage
|
||||
And I should not see "Swanky label"
|
||||
And I should not see "Swanky label 2"
|
||||
And I log out
|
||||
|
||||
@javascript
|
||||
Scenario: Visible label activity should be shown as visible.
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Test | C1 | 0 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher | Teacher | Frist | teacher1@example.com |
|
||||
| student | Student | First | student1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher | C1 | editingteacher |
|
||||
| student | C1 | student |
|
||||
Given I log in as "teacher"
|
||||
And I am on "Test" course homepage with editing mode on
|
||||
When I add a "label" to section "1" and I fill the form with:
|
||||
| Label text | Swanky label |
|
||||
| Availability | Show on course page |
|
||||
When I am on "Test" course homepage with editing mode on
|
||||
Then "Swanky label" activity should be visible
|
||||
And I log out
|
||||
And I log in as "student"
|
||||
@@ -59,23 +46,9 @@ Feature: Check label visibility works
|
||||
|
||||
@javascript
|
||||
Scenario: Teacher can not show label inside the hidden section
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Test | C1 | 0 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher | Teacher | Frist | teacher1@example.com |
|
||||
| student | Student | First | student1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher | C1 | editingteacher |
|
||||
| student | C1 | student |
|
||||
Given I log in as "teacher"
|
||||
And I am on "Test" course homepage with editing mode on
|
||||
When I add a "label" to section "1" and I fill the form with:
|
||||
| Label text | Swanky label |
|
||||
| Availability | Show on course page |
|
||||
And I hide section "1"
|
||||
When I hide section "1"
|
||||
Then "Swanky label" activity should be dimmed
|
||||
And I open "Swanky label" actions menu
|
||||
And "Swanky label" actions menu should not have "Show" item
|
||||
|
||||
@@ -56,7 +56,7 @@ class repost_crosssite_page implements renderable, templatable {
|
||||
*/
|
||||
public function __construct(string $url, array $post) {
|
||||
$this->params = array_map(function($k) use ($post) {
|
||||
return ["key" => $k, "value" => str_replace("\"", """, $post[$k])];
|
||||
return ["key" => $k, "value" => $post[$k]];
|
||||
}, array_keys($post));
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
@@ -37,9 +37,9 @@
|
||||
}
|
||||
|
||||
}}
|
||||
<form action="{{{url}}}" method="POST" id="autopostme">
|
||||
<form action="{{url}}" method="POST" id="autopostme">
|
||||
{{#params}}
|
||||
<input type="hidden" name="{{{key}}}" value="{{{value}}}">
|
||||
<input type="hidden" name="{{key}}" value="{{value}}">
|
||||
{{/params}}
|
||||
<input type="hidden" name="repost" value="true">
|
||||
</form>
|
||||
|
||||
@@ -5,12 +5,12 @@ Feature: Safe Exam Browser settings in quiz edit form
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage
|
||||
And I turn editing mode on
|
||||
And the following "activities" exist:
|
||||
| activity | course | section | name |
|
||||
| quiz | C1 | 1 | Quiz 1 |
|
||||
|
||||
Scenario: Quiz setting "Require the use of Safe Exam Browser" has all types, except "Use an existing template".
|
||||
When I add a "Quiz" to section "1"
|
||||
When I am on the "Quiz 1" "quiz activity editing" page logged in as admin
|
||||
And I expand all fieldsets
|
||||
And the "Require the use of Safe Exam Browser" select box should contain "Yes – Configure manually"
|
||||
And the "Require the use of Safe Exam Browser" select box should not contain "Yes – Use an existing template"
|
||||
@@ -22,7 +22,7 @@ Feature: Safe Exam Browser settings in quiz edit form
|
||||
Given the following "quizaccess_seb > seb templates" exist:
|
||||
| name |
|
||||
| Template 1 |
|
||||
When I add a "Quiz" to section "1"
|
||||
When I am on the "Quiz 1" "quiz activity editing" page logged in as admin
|
||||
And I expand all fieldsets
|
||||
And the "Require the use of Safe Exam Browser" select box should contain "Yes – Configure manually"
|
||||
And the "Require the use of Safe Exam Browser" select box should contain "Yes – Use an existing template"
|
||||
@@ -34,7 +34,7 @@ Feature: Safe Exam Browser settings in quiz edit form
|
||||
Given the following "quizaccess_seb > seb templates" exist:
|
||||
| name |
|
||||
| Template 1 |
|
||||
When I add a "Quiz" to section "1"
|
||||
When I am on the "Quiz 1" "quiz activity editing" page logged in as admin
|
||||
And I expand all fieldsets
|
||||
And I set the field "Require the use of Safe Exam Browser" to "No"
|
||||
Then I should not see "Upload Safe Exam Browser config file"
|
||||
@@ -66,7 +66,7 @@ Feature: Safe Exam Browser settings in quiz edit form
|
||||
Given the following "quizaccess_seb > seb templates" exist:
|
||||
| name |
|
||||
| Template 1 |
|
||||
When I add a "Quiz" to section "1"
|
||||
And I am on the "Quiz 1" "quiz activity editing" page logged in as admin
|
||||
And I expand all fieldsets
|
||||
And I set the field "Require the use of Safe Exam Browser" to "Yes – Use SEB client config"
|
||||
Then I should see "Show Safe Exam Browser download button"
|
||||
@@ -98,7 +98,7 @@ Feature: Safe Exam Browser settings in quiz edit form
|
||||
Given the following "quizaccess_seb > seb templates" exist:
|
||||
| name |
|
||||
| Template 1 |
|
||||
When I add a "Quiz" to section "1"
|
||||
And I am on the "Quiz 1" "quiz activity editing" page logged in as admin
|
||||
And I expand all fieldsets
|
||||
And I set the field "Require the use of Safe Exam Browser" to "Yes – Upload my own config"
|
||||
Then I should see "Upload Safe Exam Browser config file"
|
||||
@@ -130,7 +130,7 @@ Feature: Safe Exam Browser settings in quiz edit form
|
||||
Given the following "quizaccess_seb > seb templates" exist:
|
||||
| name |
|
||||
| Template 1 |
|
||||
When I add a "Quiz" to section "1"
|
||||
And I am on the "Quiz 1" "quiz activity editing" page logged in as admin
|
||||
And I expand all fieldsets
|
||||
And I set the field "Require the use of Safe Exam Browser" to "Yes – Use an existing template"
|
||||
Then I should see "Safe Exam Browser config template"
|
||||
@@ -164,7 +164,7 @@ Feature: Safe Exam Browser settings in quiz edit form
|
||||
Given the following "quizaccess_seb > seb templates" exist:
|
||||
| name |
|
||||
| Template 1 |
|
||||
When I add a "Quiz" to section "1"
|
||||
And I am on the "Quiz 1" "quiz activity editing" page logged in as admin
|
||||
And I expand all fieldsets
|
||||
And I set the field "Require the use of Safe Exam Browser" to "Yes – Configure manually"
|
||||
Then I should see "Show Safe Exam Browser download button"
|
||||
|
||||
+15
-9
@@ -2478,19 +2478,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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -970,8 +970,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"
|
||||
|
||||
@@ -26,10 +26,7 @@ Feature: Adding questions to a quiz from the question bank
|
||||
| Test questions | essay | question 02 name | teacher1 | Question 02 text | qidnum |
|
||||
|
||||
Scenario: The questions can be filtered by tag
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
When I navigate to "Question bank > Questions" in current page administration
|
||||
And I choose "Edit question" action for "question 01 name" in the question bank
|
||||
Given I am on the "question 01 name" "core_question > edit" page logged in as "teacher1"
|
||||
And I set the following fields to these values:
|
||||
| Tags | foo |
|
||||
And I press "id_submitbutton"
|
||||
@@ -37,7 +34,7 @@ Feature: Adding questions to a quiz from the question bank
|
||||
And I set the following fields to these values:
|
||||
| Tags | bar |
|
||||
And I press "id_submitbutton"
|
||||
And I am on the "Quiz 1" "mod_quiz > Edit" page
|
||||
When I am on the "Quiz 1" "mod_quiz > Edit" page
|
||||
And I open the "last" add to quiz menu
|
||||
And I follow "from question bank"
|
||||
Then I should see "foo" in the "question 01 name" "table_row"
|
||||
|
||||
@@ -18,39 +18,39 @@ Feature: Settings form fields disabled if not required
|
||||
| teacher | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
| student2 | C1 | student |
|
||||
And I log in as "teacher"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And the following "activities" exist:
|
||||
| activity | course | section | name |
|
||||
| quiz | C1 | 1 | Test quiz 1 |
|
||||
|
||||
@javascript
|
||||
Scenario: Depending on the number of attempts, different form fields are disabled.
|
||||
When I add a "Quiz" to section "1"
|
||||
When I am on the "Test quiz 1" "quiz activity editing" page logged in as teacher
|
||||
And I expand all fieldsets
|
||||
And I set the field "Name" to "Test quiz"
|
||||
And I set the field "Attempts allowed" to "1"
|
||||
Then the "Grading method" "field" should be disabled
|
||||
And the "Each attempt builds on the last" "field" should be disabled
|
||||
And the "id_delay1_enabled" "field" should be disabled
|
||||
And the "id_delay2_enabled" "field" should be disabled
|
||||
|
||||
When I set the field "Attempts allowed" to "2"
|
||||
Then the "Grading method" "field" should be enabled
|
||||
And I set the field "Attempts allowed" to "2"
|
||||
And the "Grading method" "field" should be enabled
|
||||
And the "Each attempt builds on the last" "field" should be enabled
|
||||
And the "id_delay1_enabled" "field" should be enabled
|
||||
And the "id_delay2_enabled" "field" should be disabled
|
||||
|
||||
When I set the field "Attempts allowed" to "3"
|
||||
Then the "Grading method" "field" should be enabled
|
||||
And I set the field "Attempts allowed" to "3"
|
||||
And the "Grading method" "field" should be enabled
|
||||
And the "Each attempt builds on the last" "field" should be enabled
|
||||
And the "id_delay1_enabled" "field" should be enabled
|
||||
And the "id_delay2_enabled" "field" should be enabled
|
||||
|
||||
When I set the field "Attempts allowed" to "Unlimited"
|
||||
Then the "Grading method" "field" should be enabled
|
||||
And I set the field "Attempts allowed" to "Unlimited"
|
||||
And the "Grading method" "field" should be enabled
|
||||
And the "Each attempt builds on the last" "field" should be enabled
|
||||
# And the "id_delay1_enabled" "field" should be enabled
|
||||
# And the "id_delay2_enabled" "field" should be enabled
|
||||
|
||||
When I press "Save and display"
|
||||
And I press "Save and display"
|
||||
And I navigate to "User overrides" in current page administration
|
||||
And I press "Add user override"
|
||||
And I set the following fields to these values:
|
||||
@@ -61,12 +61,12 @@ Feature: Settings form fields disabled if not required
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I expand all fieldsets
|
||||
And I set the field "Attempts allowed" to "1"
|
||||
Then the "Grading method" "field" should be enabled
|
||||
And the "Grading method" "field" should be enabled
|
||||
And the "Each attempt builds on the last" "field" should be enabled
|
||||
And the "id_delay1_enabled" "field" should be enabled
|
||||
And the "id_delay2_enabled" "field" should be enabled
|
||||
|
||||
When I press "Save and display"
|
||||
And I press "Save and display"
|
||||
And I navigate to "User overrides" in current page administration
|
||||
And I click on "Edit" "link" in the "region-main" "region"
|
||||
And I set the field "Attempts allowed" to "2"
|
||||
@@ -75,12 +75,12 @@ Feature: Settings form fields disabled if not required
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I expand all fieldsets
|
||||
And I set the field "Attempts allowed" to "1"
|
||||
Then the "Grading method" "field" should be enabled
|
||||
And the "Grading method" "field" should be enabled
|
||||
And the "Each attempt builds on the last" "field" should be enabled
|
||||
And the "id_delay1_enabled" "field" should be enabled
|
||||
And the "id_delay2_enabled" "field" should be disabled
|
||||
|
||||
When I press "Save and display"
|
||||
And I press "Save and display"
|
||||
And I navigate to "User overrides" in current page administration
|
||||
And I press "Add user override"
|
||||
And I set the following fields to these values:
|
||||
@@ -91,28 +91,31 @@ Feature: Settings form fields disabled if not required
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I expand all fieldsets
|
||||
And I set the field "Attempts allowed" to "1"
|
||||
Then the "Grading method" "field" should be enabled
|
||||
And the "Grading method" "field" should be enabled
|
||||
And the "Each attempt builds on the last" "field" should be enabled
|
||||
And the "id_delay1_enabled" "field" should be enabled
|
||||
And the "id_delay2_enabled" "field" should be enabled
|
||||
|
||||
@javascript
|
||||
Scenario: Depending on whether there is a close date, some review options are disabled.
|
||||
When I add a "Quiz" to section "1"
|
||||
When I log in as "teacher"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Quiz" to section "1"
|
||||
And I expand all fieldsets
|
||||
And I set the field "Name" to "Test quiz"
|
||||
Then the "id_attemptclosed" "checkbox" should be disabled
|
||||
Then the "id_correctnessclosed" "checkbox" should be disabled
|
||||
Then the "id_marksclosed" "checkbox" should be disabled
|
||||
Then the "id_specificfeedbackclosed" "checkbox" should be disabled
|
||||
Then the "id_generalfeedbackclosed" "checkbox" should be disabled
|
||||
Then the "id_rightanswerclosed" "checkbox" should be disabled
|
||||
Then the "id_overallfeedbackclosed" "checkbox" should be disabled
|
||||
And the "id_correctnessclosed" "checkbox" should be disabled
|
||||
And the "id_marksclosed" "checkbox" should be disabled
|
||||
And the "id_specificfeedbackclosed" "checkbox" should be disabled
|
||||
And the "id_generalfeedbackclosed" "checkbox" should be disabled
|
||||
And the "id_rightanswerclosed" "checkbox" should be disabled
|
||||
And the "id_overallfeedbackclosed" "checkbox" should be disabled
|
||||
And I set the field "id_timeclose_enabled" to "1"
|
||||
Then the "id_attemptclosed" "checkbox" should be enabled
|
||||
Then the "id_correctnessclosed" "checkbox" should be enabled
|
||||
Then the "id_marksclosed" "checkbox" should be enabled
|
||||
Then the "id_specificfeedbackclosed" "checkbox" should be enabled
|
||||
Then the "id_generalfeedbackclosed" "checkbox" should be enabled
|
||||
Then the "id_rightanswerclosed" "checkbox" should be enabled
|
||||
Then the "id_overallfeedbackclosed" "checkbox" should be enabled
|
||||
And the "id_attemptclosed" "checkbox" should be enabled
|
||||
And the "id_correctnessclosed" "checkbox" should be enabled
|
||||
And the "id_marksclosed" "checkbox" should be enabled
|
||||
And the "id_specificfeedbackclosed" "checkbox" should be enabled
|
||||
And the "id_generalfeedbackclosed" "checkbox" should be enabled
|
||||
And the "id_rightanswerclosed" "checkbox" should be enabled
|
||||
And the "id_overallfeedbackclosed" "checkbox" should be enabled
|
||||
And I should not see "Repaginate now"
|
||||
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Quiz module external functions tests.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @category external
|
||||
* @copyright 2016 Juan Leyva <juan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @since Moodle 3.1
|
||||
*/
|
||||
|
||||
namespace mod_quiz\external;
|
||||
|
||||
use externallib_advanced_testcase;
|
||||
use mod_quiz_external;
|
||||
use mod_quiz_display_options;
|
||||
use question_usage_by_activity;
|
||||
use quiz;
|
||||
use quiz_attempt;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
|
||||
|
||||
/**
|
||||
* Silly class to access mod_quiz_external internal methods.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2016 Juan Leyva <juan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @since Moodle 3.1
|
||||
*/
|
||||
class testable_mod_quiz_external extends mod_quiz_external {
|
||||
|
||||
/**
|
||||
* Public accessor.
|
||||
*
|
||||
* @param array $params Array of parameters including the attemptid and preflight data
|
||||
* @param bool $checkaccessrules whether to check the quiz access rules or not
|
||||
* @param bool $failifoverdue whether to return error if the attempt is overdue
|
||||
* @return array containing the attempt object and access messages
|
||||
*/
|
||||
public static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) {
|
||||
return parent::validate_attempt($params, $checkaccessrules, $failifoverdue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public accessor.
|
||||
*
|
||||
* @param array $params Array of parameters including the attemptid
|
||||
* @return array containing the attempt object and display options
|
||||
*/
|
||||
public static function validate_attempt_review($params) {
|
||||
return parent::validate_attempt_review($params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quiz module external functions tests
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @category external
|
||||
* @copyright 2016 Juan Leyva <juan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @since Moodle 3.1
|
||||
*/
|
||||
class external_test extends externallib_advanced_testcase {
|
||||
|
||||
/**
|
||||
* Set up for every test
|
||||
*/
|
||||
public function setUp(): void {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Setup test data.
|
||||
$this->course = $this->getDataGenerator()->create_course();
|
||||
$this->quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $this->course->id));
|
||||
$this->context = \context_module::instance($this->quiz->cmid);
|
||||
$this->cm = get_coursemodule_from_instance('quiz', $this->quiz->id);
|
||||
|
||||
// Create users.
|
||||
$this->student = self::getDataGenerator()->create_user();
|
||||
$this->teacher = self::getDataGenerator()->create_user();
|
||||
|
||||
// Users enrolments.
|
||||
$this->studentrole = $DB->get_record('role', array('shortname' => 'student'));
|
||||
$this->teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
|
||||
// Allow student to receive messages.
|
||||
$coursecontext = \context_course::instance($this->course->id);
|
||||
assign_capability('mod/quiz:emailnotifysubmission', CAP_ALLOW, $this->teacherrole->id, $coursecontext, true);
|
||||
|
||||
$this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual');
|
||||
$this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a sequential navigation quiz is not allowing to see questions in advance except if reviewing
|
||||
*/
|
||||
public function test_sequential_navigation_view_attempt() {
|
||||
// Test user with full capabilities.
|
||||
$quiz = $this->prepare_sequential_quiz();
|
||||
$attemptobj = $this->create_quiz_attempt_object($quiz);
|
||||
$this->setUser($this->student);
|
||||
// Check out of sequence access for view.
|
||||
$this->assertNotEmpty(mod_quiz_external::view_attempt($attemptobj->get_attemptid(), 0, []));
|
||||
try {
|
||||
mod_quiz_external::view_attempt($attemptobj->get_attemptid(), 3, []);
|
||||
$this->fail('Exception expected due to out of sequence access.');
|
||||
} catch (\moodle_exception $e) {
|
||||
$this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a sequential navigation quiz is not allowing to see questions in advance for a student
|
||||
*/
|
||||
public function test_sequential_navigation_attempt_summary() {
|
||||
// Test user with full capabilities.
|
||||
$quiz = $this->prepare_sequential_quiz();
|
||||
$attemptobj = $this->create_quiz_attempt_object($quiz);
|
||||
$this->setUser($this->student);
|
||||
// Check that we do not return other questions than the one currently viewed.
|
||||
$result = mod_quiz_external::get_attempt_summary($attemptobj->get_attemptid());
|
||||
$this->assertCount(1, $result['questions']);
|
||||
$this->assertStringContainsString('Question (1)', $result['questions'][0]['html']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a sequential navigation quiz is not allowing to see questions in advance for student
|
||||
*/
|
||||
public function test_sequential_navigation_get_attempt_data() {
|
||||
// Test user with full capabilities.
|
||||
$quiz = $this->prepare_sequential_quiz();
|
||||
$attemptobj = $this->create_quiz_attempt_object($quiz);
|
||||
$this->setUser($this->student);
|
||||
// Test invalid instance id.
|
||||
try {
|
||||
mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 2);
|
||||
$this->fail('Exception expected due to out of sequence access.');
|
||||
} catch (\moodle_exception $e) {
|
||||
$this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage());
|
||||
}
|
||||
// Now we moved to page 1, we should see page 2 and 1 but not 0 or 3.
|
||||
$attemptobj->set_currentpage(1);
|
||||
// Test invalid instance id.
|
||||
try {
|
||||
mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 0);
|
||||
$this->fail('Exception expected due to out of sequence access.');
|
||||
} catch (\moodle_exception $e) {
|
||||
$this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 3);
|
||||
$this->fail('Exception expected due to out of sequence access.');
|
||||
} catch (\moodle_exception $e) {
|
||||
$this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage());
|
||||
}
|
||||
|
||||
// Now we can see page 1.
|
||||
$result = mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 1);
|
||||
$this->assertCount(1, $result['questions']);
|
||||
$this->assertStringContainsString('Question (2)', $result['questions'][0]['html']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare quiz for sequential navigation tests
|
||||
*
|
||||
* @return quiz
|
||||
*/
|
||||
private function prepare_sequential_quiz() {
|
||||
// Create a new quiz with 5 questions and one attempt started.
|
||||
// Create a new quiz with attempts.
|
||||
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
|
||||
$data = [
|
||||
'course' => $this->course->id,
|
||||
'sumgrades' => 2,
|
||||
'preferredbehaviour' => 'deferredfeedback',
|
||||
'navmethod' => QUIZ_NAVMETHOD_SEQ
|
||||
];
|
||||
$quiz = $quizgenerator->create_instance($data);
|
||||
|
||||
// Now generate the questions.
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$cat = $questiongenerator->create_question_category();
|
||||
for ($pageindex = 1; $pageindex <= 5; $pageindex++) {
|
||||
$question = $questiongenerator->create_question('truefalse', null, [
|
||||
'category' => $cat->id,
|
||||
'questiontext' => ['text' => "Question ($pageindex)"]
|
||||
]);
|
||||
quiz_add_quiz_question($question->id, $quiz, $pageindex);
|
||||
}
|
||||
|
||||
$quizobj = quiz::create($quiz->id, $this->student->id);
|
||||
// Set grade to pass.
|
||||
$item = \grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod',
|
||||
'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null));
|
||||
$item->gradepass = 80;
|
||||
$item->update();
|
||||
return $quizobj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create question attempt
|
||||
*
|
||||
* @param quiz $quizobj
|
||||
* @param int|null $userid
|
||||
* @param bool|null $ispreview
|
||||
* @return quiz_attempt
|
||||
* @throws \moodle_exception
|
||||
*/
|
||||
private function create_quiz_attempt_object($quizobj, $userid = null, $ispreview = false) {
|
||||
global $USER;
|
||||
$timenow = time();
|
||||
// Now, do one attempt.
|
||||
$quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
|
||||
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
|
||||
$attemptnumber = 1;
|
||||
if (!empty($USER->id)) {
|
||||
$attemptnumber = count(quiz_get_user_attempts($quizobj->get_quizid(), $USER->id)) + 1;
|
||||
}
|
||||
$attempt = quiz_create_attempt($quizobj, $attemptnumber, false, $timenow, $ispreview, $userid ?? $this->student->id);
|
||||
quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow);
|
||||
quiz_attempt_save_started($quizobj, $quba, $attempt);
|
||||
$attemptobj = quiz_attempt::create($attempt->id);
|
||||
return $attemptobj;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
This files describes API changes in the quiz code.
|
||||
|
||||
=== 3.9 ===
|
||||
|
||||
* If a quiz is sequential the external functions mod_quiz_external::get_attempt_data, mod_quiz_external::get_attempt_summary will not return any data
|
||||
related to out of sequence question except while reviewing the quiz (MDL-75210).
|
||||
|
||||
=== 3.7 ===
|
||||
|
||||
* Quiz_cron() has been removed. Sub-plugins should implemented scheduled tasks, however legacy cron in subplugins are
|
||||
|
||||
@@ -39,7 +39,8 @@ class mod_resource_generator extends testing_module_generator {
|
||||
* text file.
|
||||
*
|
||||
* @param array|stdClass $record data for module being generated. Requires 'course' key
|
||||
* (an id or the full object). Also can have any fields from add module form.
|
||||
* (an id or the full object). Also can have any fields from add module form, and a
|
||||
* 'defaultfilename' to set the name of the file created if no draft ID is supplied.
|
||||
* @param null|array $options general options for course module. Since 2.6 it is
|
||||
* possible to omit this argument by merging options into $record
|
||||
* @return stdClass record from module-defined table with additional field
|
||||
@@ -72,16 +73,17 @@ class mod_resource_generator extends testing_module_generator {
|
||||
throw new coding_exception('resource generator requires a current user');
|
||||
}
|
||||
$usercontext = context_user::instance($USER->id);
|
||||
$filename = $record->defaultfilename ?? 'resource' . ($this->instancecount + 1) . '.txt';
|
||||
|
||||
// Pick a random context id for specified user.
|
||||
$record->files = file_get_unused_draft_itemid();
|
||||
|
||||
// Add actual file there.
|
||||
$filerecord = array('component' => 'user', 'filearea' => 'draft',
|
||||
$filerecord = ['component' => 'user', 'filearea' => 'draft',
|
||||
'contextid' => $usercontext->id, 'itemid' => $record->files,
|
||||
'filename' => 'resource' . ($this->instancecount+1) . '.txt', 'filepath' => '/');
|
||||
'filename' => $filename, 'filepath' => '/'];
|
||||
$fs = get_file_storage();
|
||||
$fs->create_file_from_string($filerecord, 'Test resource ' . ($this->instancecount+1) . ' file');
|
||||
$fs->create_file_from_string($filerecord, 'Test resource ' . $filename . ' file');
|
||||
}
|
||||
|
||||
// Do work to actually add the instance.
|
||||
|
||||
@@ -70,6 +70,21 @@ class mod_resource_generator_testcase extends advanced_testcase {
|
||||
// Check that generated resource module contains a file.
|
||||
$fs = get_file_storage();
|
||||
$files = $fs->get_area_files($context->id, 'mod_resource', 'content', false, '', false);
|
||||
$this->assertEquals(1, count($files));
|
||||
$file = array_values($files)[0];
|
||||
$this->assertCount(1, $files);
|
||||
$this->assertEquals('resource3.txt', $file->get_filename());
|
||||
$this->assertEquals('Test resource resource3.txt file', $file->get_content());
|
||||
|
||||
// Create a new resource specifying the file name.
|
||||
$resource = $generator->create_instance(['course' => $SITE->id, 'defaultfilename' => 'myfile.pdf']);
|
||||
|
||||
// Check that generated resource module contains a file with the specified name.
|
||||
$cm = get_coursemodule_from_instance('resource', $resource->id);
|
||||
$context = \context_module::instance($cm->id);
|
||||
$files = $fs->get_area_files($context->id, 'mod_resource', 'content', false, '', false);
|
||||
$file = array_values($files)[0];
|
||||
$this->assertCount(1, $files);
|
||||
$this->assertEquals('myfile.pdf', $file->get_filename());
|
||||
$this->assertEquals('Test resource myfile.pdf file', $file->get_content());
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -496,7 +496,7 @@ function scorm_user_complete($course, $user, $mod, $scorm) {
|
||||
$report .= html_writer::start_tag('li').html_writer::start_tag('ul', array('class' => $liststyle));
|
||||
foreach ($usertrack as $element => $value) {
|
||||
if (substr($element, 0, 3) == 'cmi') {
|
||||
$report .= html_writer::tag('li', $element.' => '.s($value));
|
||||
$report .= html_writer::tag('li', s($element) . ' => ' . s($value));
|
||||
}
|
||||
}
|
||||
$report .= html_writer::end_tag('ul').html_writer::end_tag('li');
|
||||
|
||||
@@ -144,9 +144,9 @@ foreach ($trackdata as $element => $value) {
|
||||
}
|
||||
|
||||
if (empty($string) || $table->is_downloading()) {
|
||||
$row[] = $element;
|
||||
$row[] = s($element);
|
||||
} else {
|
||||
$row[] = $element.$OUTPUT->help_icon($string, 'scorm');
|
||||
$row[] = s($element) . $OUTPUT->help_icon($string, 'scorm');
|
||||
}
|
||||
if (strpos($element, '_time') === false) {
|
||||
$row[] = s($value);
|
||||
|
||||
@@ -16,22 +16,9 @@ Feature: Scorm multi-sco review mode.
|
||||
|
||||
@javascript
|
||||
Scenario: Test review mode with a single sco completion.
|
||||
When I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Enable completion tracking | Yes |
|
||||
And I press "Save and display"
|
||||
And I add a "SCORM package" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Basic Multi-sco SCORM package |
|
||||
| Description | Description |
|
||||
| Force new attempt | No |
|
||||
And I set the field "Completed" to "1"
|
||||
And I upload "mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12.zip" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
And I should see "Basic Multi-sco SCORM package"
|
||||
And I log out
|
||||
When the following "activities" exist:
|
||||
| activity | course | name | packagefilepath | forcenewattempt |
|
||||
| scorm | C1 | Basic Multi-sco SCORM package | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12.zip | 0 |
|
||||
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page logged in as student1
|
||||
And I should see "Normal"
|
||||
And I press "Enter"
|
||||
@@ -48,24 +35,9 @@ Feature: Scorm multi-sco review mode.
|
||||
|
||||
@javascript
|
||||
Scenario: Test review mode with all scos completed.
|
||||
When I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Enable completion tracking | Yes |
|
||||
And I press "Save and display"
|
||||
And I add a "SCORM package" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | ADV Multi-sco SCORM package |
|
||||
| Description | Description |
|
||||
| Completion tracking | Show activity as complete when conditions are met |
|
||||
| Require all scos to return completion status | 1 |
|
||||
| Force new attempt | No |
|
||||
And I set the field "Completed" to "1"
|
||||
And I upload "mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12.zip" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
And I should see "ADV Multi-sco SCORM package"
|
||||
And I log out
|
||||
When the following "activities" exist:
|
||||
| activity | course | name | packagefilepath | forcenewattempt |
|
||||
| scorm | C1 | ADV Multi-sco SCORM package | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12.zip | 0 |
|
||||
And I am on the "ADV Multi-sco SCORM package" "scorm activity" page logged in as student1
|
||||
And I should see "Normal"
|
||||
And I press "Enter"
|
||||
@@ -167,17 +139,9 @@ Feature: Scorm multi-sco review mode.
|
||||
|
||||
@javascript
|
||||
Scenario: Test force completed set to Always.
|
||||
When I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "SCORM package" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Basic Multi-sco SCORM package |
|
||||
| Description | Description |
|
||||
| Force new attempt | Always |
|
||||
And I upload "mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
And I should see "Basic Multi-sco SCORM package"
|
||||
And I log out
|
||||
When the following "activities" exist:
|
||||
| activity | course | name | packagefilepath | forcenewattempt |
|
||||
| scorm | C1 | Basic Multi-sco SCORM package | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip | 2 |
|
||||
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page logged in as student1
|
||||
And I should see "Normal"
|
||||
And I press "Enter"
|
||||
@@ -197,17 +161,9 @@ Feature: Scorm multi-sco review mode.
|
||||
|
||||
@javascript
|
||||
Scenario: Test force completed set to when previous complete/passed/failed.
|
||||
When I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "SCORM package" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Basic Multi-sco SCORM package |
|
||||
| Description | Description |
|
||||
| Force new attempt | When previous attempt completed, passed or failed |
|
||||
And I upload "mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
And I should see "Basic Multi-sco SCORM package"
|
||||
And I log out
|
||||
When the following "activities" exist:
|
||||
| activity | course | name | packagefilepath | forcenewattempt |
|
||||
| scorm | C1 | Basic Multi-sco SCORM package | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip | 1 |
|
||||
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page logged in as student1
|
||||
And I should see "Normal"
|
||||
And I press "Enter"
|
||||
@@ -272,18 +228,9 @@ Feature: Scorm multi-sco review mode.
|
||||
|
||||
@javascript
|
||||
Scenario: Test force completed set to Always and student skipview
|
||||
When I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "SCORM package" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Basic Multi-sco SCORM package |
|
||||
| Description | Description |
|
||||
| Force new attempt | Always |
|
||||
| Student skip content structure page | Always |
|
||||
And I upload "mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
And I should see "Basic Multi-sco SCORM package"
|
||||
And I log out
|
||||
When the following "activities" exist:
|
||||
| activity | course | name | packagefilepath | forcenewattempt | skipview |
|
||||
| scorm | C1 | Basic Multi-sco SCORM package | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip | 2 | 2 |
|
||||
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page logged in as student1
|
||||
And I switch to "scorm_object" iframe
|
||||
And I should see "Play of the game"
|
||||
@@ -298,18 +245,9 @@ Feature: Scorm multi-sco review mode.
|
||||
|
||||
@javascript
|
||||
Scenario: Test force completed set to when previous complete/passed/failed.
|
||||
When I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "SCORM package" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Basic Multi-sco SCORM package |
|
||||
| Description | Description |
|
||||
| Force new attempt | When previous attempt completed, passed or failed |
|
||||
| Student skip content structure page | Always |
|
||||
And I upload "mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
And I should see "Basic Multi-sco SCORM package"
|
||||
And I log out
|
||||
When the following "activities" exist:
|
||||
| activity | course | name | packagefilepath | forcenewattempt | skipview |
|
||||
| scorm | C1 | Basic Multi-sco SCORM package | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip | 1 | 2 |
|
||||
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page logged in as student1
|
||||
And I switch to "scorm_object" iframe
|
||||
And I should see "Play of the game"
|
||||
|
||||
@@ -19,16 +19,11 @@ Feature: Confirm progress gets saved on unload events
|
||||
|
||||
@javascript
|
||||
Scenario: Test progress gets saved correctly when the user navigates away from the scorm activity
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "SCORM package" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Runtime Basic Calls SCORM 2004 3rd Edition package |
|
||||
| Description | Description |
|
||||
And I upload "mod/scorm/tests/packages/RuntimeBasicCalls_SCORM20043rdEdition.zip" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
And I should see "Runtime Basic Calls SCORM 2004 3rd Edition package"
|
||||
And I log out
|
||||
Given the following "activity" exists:
|
||||
| activity | scorm |
|
||||
| course | C1 |
|
||||
| name | Runtime Basic Calls SCORM 2004 3rd Edition package |
|
||||
| packagefilepath | mod/scorm/tests/packages/RuntimeBasicCalls_SCORM20043rdEdition.zip |
|
||||
When I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Runtime Basic Calls SCORM 2004 3rd Edition package"
|
||||
|
||||
@@ -14,6 +14,13 @@ Feature: Scorm with no calendar capabilites
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "activity" exists:
|
||||
| activity | scorm |
|
||||
| course | C1 |
|
||||
| name | Test scorm name |
|
||||
| timeopen | ##first day of January 2017## |
|
||||
| timeclose | ##first day of February 2017## |
|
||||
| packagefilepath | mod/scorm/tests/packages/singlesco_scorm12.zip |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Users > Permissions" in current page administration
|
||||
@@ -24,25 +31,8 @@ Feature: Scorm with no calendar capabilites
|
||||
|
||||
@javascript @_file_upload @_switch_iframe
|
||||
Scenario: Editing a scorm activity without calendar permission
|
||||
Given I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
When I add a "SCORM package" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Test scorm name |
|
||||
| Description | Test scorm description |
|
||||
| id_timeopen_enabled | 1 |
|
||||
| id_timeopen_day | 1 |
|
||||
| id_timeopen_month | 1 |
|
||||
| id_timeopen_year | 2017 |
|
||||
| id_timeclose_enabled | 1 |
|
||||
| id_timeclose_day | 1 |
|
||||
| id_timeclose_month | 2 |
|
||||
| id_timeclose_year | 2017 |
|
||||
And I upload "mod/scorm/tests/packages/singlesco_scorm12.zip" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
And I log out
|
||||
When I am on the "Test scorm name" "scorm activity editing" page logged in as teacher1
|
||||
And I set the following fields to these values:
|
||||
Given I am on the "Test scorm name" "scorm activity editing" page logged in as teacher1
|
||||
When I set the following fields to these values:
|
||||
| id_timeopen_year | 2018 |
|
||||
| id_timeclose_year | 2018 |
|
||||
And I press "Save and return to course"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user