Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d369112db4 | |||
| 62d603a054 | |||
| 4cb39164fa | |||
| d5e8561011 | |||
| 944455bcce | |||
| ee888615ea | |||
| 3f58d35337 | |||
| 6983a17fc0 | |||
| ce2d88a951 | |||
| 5a4f73d144 | |||
| 1c8396e2b5 | |||
| 5668055015 | |||
| 405cd4fb35 | |||
| 2f4b6bd2d7 | |||
| acf70635c6 | |||
| e3b1e103bd | |||
| 8d1867cdcc | |||
| 2cfa9817b1 | |||
| 641b8c31ee | |||
| 6ce1d39f4e | |||
| 9d4fb0a3ad | |||
| 4110054b51 | |||
| dd4d110c9b | |||
| 017f8ff6bf | |||
| 1ff7beef8f | |||
| 35783784b3 | |||
| 194ca4970a | |||
| d4f3852dd2 | |||
| 3fbbc7a86b | |||
| 7cff4e8bc1 | |||
| 827803ea3d | |||
| ccb639002f | |||
| df62f09a4c | |||
| 7c0a982958 | |||
| 940e4da0fa | |||
| ae531ae852 | |||
| d51f307ffa | |||
| a4018fbcdd | |||
| 2dd34c5ae4 |
@@ -3539,6 +3539,8 @@
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_max_input_vars" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_xmlrpc_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="4.0" requires="3.6">
|
||||
@@ -3729,6 +3731,200 @@
|
||||
<ON_CHECK message="iscustomadminwarnings" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_xmlrpc_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="4.1" requires="3.9">
|
||||
<UNICODE level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unicoderequired" />
|
||||
</FEEDBACK>
|
||||
</UNICODE>
|
||||
<DATABASE level="required">
|
||||
<VENDOR name="mariadb" version="10.4" />
|
||||
<VENDOR name="mysql" version="5.7" />
|
||||
<VENDOR name="postgres" version="12" />
|
||||
<VENDOR name="mssql" version="14.0" />
|
||||
<VENDOR name="oracle" version="19" />
|
||||
</DATABASE>
|
||||
<PHP version="7.4.0" level="required">
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="pcreunicodewarning" />
|
||||
</FEEDBACK>
|
||||
</PCREUNICODE>
|
||||
<PHP_EXTENSIONS>
|
||||
<PHP_EXTENSION name="iconv" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="iconvrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="mbstring" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="mbstringrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="curl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="curlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="openssl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="opensslrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="tokenizer" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="tokenizerrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xmlrpc" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="xmlrpcrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="soap" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="soaprecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="ctype" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ctyperequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zip" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ziprequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zlib" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="gd" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="gdrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="simplexml" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="simplexmlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="spl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="splrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="pcre" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="dom" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xml" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xmlreader" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="intl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="intlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="json" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="hash" level="required"/>
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="optional"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="settingmemorylimit" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="file_uploads" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="settingfileuploads" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="opcache.enable" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="opcacherecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
</PHP_SETTINGS>
|
||||
<CUSTOM_CHECKS>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbstorageengine" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="quizattemptsupgradedmessage" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="slashargumentswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unsupporteddbtablerowformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unoconvwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="libcurlwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfileformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfilepertable" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddblargeprefix" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="ishttpswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="incompleteunicodesupport" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="sixtyfourbitswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_max_input_vars" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_admin_dir_usage" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="iscustomadminwarnings" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_xmlrpc_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
</COMPATIBILITY_MATRIX>
|
||||
|
||||
@@ -7,8 +7,8 @@ Feature: Behat steps for interacting with form work
|
||||
@javascript
|
||||
Scenario: Test fields in containers
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
| fullname | shortname | summary | summaryformat |
|
||||
| Course 1 | C1 | Red | 1 |
|
||||
When I log in as "admin"
|
||||
And I am on "Course 1" course homepage
|
||||
# Just get to any form.
|
||||
@@ -17,11 +17,15 @@ Feature: Behat steps for interacting with form work
|
||||
And I set the following fields in the "Appearance" "fieldset" to these values:
|
||||
| Show activity reports | Yes |
|
||||
| Number of announcements | 1 |
|
||||
And I set the following fields in the "Description" "fieldset" to these values:
|
||||
| Course summary | Green |
|
||||
Then the field "Show activity reports" in the "Appearance" "fieldset" matches value "Yes"
|
||||
And the field "Show activity reports" in the "Appearance" "fieldset" does not match value "No"
|
||||
And the following fields in the "region-main" "region" match these values:
|
||||
| Course full name | Frog |
|
||||
| Number of announcements | 1 |
|
||||
| Course summary | Green |
|
||||
And the following fields in the "region-main" "region" do not match these values:
|
||||
| Course full name | Course 1 |
|
||||
| Number of announcements | 5 |
|
||||
| Course summary | Red |
|
||||
|
||||
@@ -63,4 +63,67 @@ class behat_tool_lp extends behat_base {
|
||||
$this->execute('behat_general::i_click_on', [$xpathtarget, 'xpath_element']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'.
|
||||
*
|
||||
* Recognised page names are:
|
||||
* | pagetype | name meaning | description |
|
||||
* | Course competencies | Course name | The course competencies page |
|
||||
*
|
||||
* @param string $page identifies which type of page this is, e.g. 'Course competencies'.
|
||||
* @param string $identifier identifies the particular page, e.g. 'C1'.
|
||||
* @return moodle_url the corresponding URL.
|
||||
* @throws Exception with a meaningful error message if the specified page cannot be found.
|
||||
*/
|
||||
protected function resolve_page_instance_url(string $page, string $identifier): moodle_url {
|
||||
switch (strtolower($page)) {
|
||||
case 'course competencies':
|
||||
$courseid = $this->get_course_id($identifier);
|
||||
return new moodle_url('/admin/tool/lp/coursecompetencies.php', [
|
||||
'courseid' => $courseid,
|
||||
]);
|
||||
default:
|
||||
throw new Exception("Unrecognised page type '{$page}'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of the exact named selectors for the component.
|
||||
*
|
||||
* @return behat_component_named_selector[]
|
||||
*/
|
||||
public static function get_exact_named_selectors(): array {
|
||||
return [
|
||||
new behat_component_named_selector('competency', [
|
||||
"//*[@data-region='coursecompetencies']//table[contains(@class,'managecompetencies')]".
|
||||
"//tr[contains(., //a[@title='View details'][contains(., %locator%)])]",
|
||||
]),
|
||||
new behat_component_named_selector('learning plan', [
|
||||
"//*[@data-region='plan-competencies']//table[contains(@class,'managecompetencies')]".
|
||||
"//tr[@data-node='user-competency'][contains(., //a[@data-usercompetency='true'][contains(., %locator%)])]",
|
||||
]),
|
||||
new behat_component_named_selector('competency description', [
|
||||
"//td/p[contains(., %locator%)]",
|
||||
]),
|
||||
new behat_component_named_selector('competency grade', [
|
||||
"//span[contains(concat(' ', normalize-space(@class), ' '), ' badge ')][contains(., %locator%)]",
|
||||
]),
|
||||
new behat_component_named_selector('learning plan rating', [
|
||||
"//td[position()=2][contains(., %locator%)]",
|
||||
]),
|
||||
new behat_component_named_selector('learning plan proficiency', [
|
||||
"//td[position()=3][contains(., %locator%)]",
|
||||
]),
|
||||
new behat_component_named_selector('competency page proficiency', [
|
||||
"//dt[contains(., 'Proficient')]/following-sibling::dd[1][contains(., %locator%)]",
|
||||
]),
|
||||
new behat_component_named_selector('competency page rating', [
|
||||
"//dt[contains(., 'Rating')]/following-sibling::dd[1][contains(., %locator%)]",
|
||||
]),
|
||||
new behat_component_named_selector('competency page related competency', [
|
||||
"//*[@data-region='relatedcompetencies']//a[contains(., %locator%)]",
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
@tool @tool_lp @javascript
|
||||
Feature: View competencies
|
||||
In order to access competency information
|
||||
As a user
|
||||
I need to view user competencies
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname |
|
||||
| student1 | Student | first |
|
||||
| teacher1 | Teacher | first |
|
||||
And the following "system role assigns" exist:
|
||||
| user | role | contextlevel |
|
||||
| teacher1 | editingteacher | System |
|
||||
And the following "permission overrides" exist:
|
||||
| capability | permission | role | contextlevel | reference |
|
||||
| moodle/competency:planview | Allow | editingteacher | System | |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "scales" exist:
|
||||
| name | scale |
|
||||
| Test Scale | Bad, Good, Great |
|
||||
And the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber | scale |
|
||||
| Cookery | cookery | Test Scale |
|
||||
| Literacy | literacy | Test Scale |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | idnumber | description | competencyframework |
|
||||
| Salads | salads | Salads are important | cookery |
|
||||
| Desserts | desserts | Desserts are important | cookery |
|
||||
| Cakes | cakes | Cakes are important | cookery |
|
||||
| Reading | reading | Reading is important | literacy |
|
||||
| Writing | writing | Writing is important | literacy |
|
||||
And the following "core_competency > related_competencies" exist:
|
||||
| competency | relatedcompetency |
|
||||
| desserts | cakes |
|
||||
And the following "core_competency > plans" exist:
|
||||
| name | description | competencies | user |
|
||||
| Cookery | Cookery is important | salads, desserts, cakes | student1 |
|
||||
| Literacy | Literacy is important | reading, writing | student1 |
|
||||
And the following "core_competency > course_competencies" exist:
|
||||
| course | competency |
|
||||
| C1 | salads |
|
||||
| C1 | desserts |
|
||||
| C1 | cakes |
|
||||
| C1 | reading |
|
||||
| C1 | writing |
|
||||
And the following "core_competency > user_competency" exist:
|
||||
| competency | user | grade |
|
||||
| salads | student1 | Good |
|
||||
| desserts | student1 | Great |
|
||||
| cakes | student1 | Great |
|
||||
And the following "core_competency > user_competency_courses" exist:
|
||||
| course | competency | user | grade |
|
||||
| C1 | salads | student1 | Good |
|
||||
| C1 | desserts | student1 | Great |
|
||||
| C1 | cakes | student1 | Great |
|
||||
|
||||
Scenario: Student view
|
||||
# Course competencies
|
||||
Given I am on the "C1" "tool_lp > course competencies" page logged in as "student1"
|
||||
Then I should see "You are proficient in 3 out of 5 competencies in this course"
|
||||
|
||||
And "Salads are important" "tool_lp > competency description" should exist in the "Salads" "tool_lp > competency"
|
||||
And "Good" "tool_lp > competency grade" should exist in the "Salads" "tool_lp > competency"
|
||||
|
||||
And "Desserts are important" "tool_lp > competency description" should exist in the "Desserts" "tool_lp > competency"
|
||||
And "Great" "tool_lp > competency grade" should exist in the "Desserts" "tool_lp > competency"
|
||||
|
||||
And "Cakes are important" "tool_lp > competency description" should exist in the "Cakes" "tool_lp > competency"
|
||||
And "Great" "tool_lp > competency grade" should exist in the "Cakes" "tool_lp > competency"
|
||||
|
||||
And "Reading is important" "tool_lp > competency description" should exist in the "Reading" "tool_lp > competency"
|
||||
And "Good" "tool_lp > competency grade" should not exist in the "Reading" "tool_lp > competency"
|
||||
And "Great" "tool_lp > competency grade" should not exist in the "Reading" "tool_lp > competency"
|
||||
And "Bad" "tool_lp > competency grade" should not exist in the "Reading" "tool_lp > competency"
|
||||
|
||||
And "Writing is important" "tool_lp > competency description" should exist in the "Writing" "tool_lp > competency"
|
||||
And "Good" "tool_lp > competency grade" should not exist in the "Writing" "tool_lp > competency"
|
||||
And "Great" "tool_lp > competency grade" should not exist in the "Writing" "tool_lp > competency"
|
||||
And "Bad" "tool_lp > competency grade" should not exist in the "Writing" "tool_lp > competency"
|
||||
|
||||
# Course competencies details
|
||||
And I click on "Desserts" "link" in the "Desserts" "tool_lp > competency"
|
||||
And I should see "Desserts are important"
|
||||
And "Yes" "tool_lp > competency page proficiency" should exist
|
||||
And "Great" "tool_lp > competency page rating" should exist
|
||||
|
||||
# Course competencies summary
|
||||
And I click on "Cakes" "link"
|
||||
And I should see "Cakes are important"
|
||||
|
||||
# Learning plans
|
||||
And I click on "Close" "button" in the "Cakes" "dialogue"
|
||||
And I follow "Profile" in the user menu
|
||||
And I click on "Learning plans" "link"
|
||||
And I should see "Cookery"
|
||||
And I should see "Literacy"
|
||||
|
||||
# Learning plans details
|
||||
And I click on "Cookery" "link"
|
||||
And I should see "Cookery is important"
|
||||
And I should see "3 out of 3 competencies are proficient"
|
||||
|
||||
And "Good" "tool_lp > learning plan rating" should exist in the "Salads" "tool_lp > learning plan"
|
||||
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Salads" "tool_lp > learning plan"
|
||||
|
||||
And "Great" "tool_lp > learning plan rating" should exist in the "Desserts" "tool_lp > learning plan"
|
||||
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Desserts" "tool_lp > learning plan"
|
||||
|
||||
And "Great" "tool_lp > learning plan rating" should exist in the "Cakes" "tool_lp > learning plan"
|
||||
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Cakes" "tool_lp > learning plan"
|
||||
|
||||
And I should not see "Literacy"
|
||||
And I should not see "Reading"
|
||||
And I should not see "Writing"
|
||||
|
||||
# Learning plans competency details
|
||||
And I click on "Desserts" "link" in the "Desserts" "tool_lp > learning plan"
|
||||
And I should see "Desserts are important"
|
||||
And "Yes" "tool_lp > competency page proficiency" should exist
|
||||
And "Great" "tool_lp > competency page rating" should exist
|
||||
|
||||
# Learning plans competency summary
|
||||
And I click on "Cakes cakes" "link"
|
||||
And I should see "Cakes are important"
|
||||
|
||||
Scenario: Teacher view
|
||||
# Participant competencies
|
||||
Given I am on the "C1" "report_competency > breakdown" page logged in as "teacher1"
|
||||
Then I should see "Student first"
|
||||
And "Good" "report_competency > breakdown rating" should exist in the "Salads" "report_competency > breakdown"
|
||||
And "Great" "report_competency > breakdown rating" should exist in the "Desserts" "report_competency > breakdown"
|
||||
And "Great" "report_competency > breakdown rating" should exist in the "Cakes" "report_competency > breakdown"
|
||||
And "Not rated" "report_competency > breakdown rating" should exist in the "Reading" "report_competency > breakdown"
|
||||
And "Not rated" "report_competency > breakdown rating" should exist in the "Writing" "report_competency > breakdown"
|
||||
|
||||
# Participant competencies details
|
||||
And I click on "Great" "report_competency > breakdown rating" in the "Desserts" "report_competency > breakdown"
|
||||
And "Yes" "tool_lp > competency page proficiency" should exist
|
||||
And "Great" "tool_lp > competency page rating" should exist
|
||||
|
||||
# Participant competencies summary
|
||||
And I click on "Cakes" "tool_lp > competency page related competency"
|
||||
And I should see "Cakes are important"
|
||||
|
||||
# Participant learning plans
|
||||
And I click on "Close" "button" in the "Cakes" "dialogue"
|
||||
And I click on "Close" "button" in the "User competency summary" "dialogue"
|
||||
And I navigate to course participants
|
||||
And I click on "Student first" "link"
|
||||
And I click on "Learning plans" "link"
|
||||
And I should see "Cookery"
|
||||
And I should see "Literacy"
|
||||
|
||||
# Participant learning plans details
|
||||
And I click on "Cookery" "link"
|
||||
And I should see "Cookery is important"
|
||||
And I should see "3 out of 3 competencies are proficient"
|
||||
|
||||
And "Good" "tool_lp > learning plan rating" should exist in the "Salads" "tool_lp > learning plan"
|
||||
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Salads" "tool_lp > learning plan"
|
||||
|
||||
And "Great" "tool_lp > learning plan rating" should exist in the "Desserts" "tool_lp > learning plan"
|
||||
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Desserts" "tool_lp > learning plan"
|
||||
|
||||
And "Great" "tool_lp > learning plan rating" should exist in the "Cakes" "tool_lp > learning plan"
|
||||
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Cakes" "tool_lp > learning plan"
|
||||
|
||||
And I should not see "Literacy"
|
||||
And I should not see "Reading"
|
||||
And I should not see "Writing"
|
||||
|
||||
# Learning plans competency details
|
||||
And I click on "Desserts" "link"
|
||||
And I should see "Desserts are important"
|
||||
And "Yes" "tool_lp > competency page proficiency" should exist
|
||||
And "Great" "tool_lp > competency page rating" should exist
|
||||
|
||||
# Learning plans competency summary
|
||||
And I click on "Cakes" "tool_lp > competency page related competency"
|
||||
And I should see "Cakes are important"
|
||||
@@ -12,9 +12,10 @@ Feature: Restore Moodle 2 course backups
|
||||
| Course 3 | C3 | 0 | topics | 2 | 0 |
|
||||
| Course 4 | C4 | 0 | topics | 20 | 0 |
|
||||
And the following "activities" exist:
|
||||
| activity | course | idnumber | name | intro | section |
|
||||
| assign | C3 | assign1 | Test assign name | Assign description | 1 |
|
||||
| data | C3 | data1 | Test database name | Database description | 2 |
|
||||
| activity | course | idnumber | name | intro | section | externalurl |
|
||||
| assign | C3 | assign1 | Test assign name | Assign description | 1 | |
|
||||
| data | C3 | data1 | Test database name | Database description | 2 | |
|
||||
| url | C1 | url1 | Test URL name | Test URL description | 3 | http://www.moodle.org |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Forum" to section "1" and I fill the form with:
|
||||
@@ -108,11 +109,7 @@ Feature: Restore Moodle 2 course backups
|
||||
|
||||
@javascript
|
||||
Scenario: Restore a backup in an existing course retaining the backup course settings
|
||||
Given I add a "URL" to section "3" and I fill the form with:
|
||||
| Name | Test URL name |
|
||||
| Description | Test URL description |
|
||||
| id_externalurl | http://www.moodle.org |
|
||||
And I hide section "3"
|
||||
Given I hide section "3"
|
||||
And I hide section "7"
|
||||
When I backup "Course 1" course using this options:
|
||||
| Confirmation | Filename | test_backup.mbz |
|
||||
@@ -134,11 +131,7 @@ Feature: Restore Moodle 2 course backups
|
||||
|
||||
@javascript
|
||||
Scenario: Restore a backup in an existing course keeping the target course settings
|
||||
Given I add a "URL" to section "3" and I fill the form with:
|
||||
| Name | Test URL name |
|
||||
| Description | Test URL description |
|
||||
| id_externalurl | http://www.moodle.org |
|
||||
And I hide section "3"
|
||||
Given I hide section "3"
|
||||
And I hide section "7"
|
||||
When I backup "Course 1" course using this options:
|
||||
| Confirmation | Filename | test_backup.mbz |
|
||||
@@ -160,11 +153,7 @@ Feature: Restore Moodle 2 course backups
|
||||
|
||||
@javascript
|
||||
Scenario: Restore a backup in an existing course deleting contents and retaining the backup course settings
|
||||
Given I add a "URL" to section "3" and I fill the form with:
|
||||
| Name | Test URL name |
|
||||
| Description | Test URL description |
|
||||
| id_externalurl | http://www.moodle.org |
|
||||
And I hide section "3"
|
||||
Given I hide section "3"
|
||||
And I hide section "7"
|
||||
When I backup "Course 1" course using this options:
|
||||
| Initial | Include enrolled users | 0 |
|
||||
@@ -189,11 +178,7 @@ Feature: Restore Moodle 2 course backups
|
||||
|
||||
@javascript
|
||||
Scenario: Restore a backup in an existing course deleting contents and keeping the current course settings
|
||||
Given I add a "URL" to section "3" and I fill the form with:
|
||||
| Name | Test URL name |
|
||||
| Description | Test URL description |
|
||||
| id_externalurl | http://www.moodle.org |
|
||||
And I hide section "3"
|
||||
Given I hide section "3"
|
||||
And I hide section "7"
|
||||
When I backup "Course 1" course using this options:
|
||||
| Initial | Include enrolled users | 0 |
|
||||
@@ -218,11 +203,7 @@ Feature: Restore Moodle 2 course backups
|
||||
|
||||
@javascript
|
||||
Scenario: Restore a backup in an existing course deleting contents decreasing the number of sections
|
||||
Given I add a "URL" to section "3" and I fill the form with:
|
||||
| Name | Test URL name |
|
||||
| Description | Test URL description |
|
||||
| id_externalurl | http://www.moodle.org |
|
||||
And I hide section "3"
|
||||
Given I hide section "3"
|
||||
And I hide section "7"
|
||||
When I backup "Course 1" course using this options:
|
||||
| Initial | Include enrolled users | 0 |
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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.';
|
||||
|
||||
|
||||
+6
-13
@@ -206,7 +206,10 @@ define('PARAM_RAW_TRIMMED', 'raw_trimmed');
|
||||
define('PARAM_SAFEDIR', 'safedir');
|
||||
|
||||
/**
|
||||
* PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths, etc.
|
||||
* PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths
|
||||
* and other references to Moodle code files.
|
||||
*
|
||||
* This is NOT intended to be used for absolute paths or any user uploaded files.
|
||||
*/
|
||||
define('PARAM_SAFEPATH', 'safepath');
|
||||
|
||||
@@ -978,18 +981,8 @@ function clean_param($param, $type) {
|
||||
return preg_replace('/[^a-zA-Z0-9_-]/i', '', $param);
|
||||
|
||||
case PARAM_SAFEPATH:
|
||||
// Replace MS \ separators.
|
||||
$param = str_replace('\\', '/', $param);
|
||||
// Remove any number of ../ to prevent path traversal.
|
||||
$param = preg_replace('/\.\.+\//', '', $param);
|
||||
// Remove everything not a-zA-Z0-9/:_- .
|
||||
$param = preg_replace('/[^a-zA-Z0-9\/:_-]/i', '', $param);
|
||||
// Remove leading slash.
|
||||
$param = ltrim($param, '/');
|
||||
if ($param === '.') {
|
||||
$param = '';
|
||||
}
|
||||
return $param;
|
||||
// Remove everything not a-zA-Z0-9/_- .
|
||||
return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', $param);
|
||||
|
||||
case PARAM_FILE:
|
||||
// Strip all suspicious characters from filename.
|
||||
|
||||
@@ -18,5 +18,3 @@ 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
|
||||
|
||||
@@ -33,4 +33,11 @@ interface Mustache_Cache
|
||||
* @param string $value
|
||||
*/
|
||||
public function cache($key, $value);
|
||||
|
||||
/**
|
||||
* Set a logger instance.
|
||||
*
|
||||
* @param Mustache_Logger|Psr\Log\LoggerInterface $logger
|
||||
*/
|
||||
public function setLogger($logger = null);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ abstract class Mustache_Cache_AbstractCache implements Mustache_Cache
|
||||
/**
|
||||
* Add a log record if logging is enabled.
|
||||
*
|
||||
* @param int $level The logging level
|
||||
* @param string $level The logging level
|
||||
* @param string $message The log message
|
||||
* @param array $context The log context
|
||||
*/
|
||||
|
||||
@@ -286,7 +286,6 @@ class Mustache_Compiler
|
||||
private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level)
|
||||
{
|
||||
$key = $this->block($nodes);
|
||||
$keystr = var_export($key, true);
|
||||
$id = var_export($id, true);
|
||||
|
||||
return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key);
|
||||
@@ -321,7 +320,6 @@ class Mustache_Compiler
|
||||
}
|
||||
|
||||
const SECTION_CALL = '
|
||||
// %s section
|
||||
$value = $context->%s(%s);%s
|
||||
$buffer .= $this->section%s($context, $indent, $value);
|
||||
';
|
||||
@@ -333,12 +331,12 @@ class Mustache_Compiler
|
||||
|
||||
if (%s) {
|
||||
$source = %s;
|
||||
$result = call_user_func($value, $source, %s);
|
||||
$result = (string) call_user_func($value, $source, %s);
|
||||
if (strpos($result, \'{{\') === false) {
|
||||
$buffer .= $result;
|
||||
} else {
|
||||
$buffer .= $this->mustache
|
||||
->loadLambda((string) $result%s)
|
||||
->loadLambda($result%s)
|
||||
->renderInternal($context);
|
||||
}
|
||||
} elseif (!empty($value)) {
|
||||
@@ -392,11 +390,10 @@ class Mustache_Compiler
|
||||
$id = var_export($id, true);
|
||||
$filters = $this->getFilters($filters, $level);
|
||||
|
||||
return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $method, $id, $filters, $key);
|
||||
return sprintf($this->prepare(self::SECTION_CALL, $level), $method, $id, $filters, $key);
|
||||
}
|
||||
|
||||
const INVERTED_SECTION = '
|
||||
// %s inverted section
|
||||
$value = $context->%s(%s);%s
|
||||
if (empty($value)) {
|
||||
%s
|
||||
@@ -419,7 +416,7 @@ class Mustache_Compiler
|
||||
$id = var_export($id, true);
|
||||
$filters = $this->getFilters($filters, $level);
|
||||
|
||||
return sprintf($this->prepare(self::INVERTED_SECTION, $level), $id, $method, $id, $filters, $this->walk($nodes, $level));
|
||||
return sprintf($this->prepare(self::INVERTED_SECTION, $level), $method, $id, $filters, $this->walk($nodes, $level));
|
||||
}
|
||||
|
||||
const PARTIAL_INDENT = ', $indent . %s';
|
||||
@@ -507,7 +504,7 @@ class Mustache_Compiler
|
||||
|
||||
const VARIABLE = '
|
||||
$value = $this->resolveValue($context->%s(%s), $context);%s
|
||||
$buffer .= %s%s;
|
||||
$buffer .= %s($value === null ? \'\' : %s);
|
||||
';
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
*/
|
||||
class Mustache_Engine
|
||||
{
|
||||
const VERSION = '2.13.0';
|
||||
const SPEC_VERSION = '1.1.2';
|
||||
const VERSION = '2.14.0';
|
||||
const SPEC_VERSION = '1.2.2';
|
||||
|
||||
const PRAGMA_FILTERS = 'FILTERS';
|
||||
const PRAGMA_BLOCKS = 'BLOCKS';
|
||||
|
||||
@@ -38,7 +38,7 @@ class Mustache_Source_FilesystemSource implements Mustache_Source
|
||||
/**
|
||||
* Get the Source key (used to generate the compiled class name).
|
||||
*
|
||||
* @throws RuntimeException when a source file cannot be read
|
||||
* @throws Mustache_Exception_RuntimeException when a source file cannot be read
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
@@ -54,7 +54,7 @@ class Mustache_Source_FilesystemSource implements Mustache_Source
|
||||
}
|
||||
|
||||
if ($this->stat === false) {
|
||||
throw new RuntimeException(sprintf('Failed to read source file "%s".', $this->fileName));
|
||||
throw new Mustache_Exception_RuntimeException(sprintf('Failed to read source file "%s".', $this->fileName));
|
||||
}
|
||||
|
||||
foreach ($this->statProps as $prop) {
|
||||
|
||||
@@ -88,11 +88,11 @@ class Mustache_Tokenizer
|
||||
* @throws Mustache_Exception_InvalidArgumentException when $delimiters string is invalid
|
||||
*
|
||||
* @param string $text Mustache template source to tokenize
|
||||
* @param string $delimiters Optionally, pass initial opening and closing delimiters (default: null)
|
||||
* @param string $delimiters Optionally, pass initial opening and closing delimiters (default: empty string)
|
||||
*
|
||||
* @return array Set of Mustache tokens
|
||||
*/
|
||||
public function scan($text, $delimiters = null)
|
||||
public function scan($text, $delimiters = '')
|
||||
{
|
||||
// Setting mbstring.func_overload makes things *really* slow.
|
||||
// Let's do everyone a favor and scan this string as ASCII instead.
|
||||
@@ -107,7 +107,7 @@ class Mustache_Tokenizer
|
||||
|
||||
$this->reset();
|
||||
|
||||
if ($delimiters = trim($delimiters)) {
|
||||
if (is_string($delimiters) && $delimiters = trim($delimiters)) {
|
||||
$this->setDelimiters($delimiters);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -839,40 +839,6 @@ class core_moodlelib_testcase extends advanced_testcase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide some tested base url and expected results.
|
||||
*
|
||||
* @return array Array of tested base url and expected results.
|
||||
*/
|
||||
public function clean_param_safepath_provider(): array {
|
||||
return [
|
||||
// MS separator test.
|
||||
['c:\temp', 'c:/temp'],
|
||||
|
||||
// Leading slash test.
|
||||
['/tmp/', 'tmp/'],
|
||||
|
||||
// Path traversal test.
|
||||
['../../../../../etc/', 'etc/'],
|
||||
['../', ''],
|
||||
['.../...//', ''],
|
||||
['.', '']
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test clean_param() method with PARAM_SAFEPATH type.
|
||||
*
|
||||
* @dataProvider clean_param_safepath_provider
|
||||
* @covers ::clean_param
|
||||
* @param string $path
|
||||
* @param string $expected
|
||||
*/
|
||||
public function test_clean_param_safepath(string $path, string $expected) {
|
||||
$result = clean_param($path, PARAM_SAFEPATH);
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public function test_validate_param() {
|
||||
try {
|
||||
$param = validate_param('11a', PARAM_INT);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+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"
|
||||
|
||||
+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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -21,11 +21,9 @@ Feature: Test exporting questions using Aiken format.
|
||||
| questioncategory | qtype | name | template |
|
||||
| Test questions | multichoice | Multi-choice-001 | two_of_four |
|
||||
| Test questions | multichoice | Multi-choice-002 | one_of_four |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
|
||||
Scenario: Aiken export
|
||||
When I navigate to "Question bank > Export" in current page administration
|
||||
When I am on the "Course 1" "core_question > course question export" page logged in as "teacher1"
|
||||
And I set the field "id_format_aiken" to "1"
|
||||
When I press "Export questions to file"
|
||||
Then following "click here" should download between "68" and "70" bytes
|
||||
|
||||
@@ -14,15 +14,13 @@ Feature: Test importing questions from Aiken format.
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher | C1 | editingteacher |
|
||||
And I log in as "teacher"
|
||||
And I am on "Course 1" course homepage
|
||||
|
||||
@javascript @_file_upload
|
||||
Scenario: import some Aiken questions
|
||||
When I navigate to "Question bank > Import" in current page administration
|
||||
Given I am on the "Course 1" "core_question > course question import" page logged in as "teacher"
|
||||
And I set the field "id_format_aiken" to "1"
|
||||
And I upload "question/format/aiken/tests/fixtures/questions.aiken.txt" file to "Import" filemanager
|
||||
And I press "id_submitbutton"
|
||||
When I press "id_submitbutton"
|
||||
Then I should see "Parsing questions from import file."
|
||||
And I should see "Importing 2 questions from file"
|
||||
And I should see "The Moodle project was started by:"
|
||||
|
||||
@@ -65,7 +65,7 @@ class qformat_blackboard_six extends qformat_blackboard_six_base {
|
||||
* @return mixed contents array or false on failure
|
||||
*/
|
||||
public function get_filecontent($path) {
|
||||
$fullpath = $this->tempdir . '/' . $path;
|
||||
$fullpath = $this->tempdir . '/' . clean_param($path, PARAM_PATH);
|
||||
if (is_file($fullpath) && is_readable($fullpath)) {
|
||||
return file_get_contents($fullpath);
|
||||
}
|
||||
@@ -152,8 +152,7 @@ class qformat_blackboard_six extends qformat_blackboard_six_base {
|
||||
}
|
||||
if ($examfile->getAttribute('type') == 'assessment/x-bb-pool') {
|
||||
if ($examfile->getAttribute('baseurl')) {
|
||||
$fileobj->filebase = clean_param($this->tempdir . '/'
|
||||
. $examfile->getAttribute('baseurl'), PARAM_SAFEPATH);
|
||||
$fileobj->filebase = $this->tempdir. '/' . clean_param($examfile->getAttribute('baseurl'), PARAM_PATH);
|
||||
}
|
||||
if ($content = $this->get_filecontent($examfile->getAttribute('file'))) {
|
||||
$fileobj->filetype = self::FILETYPE_POOL;
|
||||
|
||||
@@ -14,13 +14,11 @@ Feature: Test importing questions from GIFT format.
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher | C1 | editingteacher |
|
||||
And I log in as "teacher"
|
||||
And I am on "Course 1" course homepage
|
||||
And I am on the "Course 1" "core_question > course question import" page logged in as "teacher"
|
||||
|
||||
@javascript @_file_upload
|
||||
Scenario: import some GIFT questions
|
||||
When I navigate to "Question bank > Import" in current page administration
|
||||
And I set the field "id_format_gift" to "1"
|
||||
When I set the field "id_format_gift" to "1"
|
||||
And I upload "question/format/gift/tests/fixtures/questions.gift.txt" file to "Import" filemanager
|
||||
And I press "id_submitbutton"
|
||||
Then I should see "Parsing questions from import file."
|
||||
@@ -37,8 +35,7 @@ Feature: Test importing questions from GIFT format.
|
||||
|
||||
@javascript @_file_upload
|
||||
Scenario: import a GIFT file which specifies the category
|
||||
When I navigate to "Question bank > Import" in current page administration
|
||||
And I set the field "id_format_gift" to "1"
|
||||
When I set the field "id_format_gift" to "1"
|
||||
And I upload "question/format/gift/tests/fixtures/questions_in_category.gift.txt" file to "Import" filemanager
|
||||
And I press "id_submitbutton"
|
||||
Then I should see "Parsing questions from import file."
|
||||
|
||||
@@ -14,12 +14,10 @@ Feature: Test importing questions from WebCT format.
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher | C1 | editingteacher |
|
||||
And I log in as "teacher"
|
||||
And I am on "Course 1" course homepage
|
||||
|
||||
@javascript @_file_upload
|
||||
Scenario: import some WebCT questions
|
||||
When I navigate to "Question bank > Import" in current page administration
|
||||
When I am on the "Course 1" "core_question > course question import" page logged in as "teacher"
|
||||
And I set the field "id_format_webct" to "1"
|
||||
And I upload "question/format/webct/tests/fixtures/sample_webct.txt" file to "Import" filemanager
|
||||
And I press "id_submitbutton"
|
||||
|
||||
@@ -14,12 +14,10 @@ Feature: Test importing calculated question from WebCT format.
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher | C1 | editingteacher |
|
||||
And I log in as "teacher"
|
||||
And I am on "Course 1" course homepage
|
||||
|
||||
@javascript @_file_upload
|
||||
Scenario: import a WebCT calculated question
|
||||
When I navigate to "Question bank > Import" in current page administration
|
||||
When I am on the "Course 1" "core_question > course question import" page logged in as "teacher"
|
||||
And I set the field "id_format_webct" to "1"
|
||||
And I upload "question/format/webct/tests/fixtures/sample_calculated_webct.txt" file to "Import" filemanager
|
||||
And I press "id_submitbutton"
|
||||
|
||||
@@ -14,12 +14,10 @@ Feature: Test importing questions from Moodle XML format.
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher | C1 | editingteacher |
|
||||
And I log in as "teacher"
|
||||
And I am on "Course 1" course homepage
|
||||
|
||||
@javascript @_file_upload
|
||||
Scenario: import some true/false questions from Moodle XML format
|
||||
When I navigate to "Question bank > Import" in current page administration
|
||||
When I am on the "Course 1" "core_question > course question import" page logged in as "teacher"
|
||||
And I set the field "id_format_xml" to "1"
|
||||
And I upload "question/format/xml/tests/fixtures/truefalse.xml" file to "Import" filemanager
|
||||
And I press "id_submitbutton"
|
||||
@@ -40,7 +38,7 @@ Feature: Test importing questions from Moodle XML format.
|
||||
|
||||
@javascript @_file_upload
|
||||
Scenario: import some multiple choice questions from Moodle XML format
|
||||
When I navigate to "Question bank > Import" in current page administration
|
||||
When I am on the "Course 1" "core_question > course question import" page logged in as "teacher"
|
||||
And I set the field "id_format_xml" to "1"
|
||||
And I upload "question/format/xml/tests/fixtures/multichoice.xml" file to "Import" filemanager
|
||||
And I press "id_submitbutton"
|
||||
@@ -52,7 +50,7 @@ Feature: Test importing questions from Moodle XML format.
|
||||
|
||||
@javascript @_file_upload
|
||||
Scenario: import some multi-answer questions from Moodle XML format
|
||||
When I navigate to "Question bank > Import" in current page administration
|
||||
When I am on the "Course 1" "core_question > course question import" page logged in as "teacher"
|
||||
And I set the field "id_format_xml" to "1"
|
||||
And I upload "question/format/xml/tests/fixtures/multianswer.xml" file to "Import" filemanager
|
||||
And I press "id_submitbutton"
|
||||
@@ -64,7 +62,7 @@ Feature: Test importing questions from Moodle XML format.
|
||||
|
||||
@javascript @_file_upload
|
||||
Scenario: import some questions with legacy-style images from Moodle XML format
|
||||
When I navigate to "Question bank > Import" in current page administration
|
||||
When I am on the "Course 1" "core_question > course question import" page logged in as "teacher"
|
||||
And I set the field "id_format_xml" to "1"
|
||||
And I upload "question/format/xml/tests/fixtures/sample_questions_with_old_image_tag.xml" file to "Import" filemanager
|
||||
And I press "id_submitbutton"
|
||||
|
||||
@@ -71,6 +71,10 @@ class behat_core_question extends behat_question_base {
|
||||
return new moodle_url('/question/edit.php',
|
||||
['courseid' => $this->get_course_id($identifier)]);
|
||||
|
||||
case 'course question categories':
|
||||
return new moodle_url('/question/category.php',
|
||||
['courseid' => $this->get_course_id($identifier)]);
|
||||
|
||||
case 'course question import':
|
||||
return new moodle_url('/question/import.php',
|
||||
['courseid' => $this->get_course_id($identifier)]);
|
||||
|
||||
@@ -20,9 +20,7 @@ Feature: A teacher can delete questions in the question bank
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Test questions | essay | Test question to be deleted | Write about whatever you want |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
And I am on the "Course 1" "core_question > course question bank" page logged in as "teacher1"
|
||||
|
||||
@javascript
|
||||
Scenario: A question not used anywhere can really be deleted
|
||||
@@ -52,8 +50,7 @@ Feature: A teacher can delete questions in the question bank
|
||||
And quiz "Test quiz" contains the following questions:
|
||||
| question | page | requireprevious |
|
||||
| Test used question to be deleted | 1 | 0 |
|
||||
When I am on "Course 1" course homepage
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
When I am on the "Course 1" "core_question > course question bank" page
|
||||
And I choose "Delete" action for "Test used question to be deleted" in the question bank
|
||||
And I press "Delete"
|
||||
Then I should not see "Test used question to be deleted"
|
||||
|
||||
@@ -20,9 +20,7 @@ Feature: A teacher can duplicate questions in the question bank
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext | idnumber |
|
||||
| Test questions | essay | Test question to be copied | Write about whatever you want | qid |
|
||||
And I log in as "teacher"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
And I am on the "Course 1" "core_question > course question bank" page logged in as "teacher"
|
||||
|
||||
Scenario: Duplicating a previously created question
|
||||
When I choose "Duplicate" action for "Test question to be copied" in the question bank
|
||||
|
||||
@@ -20,9 +20,7 @@ Feature: A teacher can manage tags on questions in the question bank
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Test questions | essay | Test question to be tagged | Write about whatever you want |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
And I am on the "Course 1" "core_question > course question bank" page logged in as "teacher1"
|
||||
|
||||
@javascript
|
||||
Scenario: Manage tags on a question
|
||||
|
||||
@@ -20,9 +20,7 @@ Feature: A teacher can edit questions in the question bank
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Test questions | essay | Test question to be edited | Write about whatever you want |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
And I am on the "Course 1" "core_question > course question bank" page logged in as "teacher1"
|
||||
|
||||
Scenario: Edit a previously created question
|
||||
When I choose "Edit question" action for "Test question to be edited" in the question bank
|
||||
|
||||
@@ -20,9 +20,7 @@ Feature: The questions can be tagged
|
||||
| bar | 1 |
|
||||
|
||||
Scenario: The tags autocomplete should include standard tags
|
||||
When I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
When I am on the "Course 1" "core_question > course question bank" page logged in as teacher1
|
||||
And I press "Create a new question ..."
|
||||
And I set the field "item_qtype_truefalse" to "1"
|
||||
And I click on "Add" "button" in the "Choose a question type to add" "dialogue"
|
||||
|
||||
@@ -21,14 +21,11 @@ Feature: The questions in the question bank can be filtered by tags
|
||||
| questioncategory | qtype | name | user | questiontext |
|
||||
| Test questions | essay | question 1 name | admin | Question 1 text |
|
||||
| Test questions | essay | question 2 name | teacher1 | Question 2 text |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
And I choose "Edit question" action for "question 1 name" in the question bank
|
||||
And I am on the "question 1 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"
|
||||
And I choose "Edit question" action for "question 2 name" in the question bank
|
||||
And I am on the "question 2 name" "core_question > edit" page
|
||||
And I set the following fields to these values:
|
||||
| Tags | bar |
|
||||
And I press "id_submitbutton"
|
||||
|
||||
@@ -17,12 +17,10 @@ Feature: A teacher can move question categories in the question bank
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | idnumber |
|
||||
| quiz | Test quiz | C1 | quiz1 |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I am on the "Test quiz" "mod_quiz > View" page logged in as "teacher1"
|
||||
|
||||
Scenario: A question category can be moved to another context
|
||||
When I follow "Test quiz"
|
||||
And I navigate to "Question bank > Categories" in current page administration
|
||||
When I navigate to "Question bank > Categories" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Test category |
|
||||
| Parent category | Top for Test quiz |
|
||||
@@ -31,8 +29,7 @@ Feature: A teacher can move question categories in the question bank
|
||||
Then I should see "Test category" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' questioncategories ') and contains(concat(' ', normalize-space(@class), ' '), ' contextlevel50 ')]" "xpath_element"
|
||||
|
||||
Scenario: A question category can be moved to top level
|
||||
When I follow "Test quiz"
|
||||
And I navigate to "Question bank > Categories" in current page administration
|
||||
When I navigate to "Question bank > Categories" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Test category |
|
||||
| Parent category | Default for Test quiz |
|
||||
|
||||
@@ -20,9 +20,7 @@ Feature: A teacher can preview questions in the question bank
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name |
|
||||
| Test questions | numerical | Test question to be previewed |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
And I am on the "Course 1" "core_question > course question bank" page logged in as "teacher1"
|
||||
When I choose "Preview" action for "Test question to be previewed" in the question bank
|
||||
And I switch to "questionpreview" window
|
||||
|
||||
|
||||
@@ -23,11 +23,9 @@ Feature: A teacher can put questions in categories in the question bank
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Used category | essay | Test question to be moved | Write about whatever you want |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
|
||||
Scenario: A new question category can be created
|
||||
When I navigate to "Question bank > Categories" in current page administration
|
||||
When I am on the "Course 1" "core_question > course question categories" page logged in as "teacher1"
|
||||
And I set the following fields to these values:
|
||||
| Name | New Category 1 |
|
||||
| Parent category | Top |
|
||||
@@ -39,7 +37,7 @@ Feature: A teacher can put questions in categories in the question bank
|
||||
And "New Category 1 [newcatidnumber]" "option" should exist in the "Parent category" "select"
|
||||
|
||||
Scenario: A question category can be edited
|
||||
When I navigate to "Question bank > Categories" in current page administration
|
||||
When I am on the "Course 1" "core_question > course question categories" page logged in as "teacher1"
|
||||
And I click on "Edit this category" "link" in the "Subcategory" "list_item"
|
||||
And the field "parent" matches value " Default for C1"
|
||||
And I set the following fields to these values:
|
||||
@@ -50,12 +48,12 @@ Feature: A teacher can put questions in categories in the question bank
|
||||
And I should see "I was edited" in the "New name" "list_item"
|
||||
|
||||
Scenario: An empty question category can be deleted
|
||||
When I navigate to "Question bank > Categories" in current page administration
|
||||
When I am on the "Course 1" "core_question > course question categories" page logged in as "teacher1"
|
||||
And I click on "Delete" "link" in the "Subcategory" "list_item"
|
||||
Then I should not see "Subcategory"
|
||||
|
||||
Scenario: An non-empty question category can be deleted if you move the contents elsewhere
|
||||
When I navigate to "Question bank > Categories" in current page administration
|
||||
When I am on the "Course 1" "core_question > course question categories" page logged in as "teacher1"
|
||||
And I click on "Delete" "link" in the "Used category" "list_item"
|
||||
And I should see "The category 'Used category' contains 1 questions"
|
||||
And I press "Save in category"
|
||||
@@ -64,7 +62,7 @@ Feature: A teacher can put questions in categories in the question bank
|
||||
|
||||
@javascript
|
||||
Scenario: Move a question between categories via the question page
|
||||
When I navigate to "Question bank > Questions" in current page administration
|
||||
When I am on the "Course 1" "core_question > course question bank" page logged in as "teacher1"
|
||||
And I set the field "Select a category" to "Used category"
|
||||
And I click on "Test question to be moved" "checkbox" in the "Test question to be moved" "table_row"
|
||||
And I set the field "Question category" to "Subcategory"
|
||||
@@ -76,7 +74,7 @@ Feature: A teacher can put questions in categories in the question bank
|
||||
|
||||
@javascript
|
||||
Scenario: Move a question between categories via the question settings page
|
||||
When I navigate to "Question bank > Questions" in current page administration
|
||||
When I am on the "Course 1" "core_question > course question bank" page logged in as "teacher1"
|
||||
And I set the field "Select a category" to "Used category"
|
||||
And I choose "Edit question" action for "Test question to be moved" in the question bank
|
||||
And I click on "Use this category" "checkbox"
|
||||
|
||||
@@ -57,8 +57,7 @@ Feature: A teacher can put questions with idnumbers in categories with idnumbers
|
||||
| questioncategory | qtype | name | questiontext | idnumber |
|
||||
| Used category | essay | Test question 1 | Write about whatever you want | q1 |
|
||||
| Used category | essay | Test question 2 | Write about whatever you want | q2 |
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
And I choose "Edit question" action for "Test question 2" in the question bank
|
||||
And I am on the "Test question 2" "core_question > edit" page
|
||||
And I set the field "ID number" to "q1"
|
||||
And I press "submitbutton"
|
||||
# This is the standard form warning reminding the user that the idnumber needs to be unique for a category.
|
||||
@@ -72,8 +71,7 @@ Feature: A teacher can put questions with idnumbers in categories with idnumbers
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext | idnumber |
|
||||
| Used category | essay | Test question 1 | Write about whatever you want | q1 |
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
And I choose "Edit question" action for "Test question 1" in the question bank
|
||||
And I am on the "Test question 1" "core_question > edit" page
|
||||
And I press "Save changes"
|
||||
Then I should not see "This ID number is already in use"
|
||||
|
||||
@@ -87,8 +85,7 @@ Feature: A teacher can put questions with idnumbers in categories with idnumbers
|
||||
| questioncategory | qtype | name | questiontext | idnumber |
|
||||
| Category 1 | essay | Question to edit | Write about whatever you want | q1 |
|
||||
| Category 2 | essay | Other question | Write about whatever you want | q2 |
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
And I choose "Edit question" action for "Question to edit" in the question bank
|
||||
And I am on the "Question to edit" "core_question > edit" page
|
||||
And I set the following fields to these values:
|
||||
| Use this category | 0 |
|
||||
| ID number | q2 |
|
||||
@@ -108,8 +105,7 @@ Feature: A teacher can put questions with idnumbers in categories with idnumbers
|
||||
| Used category | essay | Test question 1 | Write about whatever you want | q1 |
|
||||
| Used category | essay | Test question 2 | Write about whatever you want | q2 |
|
||||
| Subcategory | essay | Test question 3 | Write about whatever you want | q3 |
|
||||
When I navigate to "Question bank > Questions" in current page administration
|
||||
And I choose "Edit question" action for "Test question 3" in the question bank
|
||||
When I am on the "Test question 3" "core_question > edit" page
|
||||
# The q1 idnumber is allowed for this question while it is in the Subcategory.
|
||||
And I set the field "ID number" to "q1"
|
||||
And I press "submitbutton"
|
||||
|
||||
@@ -22,9 +22,7 @@ Feature: The questions in the question bank can be selected in various ways
|
||||
| Test questions | essay | A question 1 name | admin | Question 1 text |
|
||||
| Test questions | essay | B question 2 name | teacher1 | Question 2 text |
|
||||
| Test questions | numerical | C question 3 name | teacher1 | Question 3 text |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
And I am on the "Course 1" "core_question > course question bank" page logged in as "teacher1"
|
||||
|
||||
@javascript
|
||||
Scenario: The question text can be chosen all in the list of questions
|
||||
|
||||
@@ -22,9 +22,7 @@ Feature: The questions in the question bank can be sorted in various ways
|
||||
| Test questions | essay | A question 1 name | admin | Question 1 text | numidnum</a |
|
||||
| Test questions | essay | B question 2 name | teacher1 | Question 2 text | |
|
||||
| Test questions | numerical | C question 3 name | teacher1 | Question 3 text | numidnum</c |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
And I am on the "Course 1" "core_question > course question bank" page logged in as "teacher1"
|
||||
|
||||
@javascript
|
||||
Scenario: The questions are sorted by type by default
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<?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 competency report definitions.
|
||||
*
|
||||
* @package report_competency
|
||||
* @category test
|
||||
* @copyright 2022 Noel De Martin
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
|
||||
|
||||
/**
|
||||
* Competency report definitions.
|
||||
*
|
||||
* @package report_competency
|
||||
* @category test
|
||||
* @copyright 2022 Noel De Martin
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class behat_report_competency extends behat_base {
|
||||
|
||||
/**
|
||||
* 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 |
|
||||
* | Breakdown | Course name | The course competencies breakdown page |
|
||||
*
|
||||
* @param string $page identifies which type of page this is, e.g. 'Breakdown'.
|
||||
* @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 'breakdown':
|
||||
$courseid = $this->get_course_id($identifier);
|
||||
return new moodle_url('/report/competency/index.php', [
|
||||
'id' => $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('breakdown', [
|
||||
"//*[@data-region='competency-breakdown-report']//table".
|
||||
"//tr[contains(., //a[@data-action='competency-dialogue'][contains(., %locator%)])]",
|
||||
]),
|
||||
new behat_component_named_selector('breakdown rating', [
|
||||
"//td[position()=2][contains(., //a[@title='User competency summary'][contains(., %locator%)])]",
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
+2
-2
@@ -29,9 +29,9 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2020061515.00; // 20200615 = branching date YYYYMMDD - do not modify!
|
||||
$version = 2020061516.00; // 20200615 = branching date YYYYMMDD - do not modify!
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
$release = '3.9.15 (Build: 20220711)'; // Human-friendly version name
|
||||
$release = '3.9.16 (Build: 20220822)'; // Human-friendly version name
|
||||
$branch = '39'; // This version's branch.
|
||||
$maturity = MATURITY_STABLE; // This version's maturity level.
|
||||
|
||||
Reference in New Issue
Block a user