Compare commits

...

96 Commits

Author SHA1 Message Date
Eloy Lafuente (stronk7) 33e9b8c394 Moodle release 3.9.17 2022-09-10 12:15:47 +02:00
Jun Pataleta 5bb0f00c0e weekly release 3.9.16+ 2022-09-09 08:36:54 +08:00
Paul Holden 670b636c47 MDL-72012 mod_h5pactivity: respect user access when getting report. 2022-09-08 21:17:57 +08:00
Paul Holden e48460c8cc MDL-75695 mod_h5pactivity: add group filter to attempts report.
This is a backport from MDL-71662.
2022-09-08 17:14:00 +04:00
Michael Hawkins ad5bc5b5ca MDL-68066 output: Apply disable lambda rendering to Mustache renderer 2022-09-07 23:57:05 +08:00
Lars Bonczek 013cb2fb64 MDL-68066 output: Mustache - Add option to disable lambda rendering
This commit is based on an outstanding pull request to the Mustache repo
2022-09-07 23:57:05 +08:00
stevando c4df60bb8d MDL-75283 admin: Update admin user fetching to use sort helper 2022-09-07 13:18:55 +08:00
Paul Holden 786a4956dd MDL-75405 block_html: safer unserialize during old block conversion. 2022-09-06 17:57:15 +02:00
Jun Pataleta 323e272c85 Merge branch 'MDL-75231-39' of https://github.com/lameze/moodle into MOODLE_39_STABLE 2022-09-06 23:49:01 +08:00
Sara Arjona 6e769797fd Merge branch 'MDL-75550-39' of https://github.com/mickhawkins/moodle into MOODLE_39_STABLE 2022-09-05 08:57:43 +02:00
Jun Pataleta 0a48ddad8f Merge branch 'MDL-75641_39' of https://github.com/stronk7/moodle into MOODLE_39_STABLE 2022-09-05 14:37:02 +08:00
Eloy Lafuente (stronk7) c939cd4b6d MDL-75641 core_lang: Add string for new Europe/Kyiv timezone
With IANA TZDB (timezone database) release 2022b, Europe/Kiev
has been renamed to Europe/Kyiv. So we need to add this string
to our list to keep tests passing.

Note that the old Europe/Kiev continues working (is an alias) and,
also, there are places in core where we are using it. Just we cannot
remove/replace those uses in core until we ensure that everybody is
using PHP 8.0.23 (TZDB 2022b) or more recent versions. Hence,
not changing those uses now.
2022-09-04 10:06:39 +02:00
Ilya Tregubov fd27acfcb1 weekly release 3.9.16+ 2022-09-02 08:49:23 +04:00
Jun Pataleta 9ae0e4a58b Merge branch 'MDL-75019-39' of https://github.com/andelacruz/moodle into MOODLE_39_STABLE 2022-08-31 20:24:37 +08:00
Ilya Tregubov 874472a85d Merge branch 'MDL-74861-39' of https://github.com/andelacruz/moodle into MOODLE_39_STABLE 2022-08-31 11:51:54 +04:00
Michael Hawkins 5f893f890f MDL-75550 lib: Update Mustache const to reflect correct version
Mustache version 2.14.1 is already included, but the VERSION const
incorrectly listed 2.14.0 in the tagged Mustache release. A later
upstream commit corrected this in the library, which this now matches.
2022-08-31 15:29:59 +08:00
Simey Lameze 69e03eacba MDL-75231 files: make bucket calculations more precise
This commit changes changes the unit test calculations to
keep track of the time the bucket starts/finishes bursting
and use milliseconds instead of seconds to have a precise
time to sleep.
2022-08-22 14:24:09 +08:00
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
Angelia Dela Cruz 1dfd13841a MDL-74861 behat: H5P behat generators use UI
Replace steps that manually add H5Pactivity instances via the UI
and use Behat generators. This improves the speed of the Behat test
runs.

Co-authored-by: Simey Lameze <simey@moodle.com>
2022-08-08 12:54:02 +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
Angelia Dela Cruz 5d15e96a98 MDL-75019 behat: Folder behat generators use UI
Replace steps that manually add Folder instances via the UI and
use Behat generators. This improves the speed of the Behat test
runs.
2022-08-05 15:53:57 +08: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
Jake Dallimore 84d3ae1b08 Moodle release 3.9.15 2022-07-08 11:28:28 +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
Jun Pataleta 71f83af13c weekly release 3.9.14+ 2022-07-07 21:33:14 +08:00
Víctor Déniz d587d8b242 Merge branch 'MDL-74835-M39_update-nodejs-version-in-travis-integration-to-lts-gallium' of https://github.com/ziegenberg/moodle into MOODLE_39_STABLE 2022-07-06 12:13:40 +01:00
stevando f1b1570b02 MDL-72171 tool_mobile: Ensure auto login URL is always local 2022-07-06 14:29:10 +08:00
Mihail Geshoski f9ca67a78d MDL-71921 scorm: Sanitize element names before outputting track details 2022-07-01 12:47:18 +02:00
Alex Morris c8f9140e70 MDL-75044 assignfeedback_editpdf: Add SAFER option to GS command
Adds the SAFER option to the Ghostscript command to limit interaction
with IO and OS commands
2022-07-01 12:47:18 +02:00
Huong Nguyen 47c211ba0e MDL-72029 Lesson: Prevent arbitrary file read when importing lesson questions in blackboard format 2022-07-01 12:47:18 +02:00
Huong Nguyen 9f270d956e MDL-72029 lib: Prevent path traversal for clean_param with PARAM_SAFEPATH 2022-07-01 12:47:18 +02:00
Laurent David 32ee8e3d34 MDL-74473 core_analytics: Upgrade ML Backend to 2.6.6 2022-07-01 12:47:18 +02:00
Jake Dallimore 77a1413fa0 MDL-72299 mod_lti: fix html escaping when reposting cross site posts 2022-07-01 12:47:18 +02:00
Víctor Déniz 14760d9787 weekly release 3.9.14+ 2022-07-01 11:28:53 +01:00
Jun Pataleta 03b6b93589 Merge branch 'MDL-74613-39' of https://github.com/NoelDeMartin/moodle into MOODLE_39_STABLE 2022-06-30 20:23:48 +08:00
Noel De Martin 47402416fd MDL-74613 grade: Fix undefined behat step 2022-06-30 11:32:35 +02:00
Andrew Nicols 0087cede97 Merge branch 'MDL-75084-39' of https://github.com/NoelDeMartin/moodle into MOODLE_39_STABLE 2022-06-30 11:14:09 +08:00
Andrew Nicols 264c405192 Merge branch 'MDL-74613-39' of https://github.com/NoelDeMartin/moodle into MOODLE_39_STABLE 2022-06-30 11:05:38 +08:00
Andrew Nicols 1742e0c27c Merge branch 'MDL-74220-39' of https://github.com/NoelDeMartin/moodle into MOODLE_39_STABLE 2022-06-30 11:00:59 +08:00
Andrew Nicols 7371cb4838 Merge branch 'MDL-74744-39' of https://github.com/andelacruz/moodle into MOODLE_39_STABLE 2022-06-30 10:56:19 +08:00
Andrew Nicols c1512e8d89 Merge branch 'MDL-74841-39' of https://github.com/andelacruz/moodle into MOODLE_39_STABLE 2022-06-30 10:45:12 +08:00
Jake Dallimore e5aaeed609 Merge branch 'MDL-74674-39' of https://github.com/alfonso-salces/moodle into MOODLE_39_STABLE 2022-06-29 16:45:23 +08:00
Alfonso Salces a950ed04a7 MDL-74674 core: create notification generators 2022-06-29 10:29:33 +02:00
Noel De Martin 964c98c8d9 MDL-75084 blog: Implement behat generators 2022-06-29 09:38:55 +02:00
Jake Dallimore 635389b235 Merge branch 'MDL-74864-39' of https://github.com/dpalou/moodle into MOODLE_39_STABLE 2022-06-29 13:41:31 +08:00
Dani Palou 1385e3bc09 MDL-74864 forum: Allow specifying group in discussions generator 2022-06-28 07:00:18 +02:00
Jun Pataleta 2dc581f717 weekly release 3.9.14+ 2022-06-17 15:30:38 +08:00
Víctor Déniz 4f8369c3f1 Merge branch 'MDL-74242-39-randomize_update_check' of https://github.com/mudrd8mz/moodle into MOODLE_39_STABLE 2022-06-14 09:48:51 +01:00
Jun Pataleta 730b405d68 Merge branch 'MDL-74816-39' of https://github.com/dpalou/moodle into MOODLE_39_STABLE 2022-06-14 13:58:29 +08:00
Angelia Dela Cruz 87c04f1e0b MDL-74841 behat: Label behat generators use UI
Replace steps that manually add Label instances via the UI and use
Behat generators. This improves the speed of Behat test runs.
2022-06-10 13:58:46 +08:00
Andrew Nicols f4df13fb6c weekly release 3.9.14+ 2022-06-03 11:06:38 +08:00
Paul Holden 4a8abf77ac MDL-71276 message_email: include alternate email in privacy export. 2022-06-01 18:48:11 +02:00
Noel De Martin 7e48ec1820 MDL-74613 grade: Add missing generators 2022-05-31 18:15:23 +02:00
Jake Dallimore 05087afd40 weekly release 3.9.14+ 2022-05-27 10:02:16 +08:00
Noel De Martin 56afed9678 MDL-74220 data: Implement behat generators 2022-05-26 10:39:38 +02:00
Paul Holden af8c168f78 MDL-73801 backup: set predictably ordered backup next start time.
Prior to this change, the testcase would assert ordering based on
identical fields which could lead to random failures. Set distinct
next start time of course backup, and also remove random ordering
by sorting on defined course order (all other fields being equal).
2022-05-26 12:10:49 +08:00
Dani Palou 4c52746b41 MDL-74816 mod_resource: Allow specify default file name in generator 2022-05-25 10:41:43 +02:00
Angelia Dela Cruz 7768a35366 MDL-74744 behat: Quiz behat generators use UI
Replace steps that manually add Quiz instances via the UI and use
Behat generators. This improves the speed of Behat test runs.
2022-05-24 11:23:21 +08:00
Daniel Ziegenberg 05847affc1 MDL-74835 ci: Update nodejs version in travis integration to lts/gallium
In MDL-73915 the nodejs version was switched to lts/gallium but the
travis integration was not updated.

Signed-off-by: Daniel Ziegenberg <daniel@ziegenberg.at>
2022-05-22 23:55:08 +02:00
David Mudrák f652a2b613 MDL-74242 task: Fix calculation of random minute in the task schedule
The random minute was incorrectly set to a random value from the range
0 - 23 instead of 0 - 59 due to this copy & paste mistake.

This is a backport of MDL-73765 already present in 3.11.
2022-03-20 17:06:51 +01:00
David Mudrák 04d4cbde78 MDL-74242 admin: Check for available updates daily on random time
It's been a mistake to make every Moodle site out there to check for
available update every two hours, on the hour. We are experiencing
server load peaks that are harder and harder to deal with.

The patch addresses this by (1) checking for available updates just once
a day and (2) distributing the checking times randomly around the clock.

This is a backport of MDL-73748 already present in 3.11.
2022-03-20 17:06:51 +01:00
125 changed files with 2947 additions and 825 deletions
+2 -2
View File
@@ -36,7 +36,7 @@ jobs:
- php: 7.2
env: DB=none TASK=CITEST
- php: 7.2
env: DB=none TASK=GRUNT NVM_VERSION='lts/carbon'
env: DB=none TASK=GRUNT NVM_VERSION='lts/gallium'
- if: env(MOODLE_DATABASE) = "pgsql" OR env(MOODLE_DATABASE) = "all" OR env(MOODLE_DATABASE) IS NOT present
php: 7.2
@@ -52,7 +52,7 @@ jobs:
env: DB=none TASK=CITEST
- if: env(MOODLE_PHP) = "all"
php: 7.4
env: DB=none TASK=GRUNT NVM_VERSION='lts/carbon'
env: DB=none TASK=GRUNT NVM_VERSION='lts/gallium'
- if: env(MOODLE_PHP) = "all" AND (env(MOODLE_DATABASE) = "pgsql" OR env(MOODLE_DATABASE) = "all" OR env(MOODLE_DATABASE) IS NOT present)
php: 7.4
+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 |
@@ -20,9 +20,10 @@ Feature: Forms manipulation
Given the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
And I add a "Quiz" to section "1"
And the following "activities" exist:
| activity | course | section | name |
| quiz | C1 | 1 | Quiz 1 |
And I am on the "Quiz 1" "quiz activity editing" page logged in as admin
When I expand all fieldsets
Then I should see "Close the quiz"
And I should see "Group mode"
@@ -63,4 +63,67 @@ class behat_tool_lp extends behat_base {
$this->execute('behat_general::i_click_on', [$xpathtarget, 'xpath_element']);
}
/**
* Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'.
*
* Recognised page names are:
* | pagetype | name meaning | description |
* | Course competencies | Course name | The course competencies page |
*
* @param string $page identifies which type of page this is, e.g. 'Course competencies'.
* @param string $identifier identifies the particular page, e.g. 'C1'.
* @return moodle_url the corresponding URL.
* @throws Exception with a meaningful error message if the specified page cannot be found.
*/
protected function resolve_page_instance_url(string $page, string $identifier): moodle_url {
switch (strtolower($page)) {
case 'course competencies':
$courseid = $this->get_course_id($identifier);
return new moodle_url('/admin/tool/lp/coursecompetencies.php', [
'courseid' => $courseid,
]);
default:
throw new Exception("Unrecognised page type '{$page}'");
}
}
/**
* Return a list of the exact named selectors for the component.
*
* @return behat_component_named_selector[]
*/
public static function get_exact_named_selectors(): array {
return [
new behat_component_named_selector('competency', [
"//*[@data-region='coursecompetencies']//table[contains(@class,'managecompetencies')]".
"//tr[contains(., //a[@title='View details'][contains(., %locator%)])]",
]),
new behat_component_named_selector('learning plan', [
"//*[@data-region='plan-competencies']//table[contains(@class,'managecompetencies')]".
"//tr[@data-node='user-competency'][contains(., //a[@data-usercompetency='true'][contains(., %locator%)])]",
]),
new behat_component_named_selector('competency description', [
"//td/p[contains(., %locator%)]",
]),
new behat_component_named_selector('competency grade', [
"//span[contains(concat(' ', normalize-space(@class), ' '), ' badge ')][contains(., %locator%)]",
]),
new behat_component_named_selector('learning plan rating', [
"//td[position()=2][contains(., %locator%)]",
]),
new behat_component_named_selector('learning plan proficiency', [
"//td[position()=3][contains(., %locator%)]",
]),
new behat_component_named_selector('competency page proficiency', [
"//dt[contains(., 'Proficient')]/following-sibling::dd[1][contains(., %locator%)]",
]),
new behat_component_named_selector('competency page rating', [
"//dt[contains(., 'Rating')]/following-sibling::dd[1][contains(., %locator%)]",
]),
new behat_component_named_selector('competency page related competency', [
"//*[@data-region='relatedcompetencies']//a[contains(., %locator%)]",
]),
];
}
}
@@ -0,0 +1,187 @@
@tool @tool_lp @javascript
Feature: View competencies
In order to access competency information
As a user
I need to view user competencies
Background:
Given the following "users" exist:
| username | firstname | lastname |
| student1 | Student | first |
| teacher1 | Teacher | first |
And the following "system role assigns" exist:
| user | role | contextlevel |
| teacher1 | editingteacher | System |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/competency:planview | Allow | editingteacher | System | |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| teacher1 | C1 | editingteacher |
And the following "scales" exist:
| name | scale |
| Test Scale | Bad, Good, Great |
And the following "core_competency > frameworks" exist:
| shortname | idnumber | scale |
| Cookery | cookery | Test Scale |
| Literacy | literacy | Test Scale |
And the following "core_competency > competencies" exist:
| shortname | idnumber | description | competencyframework |
| Salads | salads | Salads are important | cookery |
| Desserts | desserts | Desserts are important | cookery |
| Cakes | cakes | Cakes are important | cookery |
| Reading | reading | Reading is important | literacy |
| Writing | writing | Writing is important | literacy |
And the following "core_competency > related_competencies" exist:
| competency | relatedcompetency |
| desserts | cakes |
And the following "core_competency > plans" exist:
| name | description | competencies | user |
| Cookery | Cookery is important | salads, desserts, cakes | student1 |
| Literacy | Literacy is important | reading, writing | student1 |
And the following "core_competency > course_competencies" exist:
| course | competency |
| C1 | salads |
| C1 | desserts |
| C1 | cakes |
| C1 | reading |
| C1 | writing |
And the following "core_competency > user_competency" exist:
| competency | user | grade |
| salads | student1 | Good |
| desserts | student1 | Great |
| cakes | student1 | Great |
And the following "core_competency > user_competency_courses" exist:
| course | competency | user | grade |
| C1 | salads | student1 | Good |
| C1 | desserts | student1 | Great |
| C1 | cakes | student1 | Great |
Scenario: Student view
# Course competencies
Given I am on the "C1" "tool_lp > course competencies" page logged in as "student1"
Then I should see "You are proficient in 3 out of 5 competencies in this course"
And "Salads are important" "tool_lp > competency description" should exist in the "Salads" "tool_lp > competency"
And "Good" "tool_lp > competency grade" should exist in the "Salads" "tool_lp > competency"
And "Desserts are important" "tool_lp > competency description" should exist in the "Desserts" "tool_lp > competency"
And "Great" "tool_lp > competency grade" should exist in the "Desserts" "tool_lp > competency"
And "Cakes are important" "tool_lp > competency description" should exist in the "Cakes" "tool_lp > competency"
And "Great" "tool_lp > competency grade" should exist in the "Cakes" "tool_lp > competency"
And "Reading is important" "tool_lp > competency description" should exist in the "Reading" "tool_lp > competency"
And "Good" "tool_lp > competency grade" should not exist in the "Reading" "tool_lp > competency"
And "Great" "tool_lp > competency grade" should not exist in the "Reading" "tool_lp > competency"
And "Bad" "tool_lp > competency grade" should not exist in the "Reading" "tool_lp > competency"
And "Writing is important" "tool_lp > competency description" should exist in the "Writing" "tool_lp > competency"
And "Good" "tool_lp > competency grade" should not exist in the "Writing" "tool_lp > competency"
And "Great" "tool_lp > competency grade" should not exist in the "Writing" "tool_lp > competency"
And "Bad" "tool_lp > competency grade" should not exist in the "Writing" "tool_lp > competency"
# Course competencies details
And I click on "Desserts" "link" in the "Desserts" "tool_lp > competency"
And I should see "Desserts are important"
And "Yes" "tool_lp > competency page proficiency" should exist
And "Great" "tool_lp > competency page rating" should exist
# Course competencies summary
And I click on "Cakes" "link"
And I should see "Cakes are important"
# Learning plans
And I click on "Close" "button" in the "Cakes" "dialogue"
And I follow "Profile" in the user menu
And I click on "Learning plans" "link"
And I should see "Cookery"
And I should see "Literacy"
# Learning plans details
And I click on "Cookery" "link"
And I should see "Cookery is important"
And I should see "3 out of 3 competencies are proficient"
And "Good" "tool_lp > learning plan rating" should exist in the "Salads" "tool_lp > learning plan"
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Salads" "tool_lp > learning plan"
And "Great" "tool_lp > learning plan rating" should exist in the "Desserts" "tool_lp > learning plan"
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Desserts" "tool_lp > learning plan"
And "Great" "tool_lp > learning plan rating" should exist in the "Cakes" "tool_lp > learning plan"
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Cakes" "tool_lp > learning plan"
And I should not see "Literacy"
And I should not see "Reading"
And I should not see "Writing"
# Learning plans competency details
And I click on "Desserts" "link" in the "Desserts" "tool_lp > learning plan"
And I should see "Desserts are important"
And "Yes" "tool_lp > competency page proficiency" should exist
And "Great" "tool_lp > competency page rating" should exist
# Learning plans competency summary
And I click on "Cakes cakes" "link"
And I should see "Cakes are important"
Scenario: Teacher view
# Participant competencies
Given I am on the "C1" "report_competency > breakdown" page logged in as "teacher1"
Then I should see "Student first"
And "Good" "report_competency > breakdown rating" should exist in the "Salads" "report_competency > breakdown"
And "Great" "report_competency > breakdown rating" should exist in the "Desserts" "report_competency > breakdown"
And "Great" "report_competency > breakdown rating" should exist in the "Cakes" "report_competency > breakdown"
And "Not rated" "report_competency > breakdown rating" should exist in the "Reading" "report_competency > breakdown"
And "Not rated" "report_competency > breakdown rating" should exist in the "Writing" "report_competency > breakdown"
# Participant competencies details
And I click on "Great" "report_competency > breakdown rating" in the "Desserts" "report_competency > breakdown"
And "Yes" "tool_lp > competency page proficiency" should exist
And "Great" "tool_lp > competency page rating" should exist
# Participant competencies summary
And I click on "Cakes" "tool_lp > competency page related competency"
And I should see "Cakes are important"
# Participant learning plans
And I click on "Close" "button" in the "Cakes" "dialogue"
And I click on "Close" "button" in the "User competency summary" "dialogue"
And I navigate to course participants
And I click on "Student first" "link"
And I click on "Learning plans" "link"
And I should see "Cookery"
And I should see "Literacy"
# Participant learning plans details
And I click on "Cookery" "link"
And I should see "Cookery is important"
And I should see "3 out of 3 competencies are proficient"
And "Good" "tool_lp > learning plan rating" should exist in the "Salads" "tool_lp > learning plan"
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Salads" "tool_lp > learning plan"
And "Great" "tool_lp > learning plan rating" should exist in the "Desserts" "tool_lp > learning plan"
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Desserts" "tool_lp > learning plan"
And "Great" "tool_lp > learning plan rating" should exist in the "Cakes" "tool_lp > learning plan"
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Cakes" "tool_lp > learning plan"
And I should not see "Literacy"
And I should not see "Reading"
And I should not see "Writing"
# Learning plans competency details
And I click on "Desserts" "link"
And I should see "Desserts are important"
And "Yes" "tool_lp > competency page proficiency" should exist
And "Great" "tool_lp > competency page rating" should exist
# Learning plans competency summary
And I click on "Cakes" "tool_lp > competency page related competency"
And I should see "Cakes are important"
+2 -1
View File
@@ -27,7 +27,8 @@ require_once($CFG->libdir . '/externallib.php');
$userid = required_param('userid', PARAM_INT); // The user id the key belongs to (for double-checking).
$key = required_param('key', PARAM_ALPHANUMEXT); // The key generated by the tool_mobile_external::get_autologin_key() external function.
$urltogo = optional_param('urltogo', $CFG->wwwroot, PARAM_URL); // URL to redirect.
$urltogo = optional_param('urltogo', $CFG->wwwroot, PARAM_LOCALURL); // URL to redirect.
$urltogo = $urltogo ?: $CFG->wwwroot;
$context = context_system::instance();
$PAGE->set_context($context);
@@ -19,14 +19,13 @@ Feature: Backup user data
And the following config values are set as admin:
| coursebinenable | 1 | tool_recyclebin |
| autohide | 0 | tool_recyclebin |
And the following "activities" exist:
| activity | course | section | name | intro |
| quiz | C1 | 1 | Quiz 1 | Test quiz description |
@javascript
Scenario: Delete and restore a quiz with user data
Given I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I add a "Quiz" to section "1" and I fill the form with:
| Name | Quiz 1 |
| Description | Test quiz description |
Given I am on the "Quiz 1" "quiz activity" page logged in as teacher1
And I add a "True/False" question to the "Quiz 1" quiz with:
| Question name | TF1 |
| Question text | First question |
@@ -41,6 +40,8 @@ Feature: Backup user data
| Correct answer | False |
| Feedback for the response 'True'. | So you think it is true |
| Feedback for the response 'False'. | So you think it is false |
And I set the field "maxgrade" to "10.0"
And I press "savechanges"
And I log out
When I am on the "Quiz 1" "quiz activity" page logged in as student1
And I press "Attempt quiz now"
@@ -77,6 +77,7 @@ abstract class backup_cron_automated_helper {
* First backup courses that do not have an entry in backup_courses first,
* as they are likely new and never been backed up. Do the oldest modified courses first.
* Then backup courses that have previously been backed up starting with the oldest next start time.
* Finally, all else being equal, defer to the sortorder of the courses.
*
* @param null|int $now timestamp to use in course selection.
* @return moodle_recordset The recordset of matching courses.
@@ -93,7 +94,8 @@ abstract class backup_cron_automated_helper {
LEFT JOIN {backup_courses} bc ON bc.courseid = c.id
WHERE bc.nextstarttime IS NULL OR bc.nextstarttime < ?
ORDER BY nextstarttime ASC,
c.timemodified DESC';
c.timemodified DESC,
c.sortorder';
$params = array(
$now, // Only get courses where the backup start time is in the past.
+1 -1
View File
@@ -383,7 +383,7 @@ class backup_cron_helper_testcase extends advanced_testcase {
$backupcourse3 = new stdClass;
$backupcourse3->courseid = $course3->id;
$backupcourse3->laststatus = testable_backup_cron_automated_helper::BACKUP_STATUS_OK;
$backupcourse3->nextstarttime = 1554858160;
$backupcourse3->nextstarttime = 1554822160;
$DB->insert_record('backup_courses', $backupcourse3);
$backupcourse4 = new stdClass;
@@ -12,9 +12,10 @@ Feature: Restore Moodle 2 course backups
| Course 3 | C3 | 0 | topics | 2 | 0 |
| Course 4 | C4 | 0 | topics | 20 | 0 |
And the following "activities" exist:
| activity | course | idnumber | name | intro | section |
| assign | C3 | assign1 | Test assign name | Assign description | 1 |
| data | C3 | data1 | Test database name | Database description | 2 |
| activity | course | idnumber | name | intro | section | externalurl |
| assign | C3 | assign1 | Test assign name | Assign description | 1 | |
| data | C3 | data1 | Test database name | Database description | 2 | |
| url | C1 | url1 | Test URL name | Test URL description | 3 | http://www.moodle.org |
And I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
And I add a "Forum" to section "1" and I fill the form with:
@@ -108,11 +109,7 @@ Feature: Restore Moodle 2 course backups
@javascript
Scenario: Restore a backup in an existing course retaining the backup course settings
Given I add a "URL" to section "3" and I fill the form with:
| Name | Test URL name |
| Description | Test URL description |
| id_externalurl | http://www.moodle.org |
And I hide section "3"
Given I hide section "3"
And I hide section "7"
When I backup "Course 1" course using this options:
| Confirmation | Filename | test_backup.mbz |
@@ -134,11 +131,7 @@ Feature: Restore Moodle 2 course backups
@javascript
Scenario: Restore a backup in an existing course keeping the target course settings
Given I add a "URL" to section "3" and I fill the form with:
| Name | Test URL name |
| Description | Test URL description |
| id_externalurl | http://www.moodle.org |
And I hide section "3"
Given I hide section "3"
And I hide section "7"
When I backup "Course 1" course using this options:
| Confirmation | Filename | test_backup.mbz |
@@ -160,11 +153,7 @@ Feature: Restore Moodle 2 course backups
@javascript
Scenario: Restore a backup in an existing course deleting contents and retaining the backup course settings
Given I add a "URL" to section "3" and I fill the form with:
| Name | Test URL name |
| Description | Test URL description |
| id_externalurl | http://www.moodle.org |
And I hide section "3"
Given I hide section "3"
And I hide section "7"
When I backup "Course 1" course using this options:
| Initial | Include enrolled users | 0 |
@@ -189,11 +178,7 @@ Feature: Restore Moodle 2 course backups
@javascript
Scenario: Restore a backup in an existing course deleting contents and keeping the current course settings
Given I add a "URL" to section "3" and I fill the form with:
| Name | Test URL name |
| Description | Test URL description |
| id_externalurl | http://www.moodle.org |
And I hide section "3"
Given I hide section "3"
And I hide section "7"
When I backup "Course 1" course using this options:
| Initial | Include enrolled users | 0 |
@@ -218,11 +203,7 @@ Feature: Restore Moodle 2 course backups
@javascript
Scenario: Restore a backup in an existing course deleting contents decreasing the number of sections
Given I add a "URL" to section "3" and I fill the form with:
| Name | Test URL name |
| Description | Test URL description |
| id_externalurl | http://www.moodle.org |
And I hide section "3"
Given I hide section "3"
And I hide section "7"
When I backup "Course 1" course using this options:
| Initial | Include enrolled users | 0 |
@@ -30,9 +30,7 @@ Feature: Award badges with separate groups
| teacher1 | CB |
| student2 | CA |
| teacher2 | CA |
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Edit settings" in current page administration
And I am on the "Course 1" "course editing" page logged in as "teacher1"
And I expand all fieldsets
And I set the field "Group mode" to "Separate groups"
And I press "Save and display"
+2 -2
View File
@@ -36,7 +36,7 @@ class moodle1_block_html_handler extends moodle1_block_handler {
$contextid = $this->converter->get_contextid(CONTEXT_BLOCK, $olddata['id']);
$decodeddata = base64_decode($olddata['configdata']);
list($updated, $configdata) = upgrade_fix_serialized_objects($decodeddata);
$configdata = unserialize($configdata);
$configdata = unserialize_object($configdata);
// get a fresh new file manager for this instance
$this->fileman = $this->converter->get_file_manager($contextid, 'block_html');
@@ -44,7 +44,7 @@ class moodle1_block_html_handler extends moodle1_block_handler {
// convert course files embedded in the block content
$this->fileman->filearea = 'content';
$this->fileman->itemid = 0;
$configdata->text = moodle1_converter::migrate_referenced_files($configdata->text, $this->fileman);
$configdata->text = moodle1_converter::migrate_referenced_files($configdata->text ?? '', $this->fileman);
$configdata->format = FORMAT_HTML;
return base64_encode(serialize($configdata));
@@ -10,10 +10,18 @@ Feature: Add URL to main menu block
And I am on site homepage
And I navigate to "Turn editing on" in current page administration
And I add the "Main menu" block
When I add a "URL" to section "0" and I fill the form with:
| Name | google |
| Description | gooooooooogle |
| External URL | http://www.google.com |
And the following "activity" exists:
| activity | url |
| course | Acceptance test site |
| name | google |
| intro | gooooooooogle |
| externalurl | http://www.google.com |
| section | 0 |
When I am on the "google" "url activity editing" page
And I expand all fieldsets
And I set the following fields to these values:
| id_display | In pop-up |
And I press "Save and return to course"
Then "google" "link" should exist in the "Main menu" "block"
And "Add an activity" "button" should exist in the "Main menu" "block"
And "Add an activity or resource" "button" should exist in the "Main menu" "block"
+3 -9
View File
@@ -9,6 +9,9 @@ Feature: Comment on a blog entry
| username | firstname | lastname | email |
| testuser | Test | User | moodle@example.com |
| testuser2 | Test2 | User2 | moodle2@example.com |
And the following "core_blog > entries" exist:
| subject | body | user |
| Blog post from user 1 | User 1 blog post content | testuser |
And I log in as "admin"
And I am on site homepage
And I turn editing mode on
@@ -19,15 +22,6 @@ Feature: Comment on a blog entry
| Page contexts | Display throughout the entire site |
And I press "Save changes"
And I log out
And I log in as "testuser"
And I click on "Site pages" "list_item" in the "Navigation" "block"
And I click on "Site blogs" "link" in the "Navigation" "block"
And I follow "Add a new entry"
And I set the following fields to these values:
| Entry title | Blog post from user 1 |
| Blog entry body | User 1 blog post content |
And I press "Save changes"
And I log out
@javascript
Scenario: Commenting on my own blog entry
+4 -11
View File
@@ -8,6 +8,10 @@ Feature: Delete a blog entry
Given the following "users" exist:
| username | firstname | lastname | email |
| testuser | Test | User | moodle@example.com |
And the following "core_blog > entries" exist:
| subject | body | user |
| Blog post one | User 1 blog post content | testuser |
| Blog post two | User 1 blog post content | testuser |
And I log in as "admin"
And I am on site homepage
And I turn editing mode on
@@ -19,17 +23,6 @@ Feature: Delete a blog entry
And I press "Save changes"
And I log out
And I log in as "testuser"
And I click on "Site blogs" "link" in the "Navigation" "block"
And I follow "Add a new entry"
And I set the following fields to these values:
| Entry title | Blog post one |
| Blog entry body | User 1 blog post content |
And I press "Save changes"
And I follow "Add a new entry"
And I set the following fields to these values:
| Entry title | Blog post two |
| Blog entry body | User 1 blog post content |
And I press "Save changes"
And I am on site homepage
And I click on "Site blogs" "link" in the "Navigation" "block"
@@ -0,0 +1,51 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Behat data generator for core_blog.
*
* @package core_blog
* @category test
* @copyright 2022 Noel De Martin
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Behat data generator for core_blog.
*
* @package core_blog
* @category test
* @copyright 2022 Noel De Martin
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_core_blog_generator extends behat_generator_base {
/**
* Get a list of the entities that can be created.
*
* @return array entity name => information about how to generate.
*/
protected function get_creatable_entities(): array {
return [
'entries' => [
'singular' => 'entry',
'datagenerator' => 'entry',
'required' => ['subject', 'body'],
'switchids' => ['user' => 'userid'],
],
];
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Generator for blog area.
*
* @package core_blog
* @category test
* @copyright 2022 Noel De Martin
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/blog/locallib.php');
/**
* Blog module test data generator class
*
* @package core_blog
* @category test
* @copyright 2022 Noel De Martin
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_blog_generator extends component_generator_base {
/**
* Create a blog entry
*
* @param array $data Entry data.
* @return blog_entry Entry instance.
*/
public function create_entry(array $data = []): blog_entry {
$data['publishstate'] = $data['publishstate'] ?? 'site';
$data['summary'] = $data['summary'] ?? $data['body'];
$entry = new blog_entry(null, $data);
$entry->add();
return $entry;
}
}
@@ -0,0 +1,261 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
use core_competency\competency;
use core_competency\competency_framework;
use core_competency\plan;
/**
* Behat data generator for core_competency.
*
* @package core_competency
* @category test
* @copyright 2022 Noel De Martin
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_core_competency_generator extends behat_generator_base {
/**
* Get a list of the entities that Behat can create using the generator step.
*
* @return array
*/
protected function get_creatable_entities(): array {
return [
'competencies' => [
'singular' => 'competency',
'datagenerator' => 'competency',
'required' => ['shortname', 'competencyframework'],
'switchids' => ['competencyframework' => 'competencyframeworkid'],
],
'course_competencies' => [
'singular' => 'course_competency',
'datagenerator' => 'course_competency',
'required' => ['course', 'competency'],
'switchids' => ['course' => 'courseid', 'competency' => 'competencyid'],
],
'frameworks' => [
'singular' => 'framework',
'datagenerator' => 'framework',
'required' => ['shortname'],
'switchids' => ['scale' => 'scaleid'],
],
'plans' => [
'singular' => 'plan',
'datagenerator' => 'plan',
'required' => ['name'],
'switchids' => ['user' => 'userid'],
],
'related_competencies' => [
'singular' => 'related_competency',
'datagenerator' => 'related_competency',
'required' => ['competency', 'relatedcompetency'],
'switchids' => ['competency' => 'competencyid', 'relatedcompetency' => 'relatedcompetencyid'],
],
'user_competency' => [
'singular' => 'user_competency',
'datagenerator' => 'user_competency',
'required' => ['competency', 'user'],
'switchids' => ['competency' => 'competencyid', 'user' => 'userid'],
],
'user_competency_courses' => [
'singular' => 'user_competency_course',
'datagenerator' => 'user_competency_course',
'required' => ['course', 'competency', 'user'],
'switchids' => ['course' => 'courseid', 'competency' => 'competencyid', 'user' => 'userid'],
],
'user_competency_plans' => [
'singular' => 'user_competency_plan',
'datagenerator' => 'user_competency_plan',
'required' => ['plan', 'competency', 'user'],
'switchids' => ['plan' => 'planid', 'competency' => 'competencyid', 'user' => 'userid'],
],
];
}
/**
* Get the competecy framework id using an idnumber.
*
* @param string $idnumber
* @return int The competecy framework id
*/
protected function get_competencyframework_id(string $idnumber): int {
global $DB;
if (!$id = $DB->get_field('competency_framework', 'id', ['idnumber' => $idnumber])) {
throw new Exception('The specified competency framework with idnumber "' . $idnumber . '" could not be found.');
}
return $id;
}
/**
* Get the competecy id using an idnumber.
*
* @param string $idnumber
* @return int The competecy id
*/
protected function get_competency_id(string $idnumber): int {
global $DB;
if (!$id = $DB->get_field('competency', 'id', ['idnumber' => $idnumber])) {
throw new Exception('The specified competency with idnumber "' . $idnumber . '" could not be found.');
}
return $id;
}
/**
* Get the learning plan id using a name.
*
* @param string $name
* @return int The learning plan id
*/
protected function get_plan_id(string $name): int {
global $DB;
if (!$id = $DB->get_field('competency_plan', 'id', ['name' => $name])) {
throw new Exception('The specified learning plan with name "' . $name . '" could not be found.');
}
return $id;
}
/**
* Get the related competecy id using an idnumber.
*
* @param string $idnumber
* @return int The related competecy id
*/
protected function get_relatedcompetency_id(string $idnumber): int {
return $this->get_competency_id($idnumber);
}
/**
* Add a plan.
*
* @param array $data Plan data.
*/
public function process_plan(array $data): void {
$generator = $this->get_data_generator();
$competencyids = $data['competencyids'] ?? [];
unset($data['competencyids']);
$plan = $generator->create_plan($data);
foreach ($competencyids as $competencyid) {
$generator->create_plan_competency([
'planid' => $plan->get('id'),
'competencyid' => $competencyid,
]);
}
}
/**
* Preprocess user competency data.
*
* @param array $data Raw data.
* @return array Processed data.
*/
protected function preprocess_user_competency(array $data): array {
$this->prepare_grading($data);
return $data;
}
/**
* Preprocess user course competency data.
*
* @param array $data Raw data.
* @return array Processed data.
*/
protected function preprocess_user_competency_course(array $data): array {
$this->prepare_grading($data);
return $data;
}
/**
* Preprocess user learning plan competency data.
*
* @param array $data Raw data.
* @return array Processed data.
*/
protected function preprocess_user_competency_plan(array $data): array {
$this->prepare_grading($data);
return $data;
}
/**
* Preprocess plan data.
*
* @param array $data Raw data.
* @return array Processed data.
*/
protected function preprocess_plan(array $data): array {
if (isset($data['competencies'])) {
$competencies = array_map('trim', str_getcsv($data['competencies']));
$data['competencyids'] = array_map([$this, 'get_competency_id'], $competencies);
unset($data['competencies']);
}
global $USER;
return $data + [
'userid' => $USER->id,
'status' => plan::STATUS_ACTIVE,
];
}
/**
* Prepare grading attributes for record data.
*
* @param array $data Record data.
*/
protected function prepare_grading(array &$data): void {
if (!isset($data['grade'])) {
return;
}
global $DB;
$competency = competency::get_record(['id' => $data['competencyid']]);
$competencyframework = competency_framework::get_record(['id' => $competency->get('competencyframeworkid')]);
$scale = $DB->get_field('scale', 'scale', ['id' => $competencyframework->get('scaleid')]);
$grades = array_map('trim', explode(',', $scale));
$grade = array_search($data['grade'], $grades);
if ($grade === false) {
throw new Exception('The grade "'.$data['grade'].'" was not found in the "'.
$competencyframework->get('shortname').'" competency framework.');
}
$data['proficiency'] = true;
$data['grade'] = $grade + 1;
}
/**
* Get the module data generator.
*
* @return core_competency_generator Competency data generator.
*/
protected function get_data_generator(): core_competency_generator {
return $this->componentdatagenerator;
}
}
+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 {
}
}
@@ -34,9 +34,9 @@ Feature: Restrict sections availability through completion or grade conditions
And I set the following fields to these values:
| Enable completion tracking | Yes |
And I press "Save and display"
And I add a "Label" to section "1" and I fill the form with:
| Label text | Test label |
| Completion tracking | Students can manually mark the activity as completed |
And the following "activities" exist:
| activity | course | section | intro | completion | idnumber |
| label | C1 | 1 | Test label | 1 | 1 |
And I add a "Page" to section "2" and I fill the form with:
| Name | Test page name |
| Description | Test page description |
+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"
@@ -14,13 +14,10 @@ Feature: View subfolders in a course in-line
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I add a "Folder" to section "3" and I fill the form with:
| Name | Test folder |
| Display folder contents | On a separate page |
| Show subfolders expanded | |
And I am on the "Test folder" "folder activity" page
And the following "activities" exist:
| activity | course | name | display | showexpanded |
| folder | C1 | Test folder | 0 | 0 |
And I am on the "Test folder" "folder activity" page logged in as "teacher1"
And I press "Edit"
And I press "Create folder"
And I set the field "New folder name" to "Test subfolder 1"
@@ -12,7 +12,7 @@ Feature: We can change what we are viewing on the grader report
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
| student2 | Student | 1 | student2@example.com |
| student2 | Student | 2 | student2@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
@@ -26,19 +26,15 @@ Feature: We can change what we are viewing on the grader report
| assign | user | onlinetext |
| Test assignment name 1 | student1 | This is a submission for assignment 1 |
| Test assignment name 2 | student1 | This is a submission for assignment 2 |
And I am on the "Test assignment name 1" "assign activity" page logged in as student1
Then I should see "Submitted for grading"
And I am on the "Test assignment name 2" "assign activity" page
Then I should see "Submitted for grading"
And I log out
And the following "grade items" exist:
| itemname | grademin | grademax | course |
| Manual grade | 20 | 40 | C1 |
And the following "grade grades" exist:
| gradeitem | user | grade |
| Test assignment name 1 | student1 | 80 |
| Test assignment name 2 | student1 | 90 |
| Manual grade | student1 | 30 |
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "View > Grader report" in the course gradebook
And I turn editing mode on
And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment name 1"
And I give the grade "90.00" to the user "Student 1" for the grade item "Test assignment name 2"
And I press "Save changes"
And I turn editing mode off
@javascript
Scenario: View and minimise the grader report containing hidden activities
@@ -49,13 +45,15 @@ Feature: We can change what we are viewing on the grader report
And I navigate to "View > Grader report" in the course gradebook
And I should see "Test assignment name 1"
And I should see "Test assignment name 2"
And I should see "Manual grade"
And I should see "Course total"
And the following should exist in the "user-grades" table:
| -1- | -4- | -5- | -6- |
| Student 1 | 80 | 90 | 170 |
| -1- | -4- | -5- | -6- | -7- |
| Student 1 | 80 | 90 | 30 | 170 |
And I click on "Change to aggregates only" "link"
And I should not see "Test assignment name 1"
And I should not see "Test assignment name 2"
And I should not see "Manual grade"
And I should see "Course total"
And the following should exist in the "user-grades" table:
| -1- | -4- |
@@ -63,10 +61,11 @@ Feature: We can change what we are viewing on the grader report
And I click on "Change to grades only" "link"
And I should see "Test assignment name 1"
And I should see "Test assignment name 2"
And I should see "Manual grade"
And I should not see "Course total"
And the following should exist in the "user-grades" table:
| -1- | -4- | -5- |
| Student 1 | 80 | 90 |
| -1- | -4- | -5- | -6- |
| Student 1 | 80 | 90 | 30 |
@javascript @skip_chrome_zerosize
Scenario: View and minimise the grader report containing hidden activities without the 'moodle/grade:viewhidden' capability
@@ -84,21 +83,24 @@ Feature: We can change what we are viewing on the grader report
And I navigate to "View > Grader report" in the course gradebook
And I should see "Test assignment name 1"
And I should see "Test assignment name 2"
And I should see "Manual grade"
And I should see "Course total"
And the following should exist in the "user-grades" table:
| -1- | -4- | -5- | -6- |
| Student 1 | 80 | - | 80 |
| -1- | -4- | -5- | -6- | -7- |
| Student 1 | 80 | - | 30 | 105.71 |
And I click on "Change to aggregates only" "link"
And I should not see "Test assignment name 1"
And I should not see "Test assignment name 2"
And I should not see "Manual grade"
And I should see "Course total"
And the following should exist in the "user-grades" table:
| -1- | -4- |
| Student 1 | 80 |
| Student 1 | 105.71 |
And I click on "Change to grades only" "link"
And I should see "Test assignment name 1"
And I should see "Test assignment name 2"
And I should see "Manual grade"
And I should not see "Course total"
And the following should exist in the "user-grades" table:
| -1- | -4- | -5- |
| Student 1 | 80 | - |
| -1- | -4- | -5- | -6- |
| Student 1 | 80 | - | 30 |
+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.';
+1
View File
@@ -363,6 +363,7 @@ $string['europe/jersey'] = 'Europe/Jersey';
$string['europe/kaliningrad'] = 'Europe/Kaliningrad';
$string['europe/kiev'] = 'Europe/Kiev';
$string['europe/kirov'] = 'Europe/Kirov';
$string['europe/kyiv'] = 'Europe/Kyiv';
$string['europe/lisbon'] = 'Europe/Lisbon';
$string['europe/ljubljana'] = 'Europe/Ljubljana';
$string['europe/london'] = 'Europe/London';
@@ -150,6 +150,12 @@ class behat_core_generator extends behat_generator_base {
'required' => ['fullname', 'course'],
'switchids' => ['course' => 'courseid', 'gradecategory' => 'parent'],
],
'grade grades' => [
'singular' => 'grade grade',
'datagenerator' => 'grade_grade',
'required' => ['gradeitem'],
'switchids' => ['user' => 'userid', 'gradeitem' => 'itemid'],
],
'grade items' => [
'singular' => 'grade item',
'datagenerator' => 'grade_item',
@@ -275,11 +281,33 @@ class behat_core_generator extends behat_generator_base {
'required' => ['user', 'course', 'lastaccess'],
'switchids' => ['user' => 'userid', 'course' => 'courseid'],
],
'notifications' => [
'singular' => 'notification',
'datagenerator' => 'notification',
'required' => ['subject', 'userfrom', 'userto'],
'switchids' => ['userfrom' => 'userfromid', 'userto' => 'usertoid'],
],
];
return $entities;
}
/**
* Get the grade item id using a name.
*
* @param string $name
* @return int The grade item id
*/
protected function get_gradeitem_id(string $name): int {
global $DB;
if (!$id = $DB->get_field('grade_items', 'id', ['itemname' => $name])) {
throw new Exception('The specified grade item with name "' . $name . '" could not be found.');
}
return $id;
}
/**
* Remove any empty custom fields, to avoid errors when creating the course.
*
@@ -1009,6 +1037,39 @@ class behat_core_generator extends behat_generator_base {
$DB->insert_record('badge_backpack', $backpack);
}
/**
* Creates notifications to specific user.
*
* @param array $data
* @return void
*/
protected function process_notification(array $data) {
global $DB;
$notification = new stdClass();
$notification->useridfrom = $data['userfromid'];
$notification->useridto = $data['usertoid'];
$notification->subject = $data['subject'];
$notification->fullmessage = $data['subject'] . ' description';
$notification->smallmessage = $data['subject'] . ' description';
$notification->fullmessagehtml = $data['subject'] . ' description';
if ($data['timecreated'] !== 'null') {
$notification->timecreated = $data['timecreated'];
}
if ($data['timeread'] !== 'null') {
$notification->timeread = $data['timeread'];
}
if (!empty($data)) {
$popupnotification = new stdClass();
$popupnotification->notificationid = $DB->insert_record('notifications', $notification);
$DB->insert_record('message_popup_notifications', $popupnotification);
}
}
/**
* Creates user last access data within given courses.
*
@@ -316,6 +316,36 @@ abstract class behat_generator_base {
return $id;
}
/**
* Gets the user id from it's username.
* @throws Exception
* @param string $username
* @return int
*/
protected function get_userfrom_id(string $username) {
global $DB;
if (!$id = $DB->get_field('user', 'id', ['username' => $username])) {
throw new Exception('The specified user with username "' . $username . '" does not exist');
}
return $id;
}
/**
* Gets the user id from it's username.
* @throws Exception
* @param string $username
* @return int
*/
protected function get_userto_id(string $username) {
global $DB;
if (!$id = $DB->get_field('user', 'id', ['username' => $username])) {
throw new Exception('The specified user with username "' . $username . '" does not exist');
}
return $id;
}
/**
* Gets the role id from it's shortname.
* @throws Exception
+1 -1
View File
@@ -111,7 +111,7 @@ abstract class scheduled_task extends task_base {
*/
public function set_minute($minute, $expandr = true) {
if ($minute === 'R' && $expandr) {
$minute = mt_rand(self::HOURMIN, self::HOURMAX);
$minute = mt_rand(self::MINUTEMIN, self::MINUTEMAX);
}
$this->minute = $minute;
}
+13 -7
View File
@@ -506,21 +506,27 @@ function get_users_listing($sort='lastaccess', $dir='ASC', $page=0, $recordsperp
$params = $params + (array)$extraparams;
}
if ($sort) {
$sort = " ORDER BY $sort $dir";
}
// If a context is specified, get extra user fields that the current user
// is supposed to see.
$extrafields = '';
$includedfields = ['id', 'username', 'email', 'firstname', 'lastname', 'city', 'country',
'lastaccess', 'confirmed', 'mnethostid', 'suspended'];
if ($extracontext) {
$extrafields = get_extra_user_fields_sql($extracontext, '', '',
array('id', 'username', 'email', 'firstname', 'lastname', 'city', 'country',
'lastaccess', 'confirmed', 'mnethostid'));
$extrafields = get_extra_user_fields_sql($extracontext, '', '', $includedfields);
}
$namefields = get_all_user_name_fields(true);
$extrafields = "$extrafields, $namefields";
if ($sort) {
$orderbymap = trim($extrafields, ', ');
$orderbymap = array_merge(explode(',', $orderbymap), $includedfields);
$neworderbymap = ['default' => 'lastaccess'];
foreach($orderbymap as $value) {
$neworderbymap[$value] = $value;
}
$sort = get_safe_orderby($neworderbymap, $sort, $dir);
}
// warning: will return UNCONFIRMED USERS
return $DB->get_records_sql("SELECT id, username, email, city, country, lastaccess, confirmed, mnethostid, suspended $extrafields
FROM {user}
+2 -2
View File
@@ -242,8 +242,8 @@ $tasks = array(
array(
'classname' => 'core\task\check_for_updates_task',
'blocking' => 0,
'minute' => '0',
'hour' => '*/2',
'minute' => 'R',
'hour' => 'R',
'day' => '*',
'dayofweek' => '*',
'month' => '*'
+1 -1
View File
@@ -38,7 +38,7 @@ class processor implements \core_analytics\classifier, \core_analytics\regresso
/**
* The required version of the python package that performs all calculations.
*/
const REQUIRED_PIP_PACKAGE_VERSION = '2.6.5';
const REQUIRED_PIP_PACKAGE_VERSION = '2.6.6';
/**
* The python package is installed in a server.
+4 -1
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');
+2 -2
View File
@@ -18,5 +18,5 @@ Note: All this changes need to be reviewed on every upgrade and, if they have
been already applied upstream for the release being used, can be removed
from the list. If still not available upstream, they will need to be re-applied.
- MDL-67114: PHP 7.4 compatibility. Array operations on scalar value.
This corresponds to upstream https://github.com/bobthecow/mustache.php/pull/352
1) If the relevant pull request has not been accepted yet, apply the following commit, so we are able to disable unnecessary rendering:
https://github.com/bobthecow/mustache.php/pull/402/commits/db771014c7e346438f68077813ebdda3fdae12df#
+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
*/
+56 -32
View File
@@ -26,31 +26,34 @@ class Mustache_Compiler
private $entityFlags;
private $charset;
private $strictCallables;
private $disableLambdaRendering;
/**
* Compile a Mustache token parse tree into PHP source code.
*
* @param string $source Mustache Template source code
* @param string $tree Parse tree of Mustache tokens
* @param string $name Mustache Template class name
* @param bool $customEscape (default: false)
* @param string $charset (default: 'UTF-8')
* @param bool $strictCallables (default: false)
* @param int $entityFlags (default: ENT_COMPAT)
* @param string $source Mustache Template source code
* @param array $tree Parse tree of Mustache tokens
* @param string $name Mustache Template class name
* @param bool $customEscape (default: false)
* @param string $charset (default: 'UTF-8')
* @param bool $strictCallables (default: false)
* @param int $entityFlags (default: ENT_COMPAT)
* @param bool $disableLambdaRendering (default: false)
*
* @return string Generated PHP source code
*/
public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT)
public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT, $disableLambdaRendering = false)
{
$this->pragmas = $this->defaultPragmas;
$this->sections = array();
$this->blocks = array();
$this->source = $source;
$this->indentNextLine = true;
$this->customEscape = $customEscape;
$this->entityFlags = $entityFlags;
$this->charset = $charset;
$this->strictCallables = $strictCallables;
$this->pragmas = $this->defaultPragmas;
$this->sections = array();
$this->blocks = array();
$this->source = $source;
$this->indentNextLine = true;
$this->customEscape = $customEscape;
$this->entityFlags = $entityFlags;
$this->charset = $charset;
$this->strictCallables = $strictCallables;
$this->disableLambdaRendering = $disableLambdaRendering;
return $this->writeCode($tree, $name);
}
@@ -286,7 +289,6 @@ class Mustache_Compiler
private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level)
{
$key = $this->block($nodes);
$keystr = var_export($key, true);
$id = var_export($id, true);
return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key);
@@ -321,7 +323,6 @@ class Mustache_Compiler
}
const SECTION_CALL = '
// %s section
$value = $context->%s(%s);%s
$buffer .= $this->section%s($context, $indent, $value);
';
@@ -333,14 +334,8 @@ class Mustache_Compiler
if (%s) {
$source = %s;
$result = call_user_func($value, $source, %s);
if (strpos($result, \'{{\') === false) {
$buffer .= $result;
} else {
$buffer .= $this->mustache
->loadLambda((string) $result%s)
->renderInternal($context);
}
$result = (string) call_user_func($value, $source, %s);%s
$buffer .= $result;
} elseif (!empty($value)) {
$values = $this->isIterable($value) ? $value : array($value);
foreach ($values as $value) {
@@ -354,6 +349,36 @@ class Mustache_Compiler
}
';
const SECTION_RENDER_LAMBDA = '
if (strpos($result, \'{{\') !== false) {
$result = $this->mustache
->loadLambda($result%s)
->renderInternal($context);
}
';
/**
* Helper function to compile section with and without lambda rendering.
*
* @param string $key
* @param string $callable
* @param string $source
* @param string $helper
* @param string $delims
* @param string $content
*
* @return string section code
*/
private function getSection($key, $callable, $source, $helper, $delims, $content)
{
$render = '';
if (!$this->disableLambdaRendering) {
$render = sprintf($this->prepare(self::SECTION_RENDER_LAMBDA, 2), $delims);
}
return sprintf($this->prepare(self::SECTION), $key, $callable, $source, $helper, $render, $content);
}
/**
* Generate Mustache Template section PHP source.
*
@@ -385,18 +410,17 @@ class Mustache_Compiler
$key = ucfirst(md5($delims . "\n" . $source));
if (!isset($this->sections[$key])) {
$this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $helper, $delims, $this->walk($nodes, 2));
$this->sections[$key] = $this->getSection($key, $callable, $source, $helper, $delims, $this->walk($nodes, 2));
}
$method = $this->getFindMethod($id);
$id = var_export($id, true);
$filters = $this->getFilters($filters, $level);
return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $method, $id, $filters, $key);
return sprintf($this->prepare(self::SECTION_CALL, $level), $method, $id, $filters, $key);
}
const INVERTED_SECTION = '
// %s inverted section
$value = $context->%s(%s);%s
if (empty($value)) {
%s
@@ -419,7 +443,7 @@ class Mustache_Compiler
$id = var_export($id, true);
$filters = $this->getFilters($filters, $level);
return sprintf($this->prepare(self::INVERTED_SECTION, $level), $id, $method, $id, $filters, $this->walk($nodes, $level));
return sprintf($this->prepare(self::INVERTED_SECTION, $level), $method, $id, $filters, $this->walk($nodes, $level));
}
const PARTIAL_INDENT = ', $indent . %s';
@@ -507,7 +531,7 @@ class Mustache_Compiler
const VARIABLE = '
$value = $this->resolveValue($context->%s(%s), $context);%s
$buffer .= %s%s;
$buffer .= %s($value === null ? \'\' : %s);
';
/**
+22 -11
View File
@@ -23,8 +23,8 @@
*/
class Mustache_Engine
{
const VERSION = '2.13.0';
const SPEC_VERSION = '1.1.2';
const VERSION = '2.14.1';
const SPEC_VERSION = '1.2.2';
const PRAGMA_FILTERS = 'FILTERS';
const PRAGMA_BLOCKS = 'BLOCKS';
@@ -53,6 +53,7 @@ class Mustache_Engine
private $charset = 'UTF-8';
private $logger;
private $strictCallables = false;
private $disableLambdaRendering = false;
private $pragmas = array();
private $delimiters;
@@ -130,6 +131,11 @@ class Mustache_Engine
* // This currently defaults to false, but will default to true in v3.0.
* 'strict_callables' => true,
*
* // Do not render the output of lambdas. Use this to prevent repeated rendering if the lambda already
* // takes care of rendering its content. This helps protect against mustache code injection when user
* // input is passed directly into the template. Defaults to false.
* 'disable_lambda_rendering' => true,
*
* // Enable pragmas across all templates, regardless of the presence of pragma tags in the individual
* // templates.
* 'pragmas' => [Mustache_Engine::PRAGMA_FILTERS],
@@ -204,6 +210,10 @@ class Mustache_Engine
$this->strictCallables = $options['strict_callables'];
}
if (isset($options['disable_lambda_rendering'])) {
$this->disableLambdaRendering = $options['disable_lambda_rendering'];
}
if (isset($options['delimiters'])) {
$this->delimiters = $options['delimiters'];
}
@@ -624,14 +634,15 @@ class Mustache_Engine
//
// Keep this list in alphabetical order :)
$chunks = array(
'charset' => $this->charset,
'delimiters' => $this->delimiters ? $this->delimiters : '{{ }}',
'entityFlags' => $this->entityFlags,
'escape' => isset($this->escape) ? 'custom' : 'default',
'key' => ($source instanceof Mustache_Source) ? $source->getKey() : 'source',
'pragmas' => $this->getPragmas(),
'strictCallables' => $this->strictCallables,
'version' => self::VERSION,
'charset' => $this->charset,
'delimiters' => $this->delimiters ? $this->delimiters : '{{ }}',
'entityFlags' => $this->entityFlags,
'escape' => isset($this->escape) ? 'custom' : 'default',
'key' => ($source instanceof Mustache_Source) ? $source->getKey() : 'source',
'pragmas' => $this->getPragmas(),
'strictCallables' => $this->strictCallables,
'disableLambdaRendering' => $this->disableLambdaRendering,
'version' => self::VERSION,
);
$key = json_encode($chunks);
@@ -810,7 +821,7 @@ class Mustache_Engine
$compiler = $this->getCompiler();
$compiler->setPragmas($this->getPragmas());
return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags);
return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags, $this->disableLambdaRendering);
}
/**
@@ -38,7 +38,7 @@ class Mustache_Source_FilesystemSource implements Mustache_Source
/**
* Get the Source key (used to generate the compiled class name).
*
* @throws RuntimeException when a source file cannot be read
* @throws Mustache_Exception_RuntimeException when a source file cannot be read
*
* @return string
*/
@@ -54,7 +54,7 @@ class Mustache_Source_FilesystemSource implements Mustache_Source
}
if ($this->stat === false) {
throw new RuntimeException(sprintf('Failed to read source file "%s".', $this->fileName));
throw new Mustache_Exception_RuntimeException(sprintf('Failed to read source file "%s".', $this->fileName));
}
foreach ($this->statProps as $prop) {
+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);
}
+4 -1
View File
@@ -129,7 +129,10 @@ class renderer_base {
// Don't allow the JavaScript helper to be executed from within another
// helper. If it's allowed it can be used by users to inject malicious
// JS into the page.
'blacklistednestedhelpers' => ['js']));
'blacklistednestedhelpers' => ['js'],
// Disable lambda rendering - content in helpers is already rendered, no need to render it again.
'disable_lambda_rendering' => true,
));
}
+167
View File
@@ -1002,6 +1002,57 @@ EOD;
return $gradecategory->get_record_data();
}
/**
* Create a grade_grade.
*
* @param array $record
* @return grade_grade the grade record
*/
public function create_grade_grade(?array $record = null): grade_grade {
global $DB, $USER;
$item = $DB->get_record('grade_items', ['id' => $record['itemid']]);
$userid = $record['userid'] ?? $USER->id;
unset($record['itemid']);
unset($record['userid']);
if ($item->itemtype === 'mod') {
$cm = get_coursemodule_from_instance($item->itemmodule, $item->iteminstance);
$module = new $item->itemmodule(context_module::instance($cm->id), $cm, false);
$record['attemptnumber'] = $record['attemptnumber'] ?? 0;
$module->save_grade($userid, (object) $record);
$grade = grade_grade::fetch(['userid' => $userid, 'itemid' => $item->id]);
} else {
$grade = grade_grade::fetch(['userid' => $userid, 'itemid' => $item->id]);
$record['rawgrade'] = $record['rawgrade'] ?? $record['grade'] ?? null;
$record['finalgrade'] = $record['finalgrade'] ?? $record['grade'] ?? null;
unset($record['grade']);
if ($grade) {
$fields = $grade->required_fields + array_keys($grade->optional_fields);
foreach ($fields as $field) {
$grade->{$field} = $record[$field] ?? $grade->{$field};
}
$grade->update();
} else {
$record['userid'] = $userid;
$record['itemid'] = $item->id;
$grade = new grade_grade($record, false);
$grade->insert();
}
}
return $grade;
}
/**
* Create a grade_item.
*
@@ -1256,4 +1307,120 @@ EOD;
"data generators yet. Class {$classname} not found.");
}
/**
* Create a new category for custom profile fields.
*
* @param array $data Array with 'name' and optionally 'sortorder'
* @return \stdClass New category object
*/
public function create_custom_profile_field_category(array $data): \stdClass {
global $DB;
// Pick next sortorder if not defined.
if (!array_key_exists('sortorder', $data)) {
$data['sortorder'] = (int)$DB->get_field_sql('SELECT MAX(sortorder) FROM {user_info_category}') + 1;
}
$category = (object)[
'name' => $data['name'],
'sortorder' => $data['sortorder']
];
$category->id = $DB->insert_record('user_info_category', $category);
return $category;
}
/**
* Creates a new custom profile field.
*
* Optional fields are:
*
* categoryid (or use 'category' to specify by name). If you don't specify
* either, it will add the field to a 'Testing' category, which will be created for you if
* necessary.
*
* sortorder (if you don't specify this, it will pick the next one in the category).
*
* all the other database fields (if you don't specify this, it will pick sensible defaults
* based on the data type).
*
* @param array $data Array with 'datatype', 'shortname', and 'name'
* @return \stdClass Database object from the user_info_field table
*/
public function create_custom_profile_field(array $data): \stdClass {
global $DB, $CFG;
require_once($CFG->dirroot . '/user/profile/lib.php');
// Set up category if necessary.
if (!array_key_exists('categoryid', $data)) {
if (array_key_exists('category', $data)) {
$data['categoryid'] = $DB->get_field('user_info_category', 'id',
['name' => $data['category']], MUST_EXIST);
} else {
// Make up a 'Testing' category or use existing.
$data['categoryid'] = $DB->get_field('user_info_category', 'id', ['name' => 'Testing']);
if (!$data['categoryid']) {
$created = $this->create_custom_profile_field_category(['name' => 'Testing']);
$data['categoryid'] = $created->id;
}
}
}
// Pick sort order if necessary.
if (!array_key_exists('sortorder', $data)) {
$data['sortorder'] = (int)$DB->get_field_sql(
'SELECT MAX(sortorder) FROM {user_info_field} WHERE categoryid = ?',
[$data['categoryid']]) + 1;
}
// Defaults for other values.
$defaults = [
'description' => '',
'descriptionformat' => 0,
'required' => 0,
'locked' => 0,
'visible' => PROFILE_VISIBLE_ALL,
'forceunique' => 0,
'signup' => 0,
'defaultdata' => '',
'defaultdataformat' => 0,
'param1' => '',
'param2' => '',
'param3' => '',
'param4' => '',
'param5' => ''
];
// Type-specific defaults for other values.
$typedefaults = [
'text' => [
'param1' => 30,
'param2' => 2048
],
'menu' => [
'param1' => "Yes\nNo",
'defaultdata' => 'No'
],
'datetime' => [
'param1' => '2010',
'param2' => '2015',
'param3' => 1
],
'checkbox' => [
'defaultdata' => 0
]
];
foreach ($typedefaults[$data['datatype']] ?? [] as $field => $value) {
$defaults[$field] = $value;
}
foreach ($defaults as $field => $value) {
if (!array_key_exists($field, $data)) {
$data[$field] = $value;
}
}
$data['id'] = $DB->insert_record('user_info_field', $data);
return (object)$data;
}
}
-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) {
@@ -90,7 +90,7 @@ class core_renderer_template_exploit_testcase extends advanced_testcase {
'test1' => $norender,
],
'js' => 'some nasty JS',
'expected' => 'core, move, some text',
'expected' => 'core, move, some text {{}}',
'include' => false
],
'js helper not nested' => [
@@ -133,7 +133,7 @@ class core_renderer_template_exploit_testcase extends advanced_testcase {
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move,',
'expected' => 'core, move, {{}}',
'include' => false
],
'js in context double depth with single render' => [
@@ -149,7 +149,7 @@ class core_renderer_template_exploit_testcase extends advanced_testcase {
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move, {{#js}} some nasty JS {{/js}}',
'expected' => 'core, move, {{second}}',
'include' => false
],
'js in context double depth with recursive render' => [
@@ -269,24 +269,7 @@ class core_renderer_template_exploit_testcase extends advanced_testcase {
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move,',
'include' => false
],
'partial double nested with js from context single render' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{foo}}{{/testpix}}',
'test2' => '{{#js}} some nasty JS {{/js}}',
],
'torender' => 'test',
'context' => [
'foo' => '{{{bar}}}',
'bar' => '{{> test2}}'
],
'helpers' => [
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move, {{> test2}}',
'expected' => 'core, move, {{&gt; test2}}',
'include' => false
],
'partial double nested with js from context recursive render' => [
+102
View File
@@ -684,6 +684,108 @@ class core_datalib_testcase extends advanced_testcase {
}
}
/**
* Tests the get_users_listing function.
*/
public function test_get_users_listing(): void {
global $DB;
$this->resetAfterTest();
$generator = $this->getDataGenerator();
// Set up profile field.
$generator->create_custom_profile_field(['datatype' => 'text',
'shortname' => 'specialid', 'name' => 'Special user id']);
// Set up the show user identity option.
set_config('showuseridentity', 'department');
// Get all the existing user ids (we're going to remove these from test results).
$existingids = array_fill_keys($DB->get_fieldset_select('user', 'id', '1 = 1'), true);
// Create some test user accounts.
$userids = [];
foreach (['a', 'b', 'c', 'd'] as $key) {
$record = [
'username' => 'user_' . $key,
'firstname' => $key . '_first',
'lastname' => 'last_' . $key,
'department' => 'department_' . $key,
'lastaccess' => ord($key)
];
$user = $generator->create_user($record);
$userids[] = $user->id;
}
// Check default result with no parameters.
$results = get_users_listing();
$results = array_diff_key($results, $existingids);
// It should return all the results in order.
$this->assertEquals($userids, array_keys($results));
// Results should have some general fields and name fields, check some samples.
$this->assertEquals('user_a', $results[$userids[0]]->username);
$this->assertEquals('user_a@example.com', $results[$userids[0]]->email);
$this->assertEquals(1, $results[$userids[0]]->confirmed);
$this->assertEquals('a_first', $results[$userids[0]]->firstname);
$this->assertObjectHasAttribute('firstnamephonetic', $results[$userids[0]]);
// Should not have department because no context specified.
$this->assertObjectNotHasAttribute('department', $results[$userids[0]]);
// Check sorting.
$results = get_users_listing('username', 'DESC');
$results = array_diff_key($results, $existingids);
$this->assertEquals([$userids[3], $userids[2], $userids[1], $userids[0]], array_keys($results));
// Check default fallback sort field works as expected.
$results = get_users_listing('blah2', 'ASC');
$results = array_diff_key($results, $existingids);
$this->assertEquals([$userids[0], $userids[1], $userids[2], $userids[3]], array_keys($results));
// Check default fallback sort direction works as expected.
$results = get_users_listing('lastaccess', 'blah2');
$results = array_diff_key($results, $existingids);
$this->assertEquals([$userids[0], $userids[1], $userids[2], $userids[3]], array_keys($results));
// Add the options to showuseridentity and check it returns those fields but only if you
// specify a context AND have permissions.
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', '', null,
\context_system::instance());
$this->assertObjectNotHasAttribute('department', $results[$userids[0]]);
$this->setAdminUser();
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', '', null,
\context_system::instance());
$this->assertEquals('department_a', $results[$userids[0]]->department);
// Check search (full name, email, username).
$results = get_users_listing('lastaccess', 'asc', 0, 0, 'b_first last_b');
$this->assertEquals([$userids[1]], array_keys($results));
$results = get_users_listing('lastaccess', 'asc', 0, 0, 'c@example');
$this->assertEquals([$userids[2]], array_keys($results));
$results = get_users_listing('lastaccess', 'asc', 0, 0, 'user_d');
$this->assertEquals([$userids[3]], array_keys($results));
// Check first and last initial restriction (all the test ones have same last initial).
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', 'C');
$this->assertEquals([$userids[2]], array_keys($results));
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', 'L');
$results = array_diff_key($results, $existingids);
$this->assertEquals($userids, array_keys($results));
// Check the extra where clause, either with the 'u.' prefix or not.
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', 'id IN (:x,:y)',
['x' => $userids[1], 'y' => $userids[3]]);
$results = array_diff_key($results, $existingids);
$this->assertEquals([$userids[1], $userids[3]], array_keys($results));
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', 'id IN (:x,:y)',
['x' => $userids[1], 'y' => $userids[3]]);
$results = array_diff_key($results, $existingids);
$this->assertEquals([$userids[1], $userids[3]], array_keys($results));
}
/**
* Data provider for test_get_safe_orderby().
*
+10 -2
View File
@@ -1679,6 +1679,7 @@ EOF;
}
// Burst up to the capacity and make sure that the bucket allows it.
$burststart = microtime();
for ($i = 0; $i < $capacity; $i++) {
if ($i) {
sleep(1); // A little delay so we have different timemodified value for files.
@@ -1693,9 +1694,16 @@ EOF;
// The bucket should be full after bursting.
$this->assertTrue(file_is_draft_areas_limit_reached($user->id));
// Calculate the time taken to burst up the bucket capacity.
$timetaken = microtime_diff($burststart, microtime());
// The bucket leaks so it shouldn't be full after a certain time.
// Reiterating that this test could have been faster if MDL-37327 was implemented.
sleep(ceil(1 / $leak) - ($capacity - 1));
// Items are added into the bucket at the rate of 1 item per second.
// One item leaks from the bucket every 1/$leak seconds.
// So it takes 1/$leak - ($capacity-1) seconds for the bucket to leak one item and not be full anymore.
$milliseconds = ceil(1000000 * ((1 / $leak) - ($capacity - 1)) - ($timetaken * 1000));
usleep($milliseconds);
$this->assertFalse(file_is_draft_areas_limit_reached($user->id));
// Only one item was leaked from the bucket. So the bucket should become full again if we add a single item to it.
+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
@@ -30,6 +30,7 @@ use \core_privacy\local\metadata\collection;
use \core_privacy\local\request\contextlist;
use \core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\userlist;
use core_privacy\local\request\writer;
use \core_privacy\local\request\approved_userlist;
/**
@@ -42,6 +43,7 @@ use \core_privacy\local\request\approved_userlist;
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\core_userlist_provider,
\core_privacy\local\request\user_preference_provider,
\core_privacy\local\request\plugin\provider {
/**
@@ -125,4 +127,21 @@ class provider implements
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
}
/**
* Export all user preferences for the plugin
*
* @param int $userid
*/
public static function export_user_preferences(int $userid) {
$preference = get_user_preferences('message_processor_email_email', null, $userid);
if (!empty($preference)) {
writer::export_user_preference(
'message_email',
'email',
$preference,
get_string('privacy:preference:email', 'message_email')
);
}
}
}
@@ -43,6 +43,7 @@ $string['privacy:metadata:replyto'] = 'The email address to reply to.';
$string['privacy:metadata:replytoname'] = 'Name of reply to recipient.';
$string['privacy:metadata:subject'] = 'The subject line of the message.';
$string['privacy:metadata:userfrom'] = 'The user sending the message.';
$string['privacy:preference:email'] = 'Preferred email notification address';
$string['tasksendemail'] = 'Messages digest mailings';
// Deprecated since Moodle 3.9.
+44 -12
View File
@@ -13,28 +13,27 @@
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Base class for unit tests for message_email.
*
* @package message_email
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
namespace message_email\privacy;
use context_system;
use core_message_external;
use core_privacy\local\request\writer;
use core_privacy\tests\provider_testcase;
use \core_privacy\tests\provider_testcase;
/**
* Unit tests for message\output\email\classes\privacy\provider.php
*
* @package message_email
* @covers \message_email\privacy\provider
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class message_email_testcase extends provider_testcase {
class provider_test extends provider_testcase {
/**
* Basic setup for these tests.
*/
public function setUp() {
public function setUp(): void {
$this->resetAfterTest(true);
}
@@ -56,5 +55,38 @@ class message_email_testcase extends provider_testcase {
$contextlist = \message_email\privacy\provider::get_contexts_for_userid($user->id);
$this->assertEmpty($contextlist);
}
}
/**
* Test exporting user preferences
*/
public function test_export_user_preferences(): void {
global $CFG;
require_once("{$CFG->dirroot}/message/externallib.php");
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Submit configuration form, which adds the preferences..
core_message_external::message_processor_config_form($user->id, 'email', [
[
'name' => 'email_email',
'value' => 'alternate@example.com',
],
]);
// Switch to admin user (so we can validate preferences of the correct user are being exported).
$this->setAdminUser();
provider::export_user_preferences($user->id);
$writer = writer::with_context(context_system::instance());
$this->assertTrue($writer->has_any_data());
$preferences = $writer->get_user_preferences('message_email');
$this->assertNotEmpty($preferences->email);
$this->assertEquals('alternate@example.com', $preferences->email->value);
$this->assertEquals(get_string('privacy:preference:email', 'message_email'), $preferences->email->description);
}
}
@@ -5,61 +5,41 @@ Feature: Notification popover unread notifications
I am notified about relevant events in Moodle
Background:
# This will make sure popup notifications are enabled and create
# two assignment notifications. One for the student submitting their
# assignment and another for the teacher grading it.
Given the following "courses" exist:
| fullname | shortname | category | groupmode |
| Course 1 | C1 | 0 | 1 |
# Make sure the popup notifications are enabled for assignments.
And the following config values are set as admin:
| popup_provider_mod_assign_assign_notification_permitted | permitted | message |
| message_provider_mod_assign_assign_notification_loggedin | popup | message |
| message_provider_mod_assign_assign_notification_loggedoff | popup | message |
And the following "users" exist:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "activity" exists:
| activity | assign |
| course | C1 |
| name | Test assignment name |
| assignsubmission_onlinetext_enabled | 1 |
| assignsubmission_file_enabled | 0 |
| submissiondrafts | 0 |
# This should generate a notification.
And the following "mod_assign > submissions" exist:
| assign | user | onlinetext |
| Test assignment name | student1 | I'm the student1 submission |
| student2 | Student | 2 | student2@example.com |
# This should generate some notifications
And the following "notifications" exist:
| subject | userfrom | userto | timecreated | timeread |
| Test 01 | student2 | student1 | 1654587996 | null |
| Test 02 | student2 | student1 | 1654587997 | null |
Scenario: Notification popover shows correct unread count
When I log in as "student1"
Given I log in as "student1"
# Confirm the popover is saying 1 unread notifications.
Then I should see "1" in the "#nav-notification-popover-container [data-region='count-container']" "css_element"
And I should see "2" in the "#nav-notification-popover-container [data-region='count-container']" "css_element"
# Open the popover.
And I open the notification popover
# Confirm the submission notification is visible.
And I should see "You have submitted your assignment submission for Test assignment name" in the "#nav-notification-popover-container" "css_element"
When I open the notification popover
# Confirm the notifications are visible.
Then I should see "Test 01" in the "#nav-notification-popover-container" "css_element"
And I should see "Test 02" in the "#nav-notification-popover-container" "css_element"
@_bug_phantomjs
Scenario: Clicking a notification marks it as read
When I log in as "student1"
# Open the popover.
Given I log in as "student1"
# Open the notifications.
When I open the notification popover
And I follow "Test 01"
And I open the notification popover
# Click on the submission notification.
And I follow "You have submitted your assignment submission for Test assignment name"
And I follow "Test 02"
# Confirm the count element is hidden (i.e. there are no unread notifications).
Then "[data-region='count-container']" "css_element" in the "#nav-notification-popover-container" "css_element" should not be visible
Scenario: Mark all notifications as read
When I log in as "student1"
# Open the popover.
And I open the notification popover
# Click the mark all as read button.
Given I log in as "student1"
When I open the notification popover
And I click on "Mark all as read" "link" in the "#nav-notification-popover-container" "css_element"
# Refresh the page to make sure we send a new request for the unread count.
And I reload the page
+1 -1
View File
@@ -632,7 +632,7 @@ class pdf extends TcpdfFpdi {
$gsexec = \escapeshellarg($CFG->pathtogs);
$tempdstarg = \escapeshellarg($tempdst);
$tempsrcarg = \escapeshellarg($tempsrc);
$command = "$gsexec -q -sDEVICE=pdfwrite -dBATCH -dNOPAUSE -sOutputFile=$tempdstarg $tempsrcarg";
$command = "$gsexec -q -sDEVICE=pdfwrite -dSAFER -dBATCH -dNOPAUSE -sOutputFile=$tempdstarg $tempsrcarg";
exec($command);
if (!file_exists($tempdst)) {
// Something has gone wrong in the conversion.
+17 -22
View File
@@ -24,32 +24,27 @@ Feature: Users can view and search database entries
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| data | Test database name | Database intro | C1 | data1 |
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I add a "Text input" field to "Test database name" database and I fill the form with:
| Field name | Test field name |
| Field description | Test field description |
And I add a "Text input" field to "Test database name" database and I fill the form with:
| Field name | Test field 2 name |
| Field description | Test field 2 description |
# To generate the default templates.
And I follow "Templates"
And I log out
And the following "mod_data > fields" exist:
| database | type | name | description |
| data1 | text | Test field name | Test field description |
| data1 | text | Test field 2 name | Test field 2 description |
And the following "mod_data > templates" exist:
| database | name |
| data1 | singletemplate |
| data1 | listtemplate |
| data1 | addtemplate |
| data1 | asearchtemplate |
| data1 | rsstemplate |
Scenario: Students can add view, list and search entries
Given I log in as "student1"
Given the following "mod_data > entries" exist:
| database | Test field name | Test field 2 name |
| data1 | Student entry 1 | |
| data1 | Student entry 2 | |
| data1 | Student entry 3 | |
When I log in as "student1"
And I am on "Course 1" course homepage
And I follow "Test database name"
And I add an entry to "Test database name" database with:
| Test field name | Student entry 1 |
And I press "Save and add another"
And I add an entry to "Test database name" database with:
| Test field name | Student entry 2 |
And I press "Save and add another"
And I add an entry to "Test database name" database with:
| Test field name | Student entry 3 |
And I press "Save and view"
And I follow "Test database name"
Then I should see "Student entry 1"
And I should see "Student entry 2"
And I should see "Student entry 3"
@@ -0,0 +1,136 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Behat data generator for mod_data.
*
* @package mod_data
* @category test
* @copyright 2022 Noel De Martin
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_mod_data_generator extends behat_generator_base {
/**
* Get a list of the entities that Behat can create using the generator step.
*
* @return array
*/
protected function get_creatable_entities(): array {
return [
'entries' => [
'singular' => 'entry',
'datagenerator' => 'entry',
'required' => ['database'],
'switchids' => ['database' => 'databaseid'],
],
'fields' => [
'singular' => 'field',
'datagenerator' => 'field',
'required' => ['database', 'type', 'name'],
'switchids' => ['database' => 'databaseid'],
],
'templates' => [
'singular' => 'template',
'datagenerator' => 'template',
'required' => ['database', 'name'],
'switchids' => ['database' => 'databaseid'],
],
];
}
/**
* Get the database id using an activity idnumber.
*
* @param string $idnumber
* @return int The database id
*/
protected function get_database_id(string $idnumber): int {
$cm = $this->get_cm_by_activity_name('data', $idnumber);
return $cm->instance;
}
/**
* Add an entry.
*
* @param array $data Entry data.
*/
public function process_entry(array $data): void {
global $DB;
$database = $DB->get_record('data', ['id' => $data['databaseid']], '*', MUST_EXIST);
unset($data['databaseid']);
$data = array_reduce(array_keys($data), function ($fields, $fieldname) use ($data, $database) {
global $DB;
$field = $DB->get_record('data_fields', ['name' => $fieldname, 'dataid' => $database->id], 'id', MUST_EXIST);
$fields[$field->id] = $data[$fieldname];
return $fields;
}, []);
$this->get_data_generator()->create_entry($database, $data);
}
/**
* Add a field.
*
* @param array $data Field data.
*/
public function process_field(array $data): void {
global $DB;
$database = $DB->get_record('data', ['id' => $data['databaseid']], '*', MUST_EXIST);
unset($data['databaseid']);
$this->get_data_generator()->create_field((object) $data, $database);
}
/**
* Add a template.
*
* @param array $data Template data.
*/
public function process_template(array $data): void {
global $DB;
$database = $DB->get_record('data', ['id' => $data['databaseid']], '*', MUST_EXIST);
if (empty($data['content'])) {
data_generate_default_template($database, $data['name']);
} else {
$newdata = new stdClass();
$newdata->id = $database->id;
$newdata->{$data['name']} = $data['content'];
$DB->update_record('data', $newdata);
}
}
/**
* Get the module data generator.
*
* @return mod_data_generator Database data generator.
*/
protected function get_data_generator(): mod_data_generator {
return $this->componentdatagenerator;
}
}
+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);
}
}
@@ -42,15 +42,36 @@ Feature: Posting to all groups in a separate group discussion is restricted to u
And the following "activities" exist:
| activity | name | intro | course | idnumber | groupmode |
| forum | Standard forum name | Standard forum description | C1 | sepgroups | 1 |
And the following "mod_forum > discussions" exist:
| forum | name | subject | message | group |
| sepgroups | Initial Disc ALL | Initial Disc ALL | Disc ALL content | All participants |
| sepgroups | Initial Disc G1 | Initial Disc G1 | Disc G1 content | G1 |
| sepgroups | Initial Disc G2 | Initial Disc G2 | Disc G2 content | G2 |
| sepgroups | Initial Disc G3 | Initial Disc G3 | Disc G3 content | G3 |
Scenario: Teacher with accessallgroups can view all groups
Given I log in as "teacher1"
And I am on "Course 1" course homepage
When I follow "Standard forum name"
Then the "Separate groups" select box should contain "All participants"
Then the "Separate groups" select box should contain "Group A"
Then the "Separate groups" select box should contain "Group B"
Then the "Separate groups" select box should contain "Group C"
And the "Separate groups" select box should contain "Group A"
And the "Separate groups" select box should contain "Group B"
And the "Separate groups" select box should contain "Group C"
And I select "All participants" from the "Separate groups" singleselect
And I should see "Initial Disc ALL"
And I should see "Initial Disc G1"
And I should see "Initial Disc G2"
And I should see "Initial Disc G2"
And I select "Group A" from the "Separate groups" singleselect
And I should see "Initial Disc ALL"
And I should see "Initial Disc G1"
But I should not see "Initial Disc G2"
And I should not see "Initial Disc G3"
And I select "Group B" from the "Separate groups" singleselect
And I should see "Initial Disc ALL"
And I should see "Initial Disc G2"
But I should not see "Initial Disc G1"
And I should not see "Initial Disc G3"
Scenario: Teacher with accessallgroups can select any group when posting
Given I log in as "teacher1"
@@ -249,14 +270,7 @@ Feature: Posting to all groups in a separate group discussion is restricted to u
And I should see "Post a copy to all groups"
Scenario: Students can view all participants discussions in separate groups mode
Given I log in as "teacher1"
Given I log in as "student1"
And I am on "Course 1" course homepage
When I add a new discussion to "Standard forum name" forum with:
| Subject | Forum post to all participants |
| Message | This is the body |
| Group | All participants |
And I log out
And I log in as "student1"
And I am on "Course 1" course homepage
And I follow "Standard forum name"
Then I should see "Forum post to all participants"
When I follow "Standard forum name"
Then I should see "Initial Disc ALL"
@@ -33,15 +33,36 @@ Feature: Posting to all groups in a visible group discussion is restricted to us
And the following "activities" exist:
| activity | name | intro | course | idnumber | groupmode |
| forum | Standard forum name | Standard forum description | C1 | groups | 2 |
And the following "mod_forum > discussions" exist:
| forum | name | subject | message | group |
| groups | Initial Disc ALL | Initial Disc ALL | Disc ALL content | All participants |
| groups | Initial Disc G1 | Initial Disc G1 | Disc G1 content | G1 |
| groups | Initial Disc G2 | Initial Disc G2 | Disc G2 content | G2 |
| groups | Initial Disc G3 | Initial Disc G3 | Disc G3 content | G3 |
Scenario: Teacher with accessallgroups can view all groups
Given I log in as "teacher1"
And I am on "Course 1" course homepage
When I follow "Standard forum name"
Then the "Visible groups" select box should contain "All participants"
Then the "Visible groups" select box should contain "Group A"
Then the "Visible groups" select box should contain "Group B"
Then the "Visible groups" select box should contain "Group C"
And the "Visible groups" select box should contain "Group A"
And the "Visible groups" select box should contain "Group B"
And the "Visible groups" select box should contain "Group C"
And I select "All participants" from the "Visible groups" singleselect
And I should see "Initial Disc ALL"
And I should see "Initial Disc G1"
And I should see "Initial Disc G2"
And I should see "Initial Disc G2"
And I select "Group A" from the "Visible groups" singleselect
And I should see "Initial Disc ALL"
And I should see "Initial Disc G1"
But I should not see "Initial Disc G2"
And I should not see "Initial Disc G3"
And I select "Group B" from the "Visible groups" singleselect
And I should see "Initial Disc ALL"
And I should see "Initial Disc G2"
But I should not see "Initial Disc G1"
And I should not see "Initial Disc G3"
Scenario: Teacher with accessallgroups can select any group when posting
Given I log in as "teacher1"
@@ -150,9 +171,24 @@ Feature: Posting to all groups in a visible group discussion is restricted to us
And I am on "Course 1" course homepage
When I follow "Standard forum name"
Then the "Visible groups" select box should contain "All participants"
Then the "Visible groups" select box should contain "Group A"
Then the "Visible groups" select box should contain "Group B"
Then the "Visible groups" select box should contain "Group C"
And the "Visible groups" select box should contain "Group A"
And the "Visible groups" select box should contain "Group B"
And the "Visible groups" select box should contain "Group C"
And I select "All participants" from the "Visible groups" singleselect
And I should see "Initial Disc ALL"
And I should see "Initial Disc G1"
And I should see "Initial Disc G2"
And I should see "Initial Disc G2"
And I select "Group A" from the "Visible groups" singleselect
And I should see "Initial Disc ALL"
And I should see "Initial Disc G1"
But I should not see "Initial Disc G2"
And I should not see "Initial Disc G3"
And I select "Group B" from the "Visible groups" singleselect
And I should see "Initial Disc ALL"
And I should see "Initial Disc G2"
But I should not see "Initial Disc G1"
And I should not see "Initial Disc G3"
Scenario: Students in one group can only post in their group
Given I log in as "student1"
@@ -35,7 +35,7 @@ class behat_mod_forum_generator extends behat_generator_base {
'singular' => 'discussion',
'datagenerator' => 'discussion',
'required' => ['forum'],
'switchids' => ['forum' => 'forumid', 'user' => 'userid'],
'switchids' => ['forum' => 'forumid', 'user' => 'userid', 'group' => 'groupid'],
],
'posts' => [
'singular' => 'post',
@@ -62,6 +62,21 @@ class behat_mod_forum_generator extends behat_generator_base {
return $id;
}
/**
* Gets the group id from it's idnumber. It allows using 'All participants' as idnumber.
*
* @throws Exception
* @param string $idnumber
* @return int
*/
protected function get_group_id($idnumber): int {
if ($idnumber === 'All participants') {
return -1;
}
return parent::get_group_id($idnumber);
}
/**
* Preprocess discussion data.
*
+25 -5
View File
@@ -332,15 +332,24 @@ class manager {
*
* @since Moodle 3.9.7
* @param bool $allpotentialusers if true, the join will return all active users, not only the ones with attempts.
* @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
* @return sql_join the active users attempts join
*/
public function get_active_users_join(bool $allpotentialusers = false): sql_join {
public function get_active_users_join(bool $allpotentialusers = false, $currentgroup = false): sql_join {
// Only valid users counts. By default, all users with submit capability are considered potential ones.
$context = $this->get_context();
$coursemodule = $this->get_coursemodule();
// Ensure user can view users from all groups.
if ($currentgroup === 0 && $coursemodule->effectivegroupmode == SEPARATEGROUPS
&& !has_capability('moodle/site:accessallgroups', $context)) {
return new sql_join('', '1=2', [], true);
}
// We want to present all potential users.
$capjoin = get_enrolled_with_capabilities_join($context, '', 'mod/h5pactivity:view');
$capjoin = get_enrolled_with_capabilities_join($context, '', 'mod/h5pactivity:view', $currentgroup);
if ($capjoin->cannotmatchanyrows) {
return $capjoin;
@@ -438,10 +447,13 @@ class manager {
*
* @param int $userid an opional userid to show
* @param int $attemptid an optional $attemptid to show
* @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
* @return report|null available report (or null if no report available)
*/
public function get_report(int $userid = null, int $attemptid = null): ?report {
global $USER;
public function get_report(int $userid = null, int $attemptid = null, $currentgroup = false): ?report {
global $USER, $CFG;
require_once("{$CFG->dirroot}/user/lib.php");
// If tracking is disabled, no reports are available.
if (!$this->instance->enabletracking) {
@@ -460,6 +472,14 @@ class manager {
if ($this->can_view_all_attempts()) {
$user = core_user::get_user($userid);
// Ensure user can view the attempt of specific userid, respecting access checks.
if ($user && $user->id != $USER->id) {
$course = get_course($this->coursemodule->course);
if ($this->coursemodule->effectivegroupmode == SEPARATEGROUPS && !user_can_view_profile($user, $course)) {
return null;
}
}
} else if ($this->can_view_own_attempts()) {
$user = core_user::get_user($USER->id);
if ($userid && $user->id != $userid) {
@@ -480,7 +500,7 @@ class manager {
} else if ($user) {
return new attempts($this, $user);
}
return new participants($this);
return new participants($this, $currentgroup);
}
/**
@@ -61,8 +61,9 @@ class participants extends table_sql implements report {
* Create a new participants report.
*
* @param manager $manager h5pactivitymanager object
* @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
*/
public function __construct(manager $manager) {
public function __construct(manager $manager, $currentgroup = false) {
parent::__construct('mod_h5pactivity-participants');
$this->manager = $manager;
$this->scores = $manager->get_users_scaled_score();
@@ -83,7 +84,7 @@ class participants extends table_sql implements report {
$this->no_sorting('attempts');
$this->pageable(true);
$capjoin = $this->manager->get_active_users_join(true);
$capjoin = $this->manager->get_active_users_join(true, $currentgroup);
// Final SQL.
$this->set_sql(
+5 -1
View File
@@ -43,9 +43,11 @@ if (empty($id)) {
require_login($course, true, $cm);
$currentgroup = groups_get_activity_group($cm, true);
$manager = manager::create_from_coursemodule($cm);
$report = $manager->get_report($userid, $attemptid);
$report = $manager->get_report($userid, $attemptid, $currentgroup);
if (!$report) {
print_error('permissiondenied');
}
@@ -125,6 +127,8 @@ $PAGE->set_context($context);
echo $OUTPUT->header();
groups_print_activity_menu($cm, $PAGE->url);
echo $report->print();
echo $OUTPUT->footer();
@@ -22,13 +22,13 @@ Feature: Add H5P activity
@javascript
Scenario: Add a h5pactivity activity to a course
When I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | H5P activity Description |
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Package file" filemanager
And I click on "Save and display" "button"
And I wait until the page is ready
Given the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| intro | H5P activity Description |
| packagefilepath | h5p/tests/fixtures/ipsums.h5p |
When I am on the "Awesome H5P package" "h5pactivity activity" page
Then I should see "H5P activity Description"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
@@ -40,13 +40,13 @@ Feature: Add H5P activity
@javascript
Scenario: Add a h5pactivity activity with download
When I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | Description |
| Allow download | 1 |
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Package file" filemanager
And I click on "Save and display" "button"
Given the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| displayoptions | 12 |
| packagefilepath | h5p/tests/fixtures/ipsums.h5p |
When I am on the "Awesome H5P package" "h5pactivity activity" page
And I wait until the page is ready
Then I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
@@ -57,13 +57,13 @@ Feature: Add H5P activity
@javascript
Scenario: Add a h5pactivity activity with embed
When I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | Description |
| Embed button | 1 |
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Package file" filemanager
And I click on "Save and display" "button"
Given the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| displayoptions | 10 |
| packagefilepath | h5p/tests/fixtures/ipsums.h5p |
When I am on the "Awesome H5P package" "h5pactivity activity" page
And I wait until the page is ready
Then I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
@@ -74,13 +74,13 @@ Feature: Add H5P activity
@javascript
Scenario: Add a h5pactivity activity with copyright
When I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | Description |
| Copyright button | 1 |
And I upload "h5p/tests/fixtures/guess-the-answer.h5p" file to "Package file" filemanager
And I click on "Save and display" "button"
Given the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| displayoptions | 6 |
| packagefilepath | h5p/tests/fixtures/guess-the-answer.h5p |
When I am on the "Awesome H5P package" "h5pactivity activity" page
And I wait until the page is ready
Then I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
@@ -91,13 +91,13 @@ Feature: Add H5P activity
@javascript
Scenario: Add a h5pactivity activity with copyright in a content without copyright
When I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | Description |
| Copyright button | 1 |
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Package file" filemanager
And I click on "Save and display" "button"
Given the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| displayoptions | 6 |
| packagefilepath | h5p/tests/fixtures/ipsums.h5p |
When I am on the "Awesome H5P package" "h5pactivity activity" page
And I wait until the page is ready
Then I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
@@ -108,15 +108,13 @@ Feature: Add H5P activity
@javascript
Scenario: Add a h5pactivity activity to a course with all display options enabled
When I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | Description |
| Allow download | 1 |
| Embed button | 1 |
| Copyright button | 1 |
And I upload "h5p/tests/fixtures/guess-the-answer.h5p" file to "Package file" filemanager
And I click on "Save and display" "button"
Given the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| displayoptions | 0 |
| packagefilepath | h5p/tests/fixtures/guess-the-answer.h5p |
When I am on the "Awesome H5P package" "h5pactivity activity" page
And I wait until the page is ready
Then I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
@@ -19,15 +19,11 @@ Feature: Change grading options in an H5P activity
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/h5p:updatelibraries | Allow | editingteacher | System | |
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | Description |
And I upload "h5p/tests/fixtures/multiple-choice-2-6.h5p" file to "Package file" filemanager
And I click on "Save and display" "button"
And I log out
And the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| packagefilepath | h5p/tests/fixtures/multiple-choice-2-6.h5p |
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
@@ -44,7 +40,6 @@ Feature: Change grading options in an H5P activity
And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element"
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
And I switch to the main frame
And I log out
@javascript
Scenario: Default grading is max attempt grade
@@ -17,16 +17,12 @@ Feature: View fill the blanks attempt report
And the following config values are set as admin:
# No HTML should appear even with formatstringstriptags disabled.
| formatstringstriptags | 0 |
And I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
And I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | Description |
| Grading method | Average grade |
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Package file" filemanager
And I click on "Save and display" "button"
And I log out
And the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| grademethod | 2 |
| packagefilepath | h5p/tests/fixtures/filltheblanks.h5p |
Scenario: View attempt in a fill the blanks content
Given I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
@@ -17,16 +17,12 @@ Feature: View essay attempt report
And the following config values are set as admin:
# No HTML should appear even with formatstringstriptags disabled.
| formatstringstriptags | 0 |
And I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
And I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | Description |
| Grading method | Average grade |
And I upload "h5p/tests/fixtures/basic_essay.h5p" file to "Package file" filemanager
And I click on "Save and display" "button"
And I log out
And the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| grademethod | 2 |
| packagefilepath | h5p/tests/fixtures/basic_essay.h5p |
Scenario: View attempt essay content
# Do an attempt.
@@ -19,25 +19,20 @@ Feature: Do a H5P attempt
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/h5p:updatelibraries | Allow | editingteacher | System | |
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | Description |
And I upload "h5p/tests/fixtures/multiple-choice-2-6.h5p" file to "Package file" filemanager
And the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| packagefilepath | h5p/tests/fixtures/multiple-choice-2-6.h5p |
@javascript
Scenario: View an H5P as a teacher
When I click on "Save and display" "button"
When I am on the "Awesome H5P package" "h5pactivity activity" page logged in as teacher1
And I wait until the page is ready
Then I should see "This content is displayed in preview mode"
@javascript
Scenario: To an attempts and check on course log report
When I click on "Save and return to course" "button"
And I log out
Given I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
When I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
And I wait until the page is ready
And I should not see "This content is displayed in preview mode"
And I switch to "h5p-player" class iframe
+2
View File
@@ -55,6 +55,8 @@ class mod_h5pactivity_generator extends testing_module_generator {
// Fill in optional values if not specified.
if (!isset($record->packagefilepath)) {
$record->packagefilepath = $CFG->dirroot.'/h5p/tests/fixtures/h5ptest.zip';
} else if (strpos($record->packagefilepath, $CFG->dirroot) !== 0) {
$record->packagefilepath = "{$CFG->dirroot}/{$record->packagefilepath}";
}
if (!isset($record->grade)) {
$record->grade = 100;
+54 -9
View File
@@ -14,14 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* mod_h5pactivity manager tests
*
* @package mod_h5pactivity
* @category test
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\local;
use context_module;
@@ -31,6 +23,7 @@ use stdClass;
* Manager tests class for mod_h5pactivity.
*
* @package mod_h5pactivity
* @covers \mod_h5pactivity\local\manager
* @category test
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -627,7 +620,7 @@ class manager_testcase extends \advanced_testcase {
}
/**
* Test static count_attempts of all active participants.
* Test static test_get_active_users_join of all active participants.
*
* Most method scenarios are tested in test_count_attempts_all so we only
* need to test the with $allpotentialusers true and false.
@@ -692,6 +685,58 @@ class manager_testcase extends \advanced_testcase {
];
}
/**
* Test active users joins returns appropriate results for groups
*/
public function test_get_active_users_join_groupmode(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course(['groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1]);
// Teacher/user one in group one.
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
$userone = $this->getDataGenerator()->create_and_enrol($course, 'student');
$groupone = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$this->getDataGenerator()->create_group_member(['groupid' => $groupone->id, 'userid' => $teacher->id]);
$this->getDataGenerator()->create_group_member(['groupid' => $groupone->id, 'userid' => $userone->id]);
// User two in group two.
$usertwo = $this->getDataGenerator()->create_and_enrol($course, 'student');
$grouptwo = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$this->getDataGenerator()->create_group_member(['groupid' => $grouptwo->id, 'userid' => $usertwo->id]);
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$manager = manager::create_from_instance($activity);
// Admin user can view all participants.
$usersjoin = $manager->get_active_users_join(true, 0);
$users = $DB->get_fieldset_sql("SELECT u.username FROM {user} u {$usersjoin->joins} WHERE {$usersjoin->wheres}",
$usersjoin->params);
$this->assertEqualsCanonicalizing([$teacher->username, $userone->username, $usertwo->username], $users);
// Switch to teacher, who cannot view all participants.
$this->setUser($teacher);
$usersjoin = $manager->get_active_users_join(true, 0);
$users = $DB->get_fieldset_sql("SELECT u.username FROM {user} u {$usersjoin->joins} WHERE {$usersjoin->wheres}",
$usersjoin->params);
$this->assertEmpty($users);
// Teacher can view participants inside group.
$usersjoin = $manager->get_active_users_join(true, $groupone->id);
$users = $DB->get_fieldset_sql("SELECT u.username FROM {user} u {$usersjoin->joins} WHERE {$usersjoin->wheres}",
$usersjoin->params);
$this->assertEqualsCanonicalizing([$teacher->username, $userone->username], $users);
}
/**
* Test static count_attempts.
*/
+7
View File
@@ -1,6 +1,13 @@
This files describes API changes in /mod/h5pactivity/*,
information provided here is intended especially for developers.
=== 3.9.17 ===
* The following methods now accept an optional `$currentgroup` parameter in order to better support groups:
- `\mod_h5pactivity\local\manager::get_active_users_join`
- `\mod_h5pactivity\local\manager::get_report`
- `\mod_h5pactivity\local\report\participants` constructor
=== 3.9.7 ===
* Added mod_h5pactivity\local\manager::get_active_users_join method to query all active
+5 -11
View File
@@ -5,7 +5,6 @@ Feature: set label idnumber
As a teacher
I should create label activity and set an ID number
@javascript
Scenario: label ID number input box should be shown.
Given the following "courses" exist:
| fullname | shortname | category |
@@ -18,12 +17,11 @@ Feature: set label idnumber
| user | course | role |
| teacher | C1 | editingteacher |
| student | C1 | student |
Given I log in as "teacher"
And the following "activities" exist:
| activity | course | section | intro | idnumber |
| label | C1 | 1 | Label with ID number set | C1LABEL1 |
When I log in as "teacher"
And I am on "Test" course homepage with editing mode on
When I add a "label" to section "1" and I fill the form with:
| Label text | Label with ID number set |
| Availability | Show on course page |
| ID number | C1LABEL1 |
Then "Label with ID number set" activity should be visible
And I turn editing mode off
And "Label with ID number set" activity should be visible
@@ -32,11 +30,7 @@ Feature: set label idnumber
And I am on "Test" course homepage
And I should see "Label with ID number set"
And I log out
And I log in as "teacher"
And I am on "Test" course homepage
And I turn editing mode on
And I open "Label with ID number set" actions menu
And I click on "Edit settings" "link" in the "Label with ID number set" activity
And I am on the "Label with ID number set" "label activity editing" page logged in as teacher
And I expand all fieldsets
And I should see "ID number" in the "Common module settings" "fieldset"
And the field "ID number" matches value "C1LABEL1"
+13 -40
View File
@@ -5,8 +5,7 @@ Feature: Check label visibility works
As a teacher
I should create label activity
@javascript
Scenario: Hidden label activity should be show as hidden.
Background:
Given the following "courses" exist:
| fullname | shortname | category |
| Test | C1 | 0 |
@@ -18,38 +17,26 @@ Feature: Check label visibility works
| user | course | role |
| teacher | C1 | editingteacher |
| student | C1 | student |
And the following "activities" exist:
| activity | course | section | intro | idnumber | visible |
| label | C1 | 1 | Swanky label | 1 | 1 |
| label | C1 | 1 | Swanky label 2 | 2 | 0 |
Scenario: Hidden label activity should be show as hidden.
Given I log in as "teacher"
And I am on "Test" course homepage with editing mode on
When I add a "label" to section "1" and I fill the form with:
| Label text | Swanky label |
| Availability | Hide from students |
Then "Swanky label" activity should be hidden
When I am on "Test" course homepage with editing mode on
Then "Swanky label 2" activity should be hidden
And I turn editing mode off
And "Swanky label" activity should be hidden
Then "Swanky label 2" activity should be hidden
And I log out
And I log in as "student"
And I am on "Test" course homepage
And I should not see "Swanky label"
And I should not see "Swanky label 2"
And I log out
@javascript
Scenario: Visible label activity should be shown as visible.
Given the following "courses" exist:
| fullname | shortname | category |
| Test | C1 | 0 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher | Teacher | Frist | teacher1@example.com |
| student | Student | First | student1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher | C1 | editingteacher |
| student | C1 | student |
Given I log in as "teacher"
And I am on "Test" course homepage with editing mode on
When I add a "label" to section "1" and I fill the form with:
| Label text | Swanky label |
| Availability | Show on course page |
When I am on "Test" course homepage with editing mode on
Then "Swanky label" activity should be visible
And I log out
And I log in as "student"
@@ -59,23 +46,9 @@ Feature: Check label visibility works
@javascript
Scenario: Teacher can not show label inside the hidden section
Given the following "courses" exist:
| fullname | shortname | category |
| Test | C1 | 0 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher | Teacher | Frist | teacher1@example.com |
| student | Student | First | student1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher | C1 | editingteacher |
| student | C1 | student |
Given I log in as "teacher"
And I am on "Test" course homepage with editing mode on
When I add a "label" to section "1" and I fill the form with:
| Label text | Swanky label |
| Availability | Show on course page |
And I hide section "1"
When I hide section "1"
Then "Swanky label" activity should be dimmed
And I open "Swanky label" actions menu
And "Swanky label" actions menu should not have "Show" item
@@ -56,7 +56,7 @@ class repost_crosssite_page implements renderable, templatable {
*/
public function __construct(string $url, array $post) {
$this->params = array_map(function($k) use ($post) {
return ["key" => $k, "value" => str_replace("\"", "&quot;", $post[$k])];
return ["key" => $k, "value" => $post[$k]];
}, array_keys($post));
$this->url = $url;
}
+2 -2
View File
@@ -37,9 +37,9 @@
}
}}
<form action="{{{url}}}" method="POST" id="autopostme">
<form action="{{url}}" method="POST" id="autopostme">
{{#params}}
<input type="hidden" name="{{{key}}}" value="{{{value}}}">
<input type="hidden" name="{{key}}" value="{{value}}">
{{/params}}
<input type="hidden" name="repost" value="true">
</form>
@@ -5,12 +5,12 @@ Feature: Safe Exam Browser settings in quiz edit form
Given the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And I log in as "admin"
And I am on "Course 1" course homepage
And I turn editing mode on
And the following "activities" exist:
| activity | course | section | name |
| quiz | C1 | 1 | Quiz 1 |
Scenario: Quiz setting "Require the use of Safe Exam Browser" has all types, except "Use an existing template".
When I add a "Quiz" to section "1"
When I am on the "Quiz 1" "quiz activity editing" page logged in as admin
And I expand all fieldsets
And the "Require the use of Safe Exam Browser" select box should contain "Yes Configure manually"
And the "Require the use of Safe Exam Browser" select box should not contain "Yes Use an existing template"
@@ -22,7 +22,7 @@ Feature: Safe Exam Browser settings in quiz edit form
Given the following "quizaccess_seb > seb templates" exist:
| name |
| Template 1 |
When I add a "Quiz" to section "1"
When I am on the "Quiz 1" "quiz activity editing" page logged in as admin
And I expand all fieldsets
And the "Require the use of Safe Exam Browser" select box should contain "Yes Configure manually"
And the "Require the use of Safe Exam Browser" select box should contain "Yes Use an existing template"
@@ -34,7 +34,7 @@ Feature: Safe Exam Browser settings in quiz edit form
Given the following "quizaccess_seb > seb templates" exist:
| name |
| Template 1 |
When I add a "Quiz" to section "1"
When I am on the "Quiz 1" "quiz activity editing" page logged in as admin
And I expand all fieldsets
And I set the field "Require the use of Safe Exam Browser" to "No"
Then I should not see "Upload Safe Exam Browser config file"
@@ -66,7 +66,7 @@ Feature: Safe Exam Browser settings in quiz edit form
Given the following "quizaccess_seb > seb templates" exist:
| name |
| Template 1 |
When I add a "Quiz" to section "1"
And I am on the "Quiz 1" "quiz activity editing" page logged in as admin
And I expand all fieldsets
And I set the field "Require the use of Safe Exam Browser" to "Yes Use SEB client config"
Then I should see "Show Safe Exam Browser download button"
@@ -98,7 +98,7 @@ Feature: Safe Exam Browser settings in quiz edit form
Given the following "quizaccess_seb > seb templates" exist:
| name |
| Template 1 |
When I add a "Quiz" to section "1"
And I am on the "Quiz 1" "quiz activity editing" page logged in as admin
And I expand all fieldsets
And I set the field "Require the use of Safe Exam Browser" to "Yes Upload my own config"
Then I should see "Upload Safe Exam Browser config file"
@@ -130,7 +130,7 @@ Feature: Safe Exam Browser settings in quiz edit form
Given the following "quizaccess_seb > seb templates" exist:
| name |
| Template 1 |
When I add a "Quiz" to section "1"
And I am on the "Quiz 1" "quiz activity editing" page logged in as admin
And I expand all fieldsets
And I set the field "Require the use of Safe Exam Browser" to "Yes Use an existing template"
Then I should see "Safe Exam Browser config template"
@@ -164,7 +164,7 @@ Feature: Safe Exam Browser settings in quiz edit form
Given the following "quizaccess_seb > seb templates" exist:
| name |
| Template 1 |
When I add a "Quiz" to section "1"
And I am on the "Quiz 1" "quiz activity editing" page logged in as admin
And I expand all fieldsets
And I set the field "Require the use of Safe Exam Browser" to "Yes Configure manually"
Then I should see "Show Safe Exam Browser download button"
+15 -9
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"
@@ -18,39 +18,39 @@ Feature: Settings form fields disabled if not required
| teacher | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
And I log in as "teacher"
And I am on "Course 1" course homepage with editing mode on
And the following "activities" exist:
| activity | course | section | name |
| quiz | C1 | 1 | Test quiz 1 |
@javascript
Scenario: Depending on the number of attempts, different form fields are disabled.
When I add a "Quiz" to section "1"
When I am on the "Test quiz 1" "quiz activity editing" page logged in as teacher
And I expand all fieldsets
And I set the field "Name" to "Test quiz"
And I set the field "Attempts allowed" to "1"
Then the "Grading method" "field" should be disabled
And the "Each attempt builds on the last" "field" should be disabled
And the "id_delay1_enabled" "field" should be disabled
And the "id_delay2_enabled" "field" should be disabled
When I set the field "Attempts allowed" to "2"
Then the "Grading method" "field" should be enabled
And I set the field "Attempts allowed" to "2"
And the "Grading method" "field" should be enabled
And the "Each attempt builds on the last" "field" should be enabled
And the "id_delay1_enabled" "field" should be enabled
And the "id_delay2_enabled" "field" should be disabled
When I set the field "Attempts allowed" to "3"
Then the "Grading method" "field" should be enabled
And I set the field "Attempts allowed" to "3"
And the "Grading method" "field" should be enabled
And the "Each attempt builds on the last" "field" should be enabled
And the "id_delay1_enabled" "field" should be enabled
And the "id_delay2_enabled" "field" should be enabled
When I set the field "Attempts allowed" to "Unlimited"
Then the "Grading method" "field" should be enabled
And I set the field "Attempts allowed" to "Unlimited"
And the "Grading method" "field" should be enabled
And the "Each attempt builds on the last" "field" should be enabled
# And the "id_delay1_enabled" "field" should be enabled
# And the "id_delay2_enabled" "field" should be enabled
When I press "Save and display"
And I press "Save and display"
And I navigate to "User overrides" in current page administration
And I press "Add user override"
And I set the following fields to these values:
@@ -61,12 +61,12 @@ Feature: Settings form fields disabled if not required
And I navigate to "Edit settings" in current page administration
And I expand all fieldsets
And I set the field "Attempts allowed" to "1"
Then the "Grading method" "field" should be enabled
And the "Grading method" "field" should be enabled
And the "Each attempt builds on the last" "field" should be enabled
And the "id_delay1_enabled" "field" should be enabled
And the "id_delay2_enabled" "field" should be enabled
When I press "Save and display"
And I press "Save and display"
And I navigate to "User overrides" in current page administration
And I click on "Edit" "link" in the "region-main" "region"
And I set the field "Attempts allowed" to "2"
@@ -75,12 +75,12 @@ Feature: Settings form fields disabled if not required
And I navigate to "Edit settings" in current page administration
And I expand all fieldsets
And I set the field "Attempts allowed" to "1"
Then the "Grading method" "field" should be enabled
And the "Grading method" "field" should be enabled
And the "Each attempt builds on the last" "field" should be enabled
And the "id_delay1_enabled" "field" should be enabled
And the "id_delay2_enabled" "field" should be disabled
When I press "Save and display"
And I press "Save and display"
And I navigate to "User overrides" in current page administration
And I press "Add user override"
And I set the following fields to these values:
@@ -91,28 +91,31 @@ Feature: Settings form fields disabled if not required
And I navigate to "Edit settings" in current page administration
And I expand all fieldsets
And I set the field "Attempts allowed" to "1"
Then the "Grading method" "field" should be enabled
And the "Grading method" "field" should be enabled
And the "Each attempt builds on the last" "field" should be enabled
And the "id_delay1_enabled" "field" should be enabled
And the "id_delay2_enabled" "field" should be enabled
@javascript
Scenario: Depending on whether there is a close date, some review options are disabled.
When I add a "Quiz" to section "1"
When I log in as "teacher"
And I am on "Course 1" course homepage with editing mode on
And I add a "Quiz" to section "1"
And I expand all fieldsets
And I set the field "Name" to "Test quiz"
Then the "id_attemptclosed" "checkbox" should be disabled
Then the "id_correctnessclosed" "checkbox" should be disabled
Then the "id_marksclosed" "checkbox" should be disabled
Then the "id_specificfeedbackclosed" "checkbox" should be disabled
Then the "id_generalfeedbackclosed" "checkbox" should be disabled
Then the "id_rightanswerclosed" "checkbox" should be disabled
Then the "id_overallfeedbackclosed" "checkbox" should be disabled
And the "id_correctnessclosed" "checkbox" should be disabled
And the "id_marksclosed" "checkbox" should be disabled
And the "id_specificfeedbackclosed" "checkbox" should be disabled
And the "id_generalfeedbackclosed" "checkbox" should be disabled
And the "id_rightanswerclosed" "checkbox" should be disabled
And the "id_overallfeedbackclosed" "checkbox" should be disabled
And I set the field "id_timeclose_enabled" to "1"
Then the "id_attemptclosed" "checkbox" should be enabled
Then the "id_correctnessclosed" "checkbox" should be enabled
Then the "id_marksclosed" "checkbox" should be enabled
Then the "id_specificfeedbackclosed" "checkbox" should be enabled
Then the "id_generalfeedbackclosed" "checkbox" should be enabled
Then the "id_rightanswerclosed" "checkbox" should be enabled
Then the "id_overallfeedbackclosed" "checkbox" should be enabled
And the "id_attemptclosed" "checkbox" should be enabled
And the "id_correctnessclosed" "checkbox" should be enabled
And the "id_marksclosed" "checkbox" should be enabled
And the "id_specificfeedbackclosed" "checkbox" should be enabled
And the "id_generalfeedbackclosed" "checkbox" should be enabled
And the "id_rightanswerclosed" "checkbox" should be enabled
And the "id_overallfeedbackclosed" "checkbox" should be enabled
And I should not see "Repaginate now"
+247
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
+6 -4
View File
@@ -39,7 +39,8 @@ class mod_resource_generator extends testing_module_generator {
* text file.
*
* @param array|stdClass $record data for module being generated. Requires 'course' key
* (an id or the full object). Also can have any fields from add module form.
* (an id or the full object). Also can have any fields from add module form, and a
* 'defaultfilename' to set the name of the file created if no draft ID is supplied.
* @param null|array $options general options for course module. Since 2.6 it is
* possible to omit this argument by merging options into $record
* @return stdClass record from module-defined table with additional field
@@ -72,16 +73,17 @@ class mod_resource_generator extends testing_module_generator {
throw new coding_exception('resource generator requires a current user');
}
$usercontext = context_user::instance($USER->id);
$filename = $record->defaultfilename ?? 'resource' . ($this->instancecount + 1) . '.txt';
// Pick a random context id for specified user.
$record->files = file_get_unused_draft_itemid();
// Add actual file there.
$filerecord = array('component' => 'user', 'filearea' => 'draft',
$filerecord = ['component' => 'user', 'filearea' => 'draft',
'contextid' => $usercontext->id, 'itemid' => $record->files,
'filename' => 'resource' . ($this->instancecount+1) . '.txt', 'filepath' => '/');
'filename' => $filename, 'filepath' => '/'];
$fs = get_file_storage();
$fs->create_file_from_string($filerecord, 'Test resource ' . ($this->instancecount+1) . ' file');
$fs->create_file_from_string($filerecord, 'Test resource ' . $filename . ' file');
}
// Do work to actually add the instance.
+16 -1
View File
@@ -70,6 +70,21 @@ class mod_resource_generator_testcase extends advanced_testcase {
// Check that generated resource module contains a file.
$fs = get_file_storage();
$files = $fs->get_area_files($context->id, 'mod_resource', 'content', false, '', false);
$this->assertEquals(1, count($files));
$file = array_values($files)[0];
$this->assertCount(1, $files);
$this->assertEquals('resource3.txt', $file->get_filename());
$this->assertEquals('Test resource resource3.txt file', $file->get_content());
// Create a new resource specifying the file name.
$resource = $generator->create_instance(['course' => $SITE->id, 'defaultfilename' => 'myfile.pdf']);
// Check that generated resource module contains a file with the specified name.
$cm = get_coursemodule_from_instance('resource', $resource->id);
$context = \context_module::instance($cm->id);
$files = $fs->get_area_files($context->id, 'mod_resource', 'content', false, '', false);
$file = array_values($files)[0];
$this->assertCount(1, $files);
$this->assertEquals('myfile.pdf', $file->get_filename());
$this->assertEquals('Test resource myfile.pdf file', $file->get_content());
}
}
+1 -1
View File
@@ -496,7 +496,7 @@ function scorm_user_complete($course, $user, $mod, $scorm) {
$report .= html_writer::start_tag('li').html_writer::start_tag('ul', array('class' => $liststyle));
foreach ($usertrack as $element => $value) {
if (substr($element, 0, 3) == 'cmi') {
$report .= html_writer::tag('li', $element.' => '.s($value));
$report .= html_writer::tag('li', s($element) . ' => ' . s($value));
}
}
$report .= html_writer::end_tag('ul').html_writer::end_tag('li');
+2 -2
View File
@@ -144,9 +144,9 @@ foreach ($trackdata as $element => $value) {
}
if (empty($string) || $table->is_downloading()) {
$row[] = $element;
$row[] = s($element);
} else {
$row[] = $element.$OUTPUT->help_icon($string, 'scorm');
$row[] = s($element) . $OUTPUT->help_icon($string, 'scorm');
}
if (strpos($element, '_time') === false) {
$row[] = s($value);
@@ -16,22 +16,9 @@ Feature: Scorm multi-sco review mode.
@javascript
Scenario: Test review mode with a single sco completion.
When I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| Enable completion tracking | Yes |
And I press "Save and display"
And I add a "SCORM package" to section "1"
And I set the following fields to these values:
| Name | Basic Multi-sco SCORM package |
| Description | Description |
| Force new attempt | No |
And I set the field "Completed" to "1"
And I upload "mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12.zip" file to "Package file" filemanager
And I click on "Save and display" "button"
And I should see "Basic Multi-sco SCORM package"
And I log out
When the following "activities" exist:
| activity | course | name | packagefilepath | forcenewattempt |
| scorm | C1 | Basic Multi-sco SCORM package | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12.zip | 0 |
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page logged in as student1
And I should see "Normal"
And I press "Enter"
@@ -48,24 +35,9 @@ Feature: Scorm multi-sco review mode.
@javascript
Scenario: Test review mode with all scos completed.
When I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| Enable completion tracking | Yes |
And I press "Save and display"
And I add a "SCORM package" to section "1"
And I set the following fields to these values:
| Name | ADV Multi-sco SCORM package |
| Description | Description |
| Completion tracking | Show activity as complete when conditions are met |
| Require all scos to return completion status | 1 |
| Force new attempt | No |
And I set the field "Completed" to "1"
And I upload "mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12.zip" file to "Package file" filemanager
And I click on "Save and display" "button"
And I should see "ADV Multi-sco SCORM package"
And I log out
When the following "activities" exist:
| activity | course | name | packagefilepath | forcenewattempt |
| scorm | C1 | ADV Multi-sco SCORM package | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12.zip | 0 |
And I am on the "ADV Multi-sco SCORM package" "scorm activity" page logged in as student1
And I should see "Normal"
And I press "Enter"
@@ -167,17 +139,9 @@ Feature: Scorm multi-sco review mode.
@javascript
Scenario: Test force completed set to Always.
When I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I add a "SCORM package" to section "1"
And I set the following fields to these values:
| Name | Basic Multi-sco SCORM package |
| Description | Description |
| Force new attempt | Always |
And I upload "mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip" file to "Package file" filemanager
And I click on "Save and display" "button"
And I should see "Basic Multi-sco SCORM package"
And I log out
When the following "activities" exist:
| activity | course | name | packagefilepath | forcenewattempt |
| scorm | C1 | Basic Multi-sco SCORM package | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip | 2 |
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page logged in as student1
And I should see "Normal"
And I press "Enter"
@@ -197,17 +161,9 @@ Feature: Scorm multi-sco review mode.
@javascript
Scenario: Test force completed set to when previous complete/passed/failed.
When I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I add a "SCORM package" to section "1"
And I set the following fields to these values:
| Name | Basic Multi-sco SCORM package |
| Description | Description |
| Force new attempt | When previous attempt completed, passed or failed |
And I upload "mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip" file to "Package file" filemanager
And I click on "Save and display" "button"
And I should see "Basic Multi-sco SCORM package"
And I log out
When the following "activities" exist:
| activity | course | name | packagefilepath | forcenewattempt |
| scorm | C1 | Basic Multi-sco SCORM package | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip | 1 |
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page logged in as student1
And I should see "Normal"
And I press "Enter"
@@ -272,18 +228,9 @@ Feature: Scorm multi-sco review mode.
@javascript
Scenario: Test force completed set to Always and student skipview
When I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I add a "SCORM package" to section "1"
And I set the following fields to these values:
| Name | Basic Multi-sco SCORM package |
| Description | Description |
| Force new attempt | Always |
| Student skip content structure page | Always |
And I upload "mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip" file to "Package file" filemanager
And I click on "Save and display" "button"
And I should see "Basic Multi-sco SCORM package"
And I log out
When the following "activities" exist:
| activity | course | name | packagefilepath | forcenewattempt | skipview |
| scorm | C1 | Basic Multi-sco SCORM package | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip | 2 | 2 |
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page logged in as student1
And I switch to "scorm_object" iframe
And I should see "Play of the game"
@@ -298,18 +245,9 @@ Feature: Scorm multi-sco review mode.
@javascript
Scenario: Test force completed set to when previous complete/passed/failed.
When I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I add a "SCORM package" to section "1"
And I set the following fields to these values:
| Name | Basic Multi-sco SCORM package |
| Description | Description |
| Force new attempt | When previous attempt completed, passed or failed |
| Student skip content structure page | Always |
And I upload "mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip" file to "Package file" filemanager
And I click on "Save and display" "button"
And I should see "Basic Multi-sco SCORM package"
And I log out
When the following "activities" exist:
| activity | course | name | packagefilepath | forcenewattempt | skipview |
| scorm | C1 | Basic Multi-sco SCORM package | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip | 1 | 2 |
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page logged in as student1
And I switch to "scorm_object" iframe
And I should see "Play of the game"
@@ -19,16 +19,11 @@ Feature: Confirm progress gets saved on unload events
@javascript
Scenario: Test progress gets saved correctly when the user navigates away from the scorm activity
Given I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I add a "SCORM package" to section "1"
And I set the following fields to these values:
| Name | Runtime Basic Calls SCORM 2004 3rd Edition package |
| Description | Description |
And I upload "mod/scorm/tests/packages/RuntimeBasicCalls_SCORM20043rdEdition.zip" file to "Package file" filemanager
And I click on "Save and display" "button"
And I should see "Runtime Basic Calls SCORM 2004 3rd Edition package"
And I log out
Given the following "activity" exists:
| activity | scorm |
| course | C1 |
| name | Runtime Basic Calls SCORM 2004 3rd Edition package |
| packagefilepath | mod/scorm/tests/packages/RuntimeBasicCalls_SCORM20043rdEdition.zip |
When I log in as "student1"
And I am on "Course 1" course homepage
And I follow "Runtime Basic Calls SCORM 2004 3rd Edition package"
@@ -14,6 +14,13 @@ Feature: Scorm with no calendar capabilites
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activity" exists:
| activity | scorm |
| course | C1 |
| name | Test scorm name |
| timeopen | ##first day of January 2017## |
| timeclose | ##first day of February 2017## |
| packagefilepath | mod/scorm/tests/packages/singlesco_scorm12.zip |
And I log in as "admin"
And I am on "Course 1" course homepage
And I navigate to "Users > Permissions" in current page administration
@@ -24,25 +31,8 @@ Feature: Scorm with no calendar capabilites
@javascript @_file_upload @_switch_iframe
Scenario: Editing a scorm activity without calendar permission
Given I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
When I add a "SCORM package" to section "1"
And I set the following fields to these values:
| Name | Test scorm name |
| Description | Test scorm description |
| id_timeopen_enabled | 1 |
| id_timeopen_day | 1 |
| id_timeopen_month | 1 |
| id_timeopen_year | 2017 |
| id_timeclose_enabled | 1 |
| id_timeclose_day | 1 |
| id_timeclose_month | 2 |
| id_timeclose_year | 2017 |
And I upload "mod/scorm/tests/packages/singlesco_scorm12.zip" file to "Package file" filemanager
And I click on "Save and display" "button"
And I log out
When I am on the "Test scorm name" "scorm activity editing" page logged in as teacher1
And I set the following fields to these values:
Given I am on the "Test scorm name" "scorm activity editing" page logged in as teacher1
When I set the following fields to these values:
| id_timeopen_year | 2018 |
| id_timeclose_year | 2018 |
And I press "Save and return to course"

Some files were not shown because too many files have changed in this diff Show More