Compare commits

..

94 Commits

Author SHA1 Message Date
Huong Nguyen ca15d0f2e8 Moodle release 4.4.12 2025-12-07 08:05:21 +07:00
Huong Nguyen 29dd0f2640 weekly release 4.4.11+ 2025-12-05 08:09:21 +07:00
Huong Nguyen dc447e4b3f Merge branch 'MDL-87068-404' of https://github.com/lameze/moodle into MOODLE_404_STABLE 2025-12-04 21:01:37 +07:00
Angelia Dela Cruz 6e353fbab8 MDL-87068 contentbank_h5p: Behat for restoring course contentbank 2025-12-04 10:34:23 +08:00
Huong Nguyen c377d42d87 MDL-72744 table: Improve PHPUnit test 2025-12-04 08:56:55 +07:00
David Woloszyn d946fae784 MDL-72744 core_grades: Escape formulas when exporting spreadsheets 2025-12-04 08:56:55 +07:00
David Woloszyn 77474b866c MDL-72744 dataformat: Escape formulas when exporting spreadsheets
Co-authored-by: Huong Nguyen <huongnv13@gmail.com>
2025-12-04 08:56:55 +07:00
Huong Nguyen 514fad16ca MDL-86087 core: Use correct required version 2025-12-04 08:37:23 +07:00
Huong Nguyen 8a9b201d5c Merge branch 'MDL-87091-404' of https://github.com/lameze/moodle into MOODLE_404_STABLE 2025-12-03 16:11:42 +07:00
mioyu-ubiq 723a63b5a7 MDL-87091 behat: Test authloginviaemail setting behavior 2025-12-03 15:56:33 +08:00
Huong Nguyen 3b3bc90d58 MDL-87353 core: Safer unserializing of file references 2025-12-03 14:18:01 +08:00
Jun Pataleta a5d891fe69 Merge branch 'MDL-86087-404' of https://github.com/HuongNV13/moodle into MOODLE_404_STABLE 2025-12-03 14:09:53 +08:00
Stefan Hanauska 6cc385adb6 MDL-86960 mod_glossary: Check user 2025-12-03 13:44:58 +08:00
Stefan Hanauska 619104e303 MDL-86960 mod_data: Check user 2025-12-03 13:44:47 +08:00
Stefan Hanauska 187bf24f46 MDL-86960 mod_forum: Check user 2025-12-03 13:44:30 +08:00
Jake Dallimore 6992ef3d3e MDL-87286 enrol_lti: prevent suspended user account auth in legacy LTI 2025-12-03 15:51:48 +11:00
Jake Dallimore 2a3f719f03 MDL-87286 auth_lti: prevent suspended user account auth in LTI1p3 2025-12-03 15:51:48 +11:00
Mihail Geshoski 05f233de51 MDL-82808 assign: Use blindid during submission removal in blind marking 2025-12-03 09:50:05 +08:00
Rajneel Totaram e050af3f76 MDL-82808 mod_assign: replace userid with blindid in anonymous marking 2025-12-03 09:50:05 +08:00
Huong Nguyen 5b36ea0db8 MDL-86087 core: Define environment requirements for Moodle 5.2 2025-11-28 09:09:59 +07:00
David Woloszyn 021406c8b8 MDL-86544 lib: Adjust checks on local URL params 2025-11-27 11:51:17 +00:00
Eric Merrill 48031a9e52 MDL-80317 auth_oauth2: Restrict login redirect to local url 2025-11-27 11:51:17 +00:00
David Woloszyn dc99af1031 MDL-85557 atto_equation: Sanitise equation preview input 2025-11-27 11:51:17 +00:00
David Woloszyn 84fe6fc634 MDL-85557 tiny_equation: Sanitise equation preview input 2025-11-27 11:51:17 +00:00
Stefan Hanauska 2f76e2ac11 MDL-86507 badge: Check if role is assigned 2025-11-27 11:51:17 +00:00
yusufwib01 87cdc2b544 MDL-86326 auth: Enhance validation logic in resend_confirmation_email 2025-11-27 11:51:17 +00:00
Mihail Geshoski 99f773928a weekly release 4.4.11+ 2025-11-27 19:40:58 +08:00
Mihail Geshoski df803aa0cf Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-11-27 19:40:53 +08:00
Andrew Nicols cac7686d6e Merge branch 'MDL-86255-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-11-27 11:11:57 +08:00
Mihail Geshoski 7aa7f744cd Merge branch 'MDL-87149-404' of https://github.com/junpataleta/moodle into MOODLE_404_STABLE 2025-11-26 17:31:18 +08:00
Andrew Nicols 382b5982a6 MDL-87046 core: Fix shutdown manager early return
If the `\core_shutdown_manager::initialize` method is called multiple
times, it should only succeed once.

Unfortunately whilst logging existed, the shutdown manager did not
actually exit.
2025-11-25 15:27:39 +00:00
AMOS bot 00318703e4 Automatically generated installer lang files 2025-11-23 00:08:16 +00:00
Huong Nguyen 5e655ad805 weekly release 4.4.11+ 2025-11-21 14:33:58 +07:00
Huong Nguyen 92f5c1cd6c Merge branch 'MDL-87048-404' of https://github.com/lameze/moodle into MOODLE_404_STABLE 2025-11-21 08:17:14 +07:00
Huong Nguyen 1875710825 Merge branch 'MDL-84735-404' of https://github.com/lameze/moodle into MOODLE_404_STABLE 2025-11-21 08:17:14 +07:00
Angelia Dela Cruz 5afd98eb7f MDL-87048 tool_lp: Behat for synchronising cohort with learning plan 2025-11-20 10:28:46 +08:00
Angelia Dela Cruz 058c986c13 MDL-84735 gradingform: Behat for modifying marking guide criterion 2025-11-20 07:48:40 +08:00
Julien Boulen 31a7b4981c MDL-86320 core: Fix unit test qrcode_test 2025-11-19 10:06:02 +07:00
Huong Nguyen 4346a59025 Merge branch 'MDL-86907-404' of https://github.com/mioyu-ubiq/moodle into MOODLE_404_STABLE 2025-11-19 10:03:27 +07:00
mioyu-ubiq 71bb810758 MDL-86907 mod_choice: Behat test for adding multiple choice responses 2025-11-18 15:05:07 +08:00
Simey Lameze df843852b9 MDL-87050 behat: use src for img xpath selector 2025-11-18 10:50:55 +07:00
Huong Nguyen 33ed9d98c7 Merge branch 'MDL-87050-404' of https://github.com/andelacruz/moodle into MOODLE_404_STABLE 2025-11-17 15:58:25 +07:00
Jun Pataleta 730534c26c MDL-87149 mod_feedback: Generate dependent questions
Allow the generator to create questions that depend on other questions.
2025-11-17 12:47:33 +08:00
Mihail Geshoski 1dcb6c9f56 weekly release 4.4.11+ 2025-11-13 18:49:30 +08:00
Angelia Dela Cruz a87b2b8f24 MDL-87050 mod_folder: Behat for preventing forced file downloads 2025-11-13 16:52:36 +08:00
Huong Nguyen 17c2bbf1cb Merge branch 'MDL-86895-404' of https://github.com/mioyu-ubiq/moodle into MOODLE_404_STABLE 2025-11-13 10:44:16 +07:00
Huong Nguyen c54f62fee6 Merge branch 'MDL-79249-404' of https://github.com/andelacruz/moodle into MOODLE_404_STABLE 2025-11-13 10:20:47 +07:00
Huong Nguyen 17e2b5f8cb Merge branch 'MDL-87047-404' of https://github.com/andelacruz/moodle into MOODLE_404_STABLE 2025-11-13 10:17:04 +07:00
Huong Nguyen ca06413c3d Merge branch 'MDL-78973-404' of https://github.com/andelacruz/moodle into MOODLE_404_STABLE 2025-11-13 08:53:04 +07:00
Mihail Geshoski 8fc2c1aab8 Merge branch 'MDL-86963-404' of https://github.com/andelacruz/moodle into MOODLE_404_STABLE 2025-11-12 17:16:53 +08:00
Mihail Geshoski c741717763 Merge branch 'MDL-85686-404' of https://github.com/lameze/moodle into MOODLE_404_STABLE 2025-11-12 17:01:40 +08:00
Angelia Dela Cruz 76a4c421d1 MDL-87047 tool_lp: Behat optimisation 2025-11-12 16:39:20 +08:00
Mihail Geshoski 9c771234b8 Merge branch 'MDL-86886-404' of https://github.com/andelacruz/moodle into MOODLE_404_STABLE 2025-11-12 16:38:59 +08:00
Angelia Dela Cruz bcd1cf6e3d MDL-87047 tool_lp: Behat to complete a learning plan and alter it 2025-11-12 16:35:13 +08:00
Angelia Dela Cruz b17d59accd MDL-86963 tool_lp: Behat to perform CRUD operations on competencies 2025-11-12 10:57:35 +08:00
Angelia Dela Cruz e88a6c01a4 MDL-86886 tool_lp: Behat to move and cross-reference competencies 2025-11-11 16:07:27 +08:00
Simey Lameze 837e73ce4f MDL-85686 mod_quiz: unit test coverage for questions shuffle 2025-11-11 14:21:35 +08:00
Angelia Dela Cruz 58a29073a7 MDL-78973 Behat: Behat coverage for invalid file upload in data activity 2025-11-11 13:24:18 +08:00
Mio Yu 9413a077e9 MDL-86895 core_grades: Behat to verify listing and grading submissions 2025-11-11 12:57:39 +08:00
Huong Nguyen 18c78f4a2e Merge branch 'MDL-81604-404' of https://github.com/t-schroeder/moodle into MOODLE_404_STABLE 2025-11-11 09:29:18 +07:00
Huong Nguyen 00289a360a weekly release 4.4.11+ 2025-11-07 14:49:35 +07:00
Huong Nguyen d11be64b94 Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-11-07 14:49:33 +07:00
Huong Nguyen 314d315397 MDL-69226 environment: filter extension required for 4.1 2025-11-06 18:22:40 +08:00
Mihail Geshoski 251c15ee04 Merge branch 'MDL-87073-404' of https://github.com/HuongNV13/moodle into MOODLE_404_STABLE 2025-11-06 17:09:04 +08:00
Huong Nguyen 21090c3716 MDL-87073 PHPUnit: Use non-DST timezone for comparing
Including in this commit
 - Use America/New_York instead of US/Eastern
2025-11-06 15:47:24 +07:00
Huong Nguyen 8ec1ebc9cd Merge branch 'MDL-86908-404' of https://github.com/andelacruz/moodle into MOODLE_404_STABLE 2025-11-06 15:42:47 +07:00
Huong Nguyen 00092de974 Merge branch 'MDL-87049-404' of https://github.com/lameze/moodle into MOODLE_404_STABLE 2025-11-06 15:40:42 +07:00
Andrew Nicols 9da9ed2bf8 MDL-87079 core: Call existing pcntl handlers
Whilst `register_shutdown_function` works as a queue, `pcntl_signal`
does not.

To preserve the behaviour of existing signal handlers, as are typically
found in CLI wrappers like PHPUnit and Behat, we should store any
existing handler
2025-11-06 15:38:13 +07:00
Jake Dallimore a953a3eafb Merge branch 'MDL-86379-404' of https://github.com/lameze/moodle into MOODLE_404_STABLE 2025-11-06 14:30:42 +08:00
Simey Lameze f70f1b1721 MDL-87049 behat: read showcampaigncontent setting when running behat 2025-11-06 08:21:48 +08:00
Angelia Dela Cruz 7a315f18ee MDL-87049 core: Behat to toggle campaign content banner display 2025-11-06 08:21:48 +08:00
AMOS bot 4aedbab2e4 Automatically generated installer lang files 2025-11-06 00:07:51 +00:00
Angelia Dela Cruz ca19e78795 MDL-86908 core_contentbank: Behat to set supported files of contentbank 2025-11-05 18:39:35 +08:00
Simey Lameze eb670e1115 MDL-86379 mod_quiz: move create_test_quiz function into generator 2025-11-05 11:54:22 +08:00
Angelia Dela Cruz dbf9adf8a9 MDL-79249 core_filepicker: Cancel file and folder upload using X button 2025-11-03 11:12:49 +08:00
Huong Nguyen 407bf7714e Merge branch 'MDL-69226-M404_verify-that-the-filter-php-extension-is-present' of https://github.com/ziegenberg/moodle into MOODLE_404_STABLE 2025-11-03 09:02:26 +07:00
AMOS bot 5bc19fc1ed Automatically generated installer lang files 2025-11-01 00:07:51 +00:00
Mihail Geshoski e9916f9d95 weekly release 4.4.11+ 2025-10-30 18:11:46 +08:00
Mihail Geshoski e057bc611b Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-10-30 18:11:42 +08:00
Mihail Geshoski a0a139f24c Merge branch 'MDL-86894-404' of https://github.com/Kredelirious/moodle into MOODLE_404_STABLE 2025-10-29 10:19:18 +08:00
Daniel Ziegenberg e24c8e4cde MDL-69226 environment: filter extension required
Signed-off-by: Daniel Ziegenberg <daniel@ziegenberg.at>
2025-10-28 19:40:17 +01:00
Kim Jared Lucas 115a369b97 MDL-86894 mod_forum: Behat to verify default forum RSS settings 2025-10-28 13:13:16 +08:00
AMOS bot a390968760 Automatically generated installer lang files 2025-10-28 00:09:41 +00:00
Huong Nguyen ab645b55d3 weekly release 4.4.11+ 2025-10-24 11:00:44 +07:00
Huong Nguyen 1e7b9250dd Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-10-24 11:00:42 +07:00
Paul Holden 118f5b53ca MDL-86255 behat: implement methods to assert date/time field value. 2025-10-21 11:34:55 +01:00
Mihail Geshoski 2c7984ea33 Merge branch 'MDL-85696-404' of https://github.com/andelacruz/moodle into MOODLE_404_STABLE 2025-10-20 10:29:41 +08:00
Angelia Dela Cruz 89689bceeb MDL-85696 mod_quiz: Behat to attempt password and time restricted quiz 2025-10-17 17:32:36 +08:00
Paul Holden 3ab6ab7d0e MDL-86369 mod_scorm: fix attempt deletion behaviour on submit/redirect.
Standard after a POST submission is to redirect back to where user came
from (in this case), to avoid awkward page reload behaviour. Also fix
bug where deleting an attempt for a user, where the attempt didn't exist
would delete all attempts for that user.
2025-10-15 17:13:56 +08:00
Andrew Nicols b00e03b493 Merge branch 'MDL-86186-404' of https://github.com/lameze/moodle into MOODLE_404_STABLE 2025-10-15 15:50:38 +08:00
AMOS bot 04c0d79ccc Automatically generated installer lang files 2025-10-15 00:09:27 +00:00
AMOS bot 0a2ce4bd69 Automatically generated installer lang files 2025-10-09 00:09:06 +00:00
Simey Lameze cf470b8de9 MDL-86186 behat: enlarge window size before asserting 2025-08-04 11:27:41 +08:00
Tim Schroeder c6575999d6 MDL-81604 Files: Fix broken file serving w/ Apache + PHP-FPM + PHP >=8.1 2025-07-04 19:59:56 +02:00
80 changed files with 2077 additions and 362 deletions
@@ -0,0 +1,5 @@
issueNumber: MDL-87079
notes:
core:
- message: When responding to pcntl signals, call existing signal handlers.
type: fixed
+209
View File
@@ -3835,6 +3835,7 @@
<PHP_EXTENSION name="fileinfo" level="required"/>
<PHP_EXTENSION name="sodium" level="optional"/>
<PHP_EXTENSION name="exif" level="optional"/>
<PHP_EXTENSION name="filter" level="required"/>
</PHP_EXTENSIONS>
<PHP_SETTINGS>
<PHP_SETTING name="memory_limit" value="96M" level="required">
@@ -4411,6 +4412,7 @@
<PHP_EXTENSION name="fileinfo" level="required"/>
<PHP_EXTENSION name="sodium" level="required"/>
<PHP_EXTENSION name="exif" level="optional"/>
<PHP_EXTENSION name="filter" level="required"/>
</PHP_EXTENSIONS>
<PHP_SETTINGS>
<PHP_SETTING name="memory_limit" value="96M" level="required">
@@ -4607,6 +4609,7 @@
<PHP_EXTENSION name="fileinfo" level="required"/>
<PHP_EXTENSION name="sodium" level="required"/>
<PHP_EXTENSION name="exif" level="optional"/>
<PHP_EXTENSION name="filter" level="required"/>
</PHP_EXTENSIONS>
<PHP_SETTINGS>
<PHP_SETTING name="memory_limit" value="96M" level="required">
@@ -4802,6 +4805,7 @@
<PHP_EXTENSION name="fileinfo" level="required"/>
<PHP_EXTENSION name="sodium" level="required"/>
<PHP_EXTENSION name="exif" level="optional"/>
<PHP_EXTENSION name="filter" level="required"/>
</PHP_EXTENSIONS>
<PHP_SETTINGS>
<PHP_SETTING name="memory_limit" value="96M" level="required">
@@ -4997,6 +5001,7 @@
<PHP_EXTENSION name="fileinfo" level="required"/>
<PHP_EXTENSION name="sodium" level="required"/>
<PHP_EXTENSION name="exif" level="optional"/>
<PHP_EXTENSION name="filter" level="required"/>
</PHP_EXTENSIONS>
<PHP_SETTINGS>
<PHP_SETTING name="memory_limit" value="96M" level="required">
@@ -5100,4 +5105,208 @@
</CUSTOM_CHECK>
</CUSTOM_CHECKS>
</MOODLE>
<MOODLE version="5.2" requires="4.4">
<UNICODE level="required">
<FEEDBACK>
<ON_ERROR message="unicoderequired" />
</FEEDBACK>
</UNICODE>
<DATABASE level="required">
<VENDOR name="mariadb" version="10.11.0" />
<VENDOR name="mysql" version="8.4" />
<VENDOR name="postgres" version="16" />
<VENDOR name="mssql" version="15.0" />
<VENDOR name="auroramysql" version="8.0" />
</DATABASE>
<PHP version="8.3.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="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="required"/>
<PHP_EXTENSION name="exif" level="optional"/>
<PHP_EXTENSION name="filter" level="required"/>
</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_SETTING name="zend.exception_ignore_args" value="1" level="optional">
<FEEDBACK>
<ON_CHECK message="settingzendexceptionignoreargs" />
</FEEDBACK>
</PHP_SETTING>
</PHP_SETTINGS>
<CUSTOM_CHECKS>
<CUSTOM_CHECK function="\core\environment::check_composer_dependencies_installed" level="optional" />
<CUSTOM_CHECK function="\core\environment::check_composer_developer_dependencies_not_installed" level="optional" />
<CUSTOM_CHECK function="\core\environment::check_composer_dependencies_optimised" level="optional" />
<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="required">
<FEEDBACK>
<ON_ERROR message="sixtyfourbitsrequired" />
</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_CHECK file="lib/upgradelib.php" function="check_mod_assignment" level="required">
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_db_prefix_length" level="required">
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_async_backup" level="recommended">
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_aurora_version" level="optional">
</CUSTOM_CHECK>
</CUSTOM_CHECKS>
</MOODLE>
</COMPATIBILITY_MATRIX>
+8 -2
View File
@@ -923,8 +923,14 @@ if (empty($CFG->disabledevlibdirscheck) && (is_dir($CFG->dirroot.'/vendor') || i
// Check if the site is being foced onto ssl.
$overridetossl = !empty($CFG->overridetossl);
// Check if moodle campaign content setting is enabled or not.
$showcampaigncontent = !isset($CFG->showcampaigncontent) || $CFG->showcampaigncontent;
if (defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING) {
// We need to add this in order to be able to verify the showcampaigncontent setting behaviour during behat tests.
$showcampaigncontent = get_config('core', 'showcampaigncontent');
$showcampaigncontent = $showcampaigncontent === 'true';
} else {
// Check if moodle campaign content setting is enabled or not.
$showcampaigncontent = !isset($CFG->showcampaigncontent) || $CFG->showcampaigncontent;
}
// Encourage admins to enable the user feedback feature if it is not enabled already.
$showfeedbackencouragement = empty($CFG->enableuserfeedback);
+1
View File
@@ -90,6 +90,7 @@ Feature: An administrator can browse user accounts
| username | firstname | lastname | email | confirmed |
| user3 | User | Three | three@example.com | 0 |
And I navigate to "Users > Accounts > Browse list of users" in site administration
And I change window size to "large"
Then I should see "Confirmation pending" in the "User Three" "table_row"
And I press "Resend confirmation email" action in the "User Three" report row
And I should see "Confirmation email sent successfully"
@@ -0,0 +1,17 @@
@core @core_admin
Feature: Toggle campaign banner visibility
In order to control the visibility of the campaign banner content
As an admin
I need to be able to disable the campaign banner display
Scenario Outline: Admin can disable the campaign banner display
Given the following config values are set as admin:
| showcampaigncontent | <showcampaigncontent> |
And I log in as "admin"
When I navigate to "Notifications" in site administration
Then "//iframe[@id='campaign-content']" "xpath_element" <display> exist
Examples:
| showcampaigncontent | display |
| true | should |
| false | should not |
@@ -0,0 +1,77 @@
@tool @tool_lp @tool_lp_framework
Feature: Manage CRUD operations for competencies
In order to perform CRUD operations on competencies
As a manager
I need to be able to create, read, update and delete competencies
Background:
Given the following "core_competency > frameworks" exist:
| shortname | idnumber |
| CF1 | CF1 |
And the following "core_competency > competencies" exist:
| shortname | competencyframework | idnumber | description |
| C1 | CF1 | C1ID | C1 description |
And I log in as "admin"
And I navigate to "Competencies > Competency frameworks" in site administration
And I click on "CF1 (CF1)" "link"
@javascript
Scenario: Create a new competency
# Targets the `CF1` element to make `Add competency` button visible and accessible to avoid ambiguity with the other 'CF1' element on the screen.
Given I click on "//span[text()='CF1']" "xpath_element"
When I press "Add competency"
And I set the field "Name" to "C2"
And I set the field "Description" to "C2 description"
And I set the field "ID number" to "C2ID"
And I press "Save changes"
# Access the newly created competencies to ensure that correct information was registered.
Then "Competency created" "text" should exist
And "C2" "text" should appear after "C1" "text"
And I select "C2" of the competency tree
And "C2ID" "text" should exist
And "C2 description" "text" should exist
@javascript
Scenario: Read a competency
When I select "C1" of the competency tree
# Confirm that selected competency info displayed matches registered info.
Then "C1ID" "text" should exist
And "C1 description" "text" should exist
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
# Similar to the previous step, to avoid ambiguity with the competency framework "Edit", target css element with data-action=edit.
And I click on "[data-action=edit]" "css_element"
# Confirm that the details displayed when accessing edit screen values match registered data.
And the field "Name" matches value "C1"
And the field "Description" matches value "C1 description"
And the field "ID number" matches value "C1ID"
@javascript
Scenario: Update a competency
Given I select "C1" of the competency tree
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
# Similar to the previous step, to avoid ambiguity with the competency framework "Edit", target css element with data-action=edit.
When I click on "[data-action=edit]" "css_element"
And I set the field "Name" to "C2"
And I set the field "Description" to "C2 description"
And I set the field "ID number" to "C2ID"
And I press "Save changes"
Then "Competency updated" "text" should exist
And "C1" "text" should not exist
And "C2" "text" should exist
And I select "C2" of the competency tree
And "C2ID" "text" should exist
And "C1ID" "text" should not exist
And "C2 description" "text" should exist
And "C1 description" "text" should not exist
@javascript
Scenario: Delete a competency
Given I select "C1" of the competency tree
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
When I click on "Delete" "link"
And I click on "Delete" "button" in the "Confirm" "dialogue"
# Confirm that "C1" competency was successfully deleted.
Then "C1" "text" should not exist
@@ -0,0 +1,63 @@
@tool @tool_lp @tool_lp_framework
Feature: Move and cross-reference competencies
In order to move and cross-reference competencies
As a manager
I need to be open the competency's menu items.
Background:
Given the following "core_competency > frameworks" exist:
| shortname | idnumber |
| CF1 | CF1 |
And the following "core_competency > competencies" exist:
| shortname | competencyframework |
| C1 | CF1 |
| C2 | CF1 |
| C3 | CF1 |
| C4 | CF1 |
And I log in as "admin"
And I navigate to "Competencies > Competency frameworks" in site administration
And I click on "CF1 (CF1)" "link"
@javascript
Scenario: Move a competency using Move up/Move down menu items
Given I select "C1" of the competency tree
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
When I click on "Move down" "link"
Then "C1" "text" should appear after "C2" "text"
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
And I click on "Move up" "link"
And "C1" "text" should appear before "C2" "text"
@javascript
Scenario: Move a competency using Relocate menu item
Given I select "C3" of the competency tree
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
When I click on "Relocate" "link"
And I click on "C1" "text" in the "Move competency" "dialogue"
And I click on "Move" "button" in the "Move competency" "dialogue"
Then "C1" "text" should appear before "C3" "text"
And "C3" "text" should appear before "C2" "text"
And I select "C3" of the competency tree
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
# Similar to the previous step, to avoid ambiguity with the competency framework "Edit", target css element with data-action=edit.
And I click on "[data-action=edit]" "css_element"
And "C1" "text" should exist
And "No parent (top-level competency)" "text" should not exist
And I press "Cancel"
And "C1" "text" should appear before "C3" "text"
And "C3" "text" should appear before "C2" "text"
@javascript
Scenario: Cross-reference a competency
Given I select "C1" of the competency tree
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
When I click on "Add cross-referenced competency" "link"
And I click on "C2" "text" in the "Competency picker" "dialogue"
And I click on "Add" "button" in the "Competency picker" "dialogue"
Then "Cross-referenced competencies:" "text" should exist
And I should see "C2 cmp2"
+71 -23
View File
@@ -16,16 +16,10 @@ Feature: Manage plan workflow
| usermanageownplan | User manage own plan role | user |
| manageplan | Manager all plans role | manager |
And the following "role capabilities" exist:
| role | moodle/competency:planmanageowndraft | moodle/competency:planmanageown |
| usermanageowndraftplan | allow | |
| usermanageownplan | allow | allow |
| manageplan | allow | allow |
And the following "role capability" exists:
| role | manageplan |
| moodle/competency:planmanage | allow |
| moodle/competency:planview | allow |
| moodle/competency:planreview | allow |
| moodle/competency:planrequestreview | allow |
| role | moodle/competency:planmanageowndraft | moodle/competency:planmanageown | moodle/competency:planmanage | moodle/competency:planview | moodle/competency:planreview | moodle/competency:planrequestreview |
| usermanageowndraftplan | allow | | | | | |
| usermanageownplan | allow | allow | | | | |
| manageplan | allow | allow | allow | allow | allow | allow |
And the following "role assigns" exist:
| user | role | contextlevel | reference |
| user1 | usermanageowndraftplan | System | |
@@ -53,8 +47,7 @@ Feature: Manage plan workflow
| lp | System | 1 | my-index | content |
Scenario: User can manages his own plan draft
Given I log in as "user1"
And I follow "Profile" in the user menu
Given I am on the "user1" "user > profile" page logged in as user1
When I follow "Learning plans"
Then I should see "List of learning plans"
And I should see "Test-Plan1"
@@ -63,11 +56,9 @@ Feature: Manage plan workflow
And I should see "Waiting for review"
And I click on "Cancel review" of edit menu in the "Test-Plan1" row
And I should see "Draft"
And I log out
Scenario: User can manages his own plan
Given I log in as "user2"
And I follow "Profile" in the user menu
Given I am on the "user2" "user > profile" page logged in as user2
When I follow "Learning plans"
Then I should see "List of learning plans"
And I should see "Test-Plan2"
@@ -82,11 +73,11 @@ Feature: Manage plan workflow
And I should see "Active"
And I click on "Complete this learning plan" of edit menu in the "Test-Plan2" row
And I click on "Complete this learning plan" "button" in the "Confirm" "dialogue"
And I wait until the page is ready
And I should see "Complete"
And I click on "Reopen this learning plan" of edit menu in the "Test-Plan2" row
And I click on "Reopen this learning plan" "button" in the "Confirm" "dialogue"
And I should see "Active"
And I log out
Scenario: Manager can see learning plan with status waiting for review
Given the following "core_competency > plans" exist:
@@ -96,7 +87,6 @@ Feature: Manage plan workflow
When I log in as "manager1"
Then I should see "Test-Plan3"
And I should not see "Test-Plan4"
And I log out
Scenario: Manager can start review of learning plan with status waiting for review
Given the following "core_competency > plans" exist:
@@ -108,7 +98,6 @@ Feature: Manage plan workflow
And I should see "Test-Plan3"
When I follow "Start review"
Then I should see "In review"
And I log out
Scenario: Manager can reject a learning plan with status in review
Given the following "core_competency > plans" exist:
@@ -121,7 +110,6 @@ Feature: Manage plan workflow
And I should see "In review"
When I follow "Finish review"
Then I should see "Draft"
And I log out
Scenario: Manager can accept a learning plan with status in review
Given the following "core_competency > plans" exist:
@@ -134,7 +122,6 @@ Feature: Manage plan workflow
And I should see "In review"
When I follow "Make active"
Then I should see "Active"
And I log out
Scenario: Manager send back to draft an active learning plan
Given the following "core_competency > plans" exist:
@@ -150,7 +137,6 @@ Feature: Manage plan workflow
And I follow "Learning plans"
Then I should see "Draft"
And I should not see "Active"
And I log out
Scenario: Manager change an active learning plan to completed
Given the following "core_competency > plans" exist:
@@ -169,7 +155,6 @@ Feature: Manage plan workflow
And I follow "Learning plans"
Then I should see "Complete"
And I should not see "Active"
And I log out
Scenario: Manager reopen a complete learning plan
Given the following "core_competency > plans" exist:
@@ -187,4 +172,67 @@ Feature: Manage plan workflow
And I follow "Learning plans"
Then I should see "Active"
And I should not see "Complete"
And I log out
Scenario: Student learning plan derived from templates can be completed
Given the following "core_competency > templates" exist:
| shortname |
| LPT1 |
And the following "core_competency > template_competencies" exist:
| template | competency |
| LPT1 | Test-Comp1 |
And I log in as "admin"
And I navigate to "Competencies > Learning plan templates" in site administration
# Select 1 user to assign to create learning plans for using template.
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
And I set the field "Select users" to "user1"
And I press "Create learning plans"
And I click on "LPT1" "link" in the "LPT1" "table_row"
When I click on "Complete this learning plan" "link"
And I click on "Complete this learning plan" "button" in the "Confirm" "dialogue"
# Add a short wait to ensure the page has loaded before checking that "Complete" "text" exists.
And I wait until the page is ready
# Confirm that student's learning plan template is marked as Completed.
Then "Complete" "text" should exist
And "Reopen this learning plan" "link" should exist
Scenario: Learning plan template updates are not reflected on plans already completed
Given the following "core_competency > templates" exist:
| shortname |
| LPT1 |
And the following "core_competency > template_competencies" exist:
| template | competency |
| LPT1 | Test-Comp1 |
And I log in as "admin"
And I navigate to "Competencies > Learning plan templates" in site administration
# Select 2 users to assign to create learning plans for using template.
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
And I set the field "Select users" to "user1"
And I press "Create learning plans"
And I set the field "Select users" to "user2"
And I press "Create learning plans"
# Complete the learning plan for User 1.
And I click on "LPT1" "link" in the "User 1" "table_row"
And I click on "Complete this learning plan" "link"
And I click on "Complete this learning plan" "button" in the "Confirm" "dialogue"
# Add another competency to the learning plan template.
And the following "core_competency > template_competencies" exist:
| template | competency |
| LPT1 | Test-Comp2 |
# Navigate back to the list of Learning plan templates in order to access User 1's learning plan.
And I navigate to "Competencies > Learning plan templates" in site administration
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
# Confirm that only the first competency is reflected on User 1's learning plan since it's already completed.
When I click on "LPT1" "link" in the "User 1" "table_row"
Then "Test-Comp2" "link" should not exist
And "Test-Comp2" "text" should not exist
And "Test-Comp1" "link" should exist
And "Test-Comp1" "text" should exist
# Navigate back to list of Learning plan templates in order to access User 2's learning plan.
And I navigate to "Competencies > Learning plan templates" in site administration
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
# Confirm that both competencies are reflected on User 2's learning plan since it's not yet completed.
And I click on "LPT1" "link" in the "User 2" "table_row"
And "Test-Comp2" "link" should exist
And "Test-Comp2" "text" should exist
And "Test-Comp1" "link" should exist
And "Test-Comp1" "text" should exist
@@ -0,0 +1,60 @@
@tool @tool_lp @core_cohort
Feature: Cohorts can be synchronized with learning plans
In order to create learning plans for cohort members
As an admin
I need to be able to synchronise cohorts with learning plans
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| user1 | User | One | user1@example.com |
| user2 | User | Two | user2@example.com |
And the following "cohorts" exist:
| name | idnumber |
| Cohort 1 | CH1 |
And the following "cohort members" exist:
| user | cohort |
| user1 | CH1 |
| user2 | CH1 |
And the following "core_competency > frameworks" exist:
| shortname | idnumber |
| CF1 | CF1 |
And the following "core_competency > competencies" exist:
| shortname | competencyframework | idnumber |
| C1 | CF1 | C1 |
And the following "core_competency > templates" exist:
| shortname |
| LPT1 |
And the following "core_competency > template_competencies" exist:
| template | competency |
| LPT1 | C1 |
@javascript
Scenario: Cohorts can be synchronised with learning plans
Given I log in as "admin"
# Navigate to the list of learning plan templates in order to add cohorts.
And I navigate to "Competencies > Learning plan templates" in site administration
When I click on "Add cohorts to sync" of edit menu in the "LPT1" row
And I set the field "Select cohorts to sync" to "Cohort 1"
And I press "Add cohorts"
And I wait until the page is ready
# Confirm that 2 learning plans were created for members of the cohort.
Then "2 learning plans were created" "text" should exist
# Confirm current screen is still "Cohorts synced to this learning plan template screen"
And "Cohorts synced to this learning plan template" "text" should exist
# Confirm that the cohort is now added to the learning plan template.
And the following should exist in the "generaltable" table:
| Name | Cohort ID |
| Cohort 1 | CH1 |
# Navigate back to the list of learning plan templates to view updated list.
And I navigate to "Competencies > Learning plan templates" in site administration
# Confirm that the added cohort and learning plans are now reflected on the list of Learning plan templates.
And the following should exist in the "generaltable" table:
| Name | Category | Cohorts | Learning plans |
| LPT1 | System | 1 | 2 |
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
# Confirm that learning plans were created for all cohort members.
And the following should exist in the "generaltable" table:
| Name | First name | Email address |
| LPT1 | User One | user1@example.com |
| LPT1 | User Two | user2@example.com |
+13 -6
View File
@@ -370,7 +370,7 @@ class core_auth_external extends external_api {
* @throws moodle_exception
*/
public static function resend_confirmation_email($username, $password, $redirect = '') {
global $PAGE;
global $PAGE, $CFG;
$warnings = array();
$params = self::validate_parameters(
@@ -387,20 +387,27 @@ class core_auth_external extends external_api {
$username = trim(core_text::strtolower($params['username']));
$password = $params['password'];
$user = core_user::get_user_by_username($username);
if (!empty($user) && $user->confirmed) {
if (!empty($CFG->protectusernames)) {
throw new moodle_exception('invalidlogin');
}
throw new moodle_exception('alreadyconfirmed');
}
if (is_restored_user($username)) {
if (!empty($CFG->protectusernames)) {
throw new moodle_exception('invalidlogin');
}
throw new moodle_exception('restoredaccountresetpassword', 'webservice');
}
$user = authenticate_user_login($username, $password);
if (empty($user)) {
throw new moodle_exception('invalidlogin');
}
if ($user->confirmed) {
throw new moodle_exception('alreadyconfirmed');
}
// Check if we should redirect the user once the user is confirmed.
$confirmationurl = null;
if (!empty($params['redirect'])) {
+14
View File
@@ -104,6 +104,7 @@ class auth_plugin_lti extends \auth_plugin_base {
* @param int $provisioningmode the desired account provisioning mode, which controls the auth flow for unbound users.
* @param array $legacyconsumersecrets an array of secrets used by the legacy consumer if a migration claim exists.
* @throws coding_exception if the specified provisioning mode is invalid.
* @throws moodle_exception if user authentication fails.
*/
public function complete_login(array $launchdata, moodle_url $returnurl, int $provisioningmode,
array $legacyconsumersecrets = []): void {
@@ -112,6 +113,19 @@ class auth_plugin_lti extends \auth_plugin_base {
if ($this->get_user_binding($launchdata['iss'], $launchdata['sub'])) {
$user = $this->find_or_create_user_from_launch($launchdata);
if ($user->suspended) {
$failurereason = AUTH_LOGIN_SUSPENDED;
$event = \core\event\user_login_failed::create([
'userid' => $user->id,
'other' => [
'username' => $user->username,
'reason' => $failurereason
]
]);
$event->trigger();
throw new moodle_exception('invalidlogin', 'core');
}
if (isloggedin()) {
// If a different user is currently logged in, authenticate the linked user instead.
global $USER;
+1 -1
View File
@@ -25,7 +25,7 @@
require_once('../../config.php');
$issuerid = required_param('id', PARAM_INT);
$wantsurl = new moodle_url(optional_param('wantsurl', '', PARAM_URL));
$wantsurl = new moodle_url(optional_param('wantsurl', '', PARAM_LOCALURL));
$PAGE->set_context(context_system::instance());
$PAGE->set_url(new moodle_url('/auth/oauth2/login.php', ['id' => $issuerid]));
+9
View File
@@ -181,6 +181,7 @@ final class external_test extends externallib_advanced_testcase {
$this->assertTrue($result['success']);
$this->assertEmpty($result['warnings']);
set_config('protectusernames', 0);
$_SERVER['HTTP_USER_AGENT'] = 'no browser'; // Hack around missing user agent in CLI scripts.
$this->expectException('\moodle_exception');
$this->expectExceptionMessage('error/invalidlogin');
@@ -204,6 +205,7 @@ final class external_test extends externallib_advanced_testcase {
$this->assertTrue($result['success']);
$this->assertEmpty($result['warnings']);
set_config('protectusernames', 0);
$_SERVER['HTTP_USER_AGENT'] = 'no browser'; // Hack around missing user agent in CLI scripts.
$this->expectException('\moodle_exception');
$this->expectExceptionMessage('error/invalidlogin');
@@ -234,6 +236,13 @@ final class external_test extends externallib_advanced_testcase {
$result = external_api::clean_returnvalue(core_auth_external::confirm_user_returns(), $result);
$this->assertTrue($result['success']);
// Keep protectusernames enabled so the call returns invalidlogin exception.
$this->expectException('\moodle_exception');
$this->expectExceptionMessage('error/invalidlogin');
core_auth_external::resend_confirmation_email($username, $password);
// Now disable protectusernames and expect an exception.
set_config('protectusernames', 0);
$this->expectException('\moodle_exception');
$this->expectExceptionMessage('error/alreadyconfirmed');
core_auth_external::resend_confirmation_email($username, $password);
+10
View File
@@ -82,6 +82,16 @@ if (!$badge->is_active()) {
die();
}
if (!empty($role)) {
if (!user_has_role_assignment($USER->id, $role, $context->id) && !$isadmin) {
// User does not have the role passed by the parameter.
echo $OUTPUT->header();
echo $OUTPUT->notification(get_string('wrongrole', 'badges'));
echo $OUTPUT->footer();
die();
}
}
$returnurl = new moodle_url('recipients.php', array('id' => $badge->id));
$returnlink = html_writer::link($returnurl, $strrecipients);
$actionbar = new \core_badges\output\standard_action_bar($PAGE, $badge->type, false, false, $returnurl);
+40 -40
View File
@@ -41,8 +41,8 @@ final class rrule_manager_test extends \advanced_testcase {
global $DB;
$this->resetAfterTest();
// Set our timezone based on the timezone in the RFC's samples (US/Eastern).
$tz = 'US/Eastern';
// Set our timezone based on the timezone in the RFC's samples (America/New_York).
$tz = 'America/New_York';
$this->setTimezone($tz);
$timezone = new \DateTimeZone($tz);
// Create our event's DTSTART date based on RFC's samples (most commonly used in RFC is 1997-09-02 09:00:00 EDT).
@@ -685,8 +685,8 @@ final class rrule_manager_test extends \advanced_testcase {
// Change the start date for forever events to 9am of the 2nd day of the current month and year.
$this->change_event_startdate(date('Ym02\T090000'));
$startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart));
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart));
$startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart), new \DateTimeZone('Australia/Perth'));
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart), new \DateTimeZone('Australia/Perth'));
$offsetinterval = $startdatetime->diff($startdate, true);
$interval = new \DateInterval('P12M');
@@ -842,8 +842,8 @@ final class rrule_manager_test extends \advanced_testcase {
// Change the start date for forever events to 9am of the 2nd day of the current month and year.
$this->change_event_startdate(date('Ym02\T090000'));
$startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart));
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart));
$startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart), new \DateTimeZone('Australia/Perth'));
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart), new \DateTimeZone('Australia/Perth'));
$offsetinterval = $startdatetime->diff($startdate, true);
$interval = new \DateInterval('P12M');
@@ -1201,8 +1201,8 @@ final class rrule_manager_test extends \advanced_testcase {
public function test_every_other_day_forever(): void {
global $DB;
// Change the start date for forever events to 9am of the current date in US/Eastern time.
$this->change_event_startdate(date('Ymd\T090000'), 'US/Eastern');
// Change the start date for forever events to 9am of the current date in America/New_York time.
$this->change_event_startdate(date('Ymd\T090000'), 'America/New_York');
$startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart));
$interval = new \DateInterval('P2D');
@@ -1271,7 +1271,7 @@ final class rrule_manager_test extends \advanced_testcase {
global $DB;
// Change our event's date to 01-01-1998, based on the example from the RFC.
$this->change_event_startdate('19980101T090000', 'US/Eastern');
$this->change_event_startdate('19980101T090000', 'America/New_York');
$rrule = 'FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA';
$mang = new rrule_manager($rrule);
@@ -1305,7 +1305,7 @@ final class rrule_manager_test extends \advanced_testcase {
global $DB;
// Change our event's date to 01-01-1998, based on the example from the RFC.
$this->change_event_startdate('19980101T090000', 'US/Eastern');
$this->change_event_startdate('19980101T090000', 'America/New_York');
$rrule = 'FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1';
$mang = new rrule_manager($rrule);
@@ -1403,8 +1403,8 @@ final class rrule_manager_test extends \advanced_testcase {
public function test_every_other_week_forever(): void {
global $DB;
// Change the start date for forever events to 9am of the current date in US/Eastern time.
$this->change_event_startdate(date('Ymd\T090000'), 'US/Eastern');
// Change the start date for forever events to 9am of the current date in America/New_York time.
$this->change_event_startdate(date('Ymd\T090000'), 'America/New_York');
$interval = new \DateInterval('P2W');
@@ -1614,7 +1614,7 @@ final class rrule_manager_test extends \advanced_testcase {
global $DB;
// Change our event's date to 05-09-1997, based on the example from the RFC.
$startdatetime = $this->change_event_startdate('19970905T090000', 'US/Eastern');
$startdatetime = $this->change_event_startdate('19970905T090000', 'America/New_York');
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart));
$offsetinterval = $startdatetime->diff($startdate, true);
@@ -1650,7 +1650,7 @@ final class rrule_manager_test extends \advanced_testcase {
global $DB;
// Change our event's date to 05-09-1997, based on the example from the RFC.
$startdatetime = $this->change_event_startdate('19970905T090000', 'US/Eastern');
$startdatetime = $this->change_event_startdate('19970905T090000', 'America/New_York');
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart));
$offsetinterval = $startdatetime->diff($startdate, true);
@@ -1688,7 +1688,7 @@ final class rrule_manager_test extends \advanced_testcase {
global $DB;
// Change our event's date to 05-09-1997, based on the example from the RFC.
$startdatetime = $this->change_event_startdate('19970907T090000', 'US/Eastern');
$startdatetime = $this->change_event_startdate('19970907T090000', 'America/New_York');
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart));
$offsetinterval = $startdatetime->diff($startdate, true);
@@ -1731,7 +1731,7 @@ final class rrule_manager_test extends \advanced_testcase {
global $DB;
// Change our event's date to 05-09-1997, based on the example from the RFC.
$startdatetime = $this->change_event_startdate('19970922T090000', 'US/Eastern');
$startdatetime = $this->change_event_startdate('19970922T090000', 'America/New_York');
$startdate = new \DateTime($startdatetime->format('Y-m-d'));
$offsetinterval = $startdatetime->diff($startdate, true);
@@ -1773,7 +1773,7 @@ final class rrule_manager_test extends \advanced_testcase {
global $DB;
// Change our event's date to 28 September of the current year, based on the example from the RFC.
$this->change_event_startdate(date('Y0928\T090000'), 'US/Eastern');
$this->change_event_startdate(date('Y0928\T090000'), 'America/New_York');
$rrule = 'FREQ=MONTHLY;BYMONTHDAY=-3';
$mang = new rrule_manager($rrule);
@@ -1859,7 +1859,7 @@ final class rrule_manager_test extends \advanced_testcase {
public function test_every_first_and_last_day_of_the_month_10_count(): void {
global $DB;
$startdatetime = $this->change_event_startdate('19970930T090000', 'US/Eastern');
$startdatetime = $this->change_event_startdate('19970930T090000', 'America/New_York');
$startdate = new \DateTime($startdatetime->format('Y-m-d'));
$offsetinterval = $startdatetime->diff($startdate, true);
@@ -1904,7 +1904,7 @@ final class rrule_manager_test extends \advanced_testcase {
public function test_every_18_months_days_10_to_15_10_count(): void {
global $DB;
$startdatetime = $this->change_event_startdate('19970910T090000', 'US/Eastern');
$startdatetime = $this->change_event_startdate('19970910T090000', 'America/New_York');
$rrule = 'FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15';
$mang = new rrule_manager($rrule);
@@ -1949,9 +1949,9 @@ final class rrule_manager_test extends \advanced_testcase {
public function test_every_tuesday_every_other_month_forever(): void {
global $DB;
// Change the start date for forever events to 9am of the Tuesday on or before of the current date in US/Eastern time.
// Change the start date for forever events to 9am of the Tuesday on or before of the current date in America/New_York time.
$nexttuesday = new \DateTime('next Tuesday');
$this->change_event_startdate($nexttuesday->format('Ymd\T090000'), 'US/Eastern');
$this->change_event_startdate($nexttuesday->format('Ymd\T090000'), 'America/New_York');
$rrule = 'FREQ=MONTHLY;INTERVAL=2;BYDAY=TU';
$mang = new rrule_manager($rrule);
@@ -2004,7 +2004,7 @@ final class rrule_manager_test extends \advanced_testcase {
public function test_yearly_in_june_july_10_count(): void {
global $DB;
$startdatetime = $this->change_event_startdate('19970610T090000', 'US/Eastern');
$startdatetime = $this->change_event_startdate('19970610T090000', 'America/New_York');
$rrule = 'FREQ=YEARLY;COUNT=10;BYMONTH=6,7';
$mang = new rrule_manager($rrule);
@@ -2047,7 +2047,7 @@ final class rrule_manager_test extends \advanced_testcase {
public function test_every_other_year_in_june_july_10_count(): void {
global $DB;
$startdatetime = $this->change_event_startdate('19970310T090000', 'US/Eastern');
$startdatetime = $this->change_event_startdate('19970310T090000', 'America/New_York');
$rrule = 'FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3';
$mang = new rrule_manager($rrule);
@@ -2094,7 +2094,7 @@ final class rrule_manager_test extends \advanced_testcase {
public function test_every_3_years_1st_100th_200th_days_10_count(): void {
global $DB;
$startdatetime = $this->change_event_startdate('19970101T090000', 'US/Eastern');
$startdatetime = $this->change_event_startdate('19970101T090000', 'America/New_York');
$rrule = 'FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200';
$mang = new rrule_manager($rrule);
@@ -2144,7 +2144,7 @@ final class rrule_manager_test extends \advanced_testcase {
// Change our event's date to the 20th Monday of the current year.
$twentiethmonday = new \DateTime(date('Y-01-01'));
$twentiethmonday->modify('+20 Monday');
$startdatetime = $this->change_event_startdate($twentiethmonday->format('Ymd\T000000'), 'US/Eastern');
$startdatetime = $this->change_event_startdate($twentiethmonday->format('Ymd\T000000'), 'America/New_York');
$interval = new \DateInterval('P1Y');
@@ -2190,7 +2190,7 @@ final class rrule_manager_test extends \advanced_testcase {
// Change our event's date to the start of the 20th week of the current year.
$twentiethweek = new \DateTime(date('Y-01-01'));
$twentiethweek->setISODate($twentiethweek->format('Y'), 20);
$startdatetime = $this->change_event_startdate($twentiethweek->format('Ymd\T090000'), 'US/Eastern');
$startdatetime = $this->change_event_startdate($twentiethweek->format('Ymd\T090000'), 'America/New_York');
$startdate = clone($startdatetime);
$startdate->modify($startdate->format('Y-m-d'));
@@ -2237,9 +2237,9 @@ final class rrule_manager_test extends \advanced_testcase {
public function test_every_thursday_in_march_forever(): void {
global $DB;
// Change our event's date to the first Thursday of March of the current year at 9am US/Eastern time.
// Change our event's date to the first Thursday of March of the current year at 9am America/New_York time.
$firstthursdayofmarch = new \DateTime('first Thursday of March');
$startdatetime = $this->change_event_startdate($firstthursdayofmarch->format('Ymd\T090000'), 'US/Eastern');
$startdatetime = $this->change_event_startdate($firstthursdayofmarch->format('Ymd\T090000'), 'America/New_York');
$interval = new \DateInterval('P1Y');
@@ -2296,9 +2296,9 @@ final class rrule_manager_test extends \advanced_testcase {
public function test_every_thursday_june_july_august_forever(): void {
global $DB;
// Change our event's date to the first Thursday of June in the current year at 9am US/Eastern time.
// Change our event's date to the first Thursday of June in the current year at 9am America/New_York time.
$firstthursdayofjune = new \DateTime('first Thursday of June');
$startdatetime = $this->change_event_startdate($firstthursdayofjune->format('Ymd\T090000'), 'US/Eastern');
$startdatetime = $this->change_event_startdate($firstthursdayofjune->format('Ymd\T090000'), 'America/New_York');
$startdate = new \DateTime($startdatetime->format('Y-m-d'));
@@ -2353,8 +2353,8 @@ final class rrule_manager_test extends \advanced_testcase {
public function test_friday_the_thirteenth_forever(): void {
global $DB;
// Change our event's date to the first Thursday of June in the current year at 9am US/Eastern time.
$this->change_event_startdate(date('Ymd\T090000'), 'US/Eastern');
// Change our event's date to the first Thursday of June in the current year at 9am America/New_York time.
$this->change_event_startdate(date('Ymd\T090000'), 'America/New_York');
$rrule = 'FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13';
$mang = new rrule_manager($rrule);
@@ -2390,10 +2390,10 @@ final class rrule_manager_test extends \advanced_testcase {
public function test_first_saturday_following_first_sunday_forever(): void {
global $DB;
// Change our event's date to the next Saturday after the first Sunday of the the current month at 9am US/Eastern time.
// Change our event's date to the next Saturday after the first Sunday of the current month at 9am America/New_York time.
$firstsaturdayafterfirstsunday = new \DateTime('first Sunday of this month');
$firstsaturdayafterfirstsunday->modify('next Saturday');
$startdatetime = $this->change_event_startdate($firstsaturdayafterfirstsunday->format('Ymd\T090000'), 'US/Eastern');
$startdatetime = $this->change_event_startdate($firstsaturdayafterfirstsunday->format('Ymd\T090000'), 'America/New_York');
$startdate = new \DateTime($startdatetime->format('Y-m-d'));
$offset = $startdatetime->diff($startdate, true);
@@ -2451,7 +2451,7 @@ final class rrule_manager_test extends \advanced_testcase {
$electiondate->modify('+1 Tuesday');
// Use the most recent election date as the starting date of our recurring events.
$startdatetime = $this->change_event_startdate($electiondate->format('Ymd\T090000'), 'US/Eastern');
$startdatetime = $this->change_event_startdate($electiondate->format('Ymd\T090000'), 'America/New_York');
$startdate = new \DateTime($startdatetime->format('Y-m-d'));
$offset = $startdatetime->diff($startdate, true);
@@ -2494,7 +2494,7 @@ final class rrule_manager_test extends \advanced_testcase {
public function test_monthly_bysetpos_3_count(): void {
global $DB;
$this->change_event_startdate('19970904T090000', 'US/Eastern');
$this->change_event_startdate('19970904T090000', 'America/New_York');
$rrule = 'FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3';
$mang = new rrule_manager($rrule);
@@ -2531,7 +2531,7 @@ final class rrule_manager_test extends \advanced_testcase {
public function test_second_to_the_last_weekday_of_the_month(): void {
global $DB;
$this->change_event_startdate('19970929T090000', 'US/Eastern');
$this->change_event_startdate('19970929T090000', 'America/New_York');
$rrule = 'FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2;COUNT=7';
$mang = new rrule_manager($rrule);
@@ -2756,7 +2756,7 @@ final class rrule_manager_test extends \advanced_testcase {
public function test_weekly_byday_with_wkst_mo(): void {
global $DB;
$this->change_event_startdate('19970805T090000', 'US/Eastern');
$this->change_event_startdate('19970805T090000', 'America/New_York');
$rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO';
$mang = new rrule_manager($rrule);
@@ -2789,7 +2789,7 @@ final class rrule_manager_test extends \advanced_testcase {
public function test_weekly_byday_with_wkst_su(): void {
global $DB;
$this->change_event_startdate('19970805T090000', 'US/Eastern');
$this->change_event_startdate('19970805T090000', 'America/New_York');
$rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU';
$mang = new rrule_manager($rrule);
@@ -2869,7 +2869,7 @@ final class rrule_manager_test extends \advanced_testcase {
* Change the event's timestart (DTSTART) based on the test's needs.
*
* @param string $datestr The date string. In 'Ymd\This' format. e.g. 19990902T090000.
* @param null|string $timezonestr A valid timezone string. e.g. 'US/Eastern'.
* @param null|string $timezonestr A valid timezone string. e.g. 'America/New_York'.
* If not provided, the default timezone will be used.
* @return bool|DateTime
*/
@@ -1,4 +1,4 @@
@core @core_contentbank @core_h5p @contenttype_h5p @_switch_iframe @javascript
@core @core_contentbank @core_h5p @contenttype_h5p @_switch_iframe
Feature: H5P file upload to content bank for admins
In order import new H5P content to content bank
As an admin
@@ -16,6 +16,7 @@ Feature: H5P file upload to content bank for admins
And I expand "Site pages" node
And I click on "Content bank" "link"
@javascript
Scenario: Admins can upload .h5p extension files to content bank
Given I should not see "filltheblanks.h5p"
When I click on "Upload" "link"
@@ -27,6 +28,7 @@ Feature: H5P file upload to content bank for admins
And I wait until the page is ready
Then I should see "filltheblanks.h5p"
@javascript
Scenario: Admins can see uploaded H5P contents
Given I should not see "filltheblanks.h5p"
When I click on "Upload" "link"
@@ -40,6 +42,7 @@ Feature: H5P file upload to content bank for admins
And I switch to "h5p-iframe" class iframe
Then I should see "Of which countries"
@javascript
Scenario: Users can't see content managed by disabled plugins
Given I click on "Upload" "link"
And I click on "Choose a file..." "button"
@@ -55,6 +58,7 @@ Feature: H5P file upload to content bank for admins
When I navigate to "Plugins > Content bank" in site administration
Then I should not see "filltheblanks.h5p"
@javascript
Scenario: Contents in a context are not available from other contexts
Given the following "courses" exist:
| fullname | shortname | category |
@@ -75,6 +79,7 @@ Feature: H5P file upload to content bank for admins
And I click on "Content bank" "link"
And I should not see "filltheblanks.h5p"
@javascript
Scenario: Admins can upload and deployed content types when libraries are not installed
Given I navigate to "H5P > Manage H5P content types" in site administration
And I should not see "Fill in the Blanks"
@@ -95,6 +100,7 @@ Feature: H5P file upload to content bank for admins
And I navigate to "H5P > Manage H5P content types" in site administration
And I should see "Fill in the Blanks"
@javascript
Scenario: Uploading invalid packages throws error
Given the following "user private files" exist:
| user | filepath |
@@ -122,3 +128,28 @@ Feature: H5P file upload to content bank for admins
And I wait until the page is ready
And I should see "It is not possible to unzip it"
And I should not see "Sorry, this file is not valid"
@_file_upload @javascript
Scenario: Uploading invalid file types is not allowed
Given I am on the "Content bank" page logged in as "admin"
When I click on "Upload" "link"
And I click on "Choose a file..." "button"
And I click on "Upload a file" "link" in the ".fp-repo-area" "css_element"
And I set the field "Attachment" to "#dirroot#/course/tests/fixtures/image.jpg"
And I press "Upload this file"
# Confirm that jpg files cannot be added to content bank.
Then "Image (JPEG) filetype cannot be accepted." "text" should exist
And I click on "OK" "button" in the "Error" "dialogue"
And I click on "Close" "button" in the "File picker" "dialogue"
And I click on "Cancel" "button" in the "Upload" "dialogue"
# Confirm that jpg file was not added to the content bank.
And "No content available" "text" should exist
And I should not see "1.jpg"
Scenario: File upload for content bank can be disabled by admin
Given I log in as "admin"
And I navigate to "Plugins > Content bank > Manage content types" in site administration
When I follow "Disable"
And I am on the "Content bank" page
# Confirm that "Upload" does not exist after admin disabled from Plugins > Content bank > Manage content types.
Then "Upload" "link" should not exist
@@ -0,0 +1,38 @@
@core @core_contentbank @core_h5p @contentbank_h5p
Feature: Content bank contents are retained when course is restored
In order to restore content bank contents
As a manager
I need to be able to restore course containing the content bank
Background:
Given the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| admin | C1 | editingteacher |
And the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
And I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
# TODO MDL-57120 site "Content bank" link not accessible without navigation block.
And I add the "Navigation" block if not present
@javascript
Scenario: Deleted courses with content banks can be restored
Given I navigate to "Courses > Manage courses and categories" in site administration
And I click on "delete" action for "Course 1" in management course listing
And I click on "Delete" "button" in the "Confirm" "dialogue"
And I press "Continue"
And I navigate to "Recycle bin" in current page administration
And I click on "Restore" "link" in the "Course 1" "table_row"
And I am on "Course 1" course homepage
And I expand "Site pages" node
When I click on "Content bank" "link"
And I click on "filltheblanks.h5p" "link"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
Then I should see "Of which countries are Berlin, Washington, Beijing, Canberra and Brasilia the capitals?"
+14
View File
@@ -259,6 +259,20 @@ class tool_provider extends ToolProvider {
// Get the updated user record.
$user = $DB->get_record('user', ['id' => $user->id]);
} else {
if ($dbuser->suspended) {
require_once($CFG->libdir . '/authlib.php');
$failurereason = AUTH_LOGIN_SUSPENDED;
$event = \core\event\user_login_failed::create([
'userid' => $dbuser->id,
'other' => [
'username' => $dbuser->username,
'reason' => $failurereason
]
]);
$event->trigger();
throw new moodle_exception('invalidlogin', 'core');
}
if (helper::user_match($user, $dbuser)) {
$user = $dbuser;
} else {
+15
View File
@@ -76,6 +76,16 @@ class gradingform_guide_renderer extends plugin_renderer_base {
$criterion[$key] = '';
}
}
// Set a default description format if not set.
if (!array_key_exists('descriptionformat', $criterion)) {
$criterion['descriptionformat'] = FORMAT_MOODLE;
}
// Set a default description markers format if not set.
if (!array_key_exists('descriptionmarkersformat', $criterion)) {
$criterion['descriptionmarkersformat'] = FORMAT_MOODLE;
}
}
$criteriontemplate = html_writer::start_tag('tr', array('class' => 'criterion'. $criterion['class'],
@@ -398,6 +408,11 @@ class gradingform_guide_renderer extends plugin_renderer_base {
'title' => get_string('clicktocopy', 'gradingform_guide'),
'id' => '{NAME}[comments][{COMMENT-id}]', 'class'=>'markingguidecomment'));
} else {
// Set a default description format if not set.
if (!array_key_exists('descriptionformat', $comment)) {
$comment['descriptionformat'] = FORMAT_MOODLE;
}
$description = format_text($comment['description'], $comment['descriptionformat']);
}
// Retain newlines as <br> tags when displaying 'frequently used comments'.
@@ -0,0 +1,104 @@
@gradingform @gradingform_guide
Feature: Marking guide criterion can be edited and replaced
In order to edit and replace a marking guide criterion
As a teacher
I need to have an existing marking guide criterion
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "activities" exist:
| activity | course | name | advancedgradingmethod_submissions | assignsubmission_onlinetext_enabled |
| assign | C1 | Assign 1 | guide | 1 |
And I am on the "Course 1" course page logged in as teacher1
And I go to "Assign 1" advanced grading definition page
And I set the following fields to these values:
| Name | Assign 1 marking guide |
| Description | Marking guide description |
And I define the following marking guide:
| Criterion name | Description for students | Description for markers | Maximum score |
| Grade Criteria 1 | Grade 1 description for students | Grade 1 description for markers | 70 |
| Grade Criteria 2 | Grade 2 description for students | Grade 2 description for markers | 30 |
And I define the following frequently used comments:
| Comment 1 |
| Comment 2 |
| Comment 3 |
And I press "Save marking guide and make it ready"
And the following "mod_assign > submissions" exist:
| assign | user | onlinetext |
| Assign 1 | student1 | I'm the student first submission |
And I am on the "Assign 1" "assign activity" page
And I go to "Student 1" "Assign 1" activity advanced grading page
And I grade by filling the marking guide with:
| Grade Criteria 1 | 50 | Comment 1 |
| Grade Criteria 2 | 20 | Comment 2 |
And I press "Save changes"
@javascript
Scenario: Marking guide frequently used comments can be updated and reordered
Given I go to "Assign 1" advanced grading definition page
And I click on "Move up" "button" in the "Comment 3" "table_row"
And I click on "Move down" "button" in the "Comment 1" "table_row"
When I press "Save"
# Confirm that the order of the frequently used comments are updated.
Then "Comment 3" "text" should appear before "Comment 1" "text"
And "Comment 2" "text" should appear after "Comment 1" "text"
And I am on the "Assign 1" "assign activity" page
And I go to "Student 1" "Assign 1" activity advanced grading page
# Confirm that there are no changes to the existing marking records.
And I should see "Comment 1" in the "Grade Criteria 1" "table_row"
And I should see "Comment 2" in the "Grade Criteria 2" "table_row"
@javascript
Scenario: Marking guide criterion can be modified
Given I go to "Assign 1" advanced grading definition page
And I click on "Delete criterion" "button" in the "Grade Criteria 1" "table_row"
And I press "Yes"
And I wait "1" seconds
And I should not see "Grade Criteria 1"
And I press "Add criterion"
# Used xpath_element selectors in order to differentiate multiple criterion fields.
And I click on "//table[@id='guide-criteria']//tr[@class='criterion odd last']//div[@class='criterionname']" "xpath_element"
# Set the new criterion name.
And I set the field with xpath "//input[@id='guide-criteria-NEWID1-shortname']" to "Grade Criteria 3"
And I click on "//table[@id='guide-criteria']//tr[@class='criterion odd last']//div[@class='criteriondesc']//span[@class='textvalue']" "xpath_element"
# Set the new criterion description for students.
And I set the field with xpath "//textarea[@id='guide[criteria][NEWID1][description]']" to "Grade 3 description for students"
And I click on "//table[@id='guide-criteria']//tr[@class='criterion odd last']//div[@class='criteriondescmarkers']//span[@class='textvalue']" "xpath_element"
# Set the new criterion description for markers.
And I set the field with xpath "//textarea[@id='guide[criteria][NEWID1][descriptionmarkers]']" to "Grade 3 description for markers"
And I click on "//table[@id='guide-criteria']//tr[@class='criterion odd last']//div[@class='criterionmaxscore']//span[@class='textvalue']" "xpath_element"
# Set the new criterion max score.
And I set the field with xpath "//input[@id='guide[criteria][NEWID1][maxscore]']" to "70"
When I press "Save"
Then "You are about to save significant changes to a marking guide that has already been used for grading. The gradebook value will be unchanged, but the marking guide will be hidden from students until their item is regraded." "text" should exist
And I press "Continue"
And I am on the "Assign 1" "assign activity" page logged in as student1
# Confirm that the remarks for each grading criterion are not displayed.
And "Comment 1" "text" should not exist
And "Comment 2" "text" should not exist
# Confirm that the submission is not regraded.
And I should see "70.00 / 100.00"
# Regrade the submission.
And I am on the "Course 1" course page logged in as teacher1
And I go to "Student 1" "Assign 1" activity advanced grading page
And I grade by filling the marking guide with:
| Grade Criteria 2 | 15 | Comment 3 |
| Grade Criteria 3 | 50 | Comment 2 |
And I press "Save changes"
And I am on the "Assign 1" "assign activity" page logged in as student1
# Confirm that submission is re-marked and regraded.
And I should see "65.00 / 100.00"
And I should see the marking guide information displayed as:
| criteria | description | remark | maxscore | criteriascore |
| Grade Criteria 2 | Grade 2 description for students | Comment 3 | 30 | 15 / 30 |
| Grade Criteria 3 | Grade 3 description for students | Comment 2 | 70 | 50 / 70 |
@@ -0,0 +1,44 @@
@gradingform @gradingform_guide
Feature: Verify listings and grading submissions
In order to verify grade listing
As a teacher
I need to be able to see grades in the correct column
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student | Student | One | one@example.com |
| teacher | Teacher | One | t1@example.com |
And the following "courses" exist:
| shortname | fullname |
| C1 | Course 1 |
And the following "course enrolments" exist:
| user | course | role |
| student | C1 | student |
| teacher | C1 | editingteacher |
And the following "activities" exist:
| activity | name | course | advancedgradingmethod_submissions | assignsubmission_onlinetext_enabled |
| assign | Assign1 | C1 | guide | 1 |
And I am on the "Course 1" course page logged in as teacher
And I go to "Assign1" advanced grading definition page
And I set the following fields to these values:
| Name | Assign1 marking guide |
| Description | Marking guide description |
And I define the following marking guide:
| Criterion name | Description for students | Description for markers | Maximum score |
| Criteria 1 | Grade 1 description for students | Grade 1 description for markers | 100 |
And I press "Save marking guide and make it ready"
And the following "mod_assign > submissions" exist:
| assign | user | onlinetext |
| Assign1 | student | This is a submission for assignment |
@javascript
Scenario: Mark and view all grades in submissions table
Given I am on the "Assign1" "assign activity" page
And I click on "View all submission" "link"
And I click on "Grade" "link" in the "Student One" "table_row"
And I grade by filling the marking guide with:
| Criteria 1 | 50 | Excellent work! |
And I press "Save changes"
When I follow "View all submissions"
Then "Student One" row "Grade" column of "generaltable" table should contain "50"
+1
View File
@@ -31,3 +31,4 @@ defined('MOODLE_INTERNAL') || die();
$string['clianswerno'] = '';
$string['cliansweryes'] = '';
$string['cliincorrectvalueretry'] = 'Skeivt virði, vinarliga royn aftur';
+32
View File
@@ -0,0 +1,32 @@
<?php
// This file is part of Moodle - https://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
/**
* Automatically generated strings for Moodle installer
*
* Do not edit this file manually! It contains just a subset of strings
* needed during the very first steps of installation. This file was
* generated automatically by export-installer.php (which is part of AMOS
* {@link https://moodledev.io/general/projects/api/amos}) using the
* list of strings defined in install/stringnames.txt file.
*
* @package installer
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
+1
View File
@@ -35,3 +35,4 @@ $string['cliunknowoption'] = 'Roghainnean nach deach an aithneachadh:
{$a}
Cleachd— an roghainn airson cuideachadh';
$string['environmentrequireversion'] = 'tha tionndadh {$a->needed} deatamach agus tha thusa a ruith {$a->current}';
$string['upgradekeyset'] = 'Iuchair àrdachaidh (fàg bàn gus nach tèid a shuidheachadh)';
+4
View File
@@ -29,6 +29,9 @@
defined('MOODLE_INTERNAL') || die();
$string['cannotcreatedboninstall'] = '<p>לא ניתן ליצור את מסד הנתונים.</p>
<p>מסד הנתונים שצוין אינו קיים, ולמשתמש שצוין אין הרשאה ליצור את מסד הנתונים.</p>
<p>מנהל האתר צריך לבדוק את הגדרות מסד הנתונים.</p>';
$string['cannotcreatelangdir'] = 'לא ניתן ליצור סיפריית שפה.';
$string['cannotcreatetempdir'] = 'לא ניתן ליצור סיפרייה זמנית.';
$string['cannotdownloadcomponents'] = 'לא ניתן להוריד רכיבים.';
@@ -38,6 +41,7 @@ $string['cannotsavemd5file'] = 'לא ניתן לשמור קובץ md5';
$string['cannotsavezipfile'] = 'לא ניתן לשמור קובץ ZIP';
$string['cannotunzipfile'] = 'לא ניתן לפתוח את קובץ ה-ZIP.';
$string['componentisuptodate'] = 'הרכיב מעודכן.';
$string['dmlexceptiononinstall'] = '<p>אירעה שגיאת מסד נתונים [{$a->errorcode}].<br />{$a->debuginfo}</p>';
$string['downloadedfilecheckfailed'] = 'הקובץ אשר ירד נמצא שגוי';
$string['invalidmd5'] = 'md5 לא חוקי';
$string['missingrequiredfield'] = 'חסר שדה נדרש כלשהו';
+6
View File
@@ -37,3 +37,9 @@ $string['cannotcreatetempdir'] = 'Vaqtinchalik jildni yaratib bolmadi';
$string['cannotdownloadcomponents'] = 'Komponentlarni yuklab olib bo\'lmadi';
$string['cannotdownloadzipfile'] = 'ZIP faylini yuklab olib bo\'lmadi';
$string['cannotfindcomponent'] = 'Komponentni topib bo\'lmadi';
$string['cannotsavemd5file'] = 'md5 faylini saqlab bolmadi';
$string['cannotsavezipfile'] = 'ZIP faylini saqlab bolmadi';
$string['cannotunzipfile'] = 'Faylni arxivdan chiqarib bolmadi';
$string['componentisuptodate'] = 'Komponent yangilangan holda';
$string['dmlexceptiononinstall'] = '<p>Malumotlar bazasida xatolik yuz berdi [{$a->errorcode}].<br />{$a->debuginfo}</p>';
$string['downloadedfilecheckfailed'] = 'Yuklab olingan faylni tekshirish muvaffaqiyatsiz tugadi';
+8
View File
@@ -35,3 +35,11 @@ $string['dataroot'] = 'Malumotlar katalogi';
$string['dbprefix'] = 'Jadvallar prefiksi';
$string['dirroot'] = 'Moodle katalogi';
$string['installation'] = 'O\'rnatish';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'Siz bu sahifani korayotganingizning sababi, siz kompyuteringizda <strong>{$a->packname} {$a->packversion}</strong> paketini muvaffaqiyatli ornatdingiz va ishga tushirdingiz. Tabriklaymiz!';
$string['welcomep30'] = '<strong>{$a->installername}</strong> ning ushbu relizi <strong>Moodle</strong> ishlaydigan muhitni yaratish uchun ilovalarni oz ichiga oladi, yani:';
$string['welcomep40'] = 'Paket shuningdek <strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong> ni oz ichiga oladi.';
$string['welcomep50'] = 'Ushbu paketdagi barcha ilovalardan foydalanish ularning tegishli litsenziyalari bilan tartibga solinadi. Toliq <strong>{$a->installername}</strong> paketi <a href="https://www.opensource.org/docs/definition_plain.html">ochiq kodli</a> hisoblanadi va <a href="https://www.gnu.org/copyleft/gpl.html">GPL</a> litsenziyasi ostida tarqatiladi.';
$string['welcomep60'] = 'Keyingi sahifalar kompyuteringizda <strong>Moodle</strong>ni sozlash va ornatish boyicha oson bajariladigan qadamlar orqali sizni yetaklaydi. Siz sukut boyicha sozlamalarni qabul qilishingiz yoki ixtiyoriy ravishda ularni oz ehtiyojlaringizga mos ravishda ozgartirishingiz mumkin.';
$string['welcomep70'] = '<strong>Moodle</strong>ni ornatishni davom ettirish uchun quyidagi "Keyingi" tugmasini bosing.';
$string['wwwroot'] = 'Veb-manzil';
+1
View File
@@ -593,6 +593,7 @@ $string['visible'] = 'Visible';
$string['version'] = 'Version';
$string['version_help'] = 'The version field may be used to keep track of the badge\'s development. If specified, the version is displayed on the badge page.';
$string['warnexpired'] = ' (This badge has expired!)';
$string['wrongrole'] = 'The role you want to use is not assigned to you.';
$string['year'] = 'Year(s)';
$string['includeauthdetails'] = "Include authentication details with the backpack";
+33 -12
View File
@@ -14,15 +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/>.
/**
* Date form field class.
*
* @package core_form
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/behat_form_group.php');
@@ -41,17 +32,14 @@ use Behat\Mink\Exception\ExpectationException;
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_date extends behat_form_group {
/**
* Sets the value to a date field.
*
* @param string $value The value to be assigned to the date selector field. The string value must be either
* parsable into a UNIX timestamp or equal to 'disabled' (if disabling the date selector).
* @return void
* @throws ExpectationException If the value is invalid.
*/
public function set_value($value) {
if ($value === 'disabled') {
// Disable the given date selector field.
$this->set_child_field_value('enabled', false);
@@ -77,6 +65,29 @@ class behat_form_date extends behat_form_group {
}
}
/**
* Returns the current value of the field
*
* @return int
*/
public function get_value() {
return make_timestamp(
$this->get_child_field_value('year'),
$this->get_child_field_value('month'),
$this->get_child_field_value('day'),
);
}
/**
* Matches the provided value against the current field value
*
* @param mixed $expectedvalue
* @return bool
*/
public function matches($expectedvalue) {
return (int) $expectedvalue === $this->get_value();
}
/**
* Returns the date field identifiers and the values that should be assigned to them.
*
@@ -115,4 +126,14 @@ class behat_form_date extends behat_form_group {
$childinstance->set_value($childvalue);
}
}
/**
* Gets a value of a child element in the date form field
*
* @param string $childname
* @return string
*/
protected function get_child_field_value(string $childname): string {
return $this->field->find('css', "*[name$='[{$childname}]']")->getValue();
}
}
+14 -9
View File
@@ -14,15 +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/>.
/**
* Date time form field class.
*
* @package core_form
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/behat_form_date.php');
@@ -39,6 +30,20 @@ require_once(__DIR__ . '/behat_form_date.php');
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_date_time extends behat_form_date {
/**
* Returns the current value of the field
*
* @return string
*/
public function get_value() {
return make_timestamp(
$this->get_child_field_value('year'),
$this->get_child_field_value('month'),
$this->get_child_field_value('day'),
$this->get_child_field_value('hour'),
$this->get_child_field_value('minute'),
);
}
/**
* Returns the date field identifiers and the values that should be assigned to them.
+39
View File
@@ -168,4 +168,43 @@ class dataformat {
return get_file_storage()->create_file_from_pathname($filerecord, $filepath);
}
/**
* Escape formula spreadsheet values.
*
* Check values being used in spreadsheets and make them safe for inclusion.
* Following OWASP recommendations {@link https://owasp.org/www-community/attacks/CSV_Injection}.
*
* @param mixed $value Value to check.
* @return string|null Return escaped formula if detected.
*/
public static function escape_spreadsheet_formula(mixed $value): ?string {
// Only escape strings; leave numbers and other types unchanged.
if (!is_string($value)) {
return $value;
}
// Moodle's null placeholder: exactly one dash.
if ($value === '-') {
return $value;
}
// Trim only for checking, not for modifying output.
$trimmed = ltrim($value);
if ($trimmed === '') {
return $value;
}
// Characters that trigger formula parsing in Excel/Sheets.
$formulacharacters = ['=', '+', '-', '@'];
// If trimmed version starts with formula character, escape it.
if (in_array($trimmed[0], $formulacharacters, true)) {
// Preserve original whitespace. Do not alter actual content.
return "'" . $value;
}
return $value;
}
}
+5 -1
View File
@@ -123,7 +123,11 @@ abstract class spout_base extends \core\dataformat\base {
* @param int $rownum
*/
public function write_record($record, $rownum) {
$row = Row::fromValues($this->format_record($record));
$rowvalues = $this->format_record($record);
foreach ($rowvalues as $key => $value) {
$rowvalues[$key] = \core\dataformat::escape_spreadsheet_formula($value);
}
$row = Row::fromValues($rowvalues);
$this->writer->addRow($row);
}
+4 -2
View File
@@ -1041,8 +1041,10 @@ enum param: string {
// Absolute, and matches our wwwroot.
} else {
// Relative - let's make sure there are no tricks.
if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?') &&
!preg_match('/javascript(?:.*\/{2,})?:/i', rawurldecode($param))) {
if (
validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?') &&
!preg_match('/\bjavascript\b[\s\S]*?(?:\:|\/)/i', rawurldecode($param))
) {
// Valid relative local URL.
} else {
$param = '';
+20 -2
View File
@@ -39,6 +39,9 @@ class core_shutdown_manager {
/** @var bool is this manager already registered? */
protected static $registered = false;
/** @var array A list of pcntl handlers */
protected static array $pcntlhandlers = [];
/**
* Register self as main shutdown handler.
*
@@ -47,6 +50,7 @@ class core_shutdown_manager {
public static function initialize() {
if (self::$registered) {
debugging('Shutdown manager is already initialised!');
return;
}
self::$registered = true;
register_shutdown_function(array('core_shutdown_manager', 'shutdown_handler'));
@@ -62,8 +66,18 @@ class core_shutdown_manager {
pcntl_async_signals(true);
}
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGINT, ['core_shutdown_manager', 'signal_handler']);
pcntl_signal(SIGTERM, ['core_shutdown_manager', 'signal_handler']);
$signals = [SIGINT, SIGTERM];
foreach ($signals as $signal) {
if (function_exists('pcntl_signal_get_handler')) {
$handler = pcntl_signal_get_handler($signal);
if (is_callable($handler)) {
// We can restore the original handler later if needed.
self::$pcntlhandlers[$signal] = $handler;
}
}
pcntl_signal($signal, ['core_shutdown_manager', 'signal_handler']);
}
}
}
}
@@ -111,6 +125,10 @@ class core_shutdown_manager {
error_log('Exception ignored in signal function ' . get_callable_name($callback) . ': ' . $e->getMessage());
}
}
if (array_key_exists($signo, self::$pcntlhandlers)) {
$handler = self::$pcntlhandlers[$signo];
$handler($signo);
}
if ($shouldexit) {
exit ($exitcode);
+3
View File
@@ -449,6 +449,9 @@ class csv_export_writer {
}
}
$delimiter = csv_import_reader::get_delimiter($this->delimiter);
foreach ($row as $key => $value) {
$row[$key] = \core\dataformat::escape_spreadsheet_formula($value);
}
fputcsv($this->fp, $row, $delimiter, $this->csvenclosure);
}
@@ -40,6 +40,12 @@ $action = required_param('action', PARAM_ALPHA);
if ($action === 'filtertext') {
$text = required_param('text', PARAM_RAW);
$striptags = optional_param('striptags', false, PARAM_BOOL);
// Strip all HTML tags before filtering the text (avoiding XSS risk).
if ($striptags) {
$text = clean_param($text, PARAM_NOTAGS);
}
$result = filter_manager::instance()->filter_text($text, $context);
echo $OUTPUT->header();
@@ -475,7 +475,8 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
sesskey: M.cfg.sesskey,
contextid: this.get('contextid'),
action: 'filtertext',
text: equation
text: equation,
striptags: true,
};
Y.io(url, {
File diff suppressed because one or more lines are too long
@@ -475,7 +475,8 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
sesskey: M.cfg.sesskey,
contextid: this.get('contextid'),
action: 'filtertext',
text: equation
text: equation,
striptags: true,
};
Y.io(url, {
@@ -473,7 +473,8 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
sesskey: M.cfg.sesskey,
contextid: this.get('contextid'),
action: 'filtertext',
text: equation
text: equation,
striptags: true,
};
Y.io(url, {
@@ -5,6 +5,6 @@ define("tiny_equation/repository",["exports","core/ajax"],(function(_exports,_aj
* @module tiny_equation/repository
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.filterEquation=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.filterEquation=(contextId,content)=>{const request={methodname:"tiny_equation_filter",args:{contextid:contextId,content:content}};return _ajax.default.call([request])[0]}}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.filterEquation=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.filterEquation=function(contextId,content){let stripTags=arguments.length>2&&void 0!==arguments[2]&&arguments[2];const request={methodname:"tiny_equation_filter",args:{contextid:contextId,content:content,striptags:stripTags}};return _ajax.default.call([request])[0]}}));
//# sourceMappingURL=repository.min.js.map
@@ -1 +1 @@
{"version":3,"file":"repository.min.js","sources":["../src/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * A javascript module to handle TinyMCE Equation ajax actions.\n *\n * @module tiny_equation/repository\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Ajax from 'core/ajax';\n\n/**\n * Filter the equation for given content.\n *\n * @param {Number} contextId The context id\n * @param {String} content Content to filter\n * @return {promise}\n */\nexport const filterEquation = (contextId, content) => {\n const request = {\n methodname: 'tiny_equation_filter',\n args: {\n contextid: contextId,\n content: content,\n }\n };\n\n return Ajax.call([request])[0];\n};\n"],"names":["contextId","content","request","methodname","args","contextid","Ajax","call"],"mappings":";;;;;;;wKA+B8B,CAACA,UAAWC,iBAChCC,QAAU,CACZC,WAAY,uBACZC,KAAM,CACFC,UAAWL,UACXC,QAASA,iBAIVK,cAAKC,KAAK,CAACL,UAAU"}
{"version":3,"file":"repository.min.js","sources":["../src/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * A javascript module to handle TinyMCE Equation ajax actions.\n *\n * @module tiny_equation/repository\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Ajax from 'core/ajax';\n\n/**\n * Filter the equation for given content.\n *\n * @param {Number} contextId The context id\n * @param {String} content Content to filter\n * @param {Bool} stripTags Whether to strip HTML tags (optional)\n * @return {promise}\n */\nexport const filterEquation = (contextId, content, stripTags = false) => {\n const request = {\n methodname: 'tiny_equation_filter',\n args: {\n contextid: contextId,\n content: content,\n striptags: stripTags,\n }\n };\n\n return Ajax.call([request])[0];\n};\n"],"names":["contextId","content","stripTags","request","methodname","args","contextid","striptags","Ajax","call"],"mappings":";;;;;;;wKAgC8B,SAACA,UAAWC,aAASC,wEACzCC,QAAU,CACZC,WAAY,uBACZC,KAAM,CACFC,UAAWN,UACXC,QAASA,QACTM,UAAWL,mBAIZM,cAAKC,KAAK,CAACN,UAAU"}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -27,14 +27,16 @@ import Ajax from 'core/ajax';
*
* @param {Number} contextId The context id
* @param {String} content Content to filter
* @param {Bool} stripTags Whether to strip HTML tags (optional)
* @return {promise}
*/
export const filterEquation = (contextId, content) => {
export const filterEquation = (contextId, content, stripTags = false) => {
const request = {
methodname: 'tiny_equation_filter',
args: {
contextid: contextId,
content: content,
striptags: stripTags,
}
};
@@ -204,7 +204,7 @@ const updatePreview = (contextId) => {
equation = prefix + equation.substring(0, currentPos) + cursorLatex + equation.substring(currentPos);
equation = Selectors.delimiters.start + ' ' + equation + ' ' + Selectors.delimiters.end;
TinyEquationRepository.filterEquation(contextId, equation).then((data) => {
TinyEquationRepository.filterEquation(contextId, equation, true).then((data) => {
preview.innerHTML = data.content;
notifyFilter(preview);
@@ -41,7 +41,8 @@ class filter extends external_api {
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'contextid' => new external_value(PARAM_INT, 'The context ID', VALUE_REQUIRED),
'content' => new external_value(PARAM_RAW, 'The equation content', VALUE_REQUIRED)
'content' => new external_value(PARAM_RAW, 'The equation content', VALUE_REQUIRED),
'striptags' => new external_value(PARAM_BOOL, 'Whether to strip HTML tags', VALUE_DEFAULT, false),
]);
}
@@ -50,18 +51,26 @@ class filter extends external_api {
*
* @param int $contextid Context ID.
* @param string $content Equation content.
* @param string $striptags Strip HTML tags.
* @return array
* @since Moodle 4.1
*/
public static function execute(int $contextid, string $content): array {
public static function execute(int $contextid, string $content, bool $striptags = false): array {
[
'contextid' => $contextid,
'content' => $content
'content' => $content,
'striptags' => $striptags,
] = self::validate_parameters(self::execute_parameters(), [
'contextid' => $contextid,
'content' => $content
'content' => $content,
'striptags' => $striptags,
]);
// Strip all HTML tags before filtering the text (avoiding XSS risk).
if ($striptags) {
$content = clean_param($content, PARAM_NOTAGS);
}
$context = context::instance_by_id($contextid);
self::validate_context($context);
$result = filter_manager::instance()->filter_text($content, $context);
+1
View File
@@ -202,6 +202,7 @@ class MoodleExcelWorksheet {
public function write_string($row, $col, $str, $format = null) {
// For PhpSpreadsheet library, the column indexes start on 1 (instead of 0 as before).
$col += 1;
$str = \core\dataformat::escape_spreadsheet_formula($str);
$this->worksheet->getStyleByColumnAndRow($col, $row + 1)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_TEXT);
$this->worksheet->setCellValueExplicitByColumnAndRow($col, $row + 1, $str, DataType::TYPE_STRING);
+16 -12
View File
@@ -791,7 +791,7 @@ function file_get_drafarea_files($draftitemid, $filepath = '/') {
}
// find the file this draft file was created from and count all references in local
// system pointing to that file
$source = @unserialize($file->get_source() ?? '');
$source = unserialize_object($file->get_source() ?? '');
if (isset($source->original)) {
$item->refcount = $fs->search_references_count($source->original);
}
@@ -911,9 +911,9 @@ function file_get_submitted_draft_itemid($elname) {
* @return stored_file
*/
function file_restore_source_field_from_draft_file($storedfile) {
$source = @unserialize($storedfile->get_source() ?? '');
if (!empty($source)) {
if (is_object($source)) {
if (!empty($storedfile->get_source())) {
$source = unserialize_object($storedfile->get_source());
if (isset($source->source)) {
$restoredsource = $source->source;
$storedfile->set_source($restoredsource);
} else {
@@ -1205,7 +1205,7 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea
// Let's check if we can update this file or we need to delete and create.
if ($newfile->is_directory()) {
// Directories are always ok to just update.
} else if (($source = @unserialize($newfile->get_source() ?? '')) && isset($source->original)) {
} else if (($source = unserialize_object($newfile->get_source() ?? '')) && isset($source->original)) {
// File has the 'original' - we need to update the file (it may even have not been changed at all).
$original = file_storage::unpack_reference($source->original);
if ($original['filename'] !== $oldfile->get_filename() || $original['filepath'] !== $oldfile->get_filepath()) {
@@ -1241,8 +1241,10 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea
// Field files.source for draftarea files contains serialised object with source and original information.
// We only store the source part of it for non-draft file area.
$newsource = $newfile->get_source();
if ($source = @unserialize($newfile->get_source() ?? '')) {
$newsource = $source->source;
if ($source = unserialize_object($newfile->get_source() ?? '')) {
if (isset($source->source)) {
$newsource = $source->source;
}
}
if ($oldfile->get_source() !== $newsource) {
$oldfile->set_source($newsource);
@@ -1275,10 +1277,12 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea
// the size and subdirectory tests are extra safety only, the UI should prevent it
foreach ($newhashes as $file) {
$file_record = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'timemodified'=>time());
if ($source = @unserialize($file->get_source() ?? '')) {
if ($source = unserialize_object($file->get_source() ?? '')) {
// Field files.source for draftarea files contains serialised object with source and original information.
// We only store the source part of it for non-draft file area.
$file_record['source'] = $source->source;
if (isset($source->source)) {
$file_record['source'] = $source->source;
}
}
if ($file->is_external_file()) {
@@ -2933,7 +2937,7 @@ function file_overwrite_existing_draftfile(stored_file $newfile, stored_file $ex
$fs = get_file_storage();
// Remember original file source field.
$source = @unserialize($existingfile->get_source() ?? '');
$source = unserialize_object($existingfile->get_source() ?? '');
// Remember the original sortorder.
$sortorder = $existingfile->get_sortorder();
if ($newfile->is_external_file()) {
@@ -2957,7 +2961,7 @@ function file_overwrite_existing_draftfile(stored_file $newfile, stored_file $ex
$newfile = $fs->create_file_from_storedfile($newfilerecord, $newfile);
// Preserve original file location (stored in source field) for handling references.
if (isset($source->original)) {
if (!($newfilesource = @unserialize($newfile->get_source() ?? ''))) {
if (!($newfilesource = unserialize_object($newfile->get_source() ?? ''))) {
$newfilesource = new stdClass();
}
$newfilesource->original = $source->original;
@@ -4361,7 +4365,7 @@ class curl_cache {
$fp = fopen($this->dir.$filename, 'r');
$size = filesize($this->dir.$filename);
$content = fread($fp, $size);
return unserialize($content);
return unserialize_array($content);
}
}
return false;
+28 -2
View File
@@ -27,7 +27,20 @@ Feature: Setting and validating date fields
| dategroup2[group2optionaldatetime][enabled] | 1 |
| Group2 optional date and time | ## 2023-08-31 14:45 ## |
When I press "Send form"
Then I should see "simpledateonly: 1690732800"
Then the following fields match these values:
| Simple only date | ## 2023-07-31 ## |
| Simple optional only date | ## 2023-08-31 ## |
| Simple date and time | ## 2023-07-31 11:15 ## |
| Simple optional date and time | ## 2023-08-31 14:45 ## |
| Group1 only date | ## 2023-07-31 ## |
| Group1 optional only date | ## 2023-08-31 ## |
| Group1 date and time | ## 2023-07-31 11:15 ## |
| Group1 optional date and time | ## 2023-08-31 14:45 ## |
| Group2 only date | ## 2023-07-31 ## |
| Group2 optional only date | ## 2023-08-31 ## |
| Group2 date and time | ## 2023-07-31 11:15 ## |
| Group2 optional date and time | ## 2023-08-31 14:45 ## |
And I should see "simpledateonly: 1690732800"
And I should see "simpleoptionaldateonly: 1693411200"
And I should see "simpledatetime: 1690773300"
And I should see "simpleoptionaldatetime: 1693464300"
@@ -62,7 +75,20 @@ Feature: Setting and validating date fields
| dategroup2[group2optionaldatetime][enabled] | 1 |
| dategroup2[group2optionaldatetime] | ## 2023-08-31 14:45 ## |
When I press "Send form"
Then I should see "simpledateonly: 1690732800"
Then the following fields match these values:
| simpledateonly | ## 2023-07-31 ## |
| simpleoptionaldateonly | ## 2023-08-31 ## |
| simpledatetime | ## 2023-07-31 11:15 ## |
| simpleoptionaldatetime | ## 2023-08-31 14:45 ## |
| group1dateonly | ## 2023-07-31 ## |
| group1optionaldateonly | ## 2023-08-31 ## |
| group1datetime | ## 2023-07-31 11:15 ## |
| group1optionaldatetime | ## 2023-08-31 14:45 ## |
| dategroup2[group2dateonly] | ## 2023-07-31 ## |
| dategroup2[group2optionaldateonly] | ## 2023-08-31 ## |
| dategroup2[group2datetime] | ## 2023-07-31 11:15 ## |
| dategroup2[group2optionaldatetime] | ## 2023-08-31 14:45 ## |
And I should see "simpledateonly: 1690732800"
And I should see "simpleoptionaldateonly: 1693411200"
And I should see "simpledatetime: 1690773300"
And I should see "simpleoptionaldatetime: 1693464300"
+2
View File
@@ -164,6 +164,8 @@ class MoodleODSWorksheet {
if (is_array($format)) {
$format = new MoodleODSFormat($format);
}
$str = \core\dataformat::escape_spreadsheet_formula($str);
$this->data[$row][$col]->value = $str;
$this->data[$row][$col]->type = 'string';
$this->data[$row][$col]->format = $format;
+17 -16
View File
@@ -1094,24 +1094,25 @@ function setup_get_remote_url() {
$rurl['fullpath'] = $_SERVER['REQUEST_URI'];
// Fixing a known issue with:
// - Apache versions lesser than 2.4.11
// - Apache
// - PHP deployed in Apache as PHP-FPM via mod_proxy_fcgi
// - PHP versions lesser than 5.6.3 and 5.5.18.
// - PHP versions lesser than 8.1.18 or 8.2.5.
if (isset($_SERVER['PATH_INFO']) && (php_sapi_name() === 'fpm-fcgi') && isset($_SERVER['SCRIPT_NAME'])) {
$pathinfodec = rawurldecode($_SERVER['PATH_INFO']);
$lenneedle = strlen($pathinfodec);
// Checks whether SCRIPT_NAME ends with PATH_INFO, URL-decoded.
if (substr($_SERVER['SCRIPT_NAME'], -$lenneedle) === $pathinfodec) {
// This is the "Apache 2.4.10- running PHP-FPM via mod_proxy_fcgi" fingerprint,
// at least on CentOS 7 (Apache/2.4.6 PHP/5.4.16) and Ubuntu 14.04 (Apache/2.4.7 PHP/5.5.9)
// => SCRIPT_NAME contains 'slash arguments' data too, which is wrongly exposed via PATH_INFO as URL-encoded.
// Fix both $_SERVER['PATH_INFO'] and $_SERVER['SCRIPT_NAME'].
$lenhaystack = strlen($_SERVER['SCRIPT_NAME']);
$pos = $lenhaystack - $lenneedle;
// Here $pos is greater than 0 but let's double check it.
if ($pos > 0) {
$_SERVER['PATH_INFO'] = $pathinfodec;
$_SERVER['SCRIPT_NAME'] = substr($_SERVER['SCRIPT_NAME'], 0, $pos);
$_SERVER['PATH_INFO'] = rawurldecode($_SERVER['PATH_INFO']);
if (PHP_VERSION_ID < 80118 || (PHP_VERSION_ID >= 80200 && PHP_VERSION_ID < 80205)) {
$lenneedle = strlen($_SERVER['PATH_INFO']);
// Checks whether SCRIPT_NAME ends with PATH_INFO, URL-decoded.
if (substr($_SERVER['SCRIPT_NAME'], -$lenneedle) === $_SERVER['PATH_INFO']) {
// This is the "Apache running PHP-FPM via mod_proxy_fcgi with PHP < 8.1.18 or PHP < 8.2.5" fingerprint,
// at least on CentOS 7 (Apache/2.4.6 PHP/8.0.30)
// => SCRIPT_NAME contains 'slash arguments' data too, which is wrongly exposed via PATH_INFO as URL-encoded.
// Fix $_SERVER['SCRIPT_NAME'].
$lenhaystack = strlen($_SERVER['SCRIPT_NAME']);
$pos = $lenhaystack - $lenneedle;
// Here $pos is greater than 0 but let's double check it.
if ($pos > 0) {
$_SERVER['SCRIPT_NAME'] = substr($_SERVER['SCRIPT_NAME'], 0, $pos);
}
}
}
}
+76
View File
@@ -122,4 +122,80 @@ final class dataformat_test extends \advanced_testcase {
$this->assertStringStartsWith($filerecord['filename'], $file->get_filename());
$this->assertGreaterThan(0, $file->get_filesize());
}
/**
* Data provider for test_escape_spreadsheet_formula.
*
* @return array
*/
public static function escape_spreadsheet_formula_provider(): array {
return [
'null stays null' => [
null,
null,
],
'empty string stays empty' => [
'',
'',
],
'Formula with tab' => [
'value' => "\t=SUM(1+1)",
'expected' => "'\t=SUM(1+1)",
],
'Formula with carriage return' => [
'value' => "\r=SUM(1+1)",
'expected' => "'\r=SUM(1+1)",
],
'Formula with new line' => [
'value' => "\n=SUM(1+1)",
'expected' => "'\n=SUM(1+1)",
],
'Formula starting with "="' => [
'value' => "=SUM(1+1)",
'expected' => "'=SUM(1+1)",
],
'Formula starting with "+"' => [
'value' => "+1+1",
'expected' => "'+1+1",
],
'Formula starting with "-"' => [
'value' => "-1+1",
'expected' => "'-1+1",
],
'Formula starting with "@"' => [
'value' => "@A5",
'expected' => "'@A5",
],
'Null placeholder' => [
'value' => "-",
'expected' => "-",
],
'dash with leading space is not placeholder, so escaped' => [
' -',
"' -",
],
'dash with trailing space is not placeholder, so escaped' => [
'- ',
"'- ",
],
'Non-formula' => [
'value' => "Hello there",
'expected' => "Hello there",
],
];
}
/**
* Test escape_spreadsheet_formula.
*
* @dataProvider escape_spreadsheet_formula_provider
* @param string|null $value The value to test.
* @param string|null $expected The expected value after escaping.
*/
public function test_escape_spreadsheet_formula(?string $value, ?string $expected): void {
$this->resetAfterTest();
$escapedvalue = dataformat::escape_spreadsheet_formula($value);
$this->assertEquals($expected, $escapedvalue);
}
}
+7
View File
@@ -0,0 +1,7 @@
<!DOCTYPE html>
<html>
<body>
<h1>This is an example HTML</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque cursus lectus eu magna finibus fringilla. Maecenas in ipsum non velit vehicula blandit. Nunc consequat felis sit amet lectus pellentesque bibendum. Vestibulum tincidunt, sem eu vehicula rhoncus, libero erat consequat massa, non auctor nunc nisi ac sem. Proin eu mattis elit. Morbi nec luctus ante. Nullam non augue diam. Etiam vestibulum massa et est tempor, ac iaculis justo tempor. Fusce eu magna id ligula lacinia molestie. Cras facilisis velit quis purus dapibus placerat sit amet at odio. Aliquam luctus commodo ligula iaculis dignissim. Vivamus placerat, ligula ut auctor efficitur, purus urna accumsan nisl, in placerat libero leo at mauris. Integer a diam quis purus iaculis semper ac suscipit lacus. Mauris ut orci lacinia, sollicitudin odio ut, pharetra augue.</p>
</body>
</html>
+4 -4
View File
@@ -26,8 +26,8 @@ use core_qrcode;
* @author <juan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\PHPUnit\Framework\Attributes\CoversClass(core_qrcode::class)]
final class qrcode_test extends \basic_testcase {
/**
* Basic test to generate a QR code and check that the library is not broken.
*/
@@ -36,9 +36,9 @@ final class qrcode_test extends \basic_testcase {
// binary file can be different. This is why tests are limited.
$text = 'abc';
$color = 'black';
$qrcode = new core_qrcode($text, $color);
$svgdata = $qrcode->getBarcodeSVGcode(1, 1);
$color = 'green';
$qrcode = new core_qrcode($text);
$svgdata = $qrcode->getBarcodeSVGcode(1, 1, $color);
// Just check the SVG was generated.
$this->assertStringContainsString('<desc>' . $text . '</desc>', $svgdata);
+29
View File
@@ -829,4 +829,33 @@ final class tablelib_test extends \advanced_testcase {
$this->expectOutputRegex('/' . '<caption class="inline">' . $caption . '<\/caption>' . '/');
}
/**
* Test formulas are escaped in exported tables.
*/
public function test_table_exports_escaped_formulas(): void {
$table = new flexible_table('tablelib_test_export');
$table->define_baseurl('/invalid.php');
$table->define_columns(['c0', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6']);
ob_start();
$table->is_downloadable(true);
$table->is_downloading('csv');
$table->setup();
$table->add_data([
'column0' => " =SUM(1+1)", // With spaces.
'column1' => "=SUM(1+1)",
'column2' => "=1+1",
'column3' => "+1+1",
'column4' => "-1+1",
'column5' => "@A1",
'column6' => "-", // Single dash (should not be escaped).
]);
$output = ob_get_contents();
ob_end_clean();
$matchregex = "/\"?' =SUM\(1\+1\)\"?,'=SUM\(1\+1\),'=1\+1,'\+1\+1,'-1\+1,'@A1,-/";
$this->assertMatchesRegularExpression($matchregex, $output);
}
}
+26
View File
@@ -0,0 +1,26 @@
@core
Feature: Login using email address
Users should be able to access their site
As a user
I should be able to login using email
Background:
Given the following "users" exist:
| username | password | firstname | lastname | email |
| testuser | test | Test | User | user@example.com |
Scenario Outline: A user can login using their email address
Given the following config values are set as admin:
| authloginviaemail | <authloginviaemail> |
When I follow "Log in"
And I set the field "Username" to "<login>"
And I set the field "Password" to "test"
And I press "Log in"
Then I should see "<message>"
Examples:
| authloginviaemail | login | message |
| 0 | testuser | You are logged in as |
| 0 | user@example.com | Invalid login, please try again |
| 1 | testuser | You are logged in as |
| 1 | user@example.com | You are logged in as |
+11 -19
View File
@@ -1249,11 +1249,11 @@ class assign_grading_table extends table_sql implements renderable {
if (empty($row->recordid)) {
$row->recordid = $this->assignment->get_uniqueid_for_user($row->userid);
}
$urlparams['blindid'] = $row->recordid;
$useridurlparam = ['blindid' => $row->recordid];
} else {
$urlparams['userid'] = $row->userid;
$useridurlparam = ['userid' => $row->userid];
}
$url = new moodle_url('/mod/assign/view.php', $urlparams);
$url = new moodle_url('/mod/assign/view.php', $urlparams + $useridurlparam);
$noimage = null;
if (!$row->grade) {
@@ -1292,11 +1292,10 @@ class assign_grading_table extends table_sql implements renderable {
if (!$row->locked) {
$urlparams = array('id' => $this->assignment->get_course_module()->id,
'userid' => $row->id,
'action' => 'lock',
'sesskey' => sesskey(),
'page' => $this->currpage);
$url = new moodle_url('/mod/assign/view.php', $urlparams);
$url = new moodle_url('/mod/assign/view.php', $urlparams + $useridurlparam);
$description = get_string('preventsubmissionsshort', 'assign');
$actions['lock'] = new action_menu_link_secondary(
@@ -1306,11 +1305,10 @@ class assign_grading_table extends table_sql implements renderable {
);
} else {
$urlparams = array('id' => $this->assignment->get_course_module()->id,
'userid' => $row->id,
'action' => 'unlock',
'sesskey' => sesskey(),
'page' => $this->currpage);
$url = new moodle_url('/mod/assign/view.php', $urlparams);
$url = new moodle_url('/mod/assign/view.php', $urlparams + $useridurlparam);
$description = get_string('allowsubmissionsshort', 'assign');
$actions['unlock'] = new action_menu_link_secondary(
$url,
@@ -1324,11 +1322,10 @@ class assign_grading_table extends table_sql implements renderable {
$USER->id != $row->id &&
$caneditsubmission) {
$urlparams = array('id' => $this->assignment->get_course_module()->id,
'userid' => $row->id,
'action' => 'editsubmission',
'sesskey' => sesskey(),
'page' => $this->currpage);
$url = new moodle_url('/mod/assign/view.php', $urlparams);
$url = new moodle_url('/mod/assign/view.php', $urlparams + $useridurlparam);
$description = get_string('editsubmission', 'assign');
$actions['editsubmission'] = new action_menu_link_secondary(
$url,
@@ -1340,11 +1337,10 @@ class assign_grading_table extends table_sql implements renderable {
$caneditsubmission &&
!empty($row->status)) {
$urlparams = array('id' => $this->assignment->get_course_module()->id,
'userid' => $row->id,
'action' => 'removesubmissionconfirm',
'sesskey' => sesskey(),
'page' => $this->currpage);
$url = new moodle_url('/mod/assign/view.php', $urlparams);
$url = new moodle_url('/mod/assign/view.php', $urlparams + $useridurlparam);
$description = get_string('removesubmission', 'assign');
$actions['removesubmission'] = new action_menu_link_secondary(
$url,
@@ -1357,11 +1353,10 @@ class assign_grading_table extends table_sql implements renderable {
$this->assignment->get_instance()->cutoffdate) &&
$this->hasgrantextension) {
$urlparams = array('id' => $this->assignment->get_course_module()->id,
'userid' => $row->id,
'action' => 'grantextension',
'sesskey' => sesskey(),
'page' => $this->currpage);
$url = new moodle_url('/mod/assign/view.php', $urlparams);
$url = new moodle_url('/mod/assign/view.php', $urlparams + $useridurlparam);
$description = get_string('grantextension', 'assign');
$actions['grantextension'] = new action_menu_link_secondary(
$url,
@@ -1372,11 +1367,10 @@ class assign_grading_table extends table_sql implements renderable {
if ($row->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED &&
$this->assignment->get_instance()->submissiondrafts) {
$urlparams = array('id' => $this->assignment->get_course_module()->id,
'userid' => $row->id,
'action' => 'reverttodraft',
'sesskey' => sesskey(),
'page' => $this->currpage);
$url = new moodle_url('/mod/assign/view.php', $urlparams);
$url = new moodle_url('/mod/assign/view.php', $urlparams + $useridurlparam);
$description = get_string('reverttodraftshort', 'assign');
$actions['reverttodraft'] = new action_menu_link_secondary(
$url,
@@ -1390,11 +1384,10 @@ class assign_grading_table extends table_sql implements renderable {
$submissionsopen &&
$row->id != $USER->id) {
$urlparams = array('id' => $this->assignment->get_course_module()->id,
'userid' => $row->id,
'action' => 'submitotherforgrading',
'sesskey' => sesskey(),
'page' => $this->currpage);
$url = new moodle_url('/mod/assign/view.php', $urlparams);
$url = new moodle_url('/mod/assign/view.php', $urlparams + $useridurlparam);
$description = get_string('submitforgrading', 'assign');
$actions['submitforgrading'] = new action_menu_link_secondary(
$url,
@@ -1411,11 +1404,10 @@ class assign_grading_table extends table_sql implements renderable {
if ($ismanual && $hassubmission && $notreopened && $hasattempts) {
$urlparams = array('id' => $this->assignment->get_course_module()->id,
'userid' => $row->id,
'action' => 'addattempt',
'sesskey' => sesskey(),
'page' => $this->currpage);
$url = new moodle_url('/mod/assign/view.php', $urlparams);
$url = new moodle_url('/mod/assign/view.php', $urlparams + $useridurlparam);
$description = get_string('addattempt', 'assign');
$actions['addattempt'] = new action_menu_link_secondary(
$url,
+106 -14
View File
@@ -529,7 +529,16 @@ class assign {
$nextpageparams['action'] = 'view';
}
} else if ($action == 'addattempt') {
$this->process_add_attempt(required_param('userid', PARAM_INT));
$userid = false;
if ($this->is_blind_marking()) {
$blindid = optional_param('blindid', 0, PARAM_INT);
$userid = $this->get_user_id_for_uniqueid($blindid);
}
// Fallback to userid, if not found by blindid.
if (!$userid) {
$userid = required_param('userid', PARAM_INT);
}
$this->process_add_attempt($userid);
$action = 'redirect';
$nextpageparams['action'] = 'grading';
} else if ($action == 'reverttodraft') {
@@ -3007,7 +3016,14 @@ class assign {
$users = optional_param('userid', 0, PARAM_INT);
if (!$users) {
$users = required_param('selectedusers', PARAM_SEQUENCE);
if ($this->is_blind_marking()) {
$blindid = optional_param('blindid', 0, PARAM_INT);
$users = $this->get_user_id_for_uniqueid($blindid);
}
// We need users, if not found by blindid.
if (!$users) {
$users = required_param('selectedusers', PARAM_SEQUENCE);
}
}
$userlist = explode(',', $users);
@@ -4319,7 +4335,29 @@ class assign {
protected function view_remove_submission_confirm() {
global $USER;
$userid = optional_param('userid', $USER->id, PARAM_INT);
$userid = optional_param('userid', 0, PARAM_INT);
$blindid = optional_param('blindid', 0, PARAM_INT);
// Construct the base URL parameters for the confirm and cancel actions.
$urlparams = [
'id' => $this->get_course_module()->id,
'action' => 'removesubmission',
'sesskey' => sesskey(),
];
// Determine the correct user ID.
if ($userid) {
// Real user ID was explicitly provided.
$urlparams['userid'] = $userid;
} elseif ($this->is_blind_marking() && $blindid) {
// Blind marking is in use. Resolve anonymized (blind) ID to the real user ID to obtain the user object later.
$userid = $this->get_user_id_for_uniqueid($blindid);
$urlparams['blindid'] = $blindid;
} else {
// Default to the currently logged-in user (e.g. user deleting their own submission scenario).
$userid = $USER->id;
$urlparams['userid'] = $userid;
}
if (!$this->can_edit_submission($userid, $USER->id)) {
throw new \moodle_exception('nopermission');
@@ -4333,10 +4371,6 @@ class assign {
$this->get_course_module()->id);
$o .= $this->get_renderer()->render($header);
$urlparams = array('id' => $this->get_course_module()->id,
'action' => 'removesubmission',
'userid' => $userid,
'sesskey' => sesskey());
$confirmurl = new moodle_url('/mod/assign/view.php', $urlparams);
$urlparams = array('id' => $this->get_course_module()->id,
@@ -4651,7 +4685,7 @@ class assign {
$userid = optional_param('userid', 0, PARAM_INT);
$blindid = optional_param('blindid', 0, PARAM_INT);
if (!$userid && $blindid) {
if ($this->is_blind_marking() && !$userid && $blindid) {
$userid = $this->get_user_id_for_uniqueid($blindid);
}
@@ -4785,7 +4819,17 @@ class assign {
$o = '';
require_once($CFG->dirroot . '/mod/assign/submission_form.php');
// Need submit permission to submit an assignment.
$userid = optional_param('userid', $USER->id, PARAM_INT);
$userid = optional_param('userid', 0, PARAM_INT);
$blindid = optional_param('blindid', 0, PARAM_INT);
if ($this->is_blind_marking() && !$userid && $blindid) {
$userid = $this->get_user_id_for_uniqueid($blindid);
}
// If no userid specified, default to current user.
if (!$userid) {
$userid = $USER->id;
}
$user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
$timelimitenabled = get_config('assign', 'enabletimelimit');
@@ -6841,7 +6885,17 @@ class assign {
require_sesskey();
$userid = optional_param('userid', $USER->id, PARAM_INT);
$userid = optional_param('userid', 0, PARAM_INT);
$blindid = optional_param('blindid', 0, PARAM_INT);
if ($this->is_blind_marking() && !$userid && $blindid) {
$userid = $this->get_user_id_for_uniqueid($blindid);
}
// If no userid specified, default to current user.
if (!$userid) {
$userid = $USER->id;
}
if (!$this->submissions_open($userid)) {
$notices[] = get_string('submissionsclosed', 'assign');
@@ -8305,7 +8359,15 @@ class assign {
require_sesskey();
if (!$userid) {
$userid = required_param('userid', PARAM_INT);
if ($this->is_blind_marking()) {
$blindid = optional_param('blindid', 0, PARAM_INT);
$userid = $this->get_user_id_for_uniqueid($blindid);
}
// Userid is required, if not found by blindid.
if (!$userid) {
$userid = required_param('userid', PARAM_INT);
}
}
return $this->remove_submission($userid);
@@ -8322,7 +8384,17 @@ class assign {
require_sesskey();
if (!$userid) {
$userid = required_param('userid', PARAM_INT);
if ($this->is_blind_marking()) {
$blindid = optional_param('blindid', 0, PARAM_INT);
if ($blindid) {
$userid = $this->get_user_id_for_uniqueid($blindid);
}
}
// Userid is required, if not found by blindid.
if (!$userid) {
$userid = required_param('userid', PARAM_INT);
}
}
return $this->revert_to_draft($userid);
@@ -8493,7 +8565,17 @@ class assign {
require_sesskey();
if (!$userid) {
$userid = required_param('userid', PARAM_INT);
if ($this->is_blind_marking()) {
$blindid = optional_param('blindid', 0, PARAM_INT);
if ($blindid) {
$userid = $this->get_user_id_for_uniqueid($blindid);
}
}
// Userid is required, if not found by blindid.
if (!$userid) {
$userid = required_param('userid', PARAM_INT);
}
}
return $this->lock_submission($userid);
@@ -8542,7 +8624,17 @@ class assign {
require_sesskey();
if (!$userid) {
$userid = required_param('userid', PARAM_INT);
if ($this->is_blind_marking()) {
$blindid = optional_param('blindid', 0, PARAM_INT);
if ($blindid) {
$userid = $this->get_user_id_for_uniqueid($blindid);
}
}
// Userid is required, if not found by blindid.
if (!$userid) {
$userid = required_param('userid', PARAM_INT);
}
}
return $this->unlock_submission($userid);
@@ -0,0 +1,41 @@
@mod @mod_choice
Feature: Specify any number of choice responses
In order to make a choice activity
As a teacher
I need to be able to add any number of responses
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | intro | course | option |
| choice | Choice 1 | Test choice description | C1 | one, two, three |
Scenario: Teacher can add and display any number of responses in a choice activity
Given I am on the "Choice 1" "choice activity" page logged in as "teacher1"
And I should see "one"
And I should see "two"
And I should see "three"
And I should not see "four"
And I should not see "five"
And I should not see "six"
When I am on the "Choice 1" "choice activity editing" page
And I press "Add 3 field(s) to form"
And I set the following fields to these values:
| Option 4 | four |
| Option 5 | five |
| Option 6 | six |
And I press "Save and display"
Then I should see "one"
And I should see "two"
And I should see "three"
And I should see "four"
And I should see "five"
And I should see "six"
+14 -5
View File
@@ -1714,7 +1714,7 @@ function data_rating_validate($params) {
* @throws rating_exception
*/
function mod_data_rating_can_see_item_ratings($params) {
global $DB;
global $DB, $USER;
// Check the component is mod_data.
if (!isset($params['component']) || $params['component'] != 'mod_data') {
@@ -1730,7 +1730,7 @@ function mod_data_rating_can_see_item_ratings($params) {
throw new rating_exception('invaliditemid');
}
$datasql = "SELECT d.id as dataid, d.course, r.groupid
$datasql = "SELECT d.id as dataid, d.course, r.userid, r.groupid
FROM {data_records} r
JOIN {data} d ON r.dataid = d.id
WHERE r.id = :itemid";
@@ -1740,14 +1740,23 @@ function mod_data_rating_can_see_item_ratings($params) {
throw new rating_exception('invaliditemid');
}
$course = $DB->get_record('course', array('id' => $info->course), '*', MUST_EXIST);
$cm = get_coursemodule_from_instance('data', $info->dataid, $course->id, false, MUST_EXIST);
$context = context_module::instance($cm->id);
if (!empty($info->userid)) {
$ratingpermissions = data_rating_permissions($context->id, 'mod_data', 'entry');
$requiredpermission = ($info->userid != $USER->id) ? 'viewall' : 'view';
if (!$ratingpermissions[$requiredpermission]) {
return false;
}
}
// User can see ratings of all participants.
if ($info->groupid == 0) {
return true;
}
$course = $DB->get_record('course', array('id' => $info->course), '*', MUST_EXIST);
$cm = get_coursemodule_from_instance('data', $info->dataid, $course->id, false, MUST_EXIST);
// Make sure groups allow this user to see the item they're rating.
return groups_group_visible($info->groupid, $course, $cm);
}
+39
View File
@@ -153,3 +153,42 @@ Feature: Users can add entries to database activities
| user |
| teacher1 |
| student1 |
@javascript @_file_upload
Scenario Outline: Users can't upload non-picture files into a picture field
Given the following "mod_data > fields" exist:
| database | type | name |
| data1 | picture | Test field 1 |
And I am on the "Test database name" "data activity" page logged in as student1
And I press "Add entry"
And I follow "Add..."
And I follow "Upload a file"
When I click on "Upload a file" "link" in the ".fp-repo-area" "css_element"
# Upload non-image files.
And I set the field "Attachment" to "<filepath>"
And I press "Upload this file"
# Confirm non-image files cannot be uploaded.
Then I should see "<errormessage>"
Examples:
| filepath | errormessage |
| #dirroot#/lib/tests/fixtures/empty.txt | Text file filetype cannot be accepted. |
| #dirroot#/lib/tests/fixtures/timezonewindows.xml | application/xml filetype cannot be accepted. |
| #dirroot#/mod/data/tests/fixtures/behat_preset.zip | Archive (ZIP) filetype cannot be accepted. |
| #dirroot#/mod/data/tests/fixtures/test_data_content.csv | Comma-separated values filetype cannot be accepted. |
@javascript @_file_upload
Scenario: Users can upload picture files into a picture field
Given the following "mod_data > fields" exist:
| database | type | name |
| data1 | picture | Test field 1 |
And I am on the "Test database name" "data activity" page logged in as student1
And I press "Add entry"
And I follow "Add..."
And I follow "Upload a file"
When I click on "Upload a file" "link" in the ".fp-repo-area" "css_element"
# Confirm image files can be uploaded.
And I set the field "Attachment" to "#dirroot#/lib/tests/fixtures/gd-logo.png"
Then "Error" "dialogue" should not be visible
And I press "Upload this file"
And I should see "gd-logo.png"
+13
View File
@@ -103,6 +103,19 @@ class mod_feedback_generator extends testing_module_generator {
$data['values'] = $this->format_item_values($questiontype, $data['values']);
}
if (isset($data['dependitem'])) {
$select = 'label = ? OR name = ?';
$params = [
$data['dependitem'],
$data['dependitem'],
];
$dependid = $DB->get_field_select('feedback_item', 'id', $select, $params, MUST_EXIST);
$data['dependitem'] = $dependid;
} else {
// Make sure `dependvalue` is not set if we are not creating a dependent item.
unset($data['dependvalue']);
}
return call_user_func([$this, "create_item_{$questiontype}"], $feedback, $data);
}
@@ -0,0 +1,60 @@
@mod @mod_folder @_file_upload @core_filepicker
Feature: Folder files can be viewed without downloading
In order to view folder files without downloading
As a teacher
I need to be able to disable 'Force download of files'
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | One | student1@example.com |
| teacher1 | Teacher | One | teacher1@example.com |
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 "activities" exist:
| activity | course | name |
| folder | C1 | Folder 1 |
And I am on the "Folder 1" "folder activity editing" page logged in as teacher1
And I upload "lib/tests/fixtures/test.html" file to "Files" filemanager
And I upload "course/tests/fixtures/image.jpg" file to "Files" filemanager
And I press "Save and return to course"
@javascript
Scenario Outline: Files within a folder activity is automatically downloaded
When I am on the "Folder 1" "folder activity" page
# As a teacher, confirm that file is downloaded automatically.
Then following "<filename>" should download a file that:
| Has mimetype | <mimetype> |
# Confirm the same behaviour as a student.
And I am on the "Folder 1" "folder activity" page logged in as student1
And following "<filename>" should download a file that:
| Has mimetype | <mimetype> |
Examples:
| filename | mimetype |
| image.jpg | image/jpeg |
| test.html | text/html |
@javascript
Scenario Outline: Teacher can disable force download for folder activity
Given I am on the "Folder 1" "folder activity editing" page
And I set the following fields to these values:
| forcedownload | 0 |
And I press "Save and display"
When I click on "<filename>" "link"
# As a teacher, confirm that file is not downloaded.
Then "<displayedtext>" "<elementtype>" should exist
# Confirm the same behaviour as a student.
And I am on the "Folder 1" "folder activity" page logged in as student1
And I click on "<filename>" "link"
And "<displayedtext>" "<elementtype>" should exist
Examples:
| filename | displayedtext | elementtype |
| image.jpg | //img[contains(@src, '/image.jpg')] | xpath_element |
| test.html | This is an example HTML | text |
+6
View File
@@ -2309,6 +2309,12 @@ function mod_forum_rating_can_see_item_ratings($params) {
$forum = $DB->get_record('forum', array('id' => $discussion->forum), '*', MUST_EXIST);
$course = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST);
$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id , false, MUST_EXIST);
$context = context_module::instance($cm->id);
$ratingpermissions = forum_rating_permissions($context->id, 'mod_forum', 'post');
$requiredpermission = ($post->userid != $USER->id) ? 'viewall' : 'view';
if (!$ratingpermissions[$requiredpermission]) {
return false;
}
// Perform some final capability checks.
if (!forum_user_can_see_post($forum, $discussion, $post, $USER, $cm)) {
@@ -0,0 +1,23 @@
@mod @mod_forum @core_rss
Feature: Verify that default RSS settings for forums are correctly applied.
In order to ensure consistent RSS configuration across new forums
As an admin
I need to verify that the default RSS values set at site level appear when creating a new forum.
Background:
Given the following "courses" exist:
| fullname | shortname |
| RSS Test Course | RSS101 |
And the following config values are set as admin:
| enablerssfeeds | 1 |
| forum_enablerssfeeds | 1 |
| forum_rsstype | 1 |
| forum_rssarticles | 5 |
Scenario: Admin sets default forum RSS values and verifies them on new forum creation
Given I log in as "admin"
When I am on "RSS101" course homepage with editing mode on
And I add a forum activity to course "RSS101" section "New section"
And I expand all fieldsets
Then the field "RSS feed for this activity" matches value "Discussions"
And the field "Number of RSS recent articles" matches value "5"
+49
View File
@@ -4511,3 +4511,52 @@ function mod_glossary_prepare_entry_for_edition(stdClass $entry): stdClass {
return $entry;
}
/**
* Checks whether the current user can see ratings for a given itemid.
*
* @param array $params submitted data
* contextid => int contextid [required]
* component => The component for this module - should always be mod_glossary [required]
* ratingarea => Should always be entry (the only rating area in glossary) [required]
* itemid => int the ID of the entry being rated [required]
* @return bool
*/
function mod_glossary_rating_can_see_item_ratings(array $params): bool {
global $DB, $USER;
if (!isset($params['component']) || $params['component'] != 'mod_glossary') {
throw new rating_exception('invalidcomponent');
}
if (!isset($params['ratingarea']) || $params['ratingarea'] != 'entry') {
throw new rating_exception('invalidratingarea');
}
if (!isset($params['itemid'])) {
throw new rating_exception('invaliditemid');
}
$entry = $DB->get_record('glossary_entries', ['id' => $params['itemid']], '*', MUST_EXIST);
$glossary = $DB->get_record('glossary', ['id' => $entry->glossaryid], '*', MUST_EXIST);
$cm = get_coursemodule_from_instance('glossary', $glossary->id, $glossary->course, false, MUST_EXIST);
$cminfo = get_fast_modinfo($glossary->course)->instances['glossary'][$cm->instance];
if ($cminfo->context->id != $params['contextid']) {
throw new rating_exception('invalidcontext');
}
$context = context::instance_by_id($params['contextid']);
$ratingpermissions = glossary_rating_permissions($context->id, 'mod_glossary', 'entry');
$requiredpermission = ($entry->userid != $USER->id) ? 'viewall' : 'view';
if (!$ratingpermissions[$requiredpermission]) {
return false;
}
if (!glossary_can_view_entry($entry, $cminfo)) {
return false;
}
return true;
}
+3
View File
@@ -1,6 +1,9 @@
This files describes API changes in /mod/glossary/*,
information provided here is intended especially for developers.
=== 4.4.12 ===
* Function mod_glossary_rating_can_see_item_ratings is now implemented for checking permissions to view ratings.
=== 3.10 ===
* External function get_entries_by_id now returns and additional "permissions" field indicating the user permissions for managing
the entry.
+52
View File
@@ -571,4 +571,56 @@ final class attempt_test extends \advanced_testcase {
$this->assertEquals(0, $grades[$readinggrade->id]->grade);
$this->assertEquals(1, $grades[$readinggrade->id]->maxgrade);
}
/**
* Test that enabling shuffle on the first quiz section randomizes question order between attempts.
*
* @return void
* @covers ::quiz_start_new_attempt
*/
public function test_question_shuffle(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Create quiz with two sections using create_test_quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
'Shuffled section*',
['Q1', 1, 'shortanswer'],
['Q2', 1, 'shortanswer'],
['Q3', 1, 'shortanswer'],
['Q4', 1, 'shortanswer'],
['Q5', 1, 'shortanswer'],
'Non-shuffled section',
['Q6', 2, 'shortanswer'],
['Q7', 2, 'shortanswer'],
['Q8', 2, 'shortanswer'],
['Q9', 2, 'shortanswer'],
['Q10', 2, 'shortanswer'],
]);
// Start two attempts.
$attempt1 = quiz_prepare_and_start_new_attempt($quizobj, 1, null, false, [], [], $user->id);
$attempt2 = quiz_prepare_and_start_new_attempt($quizobj, 2, null, false, [], [], $user->id);
$attemptobj1 = quiz_attempt::create($attempt1->id);
$attemptobj2 = quiz_attempt::create($attempt2->id);
// Get slot numbers for each section in both attempts.
$slots1a = $attemptobj1->get_slots(0);
$slots1b = $attemptobj2->get_slots(0);
$slots2a = $attemptobj1->get_slots(1);
$slots2b = $attemptobj2->get_slots(1);
// Get question order for each section in both attempts.
$order1a = array_map(fn($slot) => $attemptobj1->get_question_attempt($slot)->get_question()->id, $slots1a);
$order1b = array_map(fn($slot) => $attemptobj2->get_question_attempt($slot)->get_question()->id, $slots1b);
$order2a = array_map(fn($slot) => $attemptobj1->get_question_attempt($slot)->get_question()->id, $slots2a);
$order2b = array_map(fn($slot) => $attemptobj2->get_question_attempt($slot)->get_question()->id, $slots2b);
// Assert shuffled section is different, non-shuffled is the same.
$this->assertNotEquals($order1a, $order1b, 'Shuffled section should have different order between attempts.');
$this->assertEquals($order2a, $order2b, 'Non-shuffled section should have same order between attempts.');
}
}
@@ -0,0 +1,69 @@
@mod @mod_quiz
Feature: Attempt quiz with password and time limit
In order to attempt a quiz with password and time limit
As a teacher
I need to be able to set password and time limit to quiz
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | One | teacher1@example.com |
| student1 | Student | One | student1@example.com |
| student2 | Student | Two | student2@example.com |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "activities" exist:
| activity | name | course | quizpassword | browsersecurity | timelimit |
| quiz | Quiz 1 | C1 | Passw0rd | securewindow | 60 |
And the following "question categories" exist:
| contextlevel | reference | name |
| Activity module | Quiz 1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
And quiz "Quiz 1" contains the following questions:
| question | page |
| TF1 | 1 |
@javascript
Scenario: Attempt password restricted quiz
Given I am on the "Quiz 1" "quiz activity" page logged in as student1
When I press "Attempt quiz"
# TODO: remove. This is an extra step due to the bug reported in MDL-81377.
And I press "Start attempt"
# This confirms that quiz is attempted in a separate window.
And I switch to a second window
# Confirm that the time limit warning is displayed.
Then I should see "Your attempt will have a time limit of 1 min. When you start, the timer will begin to count down and cannot be paused. You must finish your attempt before it expires. Are you sure you wish to start now?"
And I set the field "Quiz password" to "Passw0rd"
And I press "Start attempt"
# Confirm that quiz timer is displayed when attempting the quiz.
And I should see "Time left"
@javascript
Scenario: Attempt quiz after entering incorrect password
Given I am on the "Quiz 1" "quiz activity" page logged in as student1
When I press "Attempt quiz"
# TODO: remove. This is an extra step due to the bug reported in MDL-81377.
And I press "Start attempt"
# This confirms that quiz is attempted in a separate window.
And I switch to a second window
# Incorrect password is entered.
And I set the field "Quiz password" to "password"
And I press "Start attempt"
# Confirm that error is displayed.
Then I should see "The password entered was incorrect"
# Confirm that the time limit warning is displayed.
And I should see "Your attempt will have a time limit of 1 min. When you start, the timer will begin to count down and cannot be paused. You must finish your attempt before it expires. Are you sure you wish to start now?"
# Enter the correct password to attempt quiz.
And I set the field "Quiz password" to "Passw0rd"
And I press "Start attempt"
# Confirm that quiz attempt proceeded.
And I should not see "The password entered was incorrect"
And I should not see "Your attempt will have a time limit of 1 min. When you start, the timer will begin to count down and cannot be paused. You must finish your attempt before it expires. Are you sure you wish to start now?"
And I should see "Time left"
+122
View File
@@ -247,4 +247,126 @@ class mod_quiz_generator extends testing_module_generator {
$gradeitem->id = $DB->insert_record('quiz_grade_items', $gradeitem);
return $gradeitem;
}
/**
* Create the structure of a quiz according to the provided layout.
*
* @param stdClass $quiz The quiz object.
* @param array $layout Layout describing questions and sections.
* @param int|null $categoryid Optional question category id.
*/
public function create_quiz_structure($quiz, array $layout, ?int $categoryid = null): void {
$questiongenerator = $this->datagenerator->get_plugin_generator('core_question');
$catid = $categoryid ?? $questiongenerator->create_question_category()->id;
$headings = [];
$lastpage = 0;
$cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course);
$course = get_course($quiz->course);
$quizobj = new quiz_settings($quiz, $cm, $course);
foreach ($layout as $item) {
if (is_string($item)) {
if (isset($headings[$lastpage + 1])) {
throw new \coding_exception('Sections cannot be empty.');
}
$headings[$lastpage + 1] = $item;
} else {
[$name, $page, $qtype] = $item;
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
if ($page === 0) {
$slots = $structure->get_slots();
$lastslot = end($slots);
$page = $lastslot ? $lastslot->page : 1;
}
if ($page < 1 || !($page == $lastpage + 1 || (!isset($headings[$lastpage + 1]) && $page == $lastpage))) {
throw new \coding_exception('Page numbers wrong.');
}
$q = $questiongenerator->create_question($qtype, null, ['name' => $name, 'category' => $catid]);
quiz_add_quiz_question($q->id, $quiz, $page);
$lastpage = $page;
}
}
// Section heading logic.
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
if (isset($headings[1])) {
[$heading, $shuffle] = $this->parse_section_name($headings[1]);
$sections = $structure->get_sections();
$firstsection = reset($sections);
$structure->set_section_heading($firstsection->id, $heading);
$structure->set_section_shuffle($firstsection->id, $shuffle);
unset($headings[1]);
}
foreach ($headings as $startpage => $heading) {
[$heading, $shuffle] = $this->parse_section_name($heading);
$id = $structure->add_section_heading($startpage, $heading);
$structure->set_section_shuffle($id, $shuffle);
}
}
/**
* Creat a test quiz.
*
* $layout looks like this:
* $layout = [
* 'Heading 1'
* ['TF1', 1, 'truefalse'],
* 'Heading 2*'
* ['TF2', 2, 'truefalse'],
* ];
* That is, either a string, which represents a section heading,
* or an array that represents a question.
*
* If the section heading ends with *, that section is shuffled.
*
* The elements in the question array are name, page number, and question type.
*
* @param array $layout as above.
* @param array $settings optional quiz settings to override defaults.
* @return quiz_settings the created quiz.
*/
public function create_test_quiz(array $layout, array $settings = []): quiz_settings {
// Default settings - only set defaults for keys not provided by the caller.
$defaults = [
'questionsperpage' => 0,
'grade' => 100.0,
'sumgrades' => 2,
'preferredbehaviour' => 'immediatefeedback',
];
// Preserve any settings passed in, only add missing defaults.
$settings += $defaults;
// Create the course if needed.
if (empty($settings['course'])) {
$course = $this->datagenerator->create_course();
$settings['course'] = $course->id;
} else {
$course = get_course($settings['course']);
}
// Create the quiz, structure it, and return the quiz settings.
$quiz = $this->create_instance($settings);
$this->create_quiz_structure($quiz, $layout);
$cm = get_coursemodule_from_instance('quiz', $quiz->id, $settings['course']);
return new quiz_settings($quiz, $cm, $course);
}
/**
* Parse the section name, optionally followed by a * to mean shuffle, as
* used by create_test_quiz as assert_quiz_layout.
*
* @param string $heading the heading.
* @return array with two elements, the heading and the shuffle setting.
*/
public function parse_section_name($heading): array {
if (str_ends_with($heading, '*')) {
return [substr($heading, 0, -1), 1];
} else {
return [$heading, 0];
}
}
}
+157 -152
View File
@@ -34,96 +34,7 @@ final class structure_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
/**
* Create a course with an empty quiz.
* @return array with three elements quiz, cm and course.
*/
protected function prepare_quiz_data() {
$this->resetAfterTest(true);
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Make a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'questionsperpage' => 0,
'grade' => 100.0, 'sumgrades' => 2, 'preferredbehaviour' => 'immediatefeedback']);
$cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id);
return [$quiz, $cm, $course];
}
/**
* Creat a test quiz.
*
* $layout looks like this:
* $layout = array(
* 'Heading 1'
* array('TF1', 1, 'truefalse'),
* 'Heading 2*'
* array('TF2', 2, 'truefalse'),
* );
* That is, either a string, which represents a section heading,
* or an array that represents a question.
*
* If the section heading ends with *, that section is shuffled.
*
* The elements in the question array are name, page number, and question type.
*
* @param array $layout as above.
* @return quiz_settings the created quiz.
*/
protected function create_test_quiz($layout) {
list($quiz, $cm, $course) = $this->prepare_quiz_data();
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$headings = [];
$slot = 1;
$lastpage = 0;
foreach ($layout as $item) {
if (is_string($item)) {
if (isset($headings[$lastpage + 1])) {
throw new \coding_exception('Sections cannot be empty.');
}
$headings[$lastpage + 1] = $item;
} else {
list($name, $page, $qtype) = $item;
if ($page < 1 || !($page == $lastpage + 1 ||
(!isset($headings[$lastpage + 1]) && $page == $lastpage))) {
throw new \coding_exception('Page numbers wrong.');
}
$q = $questiongenerator->create_question($qtype, null,
['name' => $name, 'category' => $cat->id]);
quiz_add_quiz_question($q->id, $quiz, $page);
$lastpage = $page;
}
}
$quizobj = new quiz_settings($quiz, $cm, $course);
$structure = structure::create_for_quiz($quizobj);
if (isset($headings[1])) {
list($heading, $shuffle) = $this->parse_section_name($headings[1]);
$sections = $structure->get_sections();
$firstsection = reset($sections);
$structure->set_section_heading($firstsection->id, $heading);
$structure->set_section_shuffle($firstsection->id, $shuffle);
unset($headings[1]);
}
foreach ($headings as $startpage => $heading) {
list($heading, $shuffle) = $this->parse_section_name($heading);
$id = $structure->add_section_heading($startpage, $heading);
$structure->set_section_shuffle($id, $shuffle);
}
return $quizobj;
}
/**
* Verify that the given layout matches that expected.
@@ -136,7 +47,8 @@ final class structure_test extends \advanced_testcase {
$slot = 1;
foreach ($expectedlayout as $item) {
if (is_string($item)) {
list($heading, $shuffle) = $this->parse_section_name($item);
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
[$heading, $shuffle] = $quizgenerator->parse_section_name($item);
$section = array_shift($sections);
if ($slot > 1 && $section->heading == '' && $section->firstslot == 1) {
@@ -172,22 +84,11 @@ final class structure_test extends \advanced_testcase {
}
}
/**
* Parse the section name, optionally followed by a * to mean shuffle, as
* used by create_test_quiz as assert_quiz_layout.
* @param string $heading the heading.
* @return array with two elements, the heading and the shuffle setting.
*/
protected function parse_section_name($heading) {
if (substr($heading, -1) == '*') {
return [substr($heading, 0, -1), 1];
} else {
return [$heading, 0];
}
}
public function test_get_quiz_slots(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
['TF2', 1, 'truefalse'],
]);
@@ -199,7 +100,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_quiz_has_one_section_by_default(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
]);
$structure = structure::create_for_quiz($quizobj);
@@ -214,7 +118,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_get_sections(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
'Heading 1*',
['TF1', 1, 'truefalse'],
'Heading 2*',
@@ -237,7 +144,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_remove_section_heading(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
'Heading 1',
['TF1', 1, 'truefalse'],
'Heading 2',
@@ -258,7 +168,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_cannot_remove_first_section(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
'Heading 1',
['TF1', 1, 'truefalse'],
]);
@@ -272,7 +185,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_move_slot_to_the_same_place_does_nothing(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
['TF2', 1, 'truefalse'],
['TF3', 2, 'truefalse'],
@@ -292,7 +208,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_move_slot_end_of_one_page_to_start_of_next(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
['TF2', 1, 'truefalse'],
['TF3', 2, 'truefalse'],
@@ -312,7 +231,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_move_last_slot_to_previous_page_emptying_the_last_page(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
['TF2', 2, 'truefalse'],
]);
@@ -330,7 +252,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_end_of_one_section_to_start_of_next(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
['TF2', 1, 'truefalse'],
'Heading',
@@ -352,7 +277,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_start_of_one_section_to_end_of_previous(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
'Heading',
['TF2', 2, 'truefalse'],
@@ -373,7 +301,10 @@ final class structure_test extends \advanced_testcase {
], $structure);
}
public function test_move_slot_on_same_page(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
['TF2', 1, 'truefalse'],
['TF3', 1, 'truefalse'],
@@ -393,7 +324,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_move_slot_up_onto_previous_page(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
['TF2', 2, 'truefalse'],
['TF3', 2, 'truefalse'],
@@ -413,7 +347,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_move_slot_emptying_a_page_renumbers_pages(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
['TF2', 2, 'truefalse'],
['TF3', 3, 'truefalse'],
@@ -433,7 +370,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_move_slot_too_small_page_number_detected(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
['TF2', 2, 'truefalse'],
['TF3', 3, 'truefalse'],
@@ -447,7 +387,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_move_slot_too_large_page_number_detected(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
['TF2', 2, 'truefalse'],
['TF3', 3, 'truefalse'],
@@ -461,7 +404,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_move_slot_within_section(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
'Heading 1',
['TF1', 1, 'truefalse'],
['TF2', 1, 'truefalse'],
@@ -485,7 +431,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_move_slot_to_new_section(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
'Heading 1',
['TF1', 1, 'truefalse'],
['TF2', 1, 'truefalse'],
@@ -509,7 +458,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_move_slot_to_start(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
'Heading 1',
['TF1', 1, 'truefalse'],
'Heading 2',
@@ -532,7 +484,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_move_slot_down_to_start_of_second_section(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
'Heading 1',
['TF1', 1, 'truefalse'],
['TF2', 1, 'truefalse'],
@@ -556,7 +511,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_move_first_slot_down_to_start_of_page_2(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
'Heading 1',
['TF1', 1, 'truefalse'],
['TF2', 2, 'truefalse'],
@@ -575,7 +533,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_move_first_slot_to_same_place_on_page_1(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
'Heading 1',
['TF1', 1, 'truefalse'],
['TF2', 2, 'truefalse'],
@@ -594,7 +555,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_move_first_slot_to_before_page_1(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
'Heading 1',
['TF1', 1, 'truefalse'],
['TF2', 2, 'truefalse'],
@@ -613,7 +577,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_move_slot_up_to_start_of_second_section(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
'Heading 1',
['TF1', 1, 'truefalse'],
'Heading 2',
@@ -641,7 +608,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_move_slot_does_not_violate_heading_unique_key(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
'Heading 1',
['TF1', 1, 'truefalse'],
'Heading 2',
@@ -669,7 +639,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_quiz_remove_slot(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
['TF2', 1, 'truefalse'],
'Heading 2',
@@ -693,7 +666,8 @@ final class structure_test extends \advanced_testcase {
$this->resetAfterTest(true);
$this->setAdminUser();
$quizobj = $this->create_test_quiz([
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
]);
@@ -723,7 +697,10 @@ final class structure_test extends \advanced_testcase {
* Unit test to make sue it is not possible to remove all slots in a section at once.
*/
public function test_cannot_remove_all_slots_in_a_section(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
['TF2', 1, 'truefalse'],
'Heading 2',
@@ -737,7 +714,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_cannot_remove_last_slot_in_a_section(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
['TF2', 1, 'truefalse'],
'Heading 2',
@@ -750,7 +730,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_can_remove_last_question_in_a_quiz(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
'Heading 1',
['TF1', 1, 'truefalse'],
]);
@@ -773,7 +756,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_add_question_updates_headings(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
'Heading 2',
['TF2', 2, 'truefalse'],
@@ -796,7 +782,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_add_question_updates_headings_even_with_one_question_sections(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
'Heading 1',
['TF1', 1, 'truefalse'],
'Heading 2',
@@ -825,7 +814,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_add_question_at_end_does_not_update_headings(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
'Heading 2',
['TF2', 2, 'truefalse'],
@@ -848,7 +840,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_remove_page_break(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
['TF2', 2, 'truefalse'],
]);
@@ -865,7 +860,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_add_page_break(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
['TF2', 1, 'truefalse'],
]);
@@ -882,7 +880,10 @@ final class structure_test extends \advanced_testcase {
}
public function test_update_question_dependency(): void {
$quizobj = $this->create_test_quiz([
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
['TF2', 1, 'truefalse'],
]);
@@ -905,12 +906,13 @@ final class structure_test extends \advanced_testcase {
}
public function test_update_slot_grade_item(): void {
$quizobj = $this->create_test_quiz([
['TF1', 1, 'truefalse'],
['TF2', 1, 'truefalse'],
]);
/** @var \mod_quiz_generator $quizgenerator */
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['TF1', 1, 'truefalse'],
['TF2', 1, 'truefalse'],
]);
$gradeitem = $quizgenerator->create_grade_item(
['quizid' => $quizobj->get_quizid(), 'name' => 'Awesomeness!']);
$structure = structure::create_for_quiz($quizobj);
@@ -940,11 +942,12 @@ final class structure_test extends \advanced_testcase {
}
public function test_cannot_set_nonnull_slot_grade_item_for_description(): void {
$quizobj = $this->create_test_quiz([
['Info', 1, 'description'],
]);
/** @var \mod_quiz_generator $quizgenerator */
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([
['Info', 1, 'description'],
]);
$gradeitem = $quizgenerator->create_grade_item(
['quizid' => $quizobj->get_quizid(), 'name' => 'Awesomeness!']);
$structure = structure::create_for_quiz($quizobj);
@@ -959,7 +962,8 @@ final class structure_test extends \advanced_testcase {
public function test_can_add_random_questions(): void {
$this->resetAfterTest();
$quiz = $this->create_test_quiz([]);
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_test_quiz([]);
$course = $quiz->get_course();
$generator = $this->getDataGenerator();
@@ -982,8 +986,8 @@ final class structure_test extends \advanced_testcase {
*/
public function test_get_version_choices_for_slot(): void {
$this->resetAfterTest();
$quizobj = $this->create_test_quiz([]);
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([]);
// Create a question with two versions.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
@@ -1017,7 +1021,8 @@ final class structure_test extends \advanced_testcase {
$this->resetAfterTest();
// Create a quiz with question.
$quizobj = $this->create_test_quiz([]);
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quizobj = $quizgenerator->create_test_quiz([]);
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category(['contextid' => $quizobj->get_context()->id]);
$q = $questiongenerator->create_question('essay', null,
+5
View File
@@ -1487,6 +1487,11 @@ function scorm_delete_attempt($userid, $scorm, $attemptornumber) {
$attempt = $attemptornumber;
} else {
$attempt = scorm_get_attempt($userid, $scorm->id, $attemptornumber, false);
// The attempt doesn't exist.
if ($attempt === null) {
return true;
}
}
scorm_delete_tracks($scorm->id, null, $userid, $attempt->id);
+8
View File
@@ -31,6 +31,8 @@ define('SCORM_REPORT_ATTEMPTS_STUDENTS_WITH_NO', 2);
$id = required_param('id', PARAM_INT);// Course Module ID, or ...
$download = optional_param('download', '', PARAM_RAW);
$mode = optional_param('mode', '', PARAM_ALPHA); // Report mode.
$action = optional_param('action', '', PARAM_ALPHA);
$attemptids = optional_param_array('attemptid', [], PARAM_RAW);
$cm = get_coursemodule_from_id('scorm', $id, 0, false, MUST_EXIST);
$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
@@ -56,6 +58,12 @@ $PAGE->set_pagelayout('report');
require_capability('mod/scorm:viewreport', $contextmodule);
if ($action == 'delete' && has_capability('mod/scorm:deleteresponses', $contextmodule) && confirm_sesskey()) {
if (scorm_delete_responses($attemptids, $scorm)) { // Delete responses.
redirect($PAGE->url, get_string('scormresponsedeleted', 'scorm'), null, \core\output\notification::NOTIFY_SUCCESS);
}
}
// Activate the secondary nav tab.
navigation_node::override_active_url(new moodle_url('/mod/scorm/report.php', ['id' => $id]));
@@ -38,8 +38,6 @@ class report extends \mod_scorm\report {
global $CFG, $DB, $OUTPUT, $PAGE;
$contextmodule = \context_module::instance($cm->id);
$action = optional_param('action', '', PARAM_ALPHA);
$attemptids = optional_param_array('attemptid', array(), PARAM_RAW);
$attemptsmode = optional_param('attemptsmode', SCORM_REPORT_ATTEMPTS_ALL_STUDENTS, PARAM_INT);
$PAGE->set_url(new \moodle_url($PAGE->url, array('attemptsmode' => $attemptsmode)));
@@ -50,11 +48,6 @@ class report extends \mod_scorm\report {
echo $renderer->report_actionbar($actionbar);
}
if ($action == 'delete' && has_capability('mod/scorm:deleteresponses', $contextmodule) && confirm_sesskey()) {
if (scorm_delete_responses($attemptids, $scorm)) { // Delete responses.
echo $OUTPUT->notification(get_string('scormresponsedeleted', 'scorm'), 'notifysuccess');
}
}
// Find out current groups mode.
$currentgroup = groups_get_activity_group($cm, true);
@@ -39,8 +39,6 @@ class report extends \mod_scorm\report {
global $CFG, $DB, $OUTPUT, $PAGE;
$contextmodule = \context_module::instance($cm->id);
$action = optional_param('action', '', PARAM_ALPHA);
$attemptids = optional_param_array('attemptid', array(), PARAM_RAW);
$attemptsmode = optional_param('attemptsmode', SCORM_REPORT_ATTEMPTS_ALL_STUDENTS, PARAM_INT);
$PAGE->set_url(new \moodle_url($PAGE->url, array('attemptsmode' => $attemptsmode)));
@@ -51,11 +49,6 @@ class report extends \mod_scorm\report {
echo $renderer->report_actionbar($actionbar);
}
if ($action == 'delete' && has_capability('mod/scorm:deleteresponses', $contextmodule) && confirm_sesskey()) {
if (scorm_delete_responses($attemptids, $scorm)) { // Delete responses.
echo $OUTPUT->notification(get_string('scormresponsedeleted', 'scorm'), 'notifysuccess');
}
}
// Find out current groups mode.
$currentgroup = groups_get_activity_group($cm, true);
@@ -45,8 +45,6 @@ class report extends \mod_scorm\report {
global $CFG, $DB, $OUTPUT, $PAGE;
$contextmodule = \context_module::instance($cm->id);
$action = optional_param('action', '', PARAM_ALPHA);
$attemptids = optional_param_array('attemptid', array(), PARAM_RAW);
$attemptsmode = optional_param('attemptsmode', SCORM_REPORT_ATTEMPTS_ALL_STUDENTS, PARAM_INT);
$PAGE->set_url(new \moodle_url($PAGE->url, array('attemptsmode' => $attemptsmode)));
@@ -57,11 +55,6 @@ class report extends \mod_scorm\report {
echo $renderer->report_actionbar($actionbar);
}
if ($action == 'delete' && has_capability('mod/scorm:deleteresponses', $contextmodule) && confirm_sesskey()) {
if (scorm_delete_responses($attemptids, $scorm)) { // Delete responses.
echo $OUTPUT->notification(get_string('scormresponsedeleted', 'scorm'), 'notifysuccess');
}
}
// Find out current groups mode.
$currentgroup = groups_get_activity_group($cm, true);
+1 -1
View File
@@ -296,7 +296,7 @@ switch ($action) {
if (!$file) {
echo json_encode(false);
} else {
$source = unserialize($file->get_source());
$source = unserialize_object($file->get_source());
$return = array('filename' => $filename, 'filepath' => $filepath, 'references' => array());
$browser = get_file_browser();
if (isset($source->original)) {
@@ -0,0 +1,39 @@
@core @core_filepicker
Feature: Cancel file and folder upload
In order to cancel file and folder upload
As a user
I need to be able to close the filepicker
Background:
Given the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "blocks" exist:
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
| private_files | System | 1 | my-index | side-post |
And I log in as "admin"
And I follow "Dashboard"
And I follow "Manage private files"
@javascript @_file_upload
Scenario: Cancel a file upload by pressing close button
Given I follow "Add..."
And I follow "Upload a file"
When I set the field "Attachment" to "#dirroot#/lib/tests/fixtures/empty.txt"
And I set the field "Save as" to "empty_upload.txt"
And I click on "Close" "button" in the "File picker" "dialogue"
Then I should not see "empty_upload.txt"
And I should not see "empty.txt"
@javascript
Scenario: Cancel folder creation by pressing close button
# Add the folder into the private files repository
Given I follow "Create folder"
When I set the field "New folder name" to "Test folder 1"
And I click on "button.fp-dlg-butcreate" "css_element" in the "div.fp-mkdir-dlg" "css_element"
# Ensure that folder is created
Then I should see "Test folder 1"
# Close the dialogue box using X button instead of saving
And I click on "Close" "button" in the "Manage private files" "dialogue"
# Confirm folder was not added
And I should not see "Test folder 1"
+2 -2
View File
@@ -29,9 +29,9 @@
defined('MOODLE_INTERNAL') || die();
$version = 2024042211.00; // 20240422 = branching date YYYYMMDD - do not modify!
$version = 2024042212.00; // 20240422 = branching date YYYYMMDD - do not modify!
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '4.4.11 (Build: 20251006)'; // Human-friendly version name
$release = '4.4.12 (Build: 20251208)'; // Human-friendly version name
$branch = '404'; // This version's branch.
$maturity = MATURITY_STABLE; // This version's maturity level.