Compare commits

..

39 Commits

Author SHA1 Message Date
Sara Arjona d369112db4 Moodle release 3.9.16 2022-08-19 09:50:36 +02:00
Sara Arjona 62d603a054 weekly release 3.9.15+ 2022-08-18 14:54:24 +02:00
Laurent David 4cb39164fa MDL-75210 mod_quiz: fix sequential quiz access
* As a student I should only be able to access the next question or the current question
* The API should not show more than the current question
* Once the quiz is previewed we can see the question in any order (existing behaviour)
* Related to ticket MDL-71728

Co-authored-by: Rajneel Totaram <rjnlfj@yahoo.com>
Co-authored-by: Tim Hunt <t.j.hunt@open.ac.uk>
2022-08-18 08:08:50 +04:00
Sara Arjona d5e8561011 Merge branch 'MDL-72178-39' of https://github.com/NoelDeMartin/moodle into MOODLE_39_STABLE 2022-08-16 16:58:50 +02:00
Mark Johnson 944455bcce MDL-75388 lib: Update mustache to 2.14.1
MDL-67114 local change removed as it is now available upstream.
2022-08-12 04:17:17 +02:00
Petr Skoda ee888615ea MDL-75237 core: improve PARAM_SAFEPATH docs 2022-08-12 04:17:17 +02:00
Petr Skoda 3f58d35337 MDL-75237 qformat_blackboard_six: sanitise blackboard import file paths 2022-08-12 04:17:17 +02:00
Petr Skoda 6983a17fc0 MDL-75237 core: Revert MDL-72029
This reverts commit 9f270d956e.
2022-08-12 04:17:17 +02:00
Petr Skoda ce2d88a951 MDL-75237 qformat_blackboard_six: Revert MDL-72029
This reverts commit 47c211ba0e.
2022-08-12 04:17:17 +02:00
Shamim Rezaie 5a4f73d144 weekly release 3.9.15+ 2022-08-12 11:53:00 +10:00
Noel De Martin 1c8396e2b5 MDL-72178 feedback: Implement behat generators
Based on Andrew's WIP from https://github.com/andrewnicols/moodle/commit/b3d40cb20d1f0edf0bf21077c8f94bce5dec56db

Co-authored-by: Andrew Nicols <andrew@nicols.co.uk>
2022-08-11 10:59:09 +02:00
Jun Pataleta 5668055015 Merge branch 'MDL-74636_39' of https://github.com/timhunt/moodle into MOODLE_39_STABLE 2022-08-09 11:01:00 +08:00
Jun Pataleta 405cd4fb35 Merge branch 'MDL-74211-39' of https://github.com/lameze/moodle into MOODLE_39_STABLE 2022-08-08 11:15:15 +08:00
Ilya Tregubov 2f4b6bd2d7 Merge branch 'MDL-74475-39' of https://github.com/aanabit/moodle into MOODLE_39_STABLE 2022-08-05 13:21:21 +04:00
Ilya Tregubov acf70635c6 weekly release 3.9.15+ 2022-08-05 13:06:07 +04:00
Marina Glancy e3b1e103bd MDL-74502 behat: when setting a field value it may not be visible
for example in case of editor field, the textarea element is hidden by wysiwyg widget but we still need to be able to set it
2022-08-04 11:10:43 +08:00
Andrew Nicols 8d1867cdcc Merge branch 'MDL-75080-39' of https://github.com/andelacruz/moodle into MOODLE_39_STABLE 2022-08-04 10:45:25 +08:00
Simey Lameze 2cfa9817b1 MDL-74211 behat: convert few more steps to use new data generators 2022-08-04 10:43:36 +08:00
Andrew Nicols 641b8c31ee Merge branch 'MDL-74655-39' of https://github.com/NoelDeMartin/moodle into MOODLE_39_STABLE 2022-08-04 10:09:22 +08:00
Noel De Martin 6ce1d39f4e MDL-74655 competency: Implement behat generators 2022-08-03 10:49:41 +02:00
Amaia Anabitarte 9d4fb0a3ad MDL-74475 core_badges: Behat test using new step 2022-08-03 10:30:53 +02:00
Amaia Anabitarte 4110054b51 MDL-74475 behat: Option to jump to course edit page 2022-08-03 10:30:53 +02:00
Tim Hunt dd4d110c9b MDL-74636 behat: step to skip this scenario if a plugin isn't installed 2022-07-29 09:50:56 +01:00
Simey Lameze 017f8ff6bf MDL-74211 behat: replace steps to use new categories page type 2022-07-29 13:24:44 +08:00
Simey Lameze 1ff7beef8f MDL-74211 behat: add course question categories page type 2022-07-29 13:24:35 +08:00
Simey Lameze 35783784b3 MDL-74211 quiz: make use of question data generators 2022-07-29 13:13:46 +08:00
Angelia Dela Cruz 194ca4970a MDL-75080 behat: Chat behat generators use UI
Replace steps that manually add Chat instances via the UI and use
Behat generators. This improves the speed of the Behat test runs.
2022-07-29 11:51:46 +08:00
Jun Pataleta d4f3852dd2 weekly release 3.9.15+ 2022-07-29 10:56:51 +08:00
Andrew Nicols 3fbbc7a86b Merge branch 'MDL-73788-39' of https://github.com/andelacruz/moodle into MOODLE_39_STABLE 2022-07-26 11:39:02 +08:00
Jun Pataleta 7cff4e8bc1 Merge branch 'MDL-74057-39-fix' of https://github.com/junpataleta/moodle into MOODLE_39_STABLE 2022-07-26 11:11:23 +08:00
Jun Pataleta 827803ea3d MDL-74057 mod_scorm: Fix unnecessary whitespaces at the end of the file 2022-07-26 11:09:00 +08:00
Jun Pataleta ccb639002f Merge branch 'MDL-74057-39' of https://github.com/andelacruz/moodle into MOODLE_39_STABLE 2022-07-26 11:01:37 +08:00
Angelia Dela Cruz df62f09a4c MDL-74057 behat: SCORM behat generators use UI
Replace steps that manually add SCORM instances via the UI and use
Behat generators. This improves the speed of Behat test runs.
2022-07-25 18:01:00 +08:00
Angelia Dela Cruz 7c0a982958 MDL-73788 behat: URL behat generators use UI
Replace steps that manually add URL instances via the UI and use
Behat generators. This improves the speed of Behat test runs.
2022-07-19 09:18:53 +08:00
Víctor Déniz 940e4da0fa weekly release 3.9.15+ 2022-07-14 12:06:16 +01:00
Jun Pataleta ae531ae852 Merge branch 'MDL-71747_39' of https://github.com/stronk7/moodle into MOODLE_39_STABLE 2022-07-12 10:51:13 +08:00
Eloy Lafuente (stronk7) d51f307ffa MDL-71747 environment: Ensure that check_xmlrpc_usage is in >=39
This is a partial backport of MDL-73270 to ensure that the
check_xmlrpc_usage custom check is present in all the supported
branches and it's applied to all branches able to run php80:

- MOODLE_311_STABLE
- MOODLE_400_STABLE
- master (aka, 4.1 and up)

Note that the whole patch has not been backported, only the
environmental check (xml file and check implementation).
2022-07-07 23:44:36 +02:00
Eloy Lafuente (stronk7) a4018fbcdd MDL-71747 environment: Apply agreed changes
- PHP 7.4 required.
- Moodle 3.9 required.
- MariaDB 10.4 required.
- PostgreSQL 12 required.
- Oracle 19c required.
2022-07-07 19:19:57 +02:00
Eloy Lafuente (stronk7) 2dd34c5ae4 MDL-71747 environment: 4.1 base information
This is a direct copy of the 4.0 information, to
have the changes under control in the next commit.
2022-07-07 19:19:57 +02:00
66 changed files with 1719 additions and 397 deletions
+196
View File
@@ -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;
}
}
+5 -2
View File
@@ -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 {
}
}
+3 -3
View File
@@ -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"
+3
View File
@@ -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
View File
@@ -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.
-2
View File
@@ -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
+7
View File
@@ -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
*/
+5 -8
View File
@@ -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);
';
/**
+2 -2
View File
@@ -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) {
+3 -3
View File
@@ -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);
}
-1
View File
@@ -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);
}
+15
View File
@@ -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.
*
+9
View File
@@ -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) {
-34
View File
@@ -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);
+1 -1
View File
@@ -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>
+52
View File
@@ -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
+1 -1
View File
@@ -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);
+53 -61
View File
@@ -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'],
],
];
}
}
+158 -1
View File
@@ -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
View File
@@ -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);
}
/**
+3 -2
View File
@@ -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;
}
+57 -2
View File
@@ -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"
+15 -1
View File
@@ -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 ' .
+1 -2
View File
@@ -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
View File
@@ -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;
}
}
+5
View File
@@ -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:"
+2 -3
View File
@@ -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
+1 -3
View File
@@ -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 "&nbsp;&nbsp;&nbsp;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
+1 -3
View File
@@ -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
View File
@@ -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.