Compare commits

..

574 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
Huong Nguyen 381b66b33a Moodle release 4.4.11 2025-10-04 09:02:44 +07:00
AMOS bot 90cee9f31c Automatically generated installer lang files 2025-10-04 00:07:52 +00:00
Shamim Rezaie 0a19ed5cc1 weekly release 4.4.10+ 2025-10-03 20:02:36 +10:00
Mihail Geshoski 0d944d9286 MDL-75087 mod_assign: Fix failing behat tests in classic 2025-10-03 14:22:58 +07:00
Mihail Geshoski 65de7c1e0d MDL-75087 mod_assign: Fix failing unit test 2025-10-02 18:09:54 +07:00
Rajneel Totaram ed263c2eef MDL-75087 mod_assign: Respect assignment timer
This patch has better handling of timed assignments.
Users cannot directly access the submission page without
confirming to begin a timed assignment.
2025-10-02 10:49:58 +08:00
yusufwib01 f07ae868a6 MDL-86353 mod_assign: Sync exception handling in PdfType and PdfReader 2025-09-30 09:24:10 +07:00
Paul Holden bc7e7dea82 MDL-85421 cohort: limit requests for all cohorts to system context. 2025-09-29 13:33:45 +07:00
David Woloszyn e7fe9f03ff MDL-86327 auth_webservice: Stricter check on login 2025-09-29 08:24:34 +07:00
Huong Nguyen 2d3ddd06c1 MDL-86334 core: Improve after_config plugin callbacks
Plugin callbacks should be able to throw their exceptions
2025-09-29 08:18:20 +07:00
raortegar 9868bf12c1 MDL-86334 tool_mfa: Improve multi-factor authentication validation. 2025-09-29 08:18:20 +07:00
yusufwib01 1cd858fb46 MDL-86261 calendar: Improve event form to show accessible groups 2025-09-29 08:17:57 +07:00
cescobedo c16eab825f weekly release 4.4.10+ 2025-09-26 13:44:31 +02:00
cescobedo c691cc4e69 Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-09-26 13:44:29 +02:00
Mihail Geshoski 7048df7dfb Merge branch 'MDL-86432-404' of https://github.com/timhunt/moodle into MOODLE_404_STABLE 2025-09-25 14:11:02 +08:00
Mihail Geshoski cb8592b69f Merge branch 'MDL-85675-404' of https://github.com/lameze/moodle into MOODLE_404_STABLE 2025-09-25 11:52:54 +08:00
AMOS bot 296ae75911 Automatically generated installer lang files 2025-09-25 00:07:49 +00:00
Andrew Nicols ff95fc58b3 MDL-86432 qtype_ordering: Fix broken data provider 2025-09-24 17:54:13 +01:00
Sara Arjona 42d52d0993 weekly release 4.4.10+ 2025-09-23 17:21:17 +02:00
Huong Nguyen 355b839831 Merge branch 'MDL-84870-404' of https://github.com/andimendunia/moodle into MOODLE_404_STABLE 2025-09-22 08:28:52 +07:00
Huong Nguyen 2f758dc111 weekly release 4.4.10+ 2025-09-18 08:34:27 +07:00
Huong Nguyen 3fbbae99ad Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-09-18 08:34:25 +07:00
Huong Nguyen 8580cb89d8 MDL-86495 core: Improve PHPUnit for IPLookup 2025-09-16 08:51:51 +07:00
Andi Permana 474ee999f6 MDL-84870 core: Update security.txt expiry 2025-09-15 13:04:35 +07:00
Huong Nguyen 4ce0bb6ebf MDL-86495 core: Better errors handling for IP Lookup
- Since `Geoplugin` is not free anymore, a new admin setting called `GeoPlugin API key` has been added for user to input
  their API key if they still want to use `Geoplugin` service.
- The `iplookup_find_location()` method has been improved to handle error responses more effectively.
2025-09-09 08:46:52 +07:00
AMOS bot f3e8c3a305 Automatically generated installer lang files 2025-09-06 00:07:52 +00:00
Simey Lameze f0c78f3150 MDL-85675 behat: add timefinish to quiz attempt generator
Adds support for the timefinish attribute to the quiz attempt
generator allowing Behat scenarios to specify the exact completion
time of an attempt.

This is necessary to simulate a scenario where the student finished
the attempt few minutes ago for example.

This commit also adds two scenarios that automates MDLQA-307.
2025-09-05 10:08:22 +08:00
AMOS bot ed2c79b77c Automatically generated installer lang files 2025-09-03 00:07:49 +00:00
AMOS bot 4e4f7324ea Automatically generated installer lang files 2025-09-01 00:07:46 +00:00
cescobedo 2dee6f2927 weekly release 4.4.10+ 2025-08-29 15:01:20 +02:00
Huong Nguyen 2e29b3b6ab Merge branch 'MDL-86452-404' of https://github.com/andrewnicols/moodle into MOODLE_404_STABLE 2025-08-29 08:46:26 +07:00
Huong Nguyen 431b20d103 Merge branch 'MDL-86301-404' of https://github.com/andrewnicols/moodle into MOODLE_404_STABLE 2025-08-29 08:41:35 +07:00
Andrew Nicols 120e1da7a9 MDL-86452 core: Reset navigation data between tests 2025-08-28 13:13:05 +08:00
cescobedo a7a9aa76e6 weekly release 4.4.10+ 2025-08-26 15:54:15 +02:00
cescobedo 0f8db4effe Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-08-26 15:54:13 +02:00
Mihail Geshoski 5a4fe3dc31 Merge branch 'MDL-85677-404' of https://github.com/lameze/moodle into MOODLE_404_STABLE 2025-08-26 11:56:21 +08:00
Huong Nguyen f16e99c844 Merge branch 'MDL-86315-404' of https://github.com/davewoloszyn/moodle into MOODLE_404_STABLE 2025-08-26 09:23:35 +07:00
Simey Lameze b43cd189d5 MDL-85677 behat: test to verify SEB download link visibility 2025-08-26 09:18:44 +08:00
Mihail Geshoski 8d9b0b330f Merge branch 'MDL-85676-404' of https://github.com/lameze/moodle into MOODLE_404_STABLE 2025-08-26 00:42:50 +08:00
Andrew Nicols b8aa1d15a8 MDL-86301 core: Create autoloadable externallib_testcase 2025-08-25 15:14:36 +08:00
Simey Lameze 7b19f07fbd MDL-85676 behat: validate SEB attempt message 2025-08-25 14:08:13 +08:00
AMOS bot 4e78b502ef Automatically generated installer lang files 2025-08-24 00:09:54 +00:00
Mihail Geshoski 8526d2cd7a weekly release 4.4.10+ 2025-08-22 20:40:32 +08:00
Mihail Geshoski 2b4a397579 Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-08-22 20:40:28 +08:00
Huong Nguyen 4708188c02 Merge branch 'MDL-86231-404' of https://github.com/andrewnicols/moodle into MOODLE_404_STABLE 2025-08-22 08:47:01 +07:00
AMOS bot 6a6cf5d6ca Automatically generated installer lang files 2025-08-16 00:07:50 +00:00
David Woloszyn 687559871b MDL-86315 registration: Unset emails in hub when dependant value not set 2025-08-15 13:01:06 +10:00
AMOS bot 53f715d16d Automatically generated installer lang files 2025-08-14 00:07:57 +00:00
AMOS bot e26b90c332 Automatically generated installer lang files 2025-08-13 00:07:58 +00:00
Andrew Nicols 6dd0995312 MDL-86231 behat: Support array callable syntax in execute method 2025-08-08 11:42:30 +08:00
Mihail Geshoski 8445cad014 Moodle release 4.4.10 2025-08-08 10:48:40 +08:00
Sara Arjona 0dc00ed6e9 weekly release 4.4.9+ 2025-08-07 16:45:21 +02:00
Sara Arjona 53a794538a Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-08-07 16:45:19 +02:00
Ilya Tregubov 36ded6f300 MDL-63395 user: Fix profile visibility check.
It needs to be done in user_get_user_details as well.
2025-08-07 10:30:45 +07:00
Jun Pataleta 2be167a172 Merge branch 'MDL-85674-404' of https://github.com/lameze/moodle into MOODLE_404_STABLE 2025-08-07 10:44:03 +08:00
AMOS bot 065b6436c2 Automatically generated installer lang files 2025-08-07 00:07:52 +00:00
ferran 1d914c1937 Merge branch 'MDL-86176-404' of https://github.com/sarjona/moodle into MOODLE_404_STABLE 2025-08-06 16:22:01 +02:00
Sara Arjona c5a878de43 MDL-86176 badges: Adjust badges due to Canvas Credentials changes
This change addresses necessary adjustments to how OBV2.0 badges are
sent to backpacks, specifically to ensure compatibility and correct
functionality following recent updates to Canvas Credentials' conditions.

It ensures our badge integration continues to operate smoothly despite
these external changes.
2025-08-06 15:08:00 +02:00
Simey Lameze 292a559fd7 MDL-85674 behat: replace multiple steps by a single table step 2025-08-06 18:37:23 +08:00
andelacruz 3569920982 MDL-85674 mod_grade: Behat test for multiple questions manual grading 2025-08-06 17:42:21 +08:00
Huong Nguyen 2217677c96 MDL-84464 report_log: Fix EOF 2025-08-06 10:39:19 +07:00
Laurent David 0a75e8e4cf MDL-84464 report_loglive: Hide report for teacher without groups 2025-08-06 10:22:44 +07:00
Laurent David 7a6315e676 MDL-84464 report_log: Hide report for teachers without groups
* Teachers without groups in SEPARATEGROUP mode should not see the report log page nor access it
2025-08-06 10:22:44 +07:00
Laurent David 27b30703b4 MDL-84463 mod_feedback: Fix behat test
* Fix typo in test description
* Use the right step to edit questions
2025-08-05 03:41:17 +00:00
Laurent David cfc476a722 MDL-84463 mod_feedback: Fix users in no group and email
* Non editing teachers in no group should not receive feedback emails
from students in no group
2025-08-05 03:41:17 +00:00
Laurent David 093512285f MDL-84463 mod_feedback: Fix show entries and nonrespondents
* Non editing teachers not in a groups should not see entries
from other users
2025-08-05 03:41:17 +00:00
raortegar 424f20e225 MDL-85704 mod_bigbluebuttonbn: Update sesskey handling for compatibility 2025-08-05 03:41:17 +00:00
Mihail Geshoski 8e2a748167 Merge branch 'MDL-85565_404' of https://github.com/PhilippImhof/moodle into MOODLE_404_STABLE 2025-08-05 11:34:21 +08:00
Simey Lameze cf470b8de9 MDL-86186 behat: enlarge window size before asserting 2025-08-04 11:27:41 +08:00
Sara Arjona 8ce2a2fe73 weekly release 4.4.9+ 2025-07-31 16:08:47 +02:00
Sara Arjona 753335e5fe Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-07-31 16:08:43 +02:00
Michael Hawkins cb36a45ce2 Merge branch 'MDL-85362-404' of https://github.com/HuongNV13/moodle into MOODLE_404_STABLE 2025-07-31 14:39:58 +08:00
Huong Nguyen e8870b1975 Merge branch 'MDL-84485-Select-webservice-in-wstoken-behat-breadcrumb-404' of https://github.com/farhan6318/moodle into MOODLE_404_STABLE 2025-07-31 10:17:58 +07:00
AMOS bot af12e1f3c6 Automatically generated installer lang files 2025-07-31 00:07:58 +00:00
Mihail Geshoski cbc91748b4 Merge branch 'MDL-86113-404' of https://github.com/snake/moodle into MOODLE_404_STABLE 2025-07-30 23:28:53 +08:00
Farhan Karmali 3a6e2c8f26 MDL-84485 webservice: Add Behat step for mobile web service selection 2025-07-29 16:07:05 +05:30
AMOS bot d8e559dcbe Automatically generated installer lang files 2025-07-28 13:16:54 +00:00
Philipp Imhof e4c6d0b498 MDL-85565 phpunit: check for <select> before <option>
Make sure that assert_select_options() will fail when the output does
not contain a <select> and therefore does not have the given options.
2025-07-25 13:13:52 +02:00
Jake Dallimore cfe8f40e94 MDL-86113 mod_lti: skip slow network calls during tests
This check exists to determine whether a given tool URL is a cartridge,
specifically when the URL does not end in .xml. It hits the URL to
determine this. In tests, we have many mock tool URLs, which won't
resolve, and running this code causes large hangs and random failures.
Any tests covering cartridge support still work fine, provided they
continue to use URLs ending in .xml; this change only skips the check
during tests for other non-xml URLs.
2025-07-25 16:50:49 +08:00
Michael Hawkins 4ad38cc139 weekly release 4.4.9+ 2025-07-25 12:14:47 +08:00
Huong Nguyen c9978cffbb Merge branch 'MDL-86049-404' of https://github.com/andrewnicols/moodle into MOODLE_404_STABLE 2025-07-24 09:47:59 +07:00
Huong Nguyen 32b8996a09 MDL-85362 core: Define environment requirements for Moodle 5.1 2025-07-22 10:54:24 +07:00
Andrew Nicols 1cfd9eec4f MDL-86049 core: Do not use the component cache before purge
When trying to resolve a corrupt cache where it is not possible to
complete the Moodle setup/bootstrap, it is necessary to ignore the
current cache to load the cache purging infrastructure.
2025-07-21 12:25:26 +08:00
Mihail Geshoski 4f0592f963 weekly release 4.4.9+ 2025-07-18 14:52:58 +08:00
Mihail Geshoski c9336ff7dc Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-07-18 14:52:54 +08:00
Mihail Geshoski c66e293747 Merge branch 'MDL-85055-404' of https://github.com/lameze/moodle into MOODLE_404_STABLE 2025-07-17 12:14:24 +08:00
Paul Holden 140ee4b80d MDL-85055 task: correct use of DI provided frozen Clock API.
Fixes some of the usage introduced in tests as part of a35335a9.
2025-07-17 11:57:37 +08:00
Huong Nguyen e3ac7a574c Merge branch 'MDL-86016-404' of https://github.com/andrewnicols/moodle into MOODLE_404_STABLE 2025-07-15 09:56:47 +07:00
Andrew Nicols 1111c96ac0 MDL-86016 core: Remove xmldb test in favour of unit test 2025-07-15 09:33:12 +08:00
Andrew Nicols 44d7ffeb89 MDL-86016 tests: Make install.xml test less restrictive 2025-07-15 09:33:12 +08:00
AMOS bot 3fa85e6828 Automatically generated installer lang files 2025-07-12 00:07:49 +00:00
Michael Hawkins 4ea8645494 weekly release 4.4.9+ 2025-07-11 12:49:36 +08:00
Mihail Geshoski d6b06d0cef Merge branch 'MDL-85689-404' of https://github.com/andelacruz/moodle into MOODLE_404_STABLE 2025-07-10 11:38:16 +08:00
andelacruz efee10f43e MDL-85689 mod_quiz: Behat for adding and deleting page from quiz 2025-07-09 13:36:35 +08:00
Huong Nguyen 1b2f009da2 Merge branch 'MDL-85697-404' of https://github.com/andelacruz/moodle into MOODLE_404_STABLE 2025-07-09 09:11:00 +07:00
Huong Nguyen 51c1ca0248 Merge branch 'MDL-85947-M404_bump-GHA-ubuntu-24-04' of https://github.com/ziegenberg/moodle into MOODLE_404_STABLE 2025-07-08 17:35:18 +07:00
andelacruz 41ee4a6cad MDL-85697 mod_lesson: Behat for lesson group and user overrides 2025-07-08 17:12:43 +08:00
andelacruz 85e642bec1 MDL-85697 mod_assign: Behat for assignment group and user overrides 2025-07-08 17:09:53 +08:00
andelacruz 0a25a24ff6 MDL-85697 mod_quiz: Behat for quiz group and user overrides 2025-07-08 17:09:52 +08:00
Daniel Ziegenberg 5583a50bae MDL-85947 ci: Bump GHA to Ubuntu 24.04
Signed-off-by: Daniel Ziegenberg <daniel@ziegenberg.at>
2025-07-07 16:17:02 +02:00
Mihail Geshoski 82fb415b69 weekly release 4.4.9+ 2025-07-06 09:30:58 +08:00
Mihail Geshoski b7b20e7381 Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-07-06 09:30:54 +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
Mihail Geshoski 0effcbe6b5 Merge branch 'MDL-85563-m404' of https://github.com/NeillM/moodle into MOODLE_404_STABLE 2025-07-03 11:09:05 +08:00
Mihail Geshoski cffd2be100 Merge branch 'patch/MDL-85666-404' of https://github.com/skodak/moodle into MOODLE_404_STABLE 2025-07-03 09:53:37 +08:00
Petr Skoda 8270a62377 MDL-85666 phpunit: add general plugin tests
This defines new group which can be used
to limit execution of these tests to one plugin only.

For example to execute all tests for Label module use:

phpunit --testsuite=mod_label_testsuite
phpunit --group=plugin_checks --filter=mod_label

or

phpunit --filter=mod_label
2025-07-02 07:11:49 +02:00
AMOS bot ce5a9b2800 Automatically generated installer lang files 2025-07-02 00:09:49 +00:00
Neill Magill 73f101e862 MDL-85563 phpunit: Clock API should always use Moodle system time
When using `clock::now()` the frozen cloxk and incrementing clock will now
always return a `DateTimeImmutable` that uses the configured Moodle system
time. In unit tests this will be Australia/Perth.

Before this change, it would sometimes be UCT.
2025-06-30 11:44:54 +01:00
Huong Nguyen 1761d94638 weekly release 4.4.9+ 2025-06-29 16:18:57 +07:00
Huong Nguyen d7a8c72b8f Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-06-29 16:18:55 +07:00
Mihail Geshoski 1ca9ebb3cb Merge branch 'MDL-85665-404' of https://github.com/andelacruz/moodle into MOODLE_404_STABLE 2025-06-26 10:43:04 +08:00
andelacruz af4a6daacc MDL-85665 mod_quiz: Behat to build quiz in single activity format course 2025-06-24 18:06:01 +08:00
AMOS bot 12d7c64dc7 Automatically generated installer lang files 2025-06-15 00:09:48 +00:00
Mihail Geshoski 541f1569af weekly release 4.4.9+ 2025-06-13 14:43:50 +08:00
Huong Nguyen 8bbc166cb4 MDL-85316 dml: Revert DB vendor changes 2025-06-12 17:11:37 +07:00
Huong Nguyen d26badab50 Merge branch 'MDL-85316-404-mysql-version' of https://github.com/meirzamoodle/moodle into MOODLE_404_STABLE 2025-06-11 13:34:00 +07:00
meirzamoodle 823d1b25ea MDL-85316 admin: Add environment check for AWS Aurora database 2025-06-11 10:45:33 +07:00
Mihail Geshoski 662befa64e Moodle release 4.4.9 2025-06-06 12:26:06 +08:00
Sara Arjona ff135b2d86 weekly release 4.4.8+ 2025-06-05 16:43:43 +02:00
Michael Hawkins a7d8f4d1d9 MDL-83762 core_files: Bind resolve IPs and ports to cURL calls 2025-06-05 12:10:09 +07:00
Vincent Schneider 006b2a2642 MDL-84518 course: improved course visibility state handling 2025-06-05 11:23:01 +07:00
Mark Johnson 0c5b52e6ac MDL-85323 core: Don't cache the login page 2025-06-05 11:01:57 +07:00
meirzamoodle 169827cf80 MDL-85375 libraries: Escaping a query parameter on pg_insert_id() 2025-06-02 09:55:42 +07:00
Vincent Schneider f591916893 MDL-84706 bigbluebuttonbn: improved authorization and mitigate CSRF risks 2025-05-30 08:41:27 +00:00
Huong Nguyen 6dcb54ced7 MDL-79993 course: Fix PHPUnit failure 2025-05-30 08:41:27 +00:00
raortegar f105122cef MDL-79993 course: Improve user access to recent courses service 2025-05-30 08:41:27 +00:00
Vincent Schneider 0d4a8ce45d MDL-84497 badges: fix missing action sesskey checks. 2025-05-30 08:41:27 +00:00
Michael Hawkins 000b0c2771 weekly release 4.4.8+ 2025-05-30 16:29:34 +08:00
Michael Hawkins 7afa525ff3 Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-05-30 16:29:29 +08:00
Huong Nguyen a29b5364ab Merge branch 'MDL-85525-404' of https://github.com/andelacruz/moodle into MOODLE_404_STABLE 2025-05-29 09:52:00 +07:00
Huong Nguyen fbe62c9538 Merge branch 'MDL-85400-404' of https://github.com/lameze/moodle into MOODLE_404_STABLE 2025-05-29 09:00:03 +07:00
Huong Nguyen 8523e47075 Merge branch 'MDL-85389-404' of https://github.com/andelacruz/moodle into MOODLE_404_STABLE 2025-05-29 08:52:58 +07:00
AMOS bot 575932b2ec Automatically generated installer lang files 2025-05-29 00:07:51 +00:00
andelacruz 627945e854 MDL-85525 mod_quiz: Behat test for quiz attempt page block display 2025-05-28 16:45:02 +08:00
andelacruz 2617f75596 MDL-85389 mod_lesson: Behat for filtering lesson reports/essays by group 2025-05-28 15:21:00 +08:00
sam marshall 730e1d0ae0 MDL-85400 PHPunit: Hard to test code that uses encryption
This change stops the encryption code setting created key files to
read-only during PHPunit runs, which means they will be safely
deleted when the system resets after a test.
2025-05-28 13:04:14 +08:00
ferran 7d02e08ef9 Merge branch 'MDL-84304-404' of https://github.com/laurentdavid/moodle into MOODLE_404_STABLE 2025-05-26 13:50:21 +02:00
Huong Nguyen 2ca0fb4886 weekly release 4.4.8+ 2025-05-23 11:04:00 +07:00
Huong Nguyen 78f545b165 Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-05-23 11:03:59 +07:00
Huong Nguyen 9e4cdf9eca Merge branch 'MDL-84793-m404' of https://github.com/sammarshallou/moodle into MOODLE_404_STABLE 2025-05-22 10:48:46 +07:00
Huong Nguyen be65ecd3e1 Merge branch 'MDL-84918-404' of https://github.com/HuongNV13/moodle into MOODLE_404_STABLE 2025-05-22 10:34:51 +07:00
David Woloszyn f597c8bc2d MDL-84918 core: Add zend.exception_ignore_args environment check 2025-05-22 10:11:39 +07:00
Huong Nguyen 2e541fe5b0 MDL-84918 core: Add initial environment requirements for Moodle 5.1 2025-05-22 10:11:39 +07:00
Huong Nguyen 83cc825626 Merge branch 'MDL-85374-404' of https://github.com/andrewnicols/moodle into MOODLE_404_STABLE 2025-05-21 09:56:20 +07:00
AMOS bot cd27ce2970 Automatically generated installer lang files 2025-05-21 00:07:48 +00:00
Andrew Nicols e5bbbe000d MDL-85374 report_competency: Create custom step for setting autocomplete
The autocompletes used in the competency report have been bastardised to
redirect on selection. Unfortunately this also applies when when making
null-op changes like deleting the initial "Please select a value"
option.

The autocomplete code removes all current options before creating an
option, and because of this redirect the browser is redirecting before
the new value can be entered, but we retain a reference to the field as
`$this->field`, which is now stale.
2025-05-20 12:42:26 +08:00
Laurent David 7b1bff7cca MDL-84304 core_courseformat: Remove COMPLETION_COMPLETE_FAIL test
* Because we removed this status in mod_scorm, we cannot test it
anymore against any module as no module has custom_rules that leads
to COMPLETION_COMPLETE_FAIL
2025-05-19 11:56:51 +02:00
Laurent David 75de28348a MDL-84304 mod_scorm: Remove completion failed from behat test
* Now that we removed the COMPLETION_COMPLETE_FAIL status for scorm
behat tests needs to be updated
2025-05-19 11:56:51 +02:00
Magnus Samuelsson 598fee8ec7 MDL-84304 mod_scorm: Remove the COMPLETION_COMPLETE_FAIL status 2025-05-19 11:56:51 +02:00
Mihail Geshoski e968dc4629 weekly release 4.4.8+ 2025-05-16 12:59:13 +08:00
Mihail Geshoski 170b3ea2ab Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-05-16 12:59:03 +08:00
sam marshall 3d17b48855 MDL-84793 Behat: Allow CLI scripts to run on Behat instance 2025-05-15 08:51:05 +01:00
Huong Nguyen 0bc7bd66b2 Merge branch 'MDL-85279-404' of https://github.com/andelacruz/moodle into MOODLE_404_STABLE 2025-05-15 11:42:58 +07:00
andelacruz 08ea67a272 MDL-85279 mod_assign: Renamed behat test to a more specific name 2025-05-14 15:14:03 +08:00
andelacruz 4d50167046 MDL-85279 mod_page: Fix behat to match the expected behaviour 2025-05-14 15:13:04 +08:00
Sara Arjona 333aabfaea Merge branch 'MDL-85248-404' of https://github.com/albertgasset/moodle into MOODLE_404_STABLE 2025-05-12 15:49:05 +02:00
AMOS bot f28de0cb41 Automatically generated installer lang files 2025-05-10 00:07:51 +00:00
Huong Nguyen 7e75a3680e weekly release 4.4.8+ 2025-05-09 09:50:31 +07:00
Huong Nguyen d75311461f Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-05-09 09:50:29 +07:00
Huong Nguyen 14467d84fb Merge branch 'MDL-84654-404' of https://github.com/andelacruz/moodle into MOODLE_404_STABLE 2025-05-08 08:44:42 +07:00
AMOS bot 897649d9a0 Automatically generated installer lang files 2025-05-07 00:07:45 +00:00
Angelia Dela Cruz 3edbc2c282 MDL-84654 mod_assign: Assignment additional files can be hidden 2025-05-06 13:12:06 +08:00
Huong Nguyen d39da752c6 Merge branch 'MDL-85310-404' of https://github.com/andelacruz/moodle into MOODLE_404_STABLE 2025-05-06 10:32:25 +07:00
Huong Nguyen 29381e8a0d weekly release 4.4.8+ 2025-05-02 09:16:28 +07:00
Huong Nguyen 1bd8f959e1 Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-05-02 09:16:27 +07:00
AMOS bot ebeeb9cef8 Automatically generated installer lang files 2025-05-01 00:07:52 +00:00
Angelia Dela Cruz 9071c67a2c MDL-85310 qtype: Renamed duplicate scenario names to fix warning 2025-04-30 13:35:43 +08:00
Huong Nguyen 32b8197ad0 Merge branch 'MDL-84421-404' of https://github.com/andrewnicols/moodle into MOODLE_404_STABLE 2025-04-25 09:10:36 +07:00
AMOS bot e7b223c14d Automatically generated installer lang files 2025-04-23 00:07:50 +00:00
Albert Gasset 027b1a9efe MDL-85248 qtype_ddimageortext: Revert MDL-66584
This reverts commit 701e76e301.
2025-04-22 16:58:26 +02:00
AMOS bot 8254bc5d09 Automatically generated installer lang files 2025-04-20 00:08:13 +00:00
AMOS bot 630ae866bf Automatically generated installer lang files 2025-04-19 00:07:49 +00:00
AMOS bot a3323b7d28 Automatically generated installer lang files 2025-04-18 00:07:51 +00:00
Sara Arjona 77fed64613 weekly release 4.4.8+ 2025-04-17 14:30:09 +02:00
Huong Nguyen ac0ae250f0 Merge branch 'MDL-84298-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-04-17 10:54:19 +07:00
Huong Nguyen bb10627261 Merge branch 'MDL-84625-M404_close-recordset-in-core_analytics-model-add_prediction_ids' of https://github.com/ziegenberg/moodle into MOODLE_404_STABLE 2025-04-17 10:28:26 +07:00
Huong Nguyen 1dbedf59ae Merge branch '83713-javascript-error-xml-grades-import-404' of https://github.com/DSI-Universite-Rennes2/moodle into MOODLE_404_STABLE 2025-04-17 10:12:33 +07:00
Huong Nguyen 03226fa1be Merge branch 'MDL-84232-404-avoid_recover_quiz_grades' of https://github.com/drobayo/moodle into MOODLE_404_STABLE 2025-04-17 08:39:46 +07:00
Julien Boulen 9ade94e0ea MDL-83713 form: Fix javascript selector on file picker
Co-authored-by: Meirza <meirza.arson@moodle.com>
2025-04-16 16:46:38 +02:00
Sara Arjona 9843d8a9ef Merge branch 'MDL-84820-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-04-16 15:24:45 +02:00
Amaia Anabitarte 3d27fbc77d MDL-85170 mod_choice: Remove the unnecessary ul closing tag 2025-04-16 15:12:37 +02:00
Sara Arjona 788900f048 Merge branch 'MDL-82132-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-04-16 14:55:15 +02:00
Huong Nguyen 274cb0f8b2 Merge branch 'MDL-83195-releaseanongradev2-404' of https://github.com/leonstr/moodle into MOODLE_404_STABLE 2025-04-16 09:40:16 +07:00
Paul Holden a54bdf4ade MDL-82132 user: less restrictive returning first/lastname details.
The `viewfullnames` capability check was overeager in restricting
the inclusion of user first and lastname properties in the returned
structure. Especially given that the same data was almost always
present in the fullname property of the same structure.
2025-04-15 16:00:52 +01:00
Paul Holden 3df91d98f4 MDL-82132 user: re-factor code for generating dummy user fullname. 2025-04-15 16:00:52 +01:00
Sara Arjona e54e923b55 Merge branch 'MDL-84149-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-04-15 16:45:08 +02:00
Sara Arjona 0dc8cfc9d4 Merge branch 'MDL-84140-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-04-15 16:31:25 +02:00
Sara Arjona da164d9dba Merge branch 'MDL-83990-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-04-15 16:18:21 +02:00
Sara Arjona fcfd4b3fc3 Merge branch 'MDL-83537-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-04-15 16:09:34 +02:00
Jun Pataleta b59cceeeb8 Merge branch 'MDL-83979-url-always-proxy-MOODLE_404_STABLE' of https://github.com/brendanheywood/moodle into MOODLE_404_STABLE 2025-04-15 20:43:43 +08:00
Sara Arjona d6ed761e7f Merge branch 'MDL-55034-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-04-15 13:37:04 +02:00
Sara Arjona 47cb26cde1 Merge branch 'MDL-78381-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-04-15 11:37:01 +02:00
Sara Arjona a66e710e22 Merge branch 'MDL-80337-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-04-15 11:30:41 +02:00
Sara Arjona ad4887bdf1 Merge branch 'MDL-62230-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-04-14 18:57:11 +02:00
Sara Arjona ab5cb82442 Merge branch 'MDL-81454-404' of https://github.com/daniil-berg/moodle into MOODLE_404_STABLE 2025-04-14 18:46:34 +02:00
Sara Arjona cf7a9dd5fc Merge branch 'MDL-83550-404' of https://github.com/andrewgos/moodle into MOODLE_404_STABLE 2025-04-14 17:54:31 +02:00
Sara Arjona c2b06a60bf Merge branch 'MDL-78886-404' of https://github.com/mwehr/moodle into MOODLE_404_STABLE 2025-04-14 17:50:27 +02:00
Sara Arjona 0d78fef21c Merge branch 'MDL-66584-404' of https://github.com/danghieu1407/moodle into MOODLE_404_STABLE 2025-04-14 17:47:46 +02:00
Wehr Mario bd938712f4 MDL-78886 assignfeedback_editpdf: Fix broken next page button in PDFView
Credits go to Cédric Gerbault cdgerbault@gmail.com for providing the fix.
2025-04-14 17:41:24 +02:00
Sara Arjona 27615c8cc6 Merge branch 'MDL-84833-404' of https://github.com/ferranrecio/moodle into MOODLE_404_STABLE 2025-04-14 17:11:17 +02:00
Sara Arjona 30d23e5353 Merge branch 'MDL-79464-404' of https://github.com/mihailges/moodle into MOODLE_404_STABLE 2025-04-14 17:08:36 +02:00
Daniel Ziegenberg 24d1f24175 MDL-84625 analytics: close recordset when it's not needed anymore
Signed-off-by: Daniel Ziegenberg <daniel@ziegenberg.at>
2025-04-14 10:02:11 +02:00
Jun Pataleta f22eed6b67 Moodle release 4.4.8 2025-04-13 09:52:59 +08:00
Huong Nguyen a969e35ef5 weekly release 4.4.7+ 2025-04-11 22:02:21 +07:00
Sara Arjona 43a0e4d96d MDL-83994 course: Adapt PHPUnit to lower PHP versions 2025-04-11 19:25:19 +08:00
Huong Nguyen b6cf3a1851 MDL-72704 message: Fix fetching for user has already contacted 2025-04-11 17:46:36 +08:00
Sara Arjona dd0b40c2ae Merge branch 'MDL-85139-404-2' of https://github.com/junpataleta/moodle into MOODLE_404_STABLE 2025-04-11 07:05:19 +02:00
Paul Holden c9c6dbebd6 MDL-84865 cohort: improve system report validation of parameters. 2025-04-11 12:46:48 +08:00
Jun Pataleta 13440d2edd MDL-72704 message: Additional optimisation tweaks
* Return early if the `userids` params is empty or if the filtered
user IDs end up being empty.
2025-04-11 12:18:58 +08:00
Stevani Andolo 866d5c02f3 MDL-72704 message: Fix fetching of conversation member info 2025-04-11 12:18:58 +08:00
raortegar d834f8698f MDL-85104 lib: Add additional check to local URLs params. 2025-04-11 10:22:17 +07:00
James C 72d54c6b1e MDL-83994 course: Respect course_can_delete_section() in AJAX 2025-04-11 13:05:56 +10:00
Paul Holden f113ecfba7 MDL-84473 repository_equella: safer unserializing of file references. 2025-04-11 09:52:05 +07:00
Paul Holden dffd193de5 MDL-84475 repository_dropbox: safer unserializing of file references. 2025-04-11 09:33:58 +07:00
Jun Pataleta a2be2e3067 MDL-85139 forms: Revert the addition of element IDs in group containers
This reverts the changes to core/element-group by MDL-61823 as it is
causing duplicate ID issues.
2025-04-11 10:12:25 +08:00
Alec Gullon c7d7c5a8c8 MDL-79464 mod_assign: Limit user navigation if grading fails validation 2025-04-10 16:27:52 +08:00
Michael Hawkins cb2690ca4e MDL-85152 filter_tex: Update deny list and slash handling 2025-04-10 14:21:00 +08:00
Paul Holden af704fe2b3 MDL-84478 tool_brickfield: verify sesskey prior to course analysis. 2025-04-10 13:06:58 +08:00
Paul Holden 4332522a7d MDL-84499 block_rss_client: user access checks for viewing feed. 2025-04-10 12:32:37 +08:00
Paul Holden cae8d49d99 MDL-65356 mod_data: update entry editing/deletion to not leak sesskey. 2025-04-10 10:07:53 +07:00
Paul Holden 320700db21 MDL-84750 course: require user be able to view course participants. 2025-04-10 09:29:58 +08:00
David Woloszyn d48c418aa0 MDL-84351 tool_mfa: Fix guidance page file access
File uploaded to the guidance page were not accessible due to
null values being provided to $course and $cm params for the
tool_mfa_pluginfile.
2025-04-09 16:50:47 +07:00
raortegar efd335ca90 MDL-84351 tool_mfa: Fix access to guide page when MFA login 2025-04-09 16:50:47 +07:00
David Woloszyn 588be50d53 MDL-84351 tool_mfa: Check URL against allowed components for redirect 2025-04-09 16:50:47 +07:00
David Woloszyn 52731101f6 MDL-84351 url: Add method for getting slashargument 2025-04-09 16:50:47 +07:00
Daniil Fajnberg 3b05390d62 MDL-81454 core: Remove incorrect @property-read annotation 2025-04-09 09:01:32 +02:00
Brendan Heywood a5242728c2 MDL-83979 repository_url: Always use the proxy for url downloads 2025-04-09 14:45:35 +10:00
Paul Holden ad5b72e696 MDL-84479 tool_usertours: verify sesskey prior to tour duplication. 2025-04-08 15:11:20 +00:00
Michael Hawkins a0ff17c66d MDL-85015 tool_mfa: Include extra param in email factor cancel check 2025-04-08 15:11:20 +00:00
David Woloszyn cf7fe7a547 MDL-84784 tool_mfa: Remove enrol/index.php from no redirect 2025-04-08 15:11:20 +00:00
Sara Arjona 3101137de3 weekly release 4.4.7+ 2025-04-08 17:05:49 +02:00
Sara Arjona 1c95a3a4da Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-04-08 17:05:48 +02:00
Jun Pataleta 39ce93c3b6 Merge branch 'MDL-84058-404' of https://github.com/meirzamoodle/moodle into MOODLE_404_STABLE 2025-04-08 10:07:16 +08:00
AMOS bot e8ae5de91b Automatically generated installer lang files 2025-04-08 00:07:46 +00:00
meirzamoodle 401a716147 MDL-84058 cli: Add a backup check for course module names
Co-authored-by: Jun Pataleta <jun@moodle.com>
2025-04-08 06:30:17 +07:00
Huong Nguyen d92296b89e Merge branch 'MDL-84970-404' of https://github.com/andrewnicols/moodle into MOODLE_404_STABLE 2025-04-07 10:02:14 +07:00
Jun Pataleta ede19c68df Merge branch 'MDL-85112-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-04-05 12:27:30 +08:00
Paul Holden a53ce235a8 MDL-85112 message: correct use of class to hide element content. 2025-04-04 17:08:18 +01:00
Huong Nguyen 30fed30e65 weekly release 4.4.7+ 2025-04-04 22:20:15 +07:00
Jun Pataleta 3cc3825f63 Merge branch 'MDL-84816-404' of https://github.com/meirzamoodle/moodle into MOODLE_404_STABLE 2025-04-04 12:37:20 +08:00
meirzamoodle e8eb81f399 MDL-84816 message: Make sure the 'see all' link stays visible 2025-04-04 11:27:35 +07:00
Huong Nguyen 1d21f959b4 Merge branch 'MDL-84997-404' of https://github.com/davewoloszyn/moodle into MOODLE_404_STABLE 2025-04-04 11:15:19 +07:00
Jun Pataleta 67107816a0 Merge branch 'MDL-84907-404' of https://github.com/HuongNV13/moodle into MOODLE_404_STABLE 2025-04-04 11:30:27 +08:00
Jun Pataleta 8f974b5e0e Merge branch 'MDL-84914-404-enfix' of https://github.com/mudrd8mz/moodle into MOODLE_404_STABLE 2025-04-04 09:47:24 +08:00
Shamim Rezaie 7cd120d902 Merge branch 'MDL-61823-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-04-04 09:08:43 +11:00
Paul Holden 2a0c01bb1a MDL-61823 question: exclude file element from grouped elements hack.
This removes the duplicated label, that was being matched by the
previously wide selector to make grouped element labels visible.
2025-04-03 15:36:32 +01:00
Paul Holden aaf84bc0cf MDL-61823 forms: ensure grouped elements have ID attribute.
This is used by the forms implementation for faux-label elements
so that when they are clicked on, the first input element of the
corresponding group is focused.
2025-04-03 14:52:11 +01:00
Jun Pataleta 09daa6585f MDL-75971 core: Remove unnecessary whitespace 2025-04-03 14:53:15 +08:00
Jun Pataleta 67dbbc95bd Merge branch 'MDL-75971-404' of https://github.com/lameze/moodle into MOODLE_404_STABLE 2025-04-03 14:36:47 +08:00
Simey Lameze f3eedaaf2a MDL-75971 behat: add email handling capabilities to Behat 2025-04-03 14:00:25 +08:00
Jun Pataleta 5d3fac792c Merge branch 'MDL-84823-404' of https://github.com/meirzamoodle/moodle into MOODLE_404_STABLE 2025-04-03 12:14:39 +08:00
meirzamoodle 516fed31e5 MDL-84823 message: Fix ARIA attributes in popovers and messaging drawers
> ARIA Expanded
A disclosure is a widget that allows content to be
collapsed (hidden) or expanded (visible). It has two elements:
1. Disclosure button, and
2. Section of content whose visibility is controlled by the button.

ARIA Expanded must be applied to the disclosure button element
or the element with role="button" rather than the content.

> ARIA label
Move the icon title and aria-label as they are not
interactive elements to the toggle element.
The title attribute is helpful for the tooltip,
and the aria-label benefits the screen reader.
2025-04-03 11:09:58 +07:00
Huong Nguyen 46cb497bae Merge branch 'MDL-85023_404' of https://github.com/PhilippImhof/moodle into MOODLE_404_STABLE 2025-04-03 11:08:04 +07:00
Shamim Rezaie 66c560de3d Merge branch 'MDL-61730-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-04-03 06:54:02 +11:00
Shamim Rezaie faea71c0aa Merge branch 'MDL-85000-404' of https://github.com/HuongNV13/moodle into MOODLE_404_STABLE 2025-04-03 06:13:45 +11:00
Shamim Rezaie 2e48537ee5 Merge branch 'MDL-72050-404' of https://github.com/sh-csg/moodle into MOODLE_404_STABLE 2025-04-03 00:38:35 +11:00
Shamim Rezaie d273c20427 Merge branch 'MDL-84961-404' of https://github.com/odeialba/moodle into MOODLE_404_STABLE 2025-04-02 20:55:01 +11:00
Shamim Rezaie 376c41e841 Merge branch 'MDL-80629-404' of https://github.com/andrewnicols/moodle into MOODLE_404_STABLE 2025-04-02 20:02:15 +11:00
Andrew Nicols 59f556e731 MDL-84970 editor_tiny: Use the langpack name for Moodle language 2025-04-02 16:06:22 +08:00
Paul Holden ccd7a6b9a6 MDL-61730 gradeimport_xml: set type on form iprestriction field. 2025-04-02 08:57:21 +01:00
David Mudrák 143fbe18f7 MDL-84914 lang: Use fixed strings in tests, too 2025-04-01 22:20:40 +02:00
Helen Foster a444834a5b MDL-84914 lang: Import fixed English strings (en_fix) 2025-04-01 22:20:09 +02:00
Sara Arjona 1b5e5e3c65 weekly release 4.4.7+ 2025-04-01 16:34:27 +02:00
Mihail Geshoski 1c8cc2b543 MDL-80091 mod_assign: Fix failing behat test 2025-04-01 11:34:16 +02:00
Philipp Imhof 659b50f380 MDL-85023 quiz: allow WebRTC with SEB on Mac 2025-04-01 06:52:57 +02:00
Huong Nguyen 77b6cf7568 Merge branch 'MDL-84419-404' of https://github.com/clransom/moodle into MOODLE_404_STABLE 2025-04-01 11:07:17 +07:00
Huong Nguyen 0b4c755b34 Merge branch 'MDL-83856-404' of https://github.com/andrewgos/moodle into MOODLE_404_STABLE 2025-04-01 10:48:16 +07:00
Huong Nguyen 04eb9c0b61 Merge branch 'MDL-80091-404' of https://github.com/andrewgos/moodle into MOODLE_404_STABLE 2025-04-01 10:41:51 +07:00
Paul Holden 21419b94bd MDL-55034 blog: update external blog deletion to not leak sesskey. 2025-03-31 14:04:19 +01:00
ferran 021965fd55 MDL-84833 course: completion feedback messages 2025-03-31 09:39:49 +02:00
Jun Pataleta 6685d750e7 Merge branch 'MDL-84826-404' of https://github.com/meirzamoodle/moodle into MOODLE_404_STABLE 2025-03-31 14:28:27 +08:00
Andrew Nicols 830fca4a77 Merge branch 'MDL-76801-404' of https://github.com/andrewgos/moodle into MOODLE_404_STABLE 2025-03-31 09:39:29 +08:00
Sara Arjona cdf702bea3 weekly release 4.4.7+ 2025-03-28 12:48:25 +01:00
Sara Arjona 6fb8b72782 Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-03-28 12:48:23 +01:00
Paul Holden 2e571156f9 MDL-85048 core: define mapping for new America/Coyhaique zone.
Follows same process as other timezone additions in d5ec3594.
2025-03-28 08:58:43 +00:00
Huong Nguyen 59547c9e26 Merge branch 'MDL-85048-404' of https://github.com/lameze/moodle into MOODLE_404_STABLE 2025-03-28 15:01:11 +07:00
Simey Lameze dba907c5a1 MDL-85048 phpunit: add america/coyhaique timezone 2025-03-28 15:00:16 +08:00
David Woloszyn b09ace0546 MDL-84997 core_message: Hide message pill when count is zero 2025-03-28 16:49:23 +11:00
Huong Nguyen 360812e77b Merge branch 'MDL-84952-404' of https://github.com/roland04/moodle into MOODLE_404_STABLE 2025-03-28 08:52:28 +07:00
AMOS bot 3f0fa1eb14 Automatically generated installer lang files 2025-03-28 00:08:40 +00:00
Paul Holden 1f859c16ea MDL-84140 course: more resilient check for activity footer component. 2025-03-27 08:33:02 +00:00
Mikel Martín a575fe46db MDL-84952 theme_boost: Fix spacing for collapse/expand buttons 2025-03-26 15:41:57 +01:00
Huong Nguyen f0d535b65c Merge branch 'MDL-85001-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-03-26 10:54:01 +07:00
Jake Dallimore 0dee8a90e3 Merge branch 'MDL-85007-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-03-26 11:32:26 +08:00
Huong Nguyen 0733ed3db1 Merge branch 'MDL-83272_404_STABLE' of https://github.com/marxjohnson/moodle into MOODLE_404_STABLE 2025-03-26 10:27:40 +07:00
Huong Nguyen 32547c0496 Merge branch 'MDL-84623-M404_close-recordset-in-tool_dataprivacy-expired_contexts_manager-clear_old_records' of https://github.com/ziegenberg/moodle into MOODLE_404_STABLE 2025-03-26 10:23:54 +07:00
Huong Nguyen 39111a7bc7 MDL-85000 media_videojs: Enable Ogv.JS Tech for all browsers 2025-03-26 09:34:03 +07:00
Huong Nguyen a05d3938f8 weekly release 4.4.7+ 2025-03-26 08:56:04 +07:00
Huong Nguyen a7f6daff5b Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-03-26 08:56:03 +07:00
Paul Holden 90b4e8b85c MDL-85007 core: ensure events are dispatched against the same table.
This ensures that we always dispatch the dynamic table updated event
against the same table we just updated. Previously it was possible
for another table to receive the event, where both tables were
effectively the same instance of one another (e.g. custom reports).
2025-03-25 17:02:31 +00:00
meirzamoodle 8c6b8ed7ea MDL-84826 theme_boost: Mantaining hover/focus colour for contrast 2025-03-25 22:55:32 +07:00
Dagoberto Robayo 1390809635 MDL-84232 mod_quiz: Avoid recover grades in quiz view page
- If Recover grades default is not enabled, quiz grades should not be recover.
2025-03-25 09:16:26 -05:00
Paul Holden 43a62d768c MDL-85001 reportbuilder: ensure report uniqueness in filters loader.
Where the same report exists multiple times on the page, ensure we
are still able to uniquely identify each via ID. Update the filters
module to ensure the filters form for each report is initialised.
2025-03-25 11:43:48 +00:00
danghieu1407 701e76e301 MDL-66584 qtype_ddimageortext: Move JSON data to data-attribute 2025-03-25 16:37:37 +07:00
Andrew Nicols 0b88080611 Merge branch 'MDL-84893-404' of https://github.com/davewoloszyn/moodle into MOODLE_404_STABLE 2025-03-25 11:07:04 +08:00
Andrew Nicols a8f6fad781 Merge branch 'MDL-84112-MOODLE_404_STABLE-fix_bigbluebutton_defaultcompletion_performance' of https://github.com/praxisdigital/moodle into MOODLE_404_STABLE 2025-03-25 10:55:57 +08:00
Andrew Nicols 6bef9d816c Merge branch 'MDL-84335_404' of https://github.com/PhilippImhof/moodle into MOODLE_404_STABLE 2025-03-25 10:52:58 +08:00
Andrew Nicols c328216c75 Merge branch 'MDL-76668-404' of https://github.com/cameron1729/moodle into MOODLE_404_STABLE 2025-03-25 10:38:55 +08:00
Philipp Imhof 566dc85f06 MDL-84335 question: check for null before accessing property
in function check_hint_file_access
2025-03-24 17:44:39 +01:00
Odei Alba a7c7fda91a MDL-84961 core: Avoid loading the hidden table under the chart
Remove the table from under the chart when it is disabled, instead of just hiding it.
2025-03-24 17:14:50 +01:00
Huong Nguyen 16b78d03a7 Merge branch 'MDL-83160-404' of https://github.com/mickhawkins/moodle into MOODLE_404_STABLE 2025-03-24 14:54:40 +07:00
AMOS bot 3eb7553ab5 Automatically generated installer lang files 2025-03-22 00:07:49 +00:00
Huong Nguyen 5ef6bc0d7a weekly release 4.4.7+ 2025-03-21 22:02:47 +07:00
Huong Nguyen 08abd7ea3b Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-03-21 22:02:46 +07:00
Mark Johnson 7742681329 MDL-83272 qbank_managecategories: Fix exclusion of current category
The "currentcat" parameter contains the categoryid, but is being
compared to a string like "contextid,categoryid" so was not being
matched correctly to exclude the current category from the list of
options.
2025-03-21 11:40:31 +00:00
Glenn Poder 3f8d704ee4 MDL-76668 navigation: Fix navigation for results in single activity 2025-03-21 13:51:45 +08:00
Andrew Nicols f53120985a MDL-80629 env: Moodle 4.4.x and 4.5.x do not support PHP 8.4 2025-03-21 13:18:56 +08:00
Huong Nguyen ae03be74b6 Merge branch 'MDL-84446-404' of https://github.com/andrewnicols/moodle into MOODLE_404_STABLE 2025-03-21 11:18:08 +07:00
Huong Nguyen 431605b0ba Merge branch 'MDL-84867-404' of https://github.com/roland04/moodle into MOODLE_404_STABLE 2025-03-21 09:06:09 +07:00
AMOS bot 63e52a69c2 Automatically generated installer lang files 2025-03-21 00:07:49 +00:00
Andrew Nicols 473018cb53 Merge branch 'MDL-84803-404' of https://github.com/aanabit/moodle into MOODLE_404_STABLE 2025-03-21 07:57:43 +08:00
Paul Holden e837d357b1 MDL-84149 gradereport_grader: better support for collapse preferences.
The privacy provider is now resilient against missing courses, and also
supports retrieving preferences from multiple courses for the user.
2025-03-20 15:47:45 +00:00
Paul Holden f47253d387 Merge branch 'MDL-81655_404' of https://github.com/larsbonczek/moodle into MOODLE_404_STABLE 2025-03-20 15:14:26 +00:00
Amaia Anabitarte ba1f355fd5 MDL-84803 mod_feedback: Make titles unique 2025-03-20 16:09:27 +01:00
Paul Holden 3a44e5126f MDL-84538 lang: remove duplicated badge language string. 2025-03-20 15:05:26 +00:00
Paul Holden 1b11ed787b Merge branch 'MDL-84538-M404_fix-duplicated-language-strings' of https://github.com/ziegenberg/moodle into MOODLE_404_STABLE 2025-03-20 14:45:02 +00:00
Daniel Ziegenberg 2fda6af72f MDL-84538 language: Remove duplicated lang string entries
Signed-off-by: Daniel Ziegenberg <daniel@ziegenberg.at>
2025-03-20 14:01:44 +01:00
Mikel Martín 239b833d99 MDL-84867 tool_behat: Improve SCSS deprecation failure information
- Improve SCSS deprecated-styles @mixin to receive a paramenter of the
  deprecated selector.
- Update all the current deprecated styles that are using deprecated-styles @mixin
  to include the deprecated selector as parameter.
- Improve the 'look_for_deprecated_styles' behat function.
2025-03-20 11:04:03 +01:00
Sara Arjona 15f176eb3d Merge branch 'MDL-84683-MOODLE_404_STABLE' of https://github.com/jfederico/moodle into MOODLE_404_STABLE 2025-03-19 16:19:36 +01:00
Paul Holden 7895aca40a MDL-84820 moodlenet: correct activity modal icon size/alignment. 2025-03-19 10:26:02 +00:00
Jun Pataleta 9f7839ba0b Merge branch 'MDL-84905-404' of https://github.com/HuongNV13/moodle into MOODLE_404_STABLE 2025-03-19 13:18:16 +08:00
David Woloszyn 710db6f11a MDL-84893 core_message: Fix message drawer overlap and truncation 2025-03-19 13:58:15 +11:00
Huong Nguyen ea1350cbe0 Merge branch 'MDL-84431-404' of https://github.com/andrewgos/moodle into MOODLE_404_STABLE 2025-03-19 09:55:30 +07:00
Huong Nguyen 1a9a5d1cbe MDL-84907 backup: Fix invalid CTRL characters regex 2025-03-19 09:22:54 +07:00
Huong Nguyen 8553c27e57 MDL-84905 Behat: Fix I set the following administration settings failure 2025-03-19 09:17:26 +07:00
Sara Arjona a18c3ad13a weekly release 4.4.7+ 2025-03-18 14:17:44 +01:00
Andrew Gosali 6cc2d8e5db MDL-83856 gradingform_guide: fix remark format to always be FORMAT_HTML 2025-03-18 16:31:04 +07:00
Anders Rasmussen 7b17ee7fb9 MDL-84112 bigbluebuttonbn: Added user output only if bbactivity is set 2025-03-18 10:15:44 +01:00
Andrew Gosali 3feecc5bb8 MDL-80091 assign grade: select the first user when changing filter
(if the previously selected user is no longer in the filtered list)
2025-03-18 15:23:15 +07:00
Andrew Nicols 64dbf8d6c8 Merge branch 'MDL-84222-404' of https://github.com/HuongNV13/moodle into MOODLE_404_STABLE 2025-03-18 08:04:52 +08:00
Huong Nguyen 7c9fc3fb34 Merge branch 'MDL-83591-404' of https://github.com/andrewgos/moodle into MOODLE_404_STABLE 2025-03-17 16:00:46 +07:00
Andrew Gosali 500a225afc MDL-83550 gradingform_rubric: fix submit no grade when remark disabled 2025-03-17 14:50:31 +07:00
Andrew Gosali e8054b547a MDL-83591 grade: fix issue when item is already deleted
The deleted grade is causing the error while using its id that was previously saved in the session variable
2025-03-17 10:47:39 +07:00
Paul Holden 9ab9d973c7 MDL-80337 availability: more robust checking for plugin class presence.
Co-authored-by: Dustin Brisebois <dustin.brisebois@lambdasolutions.net>
2025-03-16 19:27:37 +00:00
Daniel Ziegenberg ce2a687c2b MDL-84623 privacy: close recordset when it's not needed anymore
Signed-off-by: Daniel Ziegenberg <daniel@ziegenberg.at>
2025-03-16 19:54:14 +01:00
Jun Pataleta cbd60e5a9b Moodle release 4.4.7 2025-03-15 20:19:52 +08:00
Ilya Tregubov 6f24f29cde MDL-81945 gradereport_singleview: Fix hidden grade handling.
Seems to become broken after IR refactoring.
2025-03-14 15:21:15 +00:00
Ilya Tregubov 3ba2ebb887 MDL-81945 gradereport_user: Don't reveal visibility by color.
Seems like dimmed_text class was added without proper check so
user can guess if item is hidden.
2025-03-14 15:21:15 +00:00
Ilya Tregubov 501fc68186 MDL-81945 gradereport_user: Behat tests for user report. 2025-03-14 15:21:15 +00:00
Ilya Tregubov 42eab03367 MDL-81945 gradereport_singleview: Behat tests for singleview report. 2025-03-14 15:21:15 +00:00
Ilya Tregubov 9bbcab02b8 MDL-81945 gradereport_grader: Behat tests for grader report 2025-03-14 15:21:15 +00:00
Ilya Tregubov 0dad8a8369 MDL-81945 core_grades: create Behat step for grades action menu. 2025-03-14 15:21:15 +00:00
Ilya Tregubov 1387393a3b MDL-81945 gradereport_singleview: Fix hidden grades handling.
If a user is lacking moodle/grade:viewhidden he should not be able
to view or modify grades.
2025-03-14 15:21:15 +00:00
Sara Arjona 5919d99222 weekly release 4.4.6+ 2025-03-14 16:14:06 +01:00
Sara Arjona a86c05ee7c Merge branch 'MDL-83541-404-behat' of https://github.com/HuongNV13/moodle into MOODLE_404_STABLE 2025-03-14 11:14:44 +01:00
Huong Nguyen d58329f1e1 MDL-83541 qtype_ddimageortext: Fix Behat failure 2025-03-14 16:59:04 +07:00
Huong Nguyen 1fcd367669 MDL-83541 qtype_numerical: Fix incorrect test hints 2025-03-14 09:21:33 +01:00
Huong Nguyen d4f1cb1078 Merge branch 'MDL-83541-404' of https://github.com/HuongNV13/moodle into MOODLE_404_STABLE 2025-03-14 11:59:45 +07:00
Huong Nguyen ba75747f2e MDL-83541 mod_quiz: Fix PHPUnit failure 2025-03-14 10:31:33 +07:00
Mark Johnson 2de34d5c0e MDL-83541 qtypes: Define excluded hash fields and test hints 2025-03-14 10:31:26 +07:00
Mark Johnson 85d85a6a25 MDL-83541 question/quiz: restoring questions with identical stamps
Historically it was possible, through a series of question restores,
moves and edits, to end up with multiple questions in the same
category with the same stamp, but differences in other question
or answer fields. This, combined with changes in versions, led
to errors when restoring or duplicating quizzes using these questions.

While recent changes have made it impossible to create this situation
in current Moodle versions, as any edits will create a new question
version with a new stamp, this situation may exist on long-standing
Moodle sites which have been upgraded since pre-4.0.

This change performs a much wider-ranging comparison of restored
existing questions, generating a hash of all the data for a question in
a backup file, and a corresponding hash for each question in the target
category, to decide if a restored question matches a question already in
the database.
2025-03-14 10:31:22 +07:00
Mark Johnson eda944f427 MDL-83541 qtype_calculated: Add missing field to backups 2025-03-14 10:31:17 +07:00
Mark Johnson 5df88228a5 MDL-83541 qtype_ddimageortext: Fix test hint text
Ensure that hint text is unique to workaround MDL-84261 for
backup/restore testing.
2025-03-14 10:31:10 +07:00
Paul Holden 22b0195c6d MDL-83990 course: ensure course start/end dates set in external call. 2025-03-13 17:53:52 +00:00
Paul Holden 5ec7fa94c5 MDL-83537 check: confirm presence of instance action link. 2025-03-13 17:52:47 +00:00
Sara Arjona f35f246d19 Merge branch 'MDL-83634-404' of https://github.com/junpataleta/moodle into MOODLE_404_STABLE 2025-03-13 16:28:23 +01:00
Michael Hawkins 9186e7e0ec MDL-83160 core: Update security.txt expiry 2025-03-13 20:35:55 +08:00
Sara Arjona d00f94ff44 Merge branch 'MDL-84624-M404_close-recordset-in-core-task-h5p_clean_orphaned_records_task-execute' of https://github.com/ziegenberg/moodle into MOODLE_404_STABLE 2025-03-13 13:20:35 +01:00
Sara Arjona 25a13c0394 Merge branch '83136-fix-unknowncategory-message-404' of https://github.com/DSI-Universite-Rennes2/moodle into MOODLE_404_STABLE 2025-03-13 12:38:52 +01:00
Sara Arjona 159976d1ef Merge branch 'MDL-77646-404' of https://github.com/lucaboesch/moodle into MOODLE_404_STABLE 2025-03-13 12:33:52 +01:00
Stefan Hanauska a4c7cb3241 MDL-72050 question: Delete import draftfiles immediately 2025-03-13 08:17:43 +01:00
Stefan Hanauska 7d00d01b66 MDL-72050 filelib: Add function for deleting draftarea 2025-03-13 08:17:43 +01:00
Jun Pataleta fa36c75597 Merge branch 'MDL-80386_404' of https://github.com/timhunt/moodle into MOODLE_404_STABLE 2025-03-13 12:32:38 +08:00
Jun Pataleta cf1e71be2b Merge branch 'MDL-65233-404-no-midnight' of https://github.com/sim642/moodle into MOODLE_404_STABLE 2025-03-13 12:21:39 +08:00
Huong Nguyen 0a50f4c904 Merge branch 'MDL-84640-404' of https://github.com/andelacruz/moodle into MOODLE_404_STABLE 2025-03-12 16:32:28 +07:00
Angelia Dela Cruz 683c62f3c6 MDL-84640 analytics: Behat for obtaining prediction models insights 2025-03-12 16:48:48 +08:00
Jun Pataleta 633ddeb157 MDL-83634 tool_brickfield: Test the image alt text check's severity 2025-03-12 13:22:12 +08:00
Jun Pataleta 3c5accff73 MDL-83634 tool_brickfield: Increase the alt text limit to 750 characters
This is consistent with the alt-text length limit of 750 characters in
Moodle's editor.
2025-03-12 13:22:12 +08:00
Jun Pataleta d0afa76de6 MDL-83634 tool_brickfield: Downgrade image alt text check's severity
Given that
* There is no formally defined limit for alt text length,
* Current versions of screen readers can read alt texts longer than
125 characters,
* Accessibility checkers like axe DevTools, WAVE, etc., do not raise
errors or warnings about long image alt texts.

This patch:
- Downgrades the default severity for the `img_alt_is_too_long`
check from `BA_TEST_SEVERE` to `BA_TEST_SUGGESTION`.
- Updates the `checkdesc:imgaltistoolong` lang string to be more of a
reminder/suggestion rather than an error.
2025-03-12 13:22:11 +08:00
Huong Nguyen bc2f8102ce Merge branch 'MDL-84578_404' of https://github.com/cwarwicker/moodle into MOODLE_404_STABLE 2025-03-12 10:39:47 +07:00
Conn Warwicker 532ef50ed6 MDL-80386 core_question: Add "show all" button to filters.
This adds the "Show All" button to the question bank filters
to allow users to select all questions easily.

It also adds new behat tests to test the functionality.
2025-03-11 18:13:23 +00:00
Paul Holden e34037ea6a MDL-78381 course: observe default "showactivitydates" in external call. 2025-03-11 17:33:29 +00:00
Sara Arjona 8f4cb8c4d5 Merge branch 'MDL-84801-404' of https://github.com/HuongNV13/moodle into MOODLE_404_STABLE 2025-03-11 17:26:29 +01:00
Derek Henderson 1fc38904e1 MDL-77646 lib: Use iframe instead of object to display pdf. 2025-03-11 13:52:51 +01:00
Sara Arjona 4df87b3130 weekly release 4.4.6+ 2025-03-11 13:02:30 +01:00
Sara Arjona 551568aca2 Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-03-11 13:02:29 +01:00
Huong Nguyen 3576ec553c MDL-84801 Behat: Fix Expand all fieldsets and advanced elements failure 2025-03-11 09:22:00 +07:00
AMOS bot 7b8510441f Automatically generated installer lang files 2025-03-11 00:07:52 +00:00
Conn Warwicker e7d7d84aa3 MDL-84578 qbank_managecategories: Add category validation.
- This adds simple logic check to filter.js to stop user
submitting an invalid category.
- This adds new behat steps to check this validation.
- This adds a new behat test to test this in managecategories.
2025-03-10 14:05:38 +00:00
Sara Arjona ec9b50b135 Merge branch 'MDL-84313-404' of https://github.com/dpalou/moodle into MOODLE_404_STABLE 2025-03-10 11:35:53 +01:00
Paul Holden aa4fce5f6a Merge branch 'MDL-84620-M404_close-recordset-in-tool_brickfield-manager-check_scheduled_areas' of https://github.com/ziegenberg/moodle into MOODLE_404_STABLE 2025-03-10 10:29:40 +00:00
Daniel Ziegenberg 6bf326fb9f MDL-84620 accessibility: close recordset when it's not needed anymore
Signed-off-by: Daniel Ziegenberg <daniel@ziegenberg.at>
2025-05-06 06:41:35 +02:00
Sara Arjona 482834493e Merge branch 'MDL-84256-404' of https://github.com/laurentdavid/moodle into MOODLE_404_STABLE 2025-03-10 11:16:50 +01:00
Sara Arjona a4bd8cec97 Merge branch 'MDL-84398-404' of https://github.com/pedrojordao/moodle into MOODLE_404_STABLE 2025-03-10 11:12:16 +01:00
AMOS bot fa63ee1a8a Automatically generated installer lang files 2025-03-10 00:07:40 +00:00
Jun Pataleta f252b16672 weekly release 4.4.6+ 2025-03-08 11:44:32 +08:00
Laurent David 405a99a70b MDL-84256 core_course: Fix course icon in course index page
* In RTL language the icon should follow the course name intead
of being upfront
2025-03-07 11:16:43 +01:00
Andrew Gosali 2b5ba79954 MDL-76801 mod_assign: grader ux fixes on "requires grading"
- refresh list when the user is graded hence no longer in the list
- hide the CTA buttons when no user is selected
2025-03-07 15:59:22 +08:00
Andrew Nicols 1b50220d96 Merge branch 'MDL-81623-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-03-07 09:25:43 +08:00
Andrew Nicols ce9d97bd40 Merge branch 'MDL-84729-404' of https://github.com/HuongNV13/moodle into MOODLE_404_STABLE 2025-03-07 09:09:33 +08:00
Andrew Nicols 2c2aa043c9 Merge branch 'MDL-84532-404' of https://github.com/mihailges/moodle into MOODLE_404_STABLE 2025-03-07 09:08:32 +08:00
Sara Arjona 5556f401e0 Merge branch 'MDL-83720-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-03-06 14:56:02 +01:00
Andrew Nicols 5e2a70cb74 Merge branch 'MDL-84723-404' of https://github.com/lameze/moodle into MOODLE_404_STABLE 2025-03-06 11:50:55 +08:00
Simey Lameze 6a2092db12 MDL-84723 behat: final improvements and fixes to the test
This commit does few things:
- Remove unnecessary grade field to assign generator.
- Remove unnecessary comment lines.
- Update the warning message assertion to use the no-break space unicode character.
2025-03-06 11:24:05 +08:00
Angelia Dela Cruz 31f09682df MDL-84723 gradingform: Behat for maximum grade mismatch in marking guide 2025-03-06 11:24:05 +08:00
Andrew Nicols 02e0fdb3fe Merge branch 'MDL-84295-404' of https://github.com/laurentdavid/moodle into MOODLE_404_STABLE 2025-03-06 09:20:09 +08:00
Andrew Nicols 244166e01d Merge branch 'MDL-75442-404' of https://github.com/StudiUM/moodle into MOODLE_404_STABLE 2025-03-06 09:12:08 +08:00
Laurent David 5ad933815c MDL-84295 core_theme: Fix margin for activity description list
* Make sure lists in activity descriptions are displayed
identically on course page and activity pages
2025-03-05 10:13:10 +01:00
Andrew Nicols c111e4fe87 Merge branch 'MDL-82170-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-03-05 14:40:33 +08:00
Huong Nguyen 847068d11d Merge branch 'MDL-84425-404' of https://github.com/andelacruz/moodle into MOODLE_404_STABLE 2025-03-05 10:51:26 +07:00
Angelia Dela Cruz b577e8d621 MDL-84425 theme_boost: Behat for course section selector unavailable 2025-03-05 11:18:09 +08:00
Daniel Ziegenberg 98ad4352a9 MDL-84624 h5p: close recordset when it's not needed anymore
Signed-off-by: Daniel Ziegenberg <daniel@ziegenberg.at>
2025-03-04 19:35:48 +01:00
Sara Arjona df2723c6a8 weekly release 4.4.6+ 2025-03-04 16:13:47 +01:00
Sara Arjona 201f222789 Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-03-04 16:13:45 +01:00
Paul Holden f19403ea2f MDL-62230 mod_feedback: define exporter format for name/presentation. 2025-03-04 14:13:08 +00:00
Huong Nguyen 6c4bf9098e MDL-84729 core: Use core\clock for Tasks
This will help to solve the random failure on "test_adhoc_task_get_first_starting_time"
2025-03-04 16:37:23 +07:00
Huong Nguyen 05682343be Merge branch 'MDL-84595-404' of https://github.com/andrewnicols/moodle into MOODLE_404_STABLE 2025-03-04 10:40:24 +07:00
Huong Nguyen 3d314d6d4b MDL-84222 lib: PHPMailer - Renew the expired token
This patch includes:
 - Get a new token if it is not available.
 - Renew the token if it is expired using the refresh token.
2025-03-03 14:30:41 +07:00
AMOS bot afdc24fc07 Automatically generated installer lang files 2025-03-02 00:08:12 +00:00
jfederico bb3e575079 MDL-84683 mdl_bigbluebuttonbn: Hide Settings for RecordingsOnly 2025-02-28 15:02:42 +00:00
Pedro Jordao 4f1662e90a MDL-84398 libraries: Configure locale property for ChartJs
ChartJs has a property called locale that makes possible to format scale
numbers according to a language. This way we ensure that decimal places
will be displayed correctly in the chart labels and tooltips.
2025-02-27 21:43:31 -03:00
Paul Holden 9e41cd6609 MDL-81623 admin: sensitive toggle when prompting for upgrade key. 2025-02-27 21:14:07 +00:00
Sara Arjona b132ff1ed8 weekly release 4.4.6+ 2025-02-27 16:54:53 +01:00
Huong Nguyen 377d56c835 Merge branch 'MDL-80351-404' of https://github.com/pedrojordao/moodle into MOODLE_404_STABLE 2025-02-27 10:13:37 +07:00
Simey Lameze 12bf1da9e5 MDL-84424 behat: add template_competencies generator and improve test 2025-02-27 07:50:03 +08:00
Angelia Dela Cruz 52fbae2907 MDL-84424 tool_lp: admin can create learning plans from template 2025-02-27 07:48:19 +08:00
Sara Arjona 5eaaf5e016 Merge branch 'MDL-84609-404' of https://github.com/lameze/moodle into MOODLE_404_STABLE 2025-02-26 16:19:53 +01:00
Sara Arjona 045db899be Merge branch 'MDL-84584-404' of https://github.com/sh-csg/moodle into MOODLE_404_STABLE 2025-02-26 08:04:15 +01:00
Andrew Gosali d42c502516 MDL-84431 mod_forum: fix visual issue on forum reply button :focus 2025-02-26 10:14:18 +07:00
Simey Lameze 16c9172c63 MDL-84609 behat: add required entities and fix failures
This commit also fix some typos and misaligned pipes around the
changed steps.

Signed-off-by: Simey Lameze <simey@moodle.com>
2025-02-25 11:51:14 +08:00
Angelia Dela Cruz 6b12fa32e1 MDL-84609 tool: Replaced tool_lp generators with standard generators 2025-02-25 11:51:14 +08:00
Paul Holden 9451a354fd MDL-84298 mod_assign: target only first label for grade popout button. 2025-02-24 22:22:17 +00:00
Van Binh Dang b9ce55d66d MDL-75442 quiz: Highlight error message when manually grading 2025-02-24 14:29:44 -05:00
Mihail Geshoski cdce19d0d0 MDL-84532 qtype_ordering: Fix failing query in mssql on upgrade
The SELECT statement can only include columns that are part of the
GROUP BY clause. To resolve this issue, instead of including all columns
('*'), only the 'id' column is now included in the SELECT statement,
while still preserving the existing GROUP BY statement. This column is
the only one needed to identify the relevant qtype_ordering_options
record that requires updating in the next step.
2025-02-24 18:02:06 +08:00
sam marshall 168ca6fdef MDL-83674 Files: pluginfile.php debugging if user not allowed course 2025-02-21 14:12:29 +00:00
Julien Boulen f063ef682f MDL-83136 course: Add missing value on unknowncategory exception message 2025-02-21 09:25:18 +01:00
Huong Nguyen e06475d8ae weekly release 4.4.6+ 2025-02-21 10:15:12 +07:00
Andrew Nicols f0199e8038 MDL-84595 tool_behat: Allow accessibility tests to run for a single Node 2025-02-20 22:28:03 +08:00
Jun Pataleta cca044ce15 Merge branch 'MDL-84448-404' of https://github.com/lameze/moodle into MOODLE_404_STABLE 2025-02-20 12:10:04 +08:00
Simey Lameze a780d14710 MDL-84448 behat: return null if theme has no config 2025-02-20 12:00:42 +08:00
Amanda Doughty 3af76654fd MDL-84448 lib: Fixed behat context check for grandchild themes 2025-02-20 12:00:42 +08:00
Jun Pataleta de8176c553 Merge branch 'MDL-84592-404' of https://github.com/andrewnicols/moodle into MOODLE_404_STABLE 2025-02-20 11:45:39 +08:00
Huong Nguyen c6e0071808 Merge branch 'MDL-84528-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-02-20 10:16:54 +07:00
Jun Pataleta 04b0132b3a Merge branch 'MDL-84590-404' of https://github.com/andrewnicols/moodle into MOODLE_404_STABLE 2025-02-20 10:49:58 +08:00
Andrew Nicols 6e3cb19969 MDL-84592 tool_behat: Improve accessibility failure information 2025-02-20 10:26:30 +08:00
Andrew Nicols 8ca7f282bc MDL-84590 tool_behat: Axe tests must use all wcag rules
Rules are not additive. That is to say that the wcag22a tag does not
include wcag21a or wcag2a. They all need to be specified.
2025-02-20 09:59:48 +08:00
Stefan Hanauska 33e58ab61a MDL-84584 badges: Don't delete all draft files 2025-02-19 12:16:26 +01:00
Sara Arjona 1d40ca9de8 Merge branch 'MDL-83784_404' of https://github.com/cwarwicker/moodle into MOODLE_404_STABLE 2025-02-19 12:00:29 +01:00
Huong Nguyen c0f24543fc MDL-84341 qbank_history: Behat cleanup 2025-02-18 20:41:00 +07:00
Huong Nguyen fe0c832dcd Merge branch 'MDL-84341_404' of https://github.com/cwarwicker/moodle into MOODLE_404_STABLE 2025-02-18 20:40:42 +07:00
Sara Arjona b19296b629 Merge branch '84348-hide-h5p-preview-message-404' of https://github.com/DSI-Universite-Rennes2/moodle into MOODLE_404_STABLE 2025-02-18 13:12:19 +01:00
Andrew Nicols c98be745f5 MDL-84446 core: Check existence of pcntl_signal 2025-02-18 06:20:51 +08:00
Conn Warwicker 6a34598550 MDL-84341 qbank_history: Fix version ordering
Previously the version ordering was done on timecreated which
wasn't always correct. This now uses the version number instead.

Also set the default sorting on the history page to be version.

Also backports a change to the behat question generator to let
us set the value of fields on updated questions in the step.
2025-02-17 11:46:48 +00:00
Leon Stringer ff4f2c02ef MDL-83195 mod_assign: Individual grade release
Fix pushing individual grades into the gradebook (as opposed to the
batch release of grades) when "Allow partial release of grades while
marking anonymously" is enabled.

This change reworks MDL-73626's fix for the same issue for releasing
multiple grades in a batch operation.
2025-02-14 09:28:21 +00:00
Jun Pataleta 9508120293 weekly release 4.4.6+ 2025-02-14 09:53:52 +08:00
Jun Pataleta 0891666ec7 Merge branch 'install_404_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_404_STABLE 2025-02-14 09:53:49 +08:00
Conn Warwicker 17c3e8ecc8 MDL-83784 mod_quiz: Fix quiz unusable if question category missing.
Random questions set to question categories which no longer exist
was throwing an error making it impossible to use or edit the quiz
to fix it. This will now allow the user to view the questions and
edit the quiz in order to fix the problem of the missing category.
2025-02-13 15:48:22 +00:00
Paul Holden c6a31786f7 MDL-84528 tag: define missing language string for related tags area. 2025-02-13 15:04:01 +00:00
Pedro Jordao eb0342fa4f MDL-80351 libraries: ChartJS axis options being ignored
When chartJs was updated to version 3.8.0, chart axis were now accessed
as a object property instead of an array element. This solution aims to
update the way we set the axis properties, aiming to display the labels
and other properties.
2025-02-13 02:25:49 -03:00
Huong Nguyen da47f4dafb Merge branch 'MDL-83579-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-02-13 11:11:47 +07:00
Jun Pataleta 8cc95faa8a Merge branch 'mdl-83306_404' of https://github.com/james-cnz/moodle into MOODLE_404_STABLE 2025-02-13 11:54:59 +08:00
Jun Pataleta d5ba48ab32 Merge branch 'MDL-84102-404' of https://github.com/andelacruz/moodle into MOODLE_404_STABLE 2025-02-13 11:48:59 +08:00
Huong Nguyen d7c719f597 Merge branch 'MDL-83645-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-02-13 10:43:09 +07:00
Huong Nguyen 1145b31059 Merge branch 'MDL-69142-404' of https://github.com/paulholden/moodle into MOODLE_404_STABLE 2025-02-13 10:33:46 +07:00
Huong Nguyen 90ebc61746 Merge branch 'MDL-79265-404' of https://github.com/NashTechOpenUniversity/moodle into MOODLE_404_STABLE 2025-02-13 09:15:54 +07:00
Simmo Saan 650efc4ff7 MDL-65233 course: remove usergetmidnight from reset time shift 2025-02-12 21:56:35 +02:00
Simmo Saan 51f425ec29 MDL-65233 course: add reset time shift test with non-midnight hour 2025-02-12 21:56:35 +02:00
Angelia Dela Cruz bec6456309 MDL-84102 message: Behat for setting default notification preferences 2025-02-12 15:35:21 +08:00
Huong Nguyen 3a9fd31020 Merge branch 'MDL-83452-addbodyclass-404' of https://github.com/leonstr/moodle into MOODLE_404_STABLE 2025-02-12 11:31:13 +07:00
Huong Nguyen f7f8b14c3e Merge branch 'MOODLE_404_MDL-82601' of https://github.com/danielneis/moodle into MOODLE_404_STABLE 2025-02-11 11:46:45 +07:00
Huong Nguyen 298a3f5107 Merge branch 'MDL-82440-MOODLE_404_STABLE' of https://github.com/praxisdigital/moodle into MOODLE_404_STABLE 2025-02-11 11:39:36 +07:00
Huong Nguyen 70efbd07af Merge branch 'MDL-84414-404' of https://github.com/andelacruz/moodle into MOODLE_404_STABLE 2025-02-11 11:17:44 +07:00
Angelia Dela Cruz 5b90cc9653 MDL-84414 gradingform_guide: Behat test to set marking guide as ready 2025-02-11 11:02:45 +08:00
AMOS bot b0423acc58 Automatically generated installer lang files 2025-02-11 00:09:19 +00:00
Paul Holden c275ca43c9 MDL-69142 tool_cohortroles: fix table ordering by role name. 2025-02-10 12:34:09 +00:00
Sara Arjona 746a17f9b0 Merge branch 'MDL-67462-404' of https://github.com/albertgasset/moodle into MOODLE_404_STABLE 2025-02-10 09:38:49 +01:00
Katie Ransom d4f2a8b417 MDL-84419 core_task: Scheduled task timing is wrong when DST begins
This was only an issue for tasks set to run in the hour when the clocks
change for daylight saving time. E.g. if a task is set to run at 1am
every day in the Europe/London timezone, then the day the clocks go
forward gets skipped because 1am doesn't exist on that day.
2025-02-07 09:32:58 +00:00
Albert Gasset eb18a07be2 MDL-67462 badges: Improve tests of external functions 2025-02-07 09:13:05 +01:00
Albert Gasset e0930fce1e MDL-67462 badges: Fix badge alignments returned in external functions 2025-02-07 09:13:05 +01:00
Hieu Nguyen Dang 488a48e5f1 MDL-79265 question bank: fix which questions are listed with hidden
This fixes the bug where if the latest version of the question is hidden,
the question disappears, even if older versions exist that are still visible.
2025-02-06 17:21:15 +07:00
Paul Holden ddf3665d9c MDL-82170 user: replace increment operator on string type variable. 2025-02-05 20:10:33 +00:00
Andrew Nicols cfef02a97c MDL-84421 tool_uploadcourse: Stop MUC caching enrol instances
We should not normally cache instances of a class. This can lead to poor
design to work around the limitations of PHP Serialization, and could
theoretically cause issues with PHP versions changing within a Moodle
version.
2025-02-04 22:14:19 +08:00
Julien Boulen 0f75ce3ebf MDL-84348 h5pactivity: Hide preview mode warning for guest users 2025-01-28 12:00:05 +01:00
Paul Holden 0df63ca7da MDL-83720 tag: correct description formatting component/filearea. 2025-01-24 12:52:22 +00:00
Dani Palou 1a3e762198 MDL-84313 pluginfile: Allow fetching by chunks in cross-site requests
Without these headers, the range headers are blocked so only the first chunk of the file can be retrieved.
2025-01-23 15:54:18 +01:00
Paul Holden 5b8a7b7d73 MDL-83579 tiny_media: pass filepicker accepted types to dropzone. 2025-01-16 17:37:59 +00:00
Daniel Neis Araujo 402ea0f8d2 MDL-82601 scorm: score not being stored when force complete is used 2025-01-13 09:56:54 -03:00
Paul Holden 56a109d2c3 MDL-83645 grade: fix text formatting in external grading form method. 2025-01-09 14:52:08 +00:00
Lars Bonczek 0e9b826517 MDL-81655 theme: Fix maintenance warning positioning 2024-12-17 13:07:13 +01:00
Frederik Milling Pytlick 0d8b41f727 MDL-82440 assignfeedback_editpdf: Force ghostscript pdf's to pdf v1.4 2024-12-09 15:46:28 +01:00
James C ee87a1bcac MDL-83306 book: Book section return fix 2024-12-09 10:18:08 +13:00
Leon Stringer 9567305f70 MDL-83452 mod_assign: Remove add_body_class
Remove extraneous $PAGE->add_body_class('limitedwidth'), unneeded since
MDL-74031 added a call to this.
2024-10-14 08:44:14 +01:00
1154 changed files with 15201 additions and 4565 deletions
+3 -3
View File
@@ -19,7 +19,7 @@ env:
jobs:
Grunt:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- name: Checking out code
@@ -61,12 +61,12 @@ jobs:
matrix:
include:
# MySQL builds always run with the lowest PHP supported version.
- os: ubuntu-22.04
- os: ubuntu-24.04
php: 8.1
extensions:
db: mysqli
# PostgreSQL builds always run with the highest PHP supported version.
- os: ubuntu-22.04
- os: ubuntu-24.04
php: 8.3
db: pgsql
@@ -0,0 +1,5 @@
issueNumber: MDL-87079
notes:
core:
- message: When responding to pcntl signals, call existing signal handlers.
type: fixed
+1
View File
@@ -24,6 +24,7 @@
*/
define('CLI_SCRIPT', true);
define('IGNORE_COMPONENT_CACHE', true);
require(__DIR__.'/../../config.php');
require_once($CFG->libdir.'/clilib.php');
+40 -17
View File
@@ -55,13 +55,14 @@ If courseid is set, course module/s will be added into the course.
Options:
-f, --file=STRING Path to the backup file.
-c, --categoryid=INT ID of the course category to restore to.
-C, --courseid=INT ID of the course to restore to. This option is ignored if categoryid is set.
-c, --categoryid=INT ID of the course category to restore to. This option is ignored when restoring an activity and courseid is set.
-C, --courseid=INT ID of the course to restore to. This option is ignored when restoring a course and the categoryid is set.
-s, --showdebugging Show developer level debugging information
-h, --help Print out this help.
Example:
\$sudo -u www-data /usr/bin/php admin/cli/restore_backup.php --file=/path/to/backup/file.mbz --categoryid=1\n
\$sudo -u www-data /usr/bin/php admin/cli/restore_backup.php --file=/path/to/backup/coursebackup.mbz --categoryid=1\n
\$sudo -u www-data /usr/bin/php admin/cli/restore_backup.php --file=/path/to/backup/activitybackup.mbz --courseid=1\n
EOL;
echo $help;
@@ -84,11 +85,15 @@ if ($options['categoryid']) {
if (!$category = $DB->get_record('course_categories', ['id' => $options['categoryid']], 'id')) {
throw new \moodle_exception('invalidcategoryid');
}
} else if ($options['courseid']) {
}
if ($options['courseid']) {
if (!$course = $DB->get_record('course', ['id' => $options['courseid']], 'id')) {
throw new \moodle_exception('invalidcourseid');
}
} else {
}
if (empty($category) && empty($course)) {
throw new \moodle_exception('invalidoption');
}
@@ -100,28 +105,46 @@ $fp = get_file_packer('application/vnd.moodle.backup');
$fp->extract_to_pathname($options['file'], $path);
cli_heading(get_string('preprocessingbackupfile'));
try {
list($fullname, $shortname) = restore_dbops::calculate_course_names(0, get_string('restoringcourse', 'backup'),
get_string('restoringcourseshortname', 'backup'));
if (!empty($course)) {
$courseid = $course->id;
$rc = new restore_controller($backupdir, $courseid, backup::INTERACTIVE_NO,
backup::MODE_GENERAL, $admin->id, backup::TARGET_EXISTING_ADDING);
} else {
try {
// Create a temporary restore controller to determine the restore type.
$tmprc = new restore_controller($backupdir, SITEID, backup::INTERACTIVE_NO,
backup::MODE_GENERAL, $admin->id, backup::TARGET_EXISTING_ADDING);
// Restore the backup into a new course if:
// - It is a course backup and the category is set.
// - It is an activity backup and the course is not set.
$restoreasnewcourse = ($tmprc->get_type() === backup::TYPE_1COURSE && !empty($category)) ||
($tmprc->get_type() !== backup::TYPE_1COURSE && empty($course));
// Make sure to clean up the temporary restore controller.
$tmprc->destroy();
if ($restoreasnewcourse) {
list($fullname, $shortname) = restore_dbops::calculate_course_names(0, get_string('restoringcourse', 'backup'),
get_string('restoringcourseshortname', 'backup'));
$courseid = restore_dbops::create_new_course($fullname, $shortname, $category->id);
$rc = new restore_controller($backupdir, $courseid, backup::INTERACTIVE_NO,
backup::MODE_GENERAL, $admin->id, backup::TARGET_NEW_COURSE);
} else {
$courseid = $course->id;
$rc = new restore_controller($backupdir, $courseid, backup::INTERACTIVE_NO,
backup::MODE_GENERAL, $admin->id, backup::TARGET_EXISTING_ADDING);
}
$rc->execute_precheck();
$rc->execute_plan();
$rc->destroy();
// Rename course name if the backup is from course module and restore to category.
if (empty($course)) {
// Rename the course's full and short names with the backup file's original names if the backup file is an activity backup
// that is restored to a new course.
if ($restoreasnewcourse && $rc->get_type() !== backup::TYPE_1COURSE) {
$course = get_course($courseid);
list($fullname, $shortname) = restore_dbops::calculate_course_names(0, get_string('restoretonewcourse', 'backup'),
get_string('newcourse'));
$backupinfo = $rc->get_info();
$tmpfullname = $backupinfo->original_course_fullname ?? get_string('restoretonewcourse', 'backup');
$tmpshortname = $backupinfo->original_course_shortname ?? get_string('newcourse');
list($fullname, $shortname) = restore_dbops::calculate_course_names(
courseid: 0,
fullname: $tmpfullname,
shortname: $tmpshortname,
);
$course->fullname = $fullname;
$course->shortname = $shortname;
$course->visible = 1;
+414
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">
@@ -4327,6 +4328,7 @@
<VENDOR name="oracle" version="19" />
</DATABASE>
<PHP version="8.1.0" level="required">
<RESTRICT function="restrict_php_version_84" message="unsupportedphpversion84" />
</PHP>
<PCREUNICODE level="optional">
<FEEDBACK>
@@ -4410,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">
@@ -4522,6 +4525,7 @@
<VENDOR name="oracle" version="19" />
</DATABASE>
<PHP version="8.1.0" level="required">
<RESTRICT function="restrict_php_version_84" message="unsupportedphpversion84" />
</PHP>
<PCREUNICODE level="optional">
<FEEDBACK>
@@ -4605,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">
@@ -4714,6 +4719,7 @@
<VENDOR name="mysql" version="8.4" />
<VENDOR name="postgres" version="14" />
<VENDOR name="mssql" version="14.0" />
<VENDOR name="auroramysql" version="8.0" />
</DATABASE>
<PHP version="8.2.0" level="required">
</PHP>
@@ -4799,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">
@@ -4893,6 +4900,413 @@
</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>
<MOODLE version="5.1" requires="4.2.3">
<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="15" />
<VENDOR name="mssql" version="14.0" />
<VENDOR name="auroramysql" version="8.0" />
</DATABASE>
<PHP version="8.2.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 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>
<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);
+2
View File
@@ -23,6 +23,8 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('IGNORE_COMPONENT_CACHE', true);
require_once('../config.php');
require_once($CFG->libdir.'/adminlib.php');
+2 -1
View File
@@ -2180,10 +2180,11 @@ class core_admin_renderer extends plugin_renderer_base {
$output = '';
$output .= $this->header();
$output .= $this->container_start('upgradekeyreq');
$output .= $this->heading(get_string('upgradekeyreq', 'core_admin'));
$output .= $this->container_start('upgradekeyreq w-25');
$output .= html_writer::start_tag('form', array('method' => 'POST', 'action' => $url));
$output .= html_writer::empty_tag('input', [
'id' => 'upgradekey',
'name' => 'upgradekey',
'type' => 'password',
'class' => 'form-control w-auto',
+8
View File
@@ -51,6 +51,14 @@ if ($hassiteconfig) {
$temp->add(new admin_setting_countrycodes('allcountrycodes', new lang_string('allcountrycodes', 'core_admin'),
new lang_string('configallcountrycodes', 'core_admin')));
$temp->add(new admin_setting_configtext(
'geopluginapikey',
new lang_string('geopluginapikey', 'core_admin'),
new lang_string('geopluginapikey_desc', 'core_admin'),
'',
PARAM_TEXT,
));
}
$ADMIN->add('location', $temp);
+2
View File
@@ -81,6 +81,8 @@ class behat_admin extends behat_base {
$this->execute('behat_forms::i_set_the_field_with_xpath_to', [$fieldxpath, $value]);
$this->execute("behat_general::i_click_on", [get_string('savechanges'), 'button']);
// Wait for the page to be redirected.
$this->execute("behat_general::i_wait_to_be_redirected");
}
}
+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"
@@ -18,6 +18,7 @@ Feature: Verify the breadcrumbs in webservice tokens site administration pages
And "Manage tokens" "link" should exist in the ".breadcrumb" "css_element"
And "Web services" "link" should exist in the ".breadcrumb" "css_element"
And I set the field "User" to "John Doe"
And I set the field "Service" to "Moodle mobile web service"
And I press "Save changes"
When I click on "Delete" "link"
Then "Delete token" "text" should exist in the ".breadcrumb" "css_element"
@@ -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,79 @@
@tool @tool_analytics
Feature: Manager can obtain prediction models insights
In order to view prediction models insights
As a manager
I should be able to obtain the prediction models insights
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| s1 | Student | One | s1@example.com |
| m1 | Manager | One | m1@example.com |
| m2 | Manager | Two | m2@example.com |
| t1 | Teacher | One | t1@example.com |
And the following "role assigns" exist:
| user | role | contextlevel | reference |
| m1 | manager | System | |
| m2 | manager | System | |
# To ensure that course start date is in the future, set course start date to "tomorrow".
And the following "courses" exist:
| fullname | shortname | startdate |
| Course 1 | C1 | ##tomorrow## |
| Course 2 | C2 | ##tomorrow## |
| Course 3 | C3 | ##tomorrow## |
And the following "course enrolments" exist:
| user | course | role |
| s1 | C1 | student |
| s1 | C2 | student |
| s1 | C3 | student |
| t1 | C3 | editingteacher |
# Disable "Analytics processes execution via command line only".
And the following config values are set as admin:
| onlycli | 0 | analytics |
@javascript
Scenario: Manager can obtain prediction models insights
Given I log in as "m1"
And I navigate to "Analytics > Analytics models" in site administration
And I click on "Actions" "link" in the "Courses at risk of not starting" "table_row"
And I click on "Execute scheduled analysis" "link" in the "Courses at risk of not starting" "table_row"
And I press "Continue"
When I select "All predictions" from the "contextid" singleselect
# Confirm that only courses without teachers, Course 1 and Course 2, are listed.
Then "Course 1" "text" should exist
And "Course 2" "text" should exist
And "Course 3" "text" should not exist
And I click on "Select Course 1 for bulk action" "checkbox"
And I press "Not applicable"
And I press "Confirm"
# After m1 marks Course 1 as Not applicable, only Course 2 should be listed in the predictions for manager 1.
And "Course 1" "text" should not exist
And "Course 2" "text" should exist
And I log in as "m2"
And I click on ".popover-region-notifications" "css_element"
# Notification of new insight exists.
And "Upcoming courses have no teachers or students" "text" should exist
And I click on "View full notification" "link" in the ".popover-region-notifications" "css_element"
And I click on "View insight" "link"
# Course 1, previously marked as Not applicable by manager 1, is still listed for manager 2.
And "Course 1" "text" should exist
And I click on "View details" "link" in the "Course 1" "table_row"
# Prediction details and indicators are displayed for the selected course.
And the following should exist in the "insights-list" table:
| Description |
| Course 1 |
And the following should exist in the "prediction-calculations" table:
| -1- | -2- |
| Teacher availability | No |
| Student enrolments | Yes |
# Date of prediction analysis execution.
And "##today##%A, %d %B %Y##" "text" should exist in the "Time predicted" "table_row"
And I click on "Select Course 1 for bulk action" "checkbox"
And I press "Accept"
And I press "Confirm"
# Verify that you end up on the page listing predictions for 'No teaching'.
And "Courses at risk of not starting" "text" should exist
And "The following courses due to start in the upcoming days are at risk of not starting because they don't have teachers or students enrolled." "text" should exist
And the following should not exist in the "insights-list" table:
| Description |
| Course 1 |
+4
View File
@@ -36,6 +36,10 @@ if (function_exists('opcache_reset')) {
define('CLI_SCRIPT', true);
define('CACHE_DISABLE_ALL', true);
// It makes no sense to use BEHAT_CLI for this script (the Behat launch scripts expect to start
// from the normal environment), so in case user has set tne environment variable, disable it.
putenv('BEHAT_CLI=0');
// Basic functions.
require_once(__DIR__ . '/../../../../lib/clilib.php');
require_once(__DIR__ . '/../../../../lib/behat/lib.php');
+4
View File
@@ -31,6 +31,10 @@ define('ABORT_AFTER_CONFIG', true);
define('CACHE_DISABLE_ALL', true);
define('NO_OUTPUT_BUFFERING', true);
// It makes no sense to use BEHAT_CLI for this script (the Behat launch scripts expect to start
// from the normal environment), so in case user has set tne environment variable, disable it.
putenv('BEHAT_CLI=0');
require_once(__DIR__ .'/../../../../config.php');
require_once(__DIR__.'/../../../../lib/clilib.php');
require_once(__DIR__.'/../../../../lib/behat/lib.php');
+4
View File
@@ -39,6 +39,10 @@ define('NO_OUTPUT_BUFFERING', true);
define('IGNORE_COMPONENT_CACHE', true);
define('ABORT_AFTER_CONFIG', true);
// It makes no sense to use BEHAT_CLI for this script (the Behat launch scripts expect to start
// from the normal environment), so in case user has set tne environment variable, disable it.
putenv('BEHAT_CLI=0');
require_once(__DIR__ . '/../../../../lib/clilib.php');
// CLI options.
+4
View File
@@ -33,6 +33,10 @@ if (isset($_SERVER['REMOTE_ADDR'])) {
die(); // No access from web!.
}
// It makes no sense to use BEHAT_CLI for this script (the Behat launch scripts expect to start
// from the normal environment), so in case user has set tne environment variable, disable it.
putenv('BEHAT_CLI=0');
// Basic functions.
require_once(__DIR__ . '/../../../../lib/clilib.php');
require_once(__DIR__ . '/../../../../lib/behat/lib.php');
@@ -30,10 +30,6 @@ use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_test;
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class img_alt_is_too_long extends brickfield_accessibility_test {
/** @var int The default severity code for this test. */
public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SEVERE;
/**
* The main check function. This is called by the parent class to actually check content.
*/
@@ -41,7 +37,7 @@ class img_alt_is_too_long extends brickfield_accessibility_test {
global $alttextlengthlimit;
foreach ($this->get_all_elements('img') as $img) {
$alttextlengthlimit = 125;
$alttextlengthlimit = 750;
if ($img->hasAttribute('alt') && core_text::strlen($img->getAttribute('alt')) > $alttextlengthlimit) {
$this->add_report($img);
}
@@ -402,6 +402,8 @@ class manager {
}
}
$rs->close();
if (count($batchinserts) > 0) {
$DB->insert_records(self::DB_PROCESS, $batchinserts);
}
+1
View File
@@ -87,6 +87,7 @@ $action = optional_param('action', '', PARAM_ALPHA);
// Handle any single operation actions.
if ($action == 'requestanalysis') {
if ($courseid != 0) {
require_sesskey();
scheduler::request_course_analysis($courseid);
if ($courseid == SITEID) {
redirect(accessibility::get_plugin_url());
@@ -179,7 +179,7 @@ $string['checkdesc:headerh3'] = 'Headers following after H3 headers (the editor
$string['checkdesc:headershavetext'] = 'A header needs to contain text to be perceivable.';
$string['checkdesc:iisnotused'] = 'Italic (i) elements should not be used; "em" should be used instead.';
$string['checkdesc:imgaltisdifferent'] = 'Image alt (alternative) text should not be the image filename.';
$string['checkdesc:imgaltistoolong'] = 'Image alt (alternative) text should not be more than the maximum allowed (125) characters.';
$string['checkdesc:imgaltistoolong'] = 'Ensure that the image alt (alternative) text is concise enough to describe the image.';
$string['checkdesc:imgaltnotemptyinanchor'] = 'Image alt (alternative) text should not be empty, especially when the image has a link going elsewhere.';
$string['checkdesc:imgaltnotplaceholder'] = 'Image alt (alternative) text should not be a simple placeholder text, such as "image".';
$string['checkdesc:imghasalt'] = 'Image alt (alternative) text should not be missing for image elements, unless purely decorative with no meaning.';
@@ -24,6 +24,8 @@
namespace tool_brickfield\local\htmlchecker\common\checks;
use tool_brickfield\local\htmlchecker\brickfield_accessibility;
defined('MOODLE_INTERNAL') || die();
require_once('all_checks.php');
@@ -64,21 +66,21 @@ EOD;
*/
public static function img_alt_text_provider(): array {
return [
'Alt text <= 125 characters' => [
'Alt text <= 750 characters' => [
true,
str_repeat("Hello world!", 10),
str_repeat("Hello world!", 60),
],
'Alt text > 125 characters' => [
'Alt text > 750 characters' => [
false,
str_repeat("Hello world!", 25),
str_repeat("Hello world!", 65),
],
'Multi-byte alt text <= 125 characters' => [
'Multi-byte alt text <= 750 characters' => [
true,
str_repeat('こんにちは、世界!', 13),
str_repeat('こんにちは、世界!', 83),
],
'Multi-byte alt text > 125 characters' => [
'Multi-byte alt text > 750 characters' => [
false,
str_repeat('こんにちは、世界!', 30),
str_repeat('こんにちは、世界!', 90),
],
];
}
@@ -96,7 +98,18 @@ EOD;
if ($expectedpass) {
$this->assertEmpty($results);
} else {
$this->assertTrue($results[0]->element->tagName === 'img');
$this->assertEquals('img', $results[0]->element->tagName);
}
}
/**
* Test the severity of the {@see img_alt_is_too_long} check.
*
* @return void
*/
public function test_severity(): void {
$html = $this->get_test_html('Some alt text');
$checker = new brickfield_accessibility($html, 'brickfield', 'string');
$this->assertEquals(brickfield_accessibility::BA_TEST_SUGGESTION, $checker->get_test_severity(img_alt_is_too_long::class));
}
}
@@ -0,0 +1,70 @@
<?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/>.
namespace tool_capability;
// phpcs:disable moodle.PHPUnit.TestCaseProvider.dataProviderSyntaxMethodNotFound
/**
* Detect common problems in capability definitions of plugins.
*
* @group plugin_checks
* @package tool_capability
* @copyright 2025 Petr Skoda
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class plugin_checks_test extends \core\tests\plugin_checks_testcase {
/**
* Verify contents of plugin db/access.php file.
*
* @dataProvider all_plugins_provider
* @coversNothing
*
* @param string $component
* @param string $plugintype
* @param string $pluginname
* @param string $dir
*/
public function test_db_access_file(string $component, string $plugintype, string $pluginname, string $dir): void {
global $CFG;
$stringmanager = get_string_manager();
$corerolefile = "$CFG->dirroot/lang/en/role.php";
$langfile = "$dir/lang/en/$component.php";
$file = "$dir/db/access.php";
$capabilities = $this->fetch_array_from_file($file, 'capabilities');
if (!$capabilities) {
$this->expectNotToPerformAssertions();
return;
}
foreach ($capabilities as $capname => $capability) {
if ($plugintype === 'qbank' && str_starts_with($capname, 'moodle/question:')) {
// Question bank capabilities are irregular.
$strname = explode('/', $capname, 2)[1];
$this->assertTrue($stringmanager->string_exists($strname, 'core_role'),
"Missing capability name string '$strname' in $corerolefile");
continue;
}
$this->assertMatchesRegularExpression("|^$plugintype/$pluginname:[a-z0-9_]+$|", $capname);
$strname = substr($capname, strlen($plugintype) + 1);
$this->assertTrue($stringmanager->string_exists($strname, $component),
"Missing capability name string '$strname' in $langfile");
$this->assertSame($capname, clean_param($capname, PARAM_CAPABILITY));
}
}
}
@@ -175,7 +175,7 @@ class cohort_role_assignments_table extends table_sql {
* @return array containing sql to use and an array of params.
*/
protected function get_sql_and_params($count = false) {
$fields = 'uca.id, uca.cohortid, uca.userid, uca.roleid, ';
$fields = 'uca.id, uca.cohortid, uca.userid, uca.roleid, r.shortname AS rolename, ';
$fields .= 'c.name as cohortname, c.idnumber as cohortidnumber, c.contextid as cohortcontextid, ';
$fields .= 'c.visible as cohortvisible, c.description as cohortdescription, c.theme as cohorttheme';
@@ -193,6 +193,7 @@ class cohort_role_assignments_table extends table_sql {
$sql = "SELECT $select
FROM {tool_cohortroles} uca
JOIN {user} u ON u.id = uca.userid
JOIN {role} r ON r.id = uca.roleid
JOIN {cohort} c ON c.id = uca.cohortid";
// Check if any additional filtering is required.
@@ -123,6 +123,7 @@ class expired_contexts_manager {
$expiredcontext = new expired_context(0, $orphan);
$expiredcontext->delete();
}
$orphaned->close();
// Delete any child of a user context.
$parentpath = $DB->sql_concat('ctxuser.path', "'/%'");
@@ -143,6 +144,7 @@ class expired_contexts_manager {
$expiredcontext = new expired_context(0, $child);
$expiredcontext->delete();
}
$userchildren->close();
}
/**
@@ -127,7 +127,6 @@ $string['dporolemapping'] = 'Privacy officer role mapping';
$string['dporolemapping_desc'] = 'The privacy officer can manage data requests. The capability tool/dataprivacy:managedatarequests must be allowed for a role to be listed as a privacy officer role mapping option.';
$string['editcategories'] = 'Edit categories';
$string['editcategory'] = 'Edit category';
$string['editcategories'] = 'Edit categories';
$string['editdefaults'] = 'Edit defaults: {$a}';
$string['editmoduledefaults'] = 'Edit module defaults';
$string['editpurpose'] = 'Edit purpose';
@@ -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"
@@ -4,13 +4,13 @@ Feature: See the competencies for an activity on the course competencies page.
In order to see only the competencies for an activity in the course competencies page.
Background:
Given the following lp "frameworks" exist:
| shortname | idnumber |
| Test-Framework | ID-FW1 |
And the following lp "competencies" exist:
| shortname | framework |
| Test-Comp1 | ID-FW1 |
| Test-Comp2 | ID-FW1 |
Given the following "core_competency > frameworks" exist:
| shortname | idnumber |
| Test-Framework | ID-FW1 |
And the following "core_competency > competencies" exist:
| shortname | competencyframework |
| Test-Comp1 | ID-FW1 |
| Test-Comp2 | ID-FW1 |
Given the following "courses" exist:
| shortname | fullname | enablecompletion |
| C1 | Course 1 | 1 |
@@ -46,11 +46,11 @@ Feature: See the competencies for an activity on the course competencies page.
When I follow "Competencies"
Then I should see "Test-Comp1"
And I should see "Test-Comp2"
And I set the field "Filter competencies by resource or activity" to "PageName1"
And I set the competency filter "Filter competencies by resource or activity" to "PageName1"
And I press the enter key
And I should see "Test-Comp1"
And I should not see "Test-Comp2"
And I set the field "Filter competencies by resource or activity" to "PageName2"
And I set the competency filter "Filter competencies by resource or activity" to "PageName2"
And I press the enter key
And I should not see "Test-Comp1"
And I should not see "Test-Comp2"
@@ -29,18 +29,18 @@ Feature: Manage competency frameworks
And I should see "Science Year-1"
Scenario: Read a framework
Given the following lp "frameworks" exist:
| shortname | idnumber |
| Science Year-2 | sc-y-2 |
Given the following "core_competency > frameworks" exist:
| shortname | idnumber |
| Science Year-2 | sc-y-2 |
And I navigate to "Competencies > Competency frameworks" in site administration
And I should see "Science Year-2"
When I click on "Science Year-2" "link"
Then I should see "Science Year-2"
Scenario: Edit a framework
Given the following lp "frameworks" exist:
| shortname | idnumber |
| Science Year-3 | sc-y-3 |
Given the following "core_competency > frameworks" exist:
| shortname | idnumber |
| Science Year-3 | sc-y-3 |
And I navigate to "Competencies > Competency frameworks" in site administration
And I should see "Science Year-3"
And I click on "Edit" of edit menu in the "Science Year-3" row
@@ -52,9 +52,9 @@ Feature: Manage competency frameworks
And I should see "sc-y-3"
Scenario: Delete a framework
Given the following lp "frameworks" exist:
| shortname | idnumber |
| Science Year-4 | sc-y-4 |
Given the following "core_competency > frameworks" exist:
| shortname | idnumber |
| Science Year-4 | sc-y-4 |
And I navigate to "Competencies > Competency frameworks" in site administration
And I should see "Science Year-4"
And I should see "sc-y-4"
@@ -70,24 +70,24 @@ Feature: Manage competency frameworks
And I should not see "sc-y-4"
Scenario: Edit a framework with competencies in user competency
Given the following lp "frameworks" exist:
| shortname | idnumber |
| Science Year-5 | sc-y-5 |
And the following lp "competencies" exist:
| shortname | framework |
| Comp1 | sc-y-5 |
| Comp2 | sc-y-5 |
And the following lp "plans" exist:
| name | user | description |
Given the following "core_competency > frameworks" exist:
| shortname | idnumber |
| Science Year-5 | sc-y-5 |
And the following "core_competency > competencies" exist:
| shortname | competencyframework | idnumber |
| Comp1 | sc-y-5 | Comp1 |
| Comp2 | sc-y-5 | Comp2 |
And the following "core_competency > plans" exist:
| name | user | description |
| Plan Science-5 | admin | Plan description |
And the following lp "plancompetencies" exist:
| plan | competency |
| Plan Science-5 | Comp1 |
| Plan Science-5 | Comp2 |
And the following lp "usercompetencies" exist:
| user | competency |
| admin | Comp1 |
| admin | Comp2 |
And the following "core_competency > plan_competency" exist:
| plan | competency |
| Plan Science-5 | Comp1 |
| Plan Science-5 | Comp2 |
And the following "core_competency > user_competency" exist:
| user | competency |
| admin | Comp1 |
| admin | Comp2 |
And I navigate to "Competencies > Competency frameworks" in site administration
And I should see "Science Year-5"
And I click on "Edit" of edit menu in the "Science Year-5" row
@@ -100,24 +100,24 @@ Feature: Manage competency frameworks
And I should see "sc-y-5"
Scenario: Edit a framework with competencies in user competency plan
Given the following lp "frameworks" exist:
| shortname | idnumber |
| Science Year-6 | sc-y-6 |
And the following lp "competencies" exist:
| shortname | framework |
| Comp1 | sc-y-6 |
| Comp2 | sc-y-6 |
And the following lp "plans" exist:
| name | user | description |
Given the following "core_competency > frameworks" exist:
| shortname | idnumber |
| Science Year-6 | sc-y-6 |
And the following "core_competency > competencies" exist:
| shortname | competencyframework | idnumber |
| Comp1 | sc-y-6 | Comp1 |
| Comp2 | sc-y-6 | Comp2 |
And the following "core_competency > plans" exist:
| name | user | description |
| Plan Science-6 | admin | Plan description |
And the following lp "plancompetencies" exist:
| plan | competency |
| Plan Science-6 | Comp1 |
| Plan Science-6 | Comp2 |
And the following lp "usercompetencyplans" exist:
| user | competency | plan |
| admin | Comp1 | Plan Science-6 |
| admin | Comp2 | Plan Science-6 |
And the following "core_competency > plan_competency" exist:
| plan | competency |
| Plan Science-6 | Comp1 |
| Plan Science-6 | Comp2 |
And the following "core_competency > user_competency_plans" exist:
| user | competency | plan |
| admin | Comp1 | Plan Science-6 |
| admin | Comp2 | Plan Science-6 |
And I navigate to "Competencies > Competency frameworks" in site administration
And I should see "Science Year-6"
And I click on "Edit" of edit menu in the "Science Year-6" row
@@ -130,12 +130,12 @@ Feature: Manage competency frameworks
And I should see "sc-y-6"
Scenario: Duplicate a competency framework
Given the following lp "frameworks" exist:
Given the following "core_competency > frameworks" exist:
| shortname | idnumber |
| CF1 | CF1 |
And the following lp "competencies" exist:
| shortname | framework |
| C1 | CF1 |
And the following "core_competency > competencies" exist:
| shortname | competencyframework |
| C1 | CF1 |
And I navigate to "Competencies > Competency frameworks" in site administration
# Duplicate the selected competency framework
When I click on "Duplicate" of edit menu in the "CF1" row
@@ -0,0 +1,69 @@
@tool @tool_lp @tool_lp_plan
Feature: Admin can create learning plans from template
In order to link and unlink a learning plan from a student
As an admin
I need to be able to create a learning plan from template
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | One | student1@email.com |
And the following "core_competency > frameworks" exist:
| shortname | idnumber |
| CF1 | CF1 |
| CF2 | CF2 |
And the following "core_competency > competencies" exist:
| shortname | competencyframework | idnumber |
| CF1 Competency 1 | CF1 | CF1C1 |
| CF2 Competency 1 | CF2 | CF2C1 |
And the following "core_competency > templates" exist:
| shortname | description |
| LPT1 | LPT 1 description |
And the following "core_competency > template_competencies" exist:
| template | competency |
| LPT1 | CF1C1 |
@javascript
Scenario: Admin can assign competencies from different frameworks to a learning plan
Given I log in as "admin"
And I navigate to "Competencies > Learning plan templates" in site administration
And I follow "LPT1"
When I press "Add competencies to learning plan template"
# Add another competency from a different framework in the learning plan template.
And I set the field with xpath "//select[@data-action='chooseframework']" to "CF2 CF2"
And I select "CF2 Competency 1" of the competency tree
And I click on "Add" "button" in the "Competency picker" "dialogue"
# Confirm that both competencies from different frameworks are successfully added to learning plan template.
Then "LPT1" "text" should exist
And "Learning plan template competencies" "text" should exist
And "CF1 Competency 1" "text" should exist
And "CF2 Competency 1" "text" should exist
@javascript
Scenario: Admin can link and unlink learning plans created from template
Given I log in as "admin"
And I navigate to "Competencies > Learning plan templates" in site administration
# Initially, the number of learning plans is 0.
And the following should exist in the "generaltable" table:
| Name | Learning plans |
| LPT1 | 0 |
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
# Create a learning plan for selected student using template.
When I set the field "Select users" to "student1"
And I press "Create learning plans"
# Confirm that selected student is now in the list of learning plans.
Then "A learning plan was created" "text" should exist
And the following should exist in the "generaltable" table:
| Name | Email address |
| LPT1 | student1@email.com |
And I click on "LPT1" "link" in the "LPT1" "table_row"
# Template title and Unlink from learning plan template link exists.
And "Learning plan template" "text" should exist
And "Unlink from learning plan template" "link" should exist
# Unlink the template.
And I click on "Unlink from learning plan template" "link"
And I press "Unlink from learning plan template"
# Learning plan still exists but Template name and Unlink from learning plan template link no longer exist.
And "LPT1" "text" should exist
And "Learning plan template" "text" should not exist
And "Unlink from learning plan template" "link" should not exist
+26 -26
View File
@@ -22,8 +22,8 @@ Feature: Manage plearning plan
And I should see "Science plan"
Scenario: Create a learning plan based on template
Given the following lp "templates" exist:
| shortname | description |
Given the following "core_competency > templates" exist:
| shortname | description |
| Science template | science template description |
And I am on homepage
And I navigate to "Competencies > Learning plan templates" in site administration
@@ -36,8 +36,8 @@ Feature: Manage plearning plan
And I should see "Admin User" in the "Science template" "table_row"
Scenario: Create a learning plan from template cohort
Given the following lp "templates" exist:
| shortname | description |
Given the following "core_competency > templates" exist:
| shortname | description |
| Science template cohort | science template description |
And the following "users" exist:
| username | firstname | lastname | email |
@@ -62,8 +62,8 @@ Feature: Manage plearning plan
And I should see "Student 2"
Scenario: Read a learning plan
Given the following lp "plans" exist:
| name | user | description |
Given the following "core_competency > plans" exist:
| name | user | description |
| Science plan Year-1 | admin | science plan description |
And I follow "Learning plans"
And I should see "Science plan Year-1"
@@ -72,16 +72,16 @@ Feature: Manage plearning plan
And I should see "Learning plan competencies"
Scenario: Manage a learning plan competencies
Given the following lp "plans" exist:
| name | user | description |
Given the following "core_competency > plans" exist:
| name | user | description |
| Science plan Year-manage | admin | science plan description |
And the following lp "frameworks" exist:
And the following "core_competency > frameworks" exist:
| shortname | idnumber |
| Framework 1 | sc-y-2 |
And the following lp "competencies" exist:
| shortname | framework |
| comp1 | sc-y-2 |
| comp2 | sc-y-2 |
And the following "core_competency > competencies" exist:
| shortname | competencyframework |
| comp1 | sc-y-2 |
| comp2 | sc-y-2 |
And I follow "Learning plans"
And I should see "Science plan Year-manage"
And I follow "Science plan Year-manage"
@@ -98,8 +98,8 @@ Feature: Manage plearning plan
And "comp1" "table_row" should not exist
Scenario: Edit a learning plan
Given the following lp "plans" exist:
| name | user | description |
Given the following "core_competency > plans" exist:
| name | user | description |
| Science plan Year-2 | admin | science plan description |
| Science plan Year-3 | admin | science plan description |
And I follow "Learning plans"
@@ -113,8 +113,8 @@ Feature: Manage plearning plan
And I should see "Science plan Year-3 Edited"
Scenario: Delete a learning plan
Given the following lp "plans" exist:
| name | user | description |
Given the following "core_competency > plans" exist:
| name | user | description |
| Science plan Year-4 | admin | science plan description |
And I follow "Learning plans"
And I should see "Science plan Year-4"
@@ -130,16 +130,16 @@ Feature: Manage plearning plan
Then I should not see "Science plan Year-4"
Scenario: See a learning plan from a course
Given the following lp "plans" exist:
| name | user | description |
Given the following "core_competency > plans" exist:
| name | user | description |
| Science plan Year-manage | admin | science plan description |
And the following lp "frameworks" exist:
| shortname | idnumber |
| Framework 1 | sc-y-2 |
And the following lp "competencies" exist:
| shortname | framework |
| comp1 | sc-y-2 |
| comp2 | sc-y-2 |
And the following "core_competency > frameworks" exist:
| shortname | idnumber |
| Framework 1 | sc-y-2 |
And the following "core_competency > competencies" exist:
| shortname | competencyframework |
| comp1 | sc-y-2 |
| comp2 | sc-y-2 |
And I follow "Learning plans"
And I should see "Science plan Year-manage"
And I follow "Science plan Year-manage"
+99 -51
View File
@@ -16,34 +16,28 @@ 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 | |
| user2 | usermanageownplan | System | |
| manager1 | manageplan | System | |
And the following lp "frameworks" exist:
| shortname | idnumber |
| Test-Framework | ID-FW1 |
And the following lp "competencies" exist:
| shortname | framework |
| Test-Comp1 | ID-FW1 |
| Test-Comp2 | ID-FW1 |
And the following lp "plans" exist:
| name | user | description |
| Test-Plan1 | user1 | Description of plan for user 1 |
| Test-Plan2 | user2 | Description of plan for user 2 |
And the following lp "plancompetencies" exist:
| plan | competency |
And the following "core_competency > frameworks" exist:
| shortname | idnumber |
| Test-Framework | ID-FW1 |
And the following "core_competency > competencies" exist:
| shortname | competencyframework | idnumber |
| Test-Comp1 | ID-FW1 | Test-Comp1 |
| Test-Comp2 | ID-FW1 | Test-Comp2 |
And the following "core_competency > plans" exist:
| name | user | description | reviewer | status |
| Test-Plan1 | user1 | Description of plan for user 1 | user1 | draft |
| Test-Plan2 | user2 | Description of plan for user 2 | user2 | draft |
And the following "core_competency > plan_competency" exist:
| plan | competency |
| Test-Plan1 | Test-Comp1 |
| Test-Plan1 | Test-Comp2 |
| Test-Plan2 | Test-Comp1 |
@@ -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,25 +73,24 @@ 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 lp "plans" exist:
| name | user | description | status |
Given the following "core_competency > plans" exist:
| name | user | description | status |
| Test-Plan3 | user2 | Description of plan 3 for user 1 | waiting for review |
| Test-Plan4 | user1 | Description of plan 3 for user 1 | draft |
| Test-Plan4 | user1 | Description of plan 3 for user 1 | draft |
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 lp "plans" exist:
| name | user | description | status |
Given the following "core_competency > plans" exist:
| name | user | description | status |
| Test-Plan3 | user1 | Description of plan 3 for user 1 | waiting for review |
And I log in as "manager1"
And I follow "Test-Plan3"
@@ -108,11 +98,10 @@ 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 lp "plans" exist:
| name | user | description | status | reviewer |
Given the following "core_competency > plans" exist:
| name | user | description | status | reviewer |
| Test-Plan3 | user1 | Description of plan 3 for user 1 | in review | manager1 |
And I log in as "manager1"
And I follow "Test-Plan3"
@@ -121,11 +110,10 @@ 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 lp "plans" exist:
| name | user | description | status | reviewer |
Given the following "core_competency > plans" exist:
| name | user | description | status | reviewer |
| Test-Plan3 | user1 | Description of plan 3 for user 1 | in review | manager1 |
And I log in as "manager1"
And I follow "Test-Plan3"
@@ -134,11 +122,10 @@ 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 lp "plans" exist:
| name | user | description | status | reviewer |
Given the following "core_competency > plans" exist:
| name | user | description | status | reviewer |
| Test-Plan3 | user1 | Description of plan 3 for user 1 | active | manager1 |
| Test-Plan4 | user1 | Description of plan 4 for user 1 | active | manager1 |
And I am on the "user1" "user > profile" page logged in as "manager1"
@@ -150,11 +137,10 @@ 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 lp "plans" exist:
| name | user | description | status | reviewer |
Given the following "core_competency > plans" exist:
| name | user | description | status | reviewer |
| Test-Plan3 | user1 | Description of plan 3 for user 1 | active | manager1 |
| Test-Plan4 | user1 | Description of plan 4 for user 1 | active | manager1 |
And I am on the "user1" "user > profile" page logged in as "manager1"
@@ -169,11 +155,10 @@ 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 lp "plans" exist:
| name | user | description | status | reviewer |
Given the following "core_competency > plans" exist:
| name | user | description | status | reviewer |
| Test-Plan3 | user1 | Description of plan 3 for user 1 | complete | manager1 |
| Test-Plan4 | user1 | Description of plan 4 for user 1 | complete | manager1 |
And I am on the "user1" "user > profile" page logged in as "manager1"
@@ -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 |
@@ -21,8 +21,8 @@ Feature: Manage plearning plan templates
And I should see "Science template"
Scenario: Read a learning plan template
Given the following lp "templates" exist:
| shortname | description |
Given the following "core_competency > templates" exist:
| shortname | description |
| Science template Year-2 | science template description |
And I navigate to "Competencies > Learning plan templates" in site administration
And I should see "Science template Year-2"
@@ -31,8 +31,8 @@ Feature: Manage plearning plan templates
And I should see "Learning plan template competencies"
Scenario: Edit a learning plan template
Given the following lp "templates" exist:
| shortname | description |
Given the following "core_competency > templates" exist:
| shortname | description |
| Science template Year-3 | science template description |
And I navigate to "Competencies > Learning plan templates" in site administration
And I should see "Science template Year-3"
@@ -44,8 +44,8 @@ Feature: Manage plearning plan templates
And I should see "Science template Year-3 Edited"
Scenario: Delete a learning plan template
Given the following lp "templates" exist:
| shortname | description |
Given the following "core_competency > templates" exist:
| shortname | description |
| Science template Year-4 | science template description |
And I navigate to "Competencies > Learning plan templates" in site administration
And I should see "Science template Year-4"
@@ -5,22 +5,22 @@ Feature: Manage competencies linked to evidence of prior learning
I need to link and unlink competencies from evidence of prior learning
Background:
Given the following lp "frameworks" exist:
| shortname | idnumber |
| Test-Framework | ID-FW1 |
And the following lp "competencies" exist:
| shortname | framework |
| Test-Comp1 | ID-FW1 |
| Test-Comp2 | ID-FW1 |
And the following lp "plans" exist:
| name | user | description |
Given the following "core_competency > frameworks" exist:
| shortname | idnumber |
| Test-Framework | ID-FW1 |
And the following "core_competency > competencies" exist:
| shortname | competencyframework | idnumber |
| Test-Comp1 | ID-FW1 | Test-Comp1 |
| Test-Comp2 | ID-FW1 | Test-Comp2 |
And the following "core_competency > plans" exist:
| name | user | description |
| Test-Plan | admin | Plan description |
And the following lp "plancompetencies" exist:
| plan | competency |
And the following "core_competency > plan_competency" exist:
| plan | competency |
| Test-Plan | Test-Comp1 |
| Test-Plan | Test-Comp2 |
And the following lp "userevidence" exist:
| name | description | user |
And the following "core_competency > user_evidence" exist:
| name | description | user |
| Test-Evidence | Description evidence | admin |
When I log in as "admin"
And I follow "Profile" in the user menu
@@ -51,12 +51,12 @@ Feature: Manage competencies linked to evidence of prior learning
Then "Test-Comp2" "table_row" should exist
Scenario: Unlink competency from evidence of prior learning
Given the following lp "userevidencecompetencies" exist:
| userevidence | competency |
Given the following "core_competency > user_evidence_competency" exist:
| userevidence | competency |
| Test-Evidence | Test-Comp1 |
| Test-Evidence | Test-Comp2 |
Given the following lp "usercompetencies" exist:
| user | competency |
And the following "core_competency > user_competency" exist:
| user | competency |
| admin | Test-Comp1 |
| admin | Test-Comp2 |
And I follow "Evidence of prior learning"
@@ -24,8 +24,8 @@ Feature: Manage evidence of prior learning
And I should see "Evidence-1"
Scenario: Read an evidence of prior learning
Given the following lp "userevidence" exist:
| name | description | user |
Given the following "core_competency > user_evidence" exist:
| name | description | user |
| Evidence-2 | Evidence-2 description | user1 |
And I follow "Evidence of prior learning"
And I should see "List of evidence"
@@ -34,8 +34,8 @@ Feature: Manage evidence of prior learning
Then I should see "Evidence-2"
Scenario: Edit an evidence of prior learning
Given the following lp "userevidence" exist:
| name | description | user |
Given the following "core_competency > user_evidence" exist:
| name | description | user |
| Evidence-3 | Evidence-3 description | user1 |
And I follow "Evidence of prior learning"
And I should see "List of evidence"
@@ -47,8 +47,8 @@ Feature: Manage evidence of prior learning
And I should see "Evidence-3 Edited"
Scenario: Delete an evidence of prior learning
Given the following lp "userevidence" exist:
| name | description | user |
Given the following "core_competency > user_evidence" exist:
| name | description | user |
| Evidence-4 | Evidence-4 description | user1 |
And I follow "Evidence of prior learning"
And I should see "List of evidence"
@@ -62,10 +62,10 @@ Feature: Manage evidence of prior learning
Scenario: List evidences of prior learning
Given the following "users" exist:
| username | firstname | lastname | email |
|user2 | User | 2 | user2@example.com |
And the following lp "userevidence" exist:
| name | description | user |
| username | firstname | lastname | email |
| user2 | User | 2 | user2@example.com |
And the following "core_competency > user_evidence" exist:
| name | description | user |
| Evidence-5 | Evidence-5 description | user1 |
| Evidence-6 | Evidence-6 description | user2 |
And I follow "Evidence of prior learning"
@@ -40,9 +40,9 @@ Feature: View competencies
| competency | relatedcompetency |
| desserts | cakes |
And the following "core_competency > plans" exist:
| name | description | competencies | user |
| Cookery | Cookery is important | salads, desserts, cakes | student1 |
| Literacy | Literacy is important | reading, writing | student1 |
| name | description | competencies | user | status |
| Cookery | Cookery is important | salads, desserts, cakes | student1 | active |
| Literacy | Literacy is important | reading, writing | student1 | active |
And the following "core_competency > course_competencies" exist:
| course | competency |
| C1 | salads |
+44 -16
View File
@@ -41,6 +41,18 @@ class manager {
/** @var int */
const REDIR_LOOP_THRESHOLD = 5;
/** @var array These components and related fileareas will not redirect. */
const ALLOWED_COMPONENTS = [
'core_admin' => [
'logocompact',
'logo',
'favicon',
],
'tool_mfa' => [
'guidance',
],
];
/**
* Displays a debug table with current factor information.
*
@@ -424,6 +436,37 @@ class manager {
return self::NO_REDIRECT;
}
// Ensure we have a moodle_url object if a string is provided.
if (is_string($url)) {
$url = new \moodle_url($url);
}
// Check for pluginfile.php urls.
$pluginfileurl = new \moodle_url('/pluginfile.php');
if ($url->compare($pluginfileurl)) {
// Get the slash arguments.
$args = explode('/', ltrim($url->get_slashargument(), '/'));
// Remove the contextid because we do not need it for this check.
array_shift($args);
// Get the component and filearea.
$component = clean_param(array_shift($args), PARAM_COMPONENT);
$filearea = clean_param(array_shift($args), PARAM_AREA);
// Check allowed components.
if (!array_key_exists($component, static::ALLOWED_COMPONENTS)) {
return self::REDIRECT;
}
// Check allowed fileareas.
if (!in_array($filearea, static::ALLOWED_COMPONENTS[$component])) {
return self::REDIRECT;
}
return self::NO_REDIRECT;
}
// Remove all params before comparison.
$url->remove_all_params();
@@ -442,12 +485,6 @@ class manager {
}
}
// Dont redirect logo images from pluginfile.php (for example: logo in header).
$logourl = new \moodle_url('/pluginfile.php/1/core_admin/logocompact/');
if ($url->compare($logourl)) {
return self::NO_REDIRECT;
}
// Admin not setup.
if (!empty($CFG->adminsetuppending)) {
return self::NO_REDIRECT;
@@ -469,12 +506,6 @@ class manager {
return self::NO_REDIRECT;
}
// Enrolment.
$enrol = new \moodle_url('/enrol/index.php');
if ($enrol->compare($url, URL_MATCH_BASE)) {
return self::NO_REDIRECT;
}
// Guest access.
if (isguestuser()) {
return self::NO_REDIRECT;
@@ -500,11 +531,8 @@ class manager {
}
// WS/AJAX check.
// Prevents any potential bypassing of multi-factor authentication.
if (WS_SERVER || AJAX_SCRIPT) {
if (isset($SESSION->mfa_pending) && !empty($SESSION->mfa_pending)) {
// Allow AJAX and WS, but never from auth.php.
return self::NO_REDIRECT;
}
return self::REDIRECT_EXCEPTION;
}
@@ -41,7 +41,8 @@ class factor_email_renderer extends plugin_renderer_base {
$authurl = new \moodle_url('/admin/tool/mfa/factor/email/email.php',
['instance' => $instance->id, 'pass' => 1, 'secret' => $instance->secret]);
$authurlstring = \html_writer::link($authurl, get_string('email:link', 'factor_email'));
$blockurl = new \moodle_url('/admin/tool/mfa/factor/email/email.php', ['instance' => $instanceid]);
$blockurl = new \moodle_url('/admin/tool/mfa/factor/email/email.php',
['instance' => $instance->id, 'secret' => $instance->secret]);
$blockurlstring = \html_writer::link($blockurl, get_string('email:stoploginlink', 'factor_email'));
$geoinfo = iplookup_find_location($instance->createdfromip);
+8 -3
View File
@@ -42,7 +42,8 @@ $PAGE->set_cacheable(false);
$instance = $DB->get_record('tool_mfa', ['id' => $instanceid]);
$factor = \tool_mfa\plugininfo\factor::get_factor('email');
// If pass is set, require login to force $SESSION and user, and pass for that session.
// If pass is set, do checks and pass for this session.
// Require login to force $SESSION and user, and pass for that session.
if (!empty($instance) && $pass != 0 && $secret != 0) {
require_login();
if ($factor->get_state() === \tool_mfa\plugininfo\factor::STATE_LOCKED) {
@@ -69,8 +70,12 @@ $form = new \factor_email\form\email($url);
if ($form->is_cancelled()) {
redirect(new moodle_url('/'));
} else if ($fromform = $form->get_data()) {
if (empty($instance)) {
}
// If submitted without the pass param, is a cancel request - do checks and revoke email factor.
if ($fromform = $form->get_data()) {
// Only allow revoke attempts from requests with a valid instance and secret.
if (empty($instance) || empty($secret) || $instance->secret != $secret) {
$message = get_string('error:badcode', 'factor_email');
} else {
$user = $DB->get_record('user', ['id' => $instance->userid]);
@@ -33,7 +33,7 @@ $string['email:loginlink'] = 'Or, if you\'re on the same device, use this {$a}.'
$string['email:message'] = 'Here\'s your verification code for {$a->sitename} ({$a->siteurl}).';
$string['email:originatingip'] = 'This login request was made from \'{$a}\'';
$string['email:revokelink'] = 'If this wasn\'t you, you can {$a}.';
$string['email:revokesuccess'] = 'This code has been successfully revoked. All sessions for {$a} have been ended.
$string['email:revokesuccess'] = 'This code has been successfully revoked. All sessions for this user have been ended.
Email will not be usable as a factor until account security has been verified.';
$string['email:subject'] = 'Here\'s your verification code';
$string['email:stoploginlink'] = 'stop this login attempt';
+1 -1
View File
@@ -29,7 +29,7 @@ require_once(__DIR__ . '/../../../config.php');
$PAGE->set_context(\context_system::instance());
$PAGE->set_url(new moodle_url('/admin/tool/mfa/guide.php'));
$PAGE->set_title(get_string('guidance', 'tool_mfa'));
$PAGE->set_pagelayout('standard');
$PAGE->set_pagelayout('secure');
// If guidance page isn't enabled, just redir back to home.
if (!get_config('tool_mfa', 'guidance')) {
+3 -3
View File
@@ -109,8 +109,8 @@ function tool_mfa_after_config(): void {
/**
* Serves any files for the guidance page.
*
* @param stdClass $course
* @param stdClass $cm
* @param stdClass|null $course
* @param stdClass|null $cm
* @param context $context
* @param string $filearea
* @param array $args
@@ -118,7 +118,7 @@ function tool_mfa_after_config(): void {
* @param array $options
* @return bool
*/
function tool_mfa_pluginfile(stdClass $course, stdClass $cm, context $context, string $filearea,
function tool_mfa_pluginfile(stdClass|null $course, stdClass|null $cm, context $context, string $filearea,
array $args, bool $forcedownload, array $options = []): bool {
// Hardcode to only send guidance files from the top level.
$fs = get_file_storage();
+1 -1
View File
@@ -23,6 +23,6 @@
{
}
}}
<a href='{{config.wwwroot}}/admin/tool/mfa/guide.php'>
<a href='{{config.wwwroot}}/admin/tool/mfa/guide.php' target='_blank'>
{{#str}} guidance, tool_mfa {{/str}}
</a>
+21 -5
View File
@@ -241,11 +241,6 @@ final class manager_test extends \advanced_testcase {
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
$this->setUser($user);
// Enrolment.
$enrolurl = new \moodle_url('/enrol/index.php');
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($enrolurl, false));
// Guest User.
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
$this->setGuestUser();
@@ -266,6 +261,27 @@ final class manager_test extends \advanced_testcase {
\core\session\manager::loginas($user2->id, $syscontext, false);
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
$this->setUser($user);
// Access logocompact via pluginfile.
$logourl = new \moodle_url('/pluginfile.php/1/core_admin/logocompact/');
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($logourl, false));
// Access logo via pluginfile.
$logourl = new \moodle_url('/pluginfile.php/1/core_admin/logo/');
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($logourl, false));
// Access favicon via pluginfile.
$logourl = new \moodle_url('/pluginfile.php/1/core_admin/favicon/');
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($logourl, false));
// Access guidance files.
$guideurl = new \moodle_url('/pluginfile.php/1/tool_mfa/guidance/0/capybara.png');
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($guideurl, false));
// Access private area.
$user3 = $this->getDataGenerator()->create_user();
$privateurl = new \moodle_url("/pluginfile.php/{$user3->id}/user/private/privatefile.png");
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($privateurl, false));
}
/**
+4
View File
@@ -34,6 +34,10 @@ if (function_exists('opcache_reset')) {
define('IGNORE_COMPONENT_CACHE', true);
// It makes no sense to use BEHAT_CLI for this script (you cannot initialise PHPunit starting from
// the Behat environment), so in case user has set tne environment variable, disable it.
putenv('BEHAT_CLI=0');
require_once(__DIR__.'/../../../../lib/clilib.php');
require_once(__DIR__.'/../../../../lib/phpunit/bootstraplib.php');
require_once(__DIR__.'/../../../../lib/testing/lib.php');
+4
View File
@@ -30,6 +30,10 @@ if (isset($_SERVER['REMOTE_ADDR'])) {
define('IGNORE_COMPONENT_CACHE', true);
// It makes no sense to use BEHAT_CLI for this script (you cannot initialise PHPunit starting from
// the Behat environment), so in case user has set tne environment variable, disable it.
putenv('BEHAT_CLI=0');
require_once(__DIR__.'/../../../../lib/clilib.php');
require_once(__DIR__.'/../../../../lib/phpunit/bootstraplib.php');
require_once(__DIR__.'/../../../../lib/testing/lib.php');
+5 -5
View File
@@ -42,7 +42,7 @@ $string['activate'] = 'Set status to "Active"';
$string['activating'] = 'Activating a policy';
$string['activateconfirm'] = '<p>You are about to activate policy <em>\'{$a->name}\'</em> and make the version <em>\'{$a->revision}\'</em> the current one.</p><p>All users will be required to agree to this new policy version to be able to use the site.</p>';
$string['activateconfirmyes'] = 'Activate';
$string['agreepolicies'] = 'Please agree to the following policies';
$string['agreepolicies'] = 'Please agree to the following policies:';
$string['backtoprevious'] = 'Go back to previous page';
$string['backtotop'] = 'Back to top';
$string['cachedef_policy_optional'] = 'Cache of the optional/compulsory flag for policy versions';
@@ -74,8 +74,8 @@ $string['filterplaceholder'] = 'Search keyword or select filter';
$string['filterpolicy'] = 'Policy: {$a}';
$string['guestconsent:continue'] = 'Continue';
$string['guestconsentmessage'] = 'If you continue browsing this website, you agree to our policies:';
$string['iagree'] = 'I agree to the {$a}';
$string['idontagree'] = 'No thanks, I decline {$a}';
$string['iagree'] = 'I agree to the {$a}.';
$string['idontagree'] = 'I don\'t agree to the {$a}.';
$string['iagreetothepolicy'] = 'Give consent';
$string['inactivate'] = 'Set status to "Inactive"';
$string['inactivating'] = 'Inactivating a policy';
@@ -94,9 +94,9 @@ $string['newpolicy'] = 'New policy';
$string['newversion'] = 'New version';
$string['noactivepolicies'] = 'There are no policies with an active version.';
$string['nofiltersapplied'] = 'No filters applied';
$string['nopermissiontoagreedocs'] = 'No permission to agree to the policies';
$string['nopermissiontoagreedocs'] = 'No permission to agree to the policies.';
$string['nopermissiontoagreedocs_desc'] = 'Sorry, you do not have the required permissions to agree to the policies.<br />You will not be able to use this site until the following policies are agreed:';
$string['nopermissiontoagreedocsbehalf'] = 'No permission to agree to the policies on behalf of this user';
$string['nopermissiontoagreedocsbehalf'] = 'No permission to agree to the policies on behalf of this user.';
$string['nopermissiontoagreedocsbehalf_desc'] = 'Sorry, you do not have the required permission to agree to the following policies on behalf of {$a}:';
$string['nopermissiontoagreedocscontact'] = 'For more help:';
$string['nopermissiontoviewpolicyversion'] = 'You do not have permissions to view this policy version.';
@@ -34,13 +34,13 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
Then I should see "This site policy"
And I should not see "Course overview"
And I press "Next"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I press "Next"
And I should see "Calendar"
And I log out
And I log in as "manager"
And I press "Next"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I press "Next"
And I navigate to "Users > Privacy and policies > User agreements" in site administration
And "Accepted" "text" should exist in the "User One" "table_row"
@@ -53,7 +53,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
| tool/policy:acceptbehalf | allow |
When I log in as "manager"
And I press "Next"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I press "Next"
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
And I click on "1 of 4 (25%)" "link" in the "This site policy" "table_row"
@@ -76,7 +76,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
| tool/policy:acceptbehalf | allow |
When I log in as "manager"
And I press "Next"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I press "Next"
And I should see "Calendar"
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
@@ -104,16 +104,16 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
And I press "Next"
And I should see "This privacy policy"
And I press "Next"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This privacy policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I set the field "I agree to the This privacy policy." to "1"
And I press "Next"
And I should see "Calendar"
And I log out
And I log in as "manager"
And I press "Next"
And I press "Next"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This privacy policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I set the field "I agree to the This privacy policy." to "1"
And I press "Next"
And I navigate to "Users > Privacy and policies > User agreements" in site administration
And "Accepted" "text" should exist in the "User One" "table_row"
@@ -142,8 +142,8 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
When I log in as "manager"
And I press "Next"
And I press "Next"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This privacy policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I set the field "I agree to the This privacy policy." to "1"
And I press "Next"
And I navigate to "Users > Privacy and policies > User agreements" in site administration
And I click on "Accept This site policy" "link" in the "User One" "table_row"
@@ -176,8 +176,8 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
When I log in as "manager"
And I press "Next"
And I press "Next"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This privacy policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I set the field "I agree to the This privacy policy." to "1"
And I press "Next"
And I navigate to "Users > Privacy and policies > User agreements" in site administration
And I click on "Accept This site policy" "link" in the "User One" "table_row"
@@ -198,7 +198,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
Scenario: Policies and agreements profile link visible for current user
Given I log in as "user1"
And I press "Next"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I press "Next"
When I follow "Profile" in the user menu
# User can see his own agreements link in the profile.
@@ -217,7 +217,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
| tool/policy:acceptbehalf | allow |
And I log in as "manager"
And I press "Next"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I press "Next"
# User can see agreements link in other user profiles because has the capability for accepting on behalf of them.
When I am on "Course1" course homepage
@@ -231,7 +231,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
| tool/policy:acceptbehalf | allow |
When I log in as "manager"
And I press "Next"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I press "Next"
And I log out
And I am on the "manager" "user > profile" page logged in as "admin"
@@ -267,8 +267,8 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
When I log in as "manager"
And I press "Next"
And I press "Next"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This privacy policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I set the field "I agree to the This privacy policy." to "1"
And I press "Next"
And I navigate to "Users > Privacy and policies > User agreements" in site administration
And I click on "Select" "checkbox" in the "User One" "table_row"
@@ -290,7 +290,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
And I should see "This site policy"
And I should not see "Course overview"
And I press "Next"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I press "Next"
And I should see "Calendar"
And I log out
+94 -94
View File
@@ -4,7 +4,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
As a user
I need to be able to accept site policy during sign up
Scenario: Accept policy on sign up, no site policy
Scenario: Accept policy on sign up manage by plugin, no site policy
Given the following config values are set as admin:
| registerauth | email |
| passwordpolicy | 0 |
@@ -16,13 +16,13 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I set the following fields to these values:
| Username | user1 |
| Password | user1 |
| Email address | user1@address.invalid |
| Email (again) | user1@address.invalid |
| Email address | user1@example.com |
| Email (again) | user1@example.com |
| First name | User1 |
| Last name | L1 |
And I press "Create my new account"
And I should see "Confirm your account"
And I should see "An email should have been sent to your address at user1@address.invalid"
And I should see "An email should have been sent to your address at user1@example.com"
And I confirm email for "user1"
And I should see "Thanks, User1 L1"
And I should see "Your registration has been confirmed"
@@ -50,13 +50,13 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I set the following fields to these values:
| Username | user1 |
| Password | user1 |
| Email address | user1@address.invalid |
| Email (again) | user1@address.invalid |
| Email address | user1@example.com |
| Email (again) | user1@example.com |
| First name | User1 |
| Last name | L1 |
And I press "Create my new account"
And I should see "Confirm your account"
And I should see "An email should have been sent to your address at user1@address.invalid"
And I should see "An email should have been sent to your address at user1@example.com"
And I confirm email for "user1"
And I should see "Thanks, User1 L1"
And I should see "Your registration has been confirmed"
@@ -85,23 +85,23 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I should see "short text2"
And I should see "full text2"
And I press "Next"
And I should see "Please agree to the following policies"
And I should see "Please agree to the following policies:"
And I should see "This site policy" in the "region-main" "region"
And I should see "short text2"
And I should not see "full text2"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I press "Next"
And I should not see "I understand and agree"
And I set the following fields to these values:
| Username | user1 |
| Password | user1 |
| Email address | user1@address.invalid |
| Email (again) | user1@address.invalid |
| Email address | user1@example.com |
| Email (again) | user1@example.com |
| First name | User1 |
| Last name | L1 |
And I press "Create my new account"
And I should see "Confirm your account"
And I should see "An email should have been sent to your address at user1@address.invalid"
And I should see "An email should have been sent to your address at user1@example.com"
And I confirm email for "user1"
And I should see "Thanks, User1 L1"
And I should see "Your registration has been confirmed"
@@ -137,7 +137,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I should see "short text3"
And I should see "full text3"
And I press "Next"
And I should see "Please agree to the following policies"
And I should see "Please agree to the following policies:"
And I should see "This site policy" in the "region-main" "region"
And I should see "short text2"
And I should not see "full text2"
@@ -147,20 +147,20 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I should not see "This guests policy" in the "region-main" "region"
And I should not see "short text4"
And I should not see "full text4"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This privacy policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I set the field "I agree to the This privacy policy." to "1"
And I press "Next"
And I should not see "I understand and agree"
And I set the following fields to these values:
| Username | user1 |
| Password | user1 |
| Email address | user1@address.invalid |
| Email (again) | user1@address.invalid |
| Email address | user1@example.com |
| Email (again) | user1@example.com |
| First name | User1 |
| Last name | L1 |
And I press "Create my new account"
And I should see "Confirm your account"
And I should see "An email should have been sent to your address at user1@address.invalid"
And I should see "An email should have been sent to your address at user1@example.com"
And I confirm email for "user1"
And I should see "Thanks, User1 L1"
And I should see "Your registration has been confirmed"
@@ -197,23 +197,23 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I should see "short text2"
And I should see "full text2"
And I press "Next"
And I should see "Please agree to the following policies"
And I should see "Please agree to the following policies:"
And I should see "This site policy"
And I should see "short text2"
And I should not see "full text2"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I press "Next"
And I should not see "I understand and agree"
And I set the following fields to these values:
| Username | user1 |
| Password | user1 |
| Email address | user1@address.invalid |
| Email (again) | user1@address.invalid |
| Email address | user1@example.com |
| Email (again) | user1@example.com |
| First name | User1 |
| Last name | L1 |
And I press "Create my new account"
And I should see "Confirm your account"
And I should see "An email should have been sent to your address at user1@address.invalid"
And I should see "An email should have been sent to your address at user1@example.com"
And I confirm email for "user1"
And I should see "Thanks, User1 L1"
And I should see "Your registration has been confirmed"
@@ -244,19 +244,19 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I press "Next"
And I should see "This privacy policy"
And I press "Next"
And I should see "Please agree to the following policies"
And I should see "Please agree to the following policies:"
And I should see "This site policy"
And I should see "This privacy policy"
# Confirm that a notification is displayed if none of the policies are accepted.
When I set the field "I agree to the This site policy" to "0"
And I set the field "I agree to the This privacy policy" to "0"
When I set the field "I agree to the This site policy." to "0"
And I set the field "I agree to the This privacy policy." to "0"
And I press "Next"
Then I should see "Please agree to the following policies"
Then I should see "Please agree to the following policies:"
And I should see "Before continuing you need to acknowledge all these policies."
# Confirm that a notification is displayed if only some policies are accepted.
When I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This privacy policy" to "0"
Then I should see "Please agree to the following policies"
When I set the field "I agree to the This site policy." to "1"
And I set the field "I agree to the This privacy policy." to "0"
Then I should see "Please agree to the following policies:"
And I should see "Before continuing you need to acknowledge all these policies."
Scenario: Accept policy on login, do not accept all policies
@@ -276,23 +276,23 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I press "Next"
And I should see "This privacy policy"
And I press "Next"
And I should see "Please agree to the following policies"
And I should see "Please agree to the following policies:"
And I should see "This site policy"
And I should see "This privacy policy"
# Confirm that a notification is displayed if none of the policies are accepted.
When I set the field "I agree to the This site policy" to "0"
And I set the field "I agree to the This privacy policy" to "0"
When I set the field "I agree to the This site policy." to "0"
And I set the field "I agree to the This privacy policy." to "0"
And I press "Next"
Then I should see "Please agree to the following policies"
Then I should see "Please agree to the following policies:"
And I should see "Before continuing you need to acknowledge all these policies."
# Confirm that a notification is displayed if only some policies are accepted.
When I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This privacy policy" to "0"
Then I should see "Please agree to the following policies"
When I set the field "I agree to the This site policy." to "1"
And I set the field "I agree to the This privacy policy." to "0"
Then I should see "Please agree to the following policies:"
And I should see "Before continuing you need to acknowledge all these policies."
# Confirm that user can not browse the site (edit their profile).
When I follow "Profile" in the user menu
Then I should see "Please agree to the following policies"
Then I should see "Please agree to the following policies:"
Scenario: Accept policy on login, accept all policies
Given the following config values are set as admin:
@@ -311,14 +311,14 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I press "Next"
And I should see "This privacy policy"
And I press "Next"
And I should see "Please agree to the following policies"
And I should see "Please agree to the following policies:"
And I should see "This site policy"
And I should see "This privacy policy"
# User accepts all policies.
When I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This privacy policy" to "1"
When I set the field "I agree to the This site policy." to "1"
And I set the field "I agree to the This privacy policy." to "1"
And I press "Next"
Then I should not see "Please agree to the following policies"
Then I should not see "Please agree to the following policies:"
And I should not see "Before continuing you need to acknowledge all these policies."
# Confirm that user can login and browse the site (edit their profile).
When I open my profile in edit mode
@@ -328,7 +328,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
When I log in as "user1"
Then I should not see "This site policy"
And I should not see "This privacy policy"
And I should not see "Please agree to the following policies"
And I should not see "Please agree to the following policies:"
# Confirm that user can login and browse the site (edit their profile).
When I open my profile in edit mode
Then the field "First name" matches value "User"
@@ -350,14 +350,14 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I press "Next"
And I should see "This privacy policy"
And I press "Next"
And I should see "Please agree to the following policies"
And I should see "Please agree to the following policies:"
And I should see "This site policy"
And I should see "This privacy policy"
# User accepts all policies.
When I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This privacy policy" to "1"
When I set the field "I agree to the This site policy." to "1"
And I set the field "I agree to the This privacy policy." to "1"
And I press "Next"
Then I should not see "Please agree to the following policies"
Then I should not see "Please agree to the following policies:"
# Confirm that user can login and browse the site (edit their profile).
When I open my profile in edit mode
Then the field "First name" matches value "User"
@@ -385,7 +385,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I should not see "This privacy policy"
Then I should see "This third parties policy"
And I press "Next"
And I should see "Please agree to the following policies"
And I should see "Please agree to the following policies:"
And I should see "This third parties policy"
And I set the field "This third parties policy" to "1"
And I press "Next"
@@ -407,12 +407,12 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I log in as "user1"
And I should see "This site policy"
And I press "Next"
And I should see "Please agree to the following policies"
And I should see "Please agree to the following policies:"
And I should see "This site policy"
# User accepts policy.
When I set the field "I agree to the This site policy" to "1"
When I set the field "I agree to the This site policy." to "1"
And I press "Next"
Then I should not see "Please agree to the following policies"
Then I should not see "Please agree to the following policies:"
# Confirm that user can login and browse the site (edit their profile).
When I open my profile in edit mode
Then the field "First name" matches value "User"
@@ -437,13 +437,13 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I should see "short text2 new version"
And I should see "full text2 new version"
When I press "Next"
Then I should see "Please agree to the following policies"
Then I should see "Please agree to the following policies:"
And I should see "This site policy new version"
And I should see "short text2 new version"
# User accepts policy.
And I set the field "I agree to the This site policy new version" to "1"
And I set the field "I agree to the This site policy new version." to "1"
When I press "Next"
Then I should not see "Please agree to the following policies"
Then I should not see "Please agree to the following policies:"
# Confirm that user can login and browse the site (edit their profile).
When I open my profile in edit mode
Then the field "First name" matches value "User"
@@ -504,28 +504,28 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I should see "short text3"
And I should see "full text3"
When I press "Next"
Then I should see "Please agree to the following policies"
Then I should see "Please agree to the following policies:"
And I should see "This site policy" in the "region-main" "region"
And I should see "short text2"
And I should see "This privacy policy" in the "region-main" "region"
And I should see "short text3"
And I should not see "This guests policy" in the "region-main" "region"
And I should not see "short text4"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This privacy policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I set the field "I agree to the This privacy policy." to "1"
When I press "Next"
Then I should not see "I understand and agree"
And I should see "New account"
And I set the following fields to these values:
| Username | user1 |
| Password | user1 |
| Email address | user1@address.invalid |
| Email (again) | user1@address.invalid |
| Email address | user1@example.com |
| Email (again) | user1@example.com |
| First name | User1 |
| Last name | L1 |
When I press "Create my new account"
Then I should see "Confirm your account"
And I should see "An email should have been sent to your address at user1@address.invalid"
And I should see "An email should have been sent to your address at user1@example.com"
And I follow "Log in"
When I click on "Create new account" "link"
# Confirm that the user can view and accept policies when attempting to create another account.
@@ -537,7 +537,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I should see "short text3"
And I should see "full text3"
When I press "Next"
Then I should see "Please agree to the following policies"
Then I should see "Please agree to the following policies:"
And I should see "This site policy" in the "region-main" "region"
And I should see "short text2"
And I should not see "full text2"
@@ -547,8 +547,8 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I should not see "This guests policy" in the "region-main" "region"
And I should not see "short text4"
And I should not see "full text4"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This privacy policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I set the field "I agree to the This privacy policy." to "1"
When I press "Next"
Then I should not see "I understand and agree"
And I should see "New account"
@@ -572,8 +572,8 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I should see "Please read our This privacy policy"
And I press "Next"
And I should see "Viewing this page on behalf of User 1"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This privacy policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I set the field "I agree to the This privacy policy." to "1"
And I press "Next"
And I log out
And I log in as "user1"
@@ -599,15 +599,15 @@ Feature: User must accept policy managed by this plugin when logging in and sign
When I log in as "manager"
And I press "Next"
And I press "Next"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This privacy policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I set the field "I agree to the This privacy policy." to "1"
And I press "Next"
And I am on the "user1" "user > profile" page
And I follow "Log in as"
Then I should see "You are logged in as User 1"
And I press "Continue"
And I should see "Policies and agreements"
And I should see "No permission to agree to the policies on behalf of this user"
And I should see "No permission to agree to the policies on behalf of this user."
And I should see "Sorry, you do not have the required permission to agree to the following policies on behalf of User 1"
Scenario: Accept policy on sign up as a guest, one policy
@@ -631,23 +631,23 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I should see "short text2"
And I should see "full text2"
And I press "Next"
And I should see "Please agree to the following policies"
And I should see "Please agree to the following policies:"
And I should see "This site policy"
And I should see "short text2"
And I should not see "full text2"
And I set the field "I agree to the This site policy" to "1"
And I set the field "I agree to the This site policy." to "1"
And I press "Next"
And I should not see "I understand and agree"
And I set the following fields to these values:
| Username | user1 |
| Password | user1 |
| Email address | user1@address.invalid |
| Email (again) | user1@address.invalid |
| Email address | user1@example.com |
| Email (again) | user1@example.com |
| First name | User1 |
| Last name | L1 |
And I press "Create my new account"
And I should see "Confirm your account"
And I should see "An email should have been sent to your address at user1@address.invalid"
And I should see "An email should have been sent to your address at user1@example.com"
And I confirm email for "user1"
And I should see "Thanks, User1 L1"
And I should see "Your registration has been confirmed"
@@ -680,12 +680,12 @@ Feature: User must accept policy managed by this plugin when logging in and sign
Then I should see "Digital maturity declaration" in the "region-main" "region"
And I should see "You declare be old enough"
And I should see "Here goes content."
And I press "I agree to the Digital maturity declaration"
And I press "I agree to the Digital maturity declaration."
# The second policy with the agreement style "on its own page" must be accepted now.
And I should see "Terms of Service" in the "region-main" "region"
And I should see "We teach, you learn"
And I should see "Here goes content."
And I press "I agree to the Terms of Service"
And I press "I agree to the Terms of Service."
# Only now we see the remaining consent page policies.
And I should see "Policy 1 out of 2"
And I should see "Privacy policy" in the "region-main" "region"
@@ -697,26 +697,26 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I should see "We eat cookies, srsly"
And I should see "Here goes content."
And I press "Next"
And I should see "Please agree to the following policies"
And I should see "Please agree to the following policies:"
And I should see "Privacy policy"
And I should see "Cookies policy"
And I should not see "Digital maturity declaration" in the "region-main" "region"
And I should not see "Terms of Service" in the "region-main" "region"
And I should not see "Here goes content."
And I set the field "I agree to the Privacy policy" to "1"
And I set the field "I agree to the Cookies policy" to "1"
And I set the field "I agree to the Privacy policy." to "1"
And I set the field "I agree to the Cookies policy." to "1"
And I press "Next"
And I should see "New account"
And I set the following fields to these values:
| Username | user1 |
| Password | user1 |
| Email address | user1@address.invalid |
| Email (again) | user1@address.invalid |
| Email address | user1@example.com |
| Email (again) | user1@example.com |
| First name | User1 |
| Last name | L1 |
And I press "Create my new account"
And I should see "Confirm your account"
And I should see "An email should have been sent to your address at user1@address.invalid"
And I should see "An email should have been sent to your address at user1@example.com"
And I confirm email for "user1"
And I should see "Thanks, User1 L1"
And I should see "Your registration has been confirmed"
@@ -751,7 +751,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
Then I should see "Digital maturity declaration" in the "region-main" "region"
And I should see "You declare be old enough"
And I should see "Here goes content."
And I press "I agree to the Digital maturity declaration"
And I press "I agree to the Digital maturity declaration."
# The second policy with the agreement style "on its own page" must be accepted now.
And I should see "Terms of Service" in the "region-main" "region"
And I should see "We teach, you learn"
@@ -762,7 +762,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I should see "Terms of Service" in the "region-main" "region"
And I should see "We teach, you learn"
And I should see "Here goes content."
And I press "I agree to the Terms of Service"
And I press "I agree to the Terms of Service."
# Only now we see the remaining consent page policies.
And I should see "Policy 1 out of 2"
And I should see "Privacy policy" in the "region-main" "region"
@@ -774,14 +774,14 @@ Feature: User must accept policy managed by this plugin when logging in and sign
And I should see "We eat cookies, srsly"
And I should see "Here goes content."
And I press "Next"
And I should see "Please agree to the following policies"
And I should see "Please agree to the following policies:"
And I should see "Privacy policy"
And I should see "Cookies policy"
And I should not see "Digital maturity declaration" in the "region-main" "region"
And I should not see "Terms of Service" in the "region-main" "region"
And I should not see "Here goes content."
And I set the field "I agree to the Privacy policy" to "1"
And I set the field "I agree to the Cookies policy" to "1"
And I set the field "I agree to the Privacy policy." to "1"
And I set the field "I agree to the Cookies policy." to "1"
And I press "Next"
And I follow "Profile" in the user menu
# User can see his own agreements in the profile.
@@ -809,15 +809,15 @@ Feature: User must accept policy managed by this plugin when logging in and sign
Then I should see "Privacy policy" in the "region-main" "region"
And I should see "We scan your thoughts"
And I should see "Here goes content."
And I press "I agree to the Privacy policy"
And I press "I agree to the Privacy policy."
And I should see "Digital maturity declaration" in the "region-main" "region"
And I should see "You declare be old enough"
And I should see "Here goes content."
And I press "I agree to the Digital maturity declaration"
And I press "I agree to the Digital maturity declaration."
And I should see "Terms of Service" in the "region-main" "region"
And I should see "We teach, you learn"
And I should see "Here goes content."
And I press "I agree to the Terms of Service"
And I press "I agree to the Terms of Service."
And I follow "Profile" in the user menu
And I follow "Policies and agreements"
And "Accepted" "text" should exist in the "Privacy policy" "table_row"
@@ -844,21 +844,21 @@ Feature: User must accept policy managed by this plugin when logging in and sign
Then I should see "Digital maturity declaration" in the "region-main" "region"
And I should see "You declare be old enough"
And I should see "Here goes content."
And I press "I agree to the Digital maturity declaration"
And I press "I agree to the Digital maturity declaration."
And I should see "Cookies policy" in the "region-main" "region"
And I should see "We eat cookies, srsly"
And I press "I agree to the Cookies policy"
And I press "I agree to the Cookies policy."
And I should see "New account"
And I set the following fields to these values:
| Username | user1 |
| Password | user1 |
| Email address | user1@address.invalid |
| Email (again) | user1@address.invalid |
| Email address | user1@example.com |
| Email (again) | user1@example.com |
| First name | User1 |
| Last name | L1 |
And I press "Create my new account"
And I should see "Confirm your account"
And I should see "An email should have been sent to your address at user1@address.invalid"
And I should see "An email should have been sent to your address at user1@example.com"
And I confirm email for "user1"
And I should see "Thanks, User1 L1"
And I should see "Your registration has been confirmed"
@@ -61,7 +61,7 @@ Feature: Manage policies
| Policy1 | v1 | full text2 | short text2 | active |
And I log in as "manager"
And I press "Next"
And I set the field "I agree to the Policy1" to "1"
And I set the field "I agree to the Policy1." to "1"
And I press "Next"
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
And I click on "Edit" "link" in the "Policy1" "table_row"
@@ -83,7 +83,7 @@ Feature: Manage policies
| Policy1 | v1 | full text2 | short text2 | active |
And I log in as "manager"
And I press "Next"
And I set the field "I agree to the Policy1" to "1"
And I set the field "I agree to the Policy1." to "1"
And I press "Next"
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
And I click on "Edit" "link" in the "Policy1" "table_row"
@@ -101,7 +101,7 @@ Feature: Manage policies
| Policy1 | v1 | full text2 | short text2 | active |
And I log in as "manager"
And I press "Next"
And I set the field "I agree to the Policy1" to "1"
And I set the field "I agree to the Policy1." to "1"
And I press "Next"
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
And I click on "Edit" "link" in the "Policy1" "table_row"
@@ -180,7 +180,7 @@ Feature: Manage policies
| Policy1 | v1 | full text2 | short text2 | active |
And I log in as "manager"
And I press "Next"
And I set the field "I agree to the Policy1" to "1"
And I set the field "I agree to the Policy1." to "1"
And I press "Next"
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
And I click on "Set status to \"Inactive\"" "link" in the "Policy1" "table_row"
@@ -218,7 +218,7 @@ Feature: Manage policies
| Policy1 | v1 | full text2 | short text2 | active |
And I log in as "manager"
And I press "Next"
And I set the field "I agree to the Policy1" to "1"
And I set the field "I agree to the Policy1." to "1"
And I press "Next"
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
And I click on "Set status to \"Inactive\"" "link" in the "Policy1" "table_row"
@@ -247,7 +247,7 @@ Feature: Manage policies
| Policy1 | v1 | full text2 | short text2 | active |
And I log in as "user1"
And I press "Next"
And I set the field "I agree to the Policy1" to "1"
And I set the field "I agree to the Policy1." to "1"
And I press "Next"
And I follow "Preferences" in the user menu
And I should see "Preferences"
+21 -21
View File
@@ -70,26 +70,26 @@ Feature: Optional policies
And I should see "OwnPageCompulsory1" in the "region-main" "region"
And I should see "short text4" in the "region-main" "region"
And I should see "full text4" in the "region-main" "region"
And I press "I agree to the OwnPageCompulsory1"
And I press "I agree to the OwnPageCompulsory1."
# Compulsory policies displayed on the consent page are shown next and must be agreed.
And I should see "ConsentPageCompulsory1"
And I should see "short text3" in the "region-main" "region"
And I should see "full text3" in the "region-main" "region"
And I press "Next"
And I should see "Please agree to the following policies"
And I set the field "I agree to the ConsentPageCompulsory1" to "1"
And I should see "Please agree to the following policies:"
And I set the field "I agree to the ConsentPageCompulsory1." to "1"
And I press "Next"
# The signup form can be submitted and a new account created.
And I set the following fields to these values:
| Username | user3 |
| Password | user3 |
| Email address | user3@address.invalid |
| Email (again) | user3@address.invalid |
| Email address | user3@example.com |
| Email (again) | user3@example.com |
| First name | User3 |
| Last name | L3 |
And I press "Create my new account"
And I should see "Confirm your account"
And I should see "An email should have been sent to your address at user3@address.invalid"
And I should see "An email should have been sent to your address at user3@example.com"
And I confirm email for "user3"
And I should see "Thanks, User3 L3"
And I should see "Your registration has been confirmed"
@@ -99,7 +99,7 @@ Feature: Optional policies
Then I should see "OwnPageOptional1"
And I should see "short text5" in the "region-main" "region"
And I should see "full text5" in the "region-main" "region"
And I press "No thanks, I decline OwnPageOptional1"
And I press "I don't agree to the OwnPageOptional1"
# Then come policies displayed on the consent page.
And I should see "ConsentPageOptional1" in the "region-main" "region"
And I should see "short text1" in the "region-main" "region"
@@ -109,9 +109,9 @@ Feature: Optional policies
And I should see "short text2" in the "region-main" "region"
And I should see "full text2" in the "region-main" "region"
And I press "Next"
And I should see "Please agree to the following policies"
And I set the field "I agree to the ConsentPageOptional1" to "1"
And I set the field "No thanks, I decline ConsentPageOptional2" to "0"
And I should see "Please agree to the following policies:"
And I set the field "I agree to the ConsentPageOptional1." to "1"
And I set the field "I don't agree to the ConsentPageOptional2" to "0"
And I press "Next"
# Accepted and declined policies are shown in the profile.
And I follow "Profile" in the user menu
@@ -132,14 +132,14 @@ Feature: Optional policies
Then I should see "OwnPageOptional1"
And I should see "short text5" in the "region-main" "region"
And I should see "full text5" in the "region-main" "region"
And I press "I agree to the OwnPageOptional1"
And I press "I agree to the OwnPageOptional1."
# Then come policies displayed on the consent page.
And I should see "ConsentPageOptional1" in the "region-main" "region"
And I should see "short text1" in the "region-main" "region"
And I should see "full text1" in the "region-main" "region"
And I press "Next"
And I should see "Please agree to the following policies"
And I set the field "No thanks, I decline ConsentPageOptional1" to "0"
And I should see "Please agree to the following policies:"
And I set the field "I don't agree to the ConsentPageOptional1" to "0"
And I press "Next"
# Accepted and declined policies are shown in the profile.
And I follow "Profile" in the user menu
@@ -152,7 +152,7 @@ Feature: Optional policies
| Name | Content | Summary | Agreementstyle | Optional |
| OwnPageOptional1 | full text1 | short text1 | 1 | 1 |
And I log in as "user1"
And I press "I agree to the OwnPageOptional1"
And I press "I agree to the OwnPageOptional1."
And I follow "Profile" in the user menu
And I follow "Policies and agreements"
And "Accepted" "text" should exist in the "OwnPageOptional1" "table_row"
@@ -177,7 +177,7 @@ Feature: Optional policies
| Name | Content | Summary | Agreementstyle | Optional |
| OwnPageOptional1 | full text1 | short text1 | 1 | 1 |
And I log in as "user1"
And I press "I agree to the OwnPageOptional1"
And I press "I agree to the OwnPageOptional1."
And I follow "Profile" in the user menu
And I follow "Policies and agreements"
And "Accepted" "text" should exist in the "OwnPageOptional1" "table_row"
@@ -202,12 +202,12 @@ Feature: Optional policies
| OwnPageOptional1 | full text1 | short text1 | 1 | 1 |
| OwnPageOptional2 | full text2 | short text2 | 1 | 1 |
And I log in as "user1"
And I press "I agree to the OwnPageOptional1"
And I press "No thanks, I decline OwnPageOptional2"
And I press "I agree to the OwnPageOptional1."
And I press "I don't agree to the OwnPageOptional2"
And I log out
And I log in as "manager"
And I press "I agree to the OwnPageOptional1"
And I press "I agree to the OwnPageOptional2"
And I press "I agree to the OwnPageOptional1."
And I press "I agree to the OwnPageOptional2."
When I navigate to "Users > Privacy and policies > User agreements" in site administration
# User One has accepted just some policies.
Then "Partially accepted" "text" should exist in the "User One" "table_row"
@@ -228,8 +228,8 @@ Feature: Optional policies
| OwnPageOptional1 | full text1 | short text1 | 1 | 1 |
| OwnPageOptional2 | full text2 | short text2 | 1 | 1 |
And I log in as "user1"
And I press "I agree to the OwnPageOptional1"
And I press "No thanks, I decline OwnPageOptional2"
And I press "I agree to the OwnPageOptional1."
And I press "I don't agree to the OwnPageOptional2"
And I log out
And I log in as "admin"
When I navigate to "Users > Privacy and policies > User agreements" in site administration
+1 -6
View File
@@ -178,12 +178,7 @@ class tool_uploadcourse_helper {
* @return enrol_plugin[]
*/
public static function get_enrolment_plugins() {
$cache = cache::make('tool_uploadcourse', 'helper');
if (($enrol = $cache->get('enrol')) === false) {
$enrol = enrol_get_plugins(false);
$cache->set('enrol', $enrol);
}
return $enrol;
return enrol_get_plugins(false);
}
/**
+2 -3
View File
@@ -208,12 +208,11 @@ class helper {
* @return \moodle_url The URL.
*/
public static function get_duplicate_tour_link($tourid) {
$link = new \moodle_url('/admin/tool/usertours/configure.php', [
return new \moodle_url('/admin/tool/usertours/configure.php', [
'action' => manager::ACTION_DUPLICATETOUR,
'id' => $tourid,
'sesskey' => sesskey(),
]);
return $link;
}
/**
+2 -1
View File
@@ -503,8 +503,9 @@ class manager {
* @param int $tourid The ID of the tour to duplicate.
*/
protected function duplicate_tour($tourid) {
$tour = helper::get_tour($tourid);
require_sesskey();
$tour = helper::get_tour($tourid);
$export = $tour->to_record();
// Remove the id.
unset($export->id);
@@ -102,7 +102,6 @@ $string['name'] = 'Name';
$string['name_help'] = 'The name of a tour may be added as plain text, enclosed in multilang tags (for use with the multi-language content filter) if required.
Alternatively, a language string ID may be entered in the format identifier,component (with no brackets or space after the comma).';
$string['newstep'] = 'Create step';
$string['newstep'] = 'New step';
$string['newtour'] = 'Create a new tour';
$string['next'] = 'Next';
@@ -1,12 +0,0 @@
@tool @tool_xmldb
Feature: The Reconcile XMLDB files report works and all the files are ok
In order to ensure that all the XMLDB Editor xml files are generated properly
As a developer
I need to be able to run the Reconcile XMLDB files report and verify that everything is ok
Scenario: The Reconcile XMLDB files reports that all files are ok
Given I log in as "admin"
And I navigate to "Development > XMLDB editor" in site administration
When I follow "[Reconcile XMLDB files]"
Then I should see "Look for XMLDB files needing reconciling"
And I should see "All files are OK. No reconciling is needed."
+1
View File
@@ -1925,6 +1925,7 @@ class model {
$uniqueid = $this->get_time_splitting()->append_rangeindex($dbprediction->sampleid, $dbprediction->rangeindex);
$predictionrecords[$uniqueid]->id = $dbprediction->id;
}
$dbpredictions->close();
}
return $predictionrecords;
+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'])) {
+8 -8
View File
@@ -15,13 +15,13 @@ Feature: User must accept policy when logging in and signing up
And I set the following fields to these values:
| Username | user1 |
| Password | user1 |
| Email address | user1@address.invalid |
| Email (again) | user1@address.invalid |
| Email address | student1@example.com |
| Email (again) | student1@example.com |
| First name | User1 |
| Last name | L1 |
And I press "Create my new account"
And I should see "Confirm your account"
And I should see "An email should have been sent to your address at user1@address.invalid"
And I should see "An email should have been sent to your address at student1@example.com"
And I confirm email for "user1"
And I should see "Thanks, User1 L1"
And I should see "Your registration has been confirmed"
@@ -45,14 +45,14 @@ Feature: User must accept policy when logging in and signing up
And I set the following fields to these values:
| Username | user1 |
| Password | user1 |
| Email address | user1@address.invalid |
| Email (again) | user1@address.invalid |
| Email address | user1@example.com |
| Email (again) | user1@example.com |
| First name | User1 |
| Last name | L1 |
| I understand and agree | 1 |
And I press "Create my new account"
And I should see "Confirm your account"
And I should see "An email should have been sent to your address at user1@address.invalid"
And I should see "An email should have been sent to your address at user1@example.com"
And I confirm email for "user1"
And I should see "Thanks, User1 L1"
And I should see "Your registration has been confirmed"
@@ -67,8 +67,8 @@ Feature: User must accept policy when logging in and signing up
Scenario Outline: Email validation during email registration
Given the following config values are set as admin:
| allowaccountssameemail | <allowsameemail> |
| registerauth | email |
| passwordpolicy | 0 |
| registerauth | email |
| passwordpolicy | 0 |
And the following "users" exist:
| username | firstname | lastname | email |
| s1 | John | Doe | s1@example.com |
+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);
+9 -1
View File
@@ -73,7 +73,15 @@ class auth_plugin_webservice extends auth_plugin_base {
function user_login_webservice($username, $password) {
global $CFG, $DB;
// special web service login
if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) {
$user = $DB->get_record('user', [
'username' => $username,
'auth' => 'webservice',
'deleted' => '0',
'suspended' => '0',
'mnethostid' => $CFG->mnet_localhost_id,
]);
if ($user) {
return validate_internal_user_password($user, $password);
}
return false;
+5
View File
@@ -117,6 +117,11 @@ abstract class frontend {
foreach ($enabled as $plugin => $info) {
// Create plugin front-end object.
$class = '\availability_' . $plugin . '\frontend';
if (!class_exists($class)) {
continue;
}
/** @var \core_availability\frontend $frontend */
$frontend = new $class();
// Add to array of required YUI modules.
+2 -3
View File
@@ -792,11 +792,10 @@ abstract class info {
// plugin could also rely on the completion plugin.
$pluginmanager = \core_plugin_manager::instance();
$enabled = $pluginmanager->get_enabled_plugins('availability');
$componentparams = new \stdClass();
foreach ($enabled as $plugin => $info) {
// Use the static method.
/** @var \core_availability\condition $class */
$class = '\availability_' . $plugin . '\condition';
if ($class::completion_value_used($course, $cmid)) {
if (class_exists($class) && $class::completion_value_used($course, $cmid)) {
return true;
}
}
+2 -2
View File
@@ -220,9 +220,9 @@ class tree extends tree_node {
// First see if it's a condition. These have a defined type.
if (isset($child->type)) {
// Look for a plugin of this type.
/** @var \core_availability\condition $classname */
$classname = '\availability_' . $child->type . '\condition';
if (!array_key_exists($child->type, $enabled)) {
if (!array_key_exists($child->type, $enabled) || !class_exists($classname)) {
if ($lax) {
// On load of existing settings, ignore if class
// doesn't exist.
+209 -2
View File
@@ -47,6 +47,11 @@ abstract class restore_qtype_plugin extends restore_plugin {
*/
private $questionanswercacheid = null;
/**
* @var array List of fields to exclude form hashing during restore.
*/
protected array $excludedhashfields = [];
/**
* Add to $paths the restore_path_elements needed
* to handle question_answers for a given question
@@ -62,6 +67,10 @@ abstract class restore_qtype_plugin extends restore_plugin {
$elename = 'question_answer';
$elepath = $this->get_pathfor('/answers/answer'); // we used get_recommended_name() so this works
$paths[] = new restore_path_element($elename, $elepath);
$this->exclude_identity_hash_fields([
'/options/answers/id',
'/options/answers/question',
]);
}
/**
@@ -78,6 +87,10 @@ abstract class restore_qtype_plugin extends restore_plugin {
$elename = 'question_numerical_unit';
$elepath = $this->get_pathfor('/numerical_units/numerical_unit'); // we used get_recommended_name() so this works
$paths[] = new restore_path_element($elename, $elepath);
$this->exclude_identity_hash_fields([
'/options/units/id',
'/options/units/question',
]);
}
/**
@@ -94,6 +107,7 @@ abstract class restore_qtype_plugin extends restore_plugin {
$elename = 'question_numerical_option';
$elepath = $this->get_pathfor('/numerical_options/numerical_option'); // we used get_recommended_name() so this works
$paths[] = new restore_path_element($elename, $elepath);
$this->exclude_identity_hash_fields(['/options/question']);
}
/**
@@ -114,6 +128,21 @@ abstract class restore_qtype_plugin extends restore_plugin {
$elename = 'question_dataset_item';
$elepath = $this->get_pathfor('/dataset_definitions/dataset_definition/dataset_items/dataset_item');
$paths[] = new restore_path_element($elename, $elepath);
$this->exclude_identity_hash_fields([
'/options/datasets/id',
'/options/datasets/question',
'/options/datasets/category',
'/options/datasets/type',
'/options/datasets/items/id',
// The following fields aren't included in the backup or DB structure, but are parsed from the options field.
'/options/datasets/status',
'/options/datasets/distribution',
'/options/datasets/minimum',
'/options/datasets/maximum',
'/options/datasets/decimals',
// This field is set dynamically from the count of items in the dataset, it is not backed up.
'/options/datasets/number_of_items',
]);
}
/**
@@ -168,8 +197,7 @@ abstract class restore_qtype_plugin extends restore_plugin {
$this->questionanswercacheid = $newquestionid;
// Cache all cleaned answers for a simple text match.
foreach ($answers as $answer) {
// MDL-30018: Clean in the same way as {@link xml_writer::xml_safe_utf8()}.
$clean = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is','', $answer->answer); // Clean CTRL chars.
$clean = core_text::trim_ctrl_chars($answer->answer); // Clean CTRL chars.
$clean = preg_replace("/\r\n|\r/", "\n", $clean); // Normalize line ending.
$this->questionanswercache[$clean] = $answer->id;
}
@@ -395,4 +423,183 @@ abstract class restore_qtype_plugin extends restore_plugin {
return $contents;
}
/**
* Add fields to the list of fields excluded from hashing.
*
* This allows common methods to add fields to the exclusion list.
*
* @param array $fields
* @return void
*/
private function exclude_identity_hash_fields(array $fields): void {
$this->excludedhashfields = array_merge($this->excludedhashfields, $fields);
}
/**
* Return fields to be excluded from hashing during restores.
*
* @return array
*/
final public function get_excluded_identity_hash_fields(): array {
return array_unique(array_merge(
$this->excludedhashfields,
$this->define_excluded_identity_hash_fields(),
));
}
/**
* Return a list of paths to fields to be removed from questiondata before creating an identity hash.
*
* Fields that should be excluded from common elements such as answers or numerical units that are used by the plugin will
* be excluded automatically. This method just needs to define any specific to this plugin, such as foreign keys used in the
* plugin's tables.
*
* The returned array should be a list of slash-delimited paths to locate the fields to be removed from the questiondata object.
* For example, if you want to remove the field `$questiondata->options->questionid`, the path would be '/options/questionid'.
* If a field in the path is an array, the rest of the path will be applied to each object in the array. So if you have
* `$questiondata->options->answers[]`, the path '/options/answers/id' will remove the 'id' field from each element of the
* 'answers' array.
*
* @return array
*/
protected function define_excluded_identity_hash_fields(): array {
return [];
}
/**
* Convert the backup structure of this question type into a structure matching its question data
*
* This should take the hierarchical array of tags from the question's backup structure, and return a structure that matches
* that returned when calling {@see get_question_options()} for this question type.
* See https://docs.moodle.org/dev/Question_data_structures#Representation_1:_%24questiondata for an explanation of this
* structure.
*
* This data will then be used to produce an identity hash for comparison with questions in the database.
*
* This base implementation deals with all common backup elements created by the add_question_*_options() methods in this class,
* plus elements added by ::define_question_plugin_structure() named for the qtype. The question type will need to extend
* this function if ::define_question_plugin_structure() adds any other elements to the backup.
*
* @param array $backupdata The hierarchical array of tags from the backup.
* @return \stdClass The questiondata object.
*/
public static function convert_backup_to_questiondata(array $backupdata): \stdClass {
// Create an object from the top-level fields.
$questiondata = (object) array_filter($backupdata, fn($tag) => !is_array($tag));
$qtype = $questiondata->qtype;
$questiondata->options = new stdClass();
if (isset($backupdata["plugin_qtype_{$qtype}_question"][$qtype])) {
$questiondata->options = (object) $backupdata["plugin_qtype_{$qtype}_question"][$qtype][0];
}
if (isset($backupdata["plugin_qtype_{$qtype}_question"]['answers'])) {
$questiondata->options->answers = array_map(
fn($answer) => (object) $answer,
$backupdata["plugin_qtype_{$qtype}_question"]['answers']['answer'],
);
}
if (isset($backupdata["plugin_qtype_{$qtype}_question"]['numerical_options'])) {
$questiondata->options = (object) array_merge(
(array) $questiondata->options,
$backupdata["plugin_qtype_{$qtype}_question"]['numerical_options']['numerical_option'][0],
);
}
if (isset($backupdata["plugin_qtype_{$qtype}_question"]['numerical_units'])) {
$questiondata->options->units = array_map(
fn($unit) => (object) $unit,
$backupdata["plugin_qtype_{$qtype}_question"]['numerical_units']['numerical_unit'],
);
}
if (isset($backupdata["plugin_qtype_{$qtype}_question"]['dataset_definitions'])) {
$questiondata->options->datasets = array_map(
fn($dataset) => (object) $dataset,
$backupdata["plugin_qtype_{$qtype}_question"]['dataset_definitions']['dataset_definition'],
);
}
if (isset($questiondata->options->datasets)) {
foreach ($questiondata->options->datasets as $dataset) {
if (isset($dataset->dataset_items)) {
$dataset->items = array_map(
fn($item) => (object) $item,
$dataset->dataset_items['dataset_item'],
);
unset($dataset->dataset_items);
}
}
}
if (isset($backupdata['question_hints'])) {
$questiondata->hints = array_map(
fn($hint) => (object) $hint,
$backupdata['question_hints']['question_hint'],
);
}
return $questiondata;
}
/**
* Remove excluded fields from the questiondata structure.
*
* This removes fields that will not match or not be present in the question data structure produced by
* {@see self::convert_backup_to_questiondata()} and {@see get_question_options()} (such as IDs), so that the remaining data can
* be used to produce an identity hash for comparing the two.
*
* For plugins, it should be sufficient to override {@see self::define_excluded_identity_hash_fields()} with a list of paths
* specific to the plugin type. Overriding this method is only necessary if the plugin's
* {@see question_type::get_question_options()} method adds additional data to the question that is not included in the backup.
*
* @param stdClass $questiondata
* @param array $excludefields Paths to the fields to exclude.
* @return stdClass The $questiondata with excluded fields removed.
*/
public static function remove_excluded_question_data(stdClass $questiondata, array $excludefields = []): stdClass {
// All questions will need to exclude 'id' (used by question and other tables), 'questionid' (used by hints and options),
// 'createdby' and 'modifiedby' (since they won't map between sites).
$defaultexcludes = [
'/id',
'/createdby',
'/modifiedby',
'/hints/id',
'/hints/questionid',
'/options/id',
'/options/questionid',
];
$excludefields = array_unique(array_merge($excludefields, $defaultexcludes));
foreach ($excludefields as $excludefield) {
$pathparts = explode('/', ltrim($excludefield, '/'));
$data = $questiondata;
self::unset_excluded_fields($data, $pathparts);
}
return $questiondata;
}
/**
* Iterate through the elements of path to an excluded field, and unset the final element.
*
* If any of the elements in the path is an array, this is called recursively on each element in the array to unset fields
* in each child of the array.
*
* @param stdClass|array $data The questiondata object, or a subsection of it.
* @param array $pathparts The remaining elements in the path to the excluded field.
* @return void
*/
private static function unset_excluded_fields(stdClass|array $data, array $pathparts): void {
$element = array_shift($pathparts);
if (!isset($data->{$element})) {
// This element is not present in the data structure, nothing to unset.
return;
}
if (is_object($data->{$element})) {
self::unset_excluded_fields($data->{$element}, $pathparts);
} else if (is_array($data->{$element})) {
foreach ($data->{$element} as $item) {
self::unset_excluded_fields($item, $pathparts);
}
} else if (empty($pathparts)) {
// This is the last element of the path and it's a scalar value, unset it.
unset($data->{$element});
}
}
}
+1 -2
View File
@@ -5271,8 +5271,7 @@ class restore_create_categories_and_questions extends restore_structure_step {
$potentialhints = $DB->get_records('question_hints',
array('questionid' => $newquestionid), '', 'id, hint');
foreach ($potentialhints as $potentialhint) {
// Clean in the same way than {@link xml_writer::xml_safe_utf8()}.
$cleanhint = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is','', $potentialhint->hint); // Clean CTRL chars.
$cleanhint = core_text::trim_ctrl_chars($potentialhint->hint); // Clean CTRL chars.
$cleanhint = preg_replace("/\r\n|\r/", "\n", $cleanhint); // Normalize line ending.
if ($cleanhint === $data->hint) {
$newitemid = $data->id;
@@ -64,6 +64,8 @@ final class quiz_restore_decode_links_test extends \advanced_testcase {
$questiondata = \question_bank::load_question_data($question->id);
$DB->set_field('question', 'questiontext', $CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->cmid, ['id' => $question->id]);
$firstanswer = array_shift($questiondata->options->answers);
$DB->set_field('question_answers', 'answer', $CFG->wwwroot . '/course/view.php?id=' . $course->id,
['id' => $firstanswer->id]);
@@ -87,6 +89,7 @@ final class quiz_restore_decode_links_test extends \advanced_testcase {
$questionids = [];
foreach ($quizquestions as $quizquestion) {
if ($quizquestion->questionid) {
$this->assertEquals($CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->cmid, $quizquestion->questiontext);
$questionids[] = $quizquestion->questionid;
}
}
+44 -12
View File
@@ -567,7 +567,7 @@ abstract class restore_dbops {
* @return array A separate list of all error and warnings detected
*/
public static function prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, $contextlevel) {
global $DB;
global $DB, $CFG;
// To return any errors and warnings found
$errors = array();
@@ -669,21 +669,37 @@ abstract class restore_dbops {
} else {
self::set_backup_ids_record($restoreid, 'question_category', $category->id, $matchcat->id, $targetcontext->id);
$questions = self::restore_get_questions($restoreid, $category->id);
$transformer = self::get_backup_xml_transformer($courseid);
// Collect all the questions for this category into memory so we only talk to the DB once.
$questioncache = $DB->get_records_sql_menu('SELECT q.stamp, q.id
FROM {question} q
JOIN {question_versions} qv
ON qv.questionid = q.id
JOIN {question_bank_entries} qbe
ON qbe.id = qv.questionbankentryid
JOIN {question_categories} qc
ON qc.id = qbe.questioncategoryid
WHERE qc.id = ?', array($matchcat->id));
$recordset = $DB->get_recordset_sql(
"SELECT q.*
FROM {question} q
JOIN {question_versions} qv ON qv.questionid = q.id
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
WHERE qc.id = ?",
[$matchcat->id],
);
// Compute a hash of question and answer fields to differentiate between identical stamp-version questions.
$questioncache = [];
foreach ($recordset as $question) {
$question->export_process = true; // Include all question options required for export.
get_question_options($question);
unset($question->export_process);
// Remove some additional properties from get_question_options() that isn't included in backups
// before we produce the identity hash.
unset($question->categoryobject);
unset($question->questioncategoryid);
$cachekey = restore_questions_parser_processor::generate_question_identity_hash($question, $transformer);
$questioncache[$cachekey] = $question->id;
}
$recordset->close();
foreach ($questions as $question) {
if (isset($questioncache[$question->stamp])) {
$matchqid = $questioncache[$question->stamp];
if (isset($questioncache[$question->questionhash])) {
$matchqid = $questioncache[$question->questionhash];
} else {
$matchqid = false;
}
@@ -1918,6 +1934,22 @@ abstract class restore_dbops {
private static function password_should_be_discarded(#[\SensitiveParameter] string $password): bool {
return (bool) preg_match('/^[0-9a-f]{32}$/', $password);
}
/**
* Load required classes and return a backup XML transformer for the specified course.
*
* These classes may not have been loaded if we're only doing a restore in the current process,
* so make sure we have them here.
*
* @param int $courseid
* @return backup_xml_transformer
*/
protected static function get_backup_xml_transformer(int $courseid): backup_xml_transformer {
global $CFG;
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
require_once($CFG->dirroot . '/backup/moodle2/backup_plan_builder.class.php');
return new backup_xml_transformer($courseid);
}
}
/*
@@ -45,6 +45,9 @@ class restore_questions_parser_processor extends grouped_parser_processor {
/** @var string XML path in the questions.xml to question elements within question_category (before Moodle 4.0). */
protected const LEGACY_QUESTION_SUBPATH = '/questions/question';
/** @var string String for concatenating data into a string for hashing.*/
protected const HASHDATA_SEPARATOR = '|HASHDATA|';
/** @var string identifies the current restore. */
protected string $restoreid;
@@ -52,13 +55,44 @@ class restore_questions_parser_processor extends grouped_parser_processor {
protected int $lastcatid;
public function __construct($restoreid) {
global $CFG;
$this->restoreid = $restoreid;
$this->lastcatid = 0;
parent::__construct();
// Set the paths we are interested on
$this->add_path(self::CATEGORY_PATH);
$this->add_path(self::CATEGORY_PATH . self::QUESTION_SUBPATH);
$this->add_path(self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH);
$this->add_path(self::CATEGORY_PATH . self::QUESTION_SUBPATH, true);
$this->add_path(self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH, true);
// Add all sub-elements, including those from plugins, as grouped paths with the question tag so that
// we can create a hash of all question data for comparison with questions in the database.
$this->add_path(self::CATEGORY_PATH . self::QUESTION_SUBPATH . '/question_hints');
$this->add_path(self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH . '/question_hints');
$this->add_path(self::CATEGORY_PATH . self::QUESTION_SUBPATH . '/question_hints/question_hint');
$this->add_path(self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH . '/question_hints/question_hint');
$connectionpoint = new restore_path_element('question', self::CATEGORY_PATH . self::QUESTION_SUBPATH);
foreach (\core\plugin_manager::instance()->get_plugins_of_type('qtype') as $qtype) {
$restore = $this->get_qtype_restore($qtype->name);
if (!$restore) {
continue;
}
$structure = $restore->define_plugin_structure($connectionpoint);
foreach ($structure as $element) {
$subpath = str_replace(self::CATEGORY_PATH . self::QUESTION_SUBPATH . '/', '', $element->get_path());
$pathparts = explode('/', $subpath);
$path = self::CATEGORY_PATH . self::QUESTION_SUBPATH;
$legacypath = self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH;
foreach ($pathparts as $part) {
$path .= '/' . $part;
$legacypath .= '/' . $part;
if (!in_array($path, $this->paths)) {
$this->add_path($path);
$this->add_path($legacypath);
}
}
}
}
}
protected function dispatch_chunk($data) {
@@ -73,10 +107,19 @@ class restore_questions_parser_processor extends grouped_parser_processor {
// Prepare question record
} else if ($data['path'] == self::CATEGORY_PATH . self::QUESTION_SUBPATH ||
$data['path'] == self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH) {
$info = (object)$data['tags'];
// Remove sub-elements from the question info we're going to save.
$info = (object) array_filter($data['tags'], fn($tag) => !is_array($tag));
$itemname = 'question';
$itemid = $info->id;
$parentitemid = $this->lastcatid;
$restore = $this->get_qtype_restore($data['tags']['qtype']);
if ($restore) {
$questiondata = $restore->convert_backup_to_questiondata($data['tags']);
} else {
$questiondata = restore_qtype_plugin::convert_backup_to_questiondata($data['tags']);
}
// Store a hash of question fields for comparison with existing questions.
$info->questionhash = $this->generate_question_identity_hash($questiondata);
// Not question_category nor question, impossible. Throw exception.
} else {
@@ -106,4 +149,80 @@ class restore_questions_parser_processor extends grouped_parser_processor {
}
return $cdata;
}
/**
* Load and instantiate the restore class for the given question type.
*
* If there is no restore class, null is returned.
*
* @param string $qtype The question type name (no qtype_ prefix)
* @return ?restore_qtype_plugin
*/
protected static function get_qtype_restore(string $qtype): ?restore_qtype_plugin {
global $CFG;
$step = new restore_quiz_activity_structure_step('questions', 'question.xml');
$filepath = "{$CFG->dirroot}/question/type/{$qtype}/backup/moodle2/restore_qtype_{$qtype}_plugin.class.php";
if (!file_exists($filepath)) {
return null;
}
require_once($filepath);
$restoreclass = "restore_qtype_{$qtype}_plugin";
if (!class_exists($restoreclass)) {
return null;
}
return new $restoreclass('qtype', $qtype, $step);
}
/**
* Given a data structure containing the data for a question, reduce it to a flat array and return a sha1 hash of the data.
*
* @param stdClass $questiondata An array containing all the data for a question, including hints and qtype plugin data.
* @param ?backup_xml_transformer $transformer If provided, run the backup transformer process on all text fields. This ensures
* that values from the database are compared like-for-like with encoded values from the backup.
* @return string A sha1 hash of all question data, normalised and concatenated together.
*/
public static function generate_question_identity_hash(
stdClass $questiondata,
?backup_xml_transformer $transformer = null,
): string {
$questiondata = clone($questiondata);
$restore = self::get_qtype_restore($questiondata->qtype);
if ($restore) {
$restore->define_plugin_structure(new restore_path_element('question', self::CATEGORY_PATH . self::QUESTION_SUBPATH));
// Combine default exclusions with those specified by the plugin.
$questiondata = $restore->remove_excluded_question_data($questiondata, $restore->get_excluded_identity_hash_fields());
} else {
// The qtype has no restore class, use the default reduction method.
$questiondata = restore_qtype_plugin::remove_excluded_question_data($questiondata);
}
// Convert questiondata to a flat array of values.
$hashdata = [];
// Convert the object to a multi-dimensional array for compatibility with array_walk_recursive.
$questiondata = json_decode(json_encode($questiondata), true);
array_walk_recursive($questiondata, function($value) use (&$hashdata) {
// Normalise data types. Depending on where the data comes from, it may be a mixture of nulls, strings,
// ints and floats. Convert everything to strings, then all numbers to floats to ensure we are doing
// like-for-like comparisons without losing accuracy.
$value = (string) $value;
if (is_numeric($value)) {
$value = (float) ($value);
} else if (str_contains($value, "\r\n")) {
// Normalise line breaks.
$value = str_replace("\r\n", "\n", $value);
}
$hashdata[] = $value;
});
sort($hashdata, SORT_STRING);
$hashstring = implode(self::HASHDATA_SEPARATOR, $hashdata);
if ($transformer) {
$hashstring = $transformer->process($hashstring);
// Need to re-sort the hashdata with the transformed strings.
$hashdata = explode(self::HASHDATA_SEPARATOR, $hashstring);
sort($hashdata, SORT_STRING);
$hashstring = implode(self::HASHDATA_SEPARATOR, $hashdata);
}
return sha1($hashstring);
}
}
+3 -3
View File
@@ -253,14 +253,14 @@ class xml_writer {
}
/**
* Perform some UTF-8 cleaning, stripping the control chars (\x0-\x1f)
* but tabs (\x9), newlines (\xa) and returns (\xd). The delete control
* Perform some UTF-8 cleaning, stripping the control chars (\x00-\x1f)
* but tabs (\x09), newlines (\xa) and returns (\xd). The delete control
* char (\x7f) is also included. All them are forbiden in XML 1.0 specs.
* The expression below seems to be UTF-8 safe too because it simply
* ignores the rest of characters. Also normalize linefeeds and return chars.
*/
protected function xml_safe_utf8($content) {
$content = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is', '', $content ?? ''); // clean CTRL chars.
$content = core_text::trim_ctrl_chars($content ?? '');
$content = preg_replace("/\r\n|\r/", "\n", $content); // Normalize line&return=>line
return fix_utf8($content);
}
+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);
+112 -58
View File
@@ -60,6 +60,7 @@ if (!empty($issuedbadge->recipient->id)) {
$assertionid = $assertion->get_assertion_hash();
$assertionentityid = $assertiondata['id'];
$badgeadded = false;
$issuerexists = false;
if (badges_open_badges_backpack_api() == OPEN_BADGES_V2) {
$sitebackpack = badges_get_site_primary_backpack();
$api = new \core_badges\backpack_api($sitebackpack);
@@ -71,62 +72,75 @@ if (!empty($issuedbadge->recipient->id)) {
$issuer = $assertion->get_issuer();
if (!($issuerentityid = badges_external_get_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_ISSUER, $issuer['email']))) {
$response = $api->put_issuer($issuer);
if (!$response) {
throw new moodle_exception('invalidrequest', 'error');
if ($response) {
$issuerexists = true;
$issuerentityid = $response->id;
badges_external_create_mapping(
$sitebackpack->id,
OPEN_BADGES_V2_TYPE_ISSUER,
$issuer['email'],
$issuerentityid,
);
}
$issuerentityid = $response->id;
badges_external_create_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_ISSUER, $issuer['email'],
$issuerentityid);
}
// Create badge.
$badge = $assertion->get_badge_class(false);
$badgeid = $assertion->get_badge_id();
if (!($badgeentityid = badges_external_get_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_BADGE, $badgeid))) {
$response = $api->put_badgeclass($issuerentityid, $badge);
if (!$response) {
throw new moodle_exception('invalidrequest', 'error');
if ($issuerexists) {
// Create badge.
$badge = $assertion->get_badge_class(false);
$badgeid = $assertion->get_badge_id();
if (!($badgeentityid = badges_external_get_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_BADGE, $badgeid))) {
$response = $api->put_badgeclass($issuerentityid, $badge);
if ($response) {
$badgeentityid = $response->id;
badges_external_create_mapping(
$sitebackpack->id,
OPEN_BADGES_V2_TYPE_BADGE,
$badgeid,
$badgeentityid,
);
}
}
$badgeentityid = $response->id;
badges_external_create_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_BADGE, $badgeid,
$badgeentityid);
}
// Create assertion (Award the badge!).
$assertionentityid = badges_external_get_mapping(
$sitebackpack->id,
OPEN_BADGES_V2_TYPE_ASSERTION,
$assertionid
);
if ($assertionentityid && strpos($sitebackpack->backpackapiurl, 'badgr')) {
$assertionentityid = badges_generate_badgr_open_url(
$sitebackpack,
OPEN_BADGES_V2_TYPE_ASSERTION,
$assertionentityid
);
}
// Create an assertion for the recipient in the issuer's account.
if (!$assertionentityid) {
$response = $api->put_badgeclass_assertion($badgeentityid, $assertiondata);
if (!$response) {
throw new moodle_exception('invalidrequest', 'error');
}
$assertionentityid = badges_generate_badgr_open_url($sitebackpack, OPEN_BADGES_V2_TYPE_ASSERTION, $response->id);
$badgeadded = true;
badges_external_create_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_ASSERTION, $assertionid,
$response->id);
} else {
// An assertion already exists. Make sure it's up to date.
$internalid = badges_external_get_mapping(
// Create assertion (Award the badge!).
$assertionentityid = badges_external_get_mapping(
$sitebackpack->id,
OPEN_BADGES_V2_TYPE_ASSERTION,
$assertionid,
'externalid'
$assertionid
);
$response = $api->update_assertion($internalid, $assertiondata);
if (!$response) {
throw new moodle_exception('invalidrequest', 'error');
if ($assertionentityid && strpos($sitebackpack->backpackapiurl, 'badgr')) {
$assertionentityid = badges_generate_badgr_open_url(
$sitebackpack,
OPEN_BADGES_V2_TYPE_ASSERTION,
$assertionentityid
);
}
// Create an assertion for the recipient in the issuer's account.
if (!$assertionentityid) {
$response = $api->put_badgeclass_assertion($badgeentityid, $assertiondata);
if ($response) {
$assertionentityid = badges_generate_badgr_open_url(
$sitebackpack,
OPEN_BADGES_V2_TYPE_ASSERTION,
$response->id,
);
$badgeadded = true;
badges_external_create_mapping(
$sitebackpack->id,
OPEN_BADGES_V2_TYPE_ASSERTION,
$assertionid,
$response->id,
);
}
} else {
// An assertion already exists. Make sure it's up to date.
$internalid = badges_external_get_mapping(
$sitebackpack->id,
OPEN_BADGES_V2_TYPE_ASSERTION,
$assertionid,
'externalid'
);
$response = $api->update_assertion($internalid, $assertiondata);
}
}
}
@@ -136,21 +150,61 @@ if (!empty($issuedbadge->recipient->id)) {
// - If a user and site backpack have the same provider we can skip this as Badgr automatically maps recipients
// based on email address.
// - This is only needed when the backpacks are from different regions.
if ($assertionentityid && !badges_external_get_mapping($userbackpack->id, OPEN_BADGES_V2_TYPE_ASSERTION, $assertionid)) {
if (
$assertionentityid
&& (!$issuerexists || !badges_external_get_mapping($userbackpack->id, OPEN_BADGES_V2_TYPE_ASSERTION, $assertionid))
) {
$userapi = new \core_badges\backpack_api($userbackpack, $backpack);
$userapi->authenticate();
$response = $userapi->import_badge_assertion($assertionentityid);
if (!$response) {
throw new moodle_exception('invalidrequest', 'error');
if ($response) {
$assertionentityid = $response->id;
$badgeadded = true;
badges_external_create_mapping(
$userbackpack->id,
OPEN_BADGES_V2_TYPE_ASSERTION,
$assertionid,
$assertionentityid,
);
}
$assertionentityid = $response->id;
$badgeadded = true;
badges_external_create_mapping($userbackpack->id, OPEN_BADGES_V2_TYPE_ASSERTION, $assertionid,
$assertionentityid);
}
$response = $badgeadded ? ['success' => 'addedtobackpack'] : ['warning' => 'existsinbackpack'];
redirect(new moodle_url('/badges/mybadges.php', $response));
if ($badgeadded) {
$message = get_string('addedtobackpack', 'badges');
$messagetype = \core\output\notification::NOTIFY_SUCCESS;
} else {
if (isset($userapi) && !empty($userapi->get_errors())) {
// If the api used to import the badge to the backpack has errors, show them to inform the user.
if (array_filter($userapi->get_errors(), fn($element) => str_contains($element, "DUPLICATE_BADGE"))) {
// Duplicated badges are displayed as a warning.
$message = get_string('existsinbackpack', 'badges');
$messagetype = \core\output\notification::NOTIFY_WARNING;
} else {
// If the userapi has any other errors, we will use those to inform the user.
$message = get_string(
'error:cannotsendtobackpack',
'badges',
implode($userapi->get_errors()),
);
$messagetype = \core\output\notification::NOTIFY_ERROR;
}
} else if (isset($api) && !empty($api->get_errors())) {
// If the api used to create/update the issuer has errors, show them to inform the user.
$errors = $api->get_errors() ?? [get_string('invalidrequest', 'error')];
$message = get_string(
'error:cannotsendtobackpack',
'badges',
implode($errors),
);
$messagetype = \core\output\notification::NOTIFY_ERROR;
}
}
redirect(
url: new moodle_url('/badges/mybadges.php'),
message: $message,
messagetype: $messagetype,
);
} else {
redirect(new moodle_url('/badges/mybadges.php'));
}
+1 -1
View File
@@ -56,7 +56,7 @@ if ($action == 'delete' && $confirm && confirm_sesskey()) {
} else {
$msg = get_string('sitebackpacknotdeleted', 'badges');
}
} else if ($action == 'moveup' || $action == 'movedown') {
} else if (($action == 'moveup' || $action == 'movedown') && confirm_sesskey()) {
// If no backpack has been selected, there isn't anything to move.
if (empty($id)) {
redirect($url);
+14
View File
@@ -668,6 +668,20 @@ class backpack_api {
return backpack_api_mapping::get_authentication_error();
}
/**
* List all errors occurred during the requests to the backpack.
*
* @return array The list of errors.
*/
public function get_errors(): array {
$errors = [];
foreach ($this->mappings as $mapping) {
$errors = array_merge($errors, $mapping->get_errors());
}
return $errors;
}
/**
* Get the list of badges in a collection.
*
+40
View File
@@ -85,6 +85,9 @@ class backpack_api_mapping {
/** @var int OpenBadges version 1 or 2. */
protected $backpackapiversion;
/** @var array Errors encountered during the request. */
protected $errors = [];
/**
* Create a mapping.
*
@@ -150,6 +153,26 @@ class backpack_api_mapping {
return self::$authenticationerror;
}
/**
* Get the errors encountered during the request.
*
* @return array The list of errors.
*/
public function get_errors() {
return $this->errors;
}
/**
* Add an error to the list of errors.
*
* @param string $error The error message.
* @return self This instance for method chaining.
*/
public function add_error(string $error): self {
$this->errors[] = $error;
return $this;
}
/**
* Does the action match this mapping?
*
@@ -372,6 +395,23 @@ class backpack_api_mapping {
$response = $curl->put($url, $post, $options);
}
$response = json_decode($response);
if ($response === null) {
$this->add_error(get_string('invalidrequest', 'error'));
return null;
}
if (isset($response->status) && isset($response->status->success) && $response->status->success != true) {
// If the response wasn't successful, store the errors and return null.
if (isset($response->validationErrors)) {
$error = implode(', ', $response->validationErrors);
} else if (isset($response->status->description)) {
$error = $response->status->description;
} else {
$error = get_string('invalidrequest', 'error');
}
$this->add_error($error);
return null;
}
if (isset($response->result)) {
$response = $response->result;
}
@@ -56,13 +56,13 @@
<td> {{{backpackweburl}}} </td>
<td>
{{#canmoveup}}
<a href="{{baseurl}}?id={{id}}&action=moveup">{{#pix}}t/up, core,{{#str}}moveup{{/str}}{{/pix}}</a>
<a href="{{baseurl}}?id={{id}}&sesskey={{sesskey}}&action=moveup">{{#pix}}t/up, core,{{#str}}moveup{{/str}}{{/pix}}</a>
{{/canmoveup}}
{{^canmoveup}}
{{#pix}}spacer, moodle{{/pix}}
{{/canmoveup}}
{{#canmovedown}}
<a href="{{baseurl}}?id={{id}}&action=movedown">{{#pix}}t/down, core,{{#str}}movedown{{/str}}{{/pix}}</a>
<a href="{{baseurl}}?id={{id}}&sesskey={{sesskey}}&action=movedown">{{#pix}}t/down, core,{{#str}}movedown{{/str}}{{/pix}}</a>
{{/canmovedown}}
{{^canmovedown}}
{{#pix}}spacer, moodle{{/pix}}
@@ -14,13 +14,13 @@ Feature: Award badges based on competency completion
And the following "course enrolments" exist:
| user | course | role |
| user1 | C1 | student |
And the following lp "frameworks" exist:
| shortname | idnumber |
| Framework 1 | sc-y-2 |
And the following lp "competencies" exist:
| shortname | framework |
| comp1 | sc-y-2 |
| comp2 | sc-y-2 |
And the following "core_competency > frameworks" exist:
| shortname | idnumber |
| Framework 1 | sc-y-2 |
And the following "core_competency > competencies" exist:
| shortname | competencyframework |
| comp1 | sc-y-2 |
| comp2 | sc-y-2 |
And I log in as "admin"
@javascript
+287
View File
@@ -0,0 +1,287 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_badges\tests;
/**
* Helper trait for external function tests.
*
* @package core_badges
* @copyright 2025 Moodle Pty Ltd
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait external_helper {
/**
* Asserts that an issued badge returned by an external function matches the given data.
*
* @param array $expected Expected badge data.
* @param array $actual Actual badge data returned by the external function.
* @param bool $isrecipient True if user is the badge recipient.
* @param bool $canconfiguredetails True if user has capability "moodle/badges:configuredetails".
* @throws \PHPUnit\Framework\ExpectationFailedException
*/
protected function assert_issued_badge(array $expected, array $actual, bool $isrecipient, bool $canconfiguredetails): void {
$this->assertEquals($expected['id'], $actual['id']);
$this->assertEquals($expected['name'], $actual['name']);
$this->assertEquals($expected['type'], $actual['type']);
$this->assertEquals($expected['description'], $actual['description']);
$this->assertEquals($expected['issuername'], $actual['issuername']);
$this->assertEquals($expected['issuerurl'], $actual['issuerurl']);
$this->assertEquals($expected['issuercontact'], $actual['issuercontact']);
$this->assertEquals($expected['uniquehash'], $actual['uniquehash']);
$this->assertEquals($expected['dateissued'], $actual['dateissued']);
$this->assertEquals($expected['dateexpire'], $actual['dateexpire']);
$this->assertEquals($expected['version'], $actual['version']);
$this->assertEquals($expected['language'], $actual['language']);
$this->assertEquals($expected['imageauthorname'], $actual['imageauthorname']);
$this->assertEquals($expected['imageauthoremail'], $actual['imageauthoremail']);
$this->assertEquals($expected['imageauthorurl'], $actual['imageauthorurl']);
$this->assertEquals($expected['imagecaption'], $actual['imagecaption']);
$this->assertEquals($expected['badgeurl'], $actual['badgeurl']);
$this->assertEquals($expected['endorsement'] ?? null, $actual['endorsement'] ?? null);
if ($isrecipient || $canconfiguredetails) {
$this->assertTimeCurrent($expected['timecreated']);
$this->assertTimeCurrent($expected['timemodified']);
$this->assertEquals($expected['usercreated'], $actual['usercreated']);
$this->assertEquals($expected['usermodified'], $actual['usermodified']);
$this->assertEquals($expected['expiredate'], $actual['expiredate']);
$this->assertEquals($expected['expireperiod'], $actual['expireperiod']);
$this->assertEquals($expected['courseid'], $actual['courseid']);
$this->assertEquals($expected['message'], $actual['message']);
$this->assertEquals($expected['messagesubject'], $actual['messagesubject']);
$this->assertEquals($expected['attachment'], $actual['attachment']);
$this->assertEquals($expected['notification'], $actual['notification']);
$this->assertEquals($expected['nextcron'], $actual['nextcron']);
$this->assertEquals($expected['status'], $actual['status']);
$this->assertEquals($expected['issuedid'], $actual['issuedid']);
$this->assertEquals($expected['visible'], $actual['visible']);
$this->assertEquals($expected['email'], $actual['email']);
} else {
$this->assertEquals(0, $actual['timecreated']);
$this->assertEquals(0, $actual['timemodified']);
$this->assertArrayNotHasKey('usercreated', $actual);
$this->assertArrayNotHasKey('usermodified', $actual);
$this->assertArrayNotHasKey('expiredate', $actual);
$this->assertArrayNotHasKey('expireperiod', $actual);
$this->assertArrayNotHasKey('courseid', $actual);
$this->assertArrayNotHasKey('message', $actual);
$this->assertArrayNotHasKey('messagesubject', $actual);
$this->assertEquals(1, $actual['attachment']);
$this->assertEquals(1, $actual['notification']);
$this->assertArrayNotHasKey('nextcron', $actual);
$this->assertEquals(0, $actual['status']);
$this->assertArrayNotHasKey('issuedid', $actual);
$this->assertEquals(0, $actual['visible']);
$this->assertArrayNotHasKey('email', $actual);
}
$alignments = $expected['alignment'];
if (!$canconfiguredetails) {
foreach ($alignments as $index => $alignment) {
$alignments[$index] = [
'id' => $alignment['id'],
'badgeid' => $alignment['badgeid'],
'targetName' => $alignment['targetName'],
'targetUrl' => $alignment['targetUrl'],
];
}
}
$this->assertEquals($alignments, $actual['alignment']);
$relatedbadges = $expected['relatedbadges'];
if (!$canconfiguredetails) {
foreach ($relatedbadges as $index => $relatedbadge) {
$relatedbadges[$index] = [
'id' => $relatedbadge['id'],
'name' => $relatedbadge['name'],
];
}
}
$this->assertEquals($relatedbadges, $actual['relatedbadges']);
}
/**
* Creates and returns test data for external functions.
*
* The test data includes:
* - A teacher.
* - A student.
* - A site badge, with an endorsment and 2 alignments, issued to the student by the teacher.
* - A course badge, related to the site badge, issued to the student by the teacher.
*
* @return array
*/
private function prepare_test_data(): array {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$generator = $this->getDataGenerator();
// Setup test data.
$course = $generator->create_course();
// Create users and enrolments.
$student = $generator->create_and_enrol($course);
$teacher = $generator->create_and_enrol($course, 'editingteacher');
$badgegenerator = $generator->get_plugin_generator('core_badges');
$systemcontext = \context_system::instance();
$coursecontext = \context_course::instance($course->id);
// Create a site badge.
$now = time();
$sitebadge = $badgegenerator->create_badge([
'name' => "Test badge site",
'description' => "Testing badges site",
'timecreated' => $now,
'timemodified' => $now,
'usercreated' => (int) $teacher->id,
'usermodified' => (int) $teacher->id,
'expiredate' => $now + YEARSECS,
'expireperiod' => YEARSECS,
'type' => BADGE_TYPE_SITE,
]);
$sitebadge->issue($student->id, true);
$siteissuedbadge = $DB->get_record('badge_issued', [ 'badgeid' => $sitebadge->id ]);
// Change issued time to ensure badges are fetched in a consistent order.
$siteissuedbadge->dateissued = $now - 1;
$DB->update_record('badge_issued', $siteissuedbadge);
$sitebadgedata = [
...(array) $sitebadge,
'issuedid' => (int) $siteissuedbadge->id,
'uniquehash' => $siteissuedbadge->uniquehash,
'dateissued' => (int) $siteissuedbadge->dateissued,
'dateexpire' => $siteissuedbadge->dateexpire,
'visible' => (int) $siteissuedbadge->visible,
'badgeurl' => \moodle_url::make_webservice_pluginfile_url($systemcontext->id, 'badges', 'badgeimage',
$sitebadge->id, '/', 'f3')->out(false),
'email' => $student->email,
'endorsement' => null,
'alignment' => [],
'relatedbadges' => [],
];
// Add an endorsement for the site badge.
$endorsement = new \stdClass();
$endorsement->badgeid = $sitebadge->id;
$endorsement->issuername = 'Issuer name';
$endorsement->issuerurl = 'http://endorsement-issuer-url.domain.co.nz';
$endorsement->issueremail = 'endorsementissuer@example.com';
$endorsement->claimid = 'http://claim-url.domain.co.nz';
$endorsement->claimcomment = 'Claim comment';
$endorsement->dateissued = $now;
$endorsement->id = $sitebadge->save_endorsement($endorsement);
$sitebadgedata['endorsement'] = (array) $endorsement;
// Add 2 alignments to the site badge.
$alignment = new \stdClass();
$alignment->badgeid = $sitebadge->id;
$alignment->targetname = 'Alignment 1';
$alignment->targeturl = 'http://a1-target-url.domain.co.nz';
$alignment->targetdescription = 'A1 target description';
$alignment->targetframework = 'A1 framework';
$alignment->targetcode = 'A1 code';
$alignment->id = $sitebadge->save_alignment($alignment);
$sitebadgedata['alignment'][] = [
'id' => $alignment->id,
'badgeid' => $alignment->badgeid,
'targetName' => $alignment->targetname,
'targetUrl' => $alignment->targeturl,
'targetDescription' => $alignment->targetdescription,
'targetFramework' => $alignment->targetframework,
'targetCode' => $alignment->targetcode,
];
$alignment = new \stdClass();
$alignment->badgeid = $sitebadge->id;
$alignment->targetname = 'Alignment 2';
$alignment->targeturl = 'http://a2-target-url.domain.co.nz';
$alignment->targetdescription = 'A2 target description';
$alignment->targetframework = 'A2 framework';
$alignment->targetcode = 'A2 code';
$alignment->id = $sitebadge->save_alignment($alignment);
$sitebadgedata['alignment'][] = [
'id' => $alignment->id,
'badgeid' => $alignment->badgeid,
'targetName' => $alignment->targetname,
'targetUrl' => $alignment->targeturl,
'targetDescription' => $alignment->targetdescription,
'targetFramework' => $alignment->targetframework,
'targetCode' => $alignment->targetcode,
];
// Create a course badge.
$coursebadge = $badgegenerator->create_badge([
'name' => "Test badge course",
'description' => "Testing badges course",
'timecreated' => $now,
'timemodified' => $now,
'usercreated' => (int) $teacher->id,
'usermodified' => (int) $teacher->id,
'expiredate' => $now + YEARSECS,
'expireperiod' => YEARSECS,
'type' => BADGE_TYPE_COURSE,
'courseid' => (int) $course->id,
]);
$coursebadge->issue($student->id, true);
$courseissuedbadge = $DB->get_record('badge_issued', [ 'badgeid' => $coursebadge->id ]);
$coursebadgedata = [
...(array) $coursebadge,
'issuedid' => (int) $courseissuedbadge->id,
'uniquehash' => $courseissuedbadge->uniquehash,
'dateissued' => (int) $courseissuedbadge->dateissued,
'dateexpire' => $courseissuedbadge->dateexpire,
'visible' => (int) $courseissuedbadge->visible,
'badgeurl' => \moodle_url::make_webservice_pluginfile_url($coursecontext->id, 'badges', 'badgeimage',
$coursebadge->id, '/', 'f3')->out(false),
'email' => $student->email,
'endorsement' => null,
'alignment' => [],
'relatedbadges' => [],
];
// Add the course badge to the site badge.
$sitebadge->add_related_badges([$coursebadge->id]);
$sitebadgedata['relatedbadges'][] = [
'id' => (int) $coursebadge->id,
'name' => $coursebadge->name,
'version' => $coursebadge->version,
'language' => $coursebadge->language,
'type' => $coursebadge->type,
];
$coursebadgedata['relatedbadges'][] = [
'id' => (int) $sitebadge->id,
'name' => $sitebadge->name,
'version' => $sitebadge->version,
'language' => $sitebadge->language,
'type' => $sitebadge->type,
];
return [
'coursebadge' => $coursebadgedata,
'sitebadge' => $sitebadgedata,
'student' => $student,
'teacher' => $teacher,
];
}
}
+31 -185
View File
@@ -27,6 +27,7 @@
namespace core_badges\external;
use core_badges_external;
use core_badges\tests\external_helper;
use core_external\external_api;
use core_external\external_settings;
use externallib_advanced_testcase;
@@ -48,221 +49,66 @@ require_once($CFG->libdir . '/badgeslib.php');
* @since Moodle 3.1
*/
final class external_test extends externallib_advanced_testcase {
/** @var stdClass $course */
private $course;
/** @var stdClass $student */
private $student;
/** @var stdClass $teacher */
private $teacher;
/**
* Set up for every test
*/
public function setUp(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Setup test data.
$this->course = $this->getDataGenerator()->create_course();
// Create users and enrolments.
$this->student = $this->getDataGenerator()->create_and_enrol($this->course, 'student');
$this->teacher = $this->getDataGenerator()->create_and_enrol($this->course, 'editingteacher');
// Mock up a site badge.
$now = time();
$badge = new \stdClass();
$badge->id = null;
$badge->name = "Test badge site";
$badge->description = "Testing badges site";
$badge->timecreated = $now - 12;
$badge->timemodified = $now - 12;
$badge->usercreated = $this->teacher->id;
$badge->usermodified = $this->teacher->id;
$badge->issuername = "Test issuer";
$badge->issuerurl = "http://issuer-url.domain.co.nz";
$badge->issuercontact = "issuer@example.com";
$badge->expiredate = null;
$badge->expireperiod = null;
$badge->type = BADGE_TYPE_SITE;
$badge->courseid = null;
$badge->messagesubject = "Test message subject for badge";
$badge->message = "Test message body for badge";
$badge->attachment = 1;
$badge->notification = 0;
$badge->status = BADGE_STATUS_ACTIVE;
$badge->version = '1';
$badge->language = 'en';
$badge->imageauthorname = 'Image author';
$badge->imageauthoremail = 'imageauthor@example.com';
$badge->imageauthorurl = 'http://image-author-url.domain.co.nz';
$badge->imagecaption = 'Caption';
$badgeid = $DB->insert_record('badge', $badge, true);
$badge = new \badge($badgeid);
$badge->issue($this->student->id, true);
// Hack the database to adjust the time each badge was issued.
$DB->set_field('badge_issued', 'dateissued', $now - 11, array('userid' => $this->student->id, 'badgeid' => $badgeid));
// Add an endorsement for the badge.
$endorsement = new \stdClass();
$endorsement->badgeid = $badgeid;
$endorsement->issuername = 'Issuer name';
$endorsement->issuerurl = 'http://endorsement-issuer-url.domain.co.nz';
$endorsement->issueremail = 'endorsementissuer@example.com';
$endorsement->claimid = 'http://claim-url.domain.co.nz';
$endorsement->claimcomment = 'Claim comment';
$endorsement->dateissued = $now;
$badge->save_endorsement($endorsement);
// Add 2 alignments.
$alignment = new \stdClass();
$alignment->badgeid = $badgeid;
$alignment->targetname = 'Alignment 1';
$alignment->targeturl = 'http://a1-target-url.domain.co.nz';
$alignment->targetdescription = 'A1 target description';
$alignment->targetframework = 'A1 framework';
$alignment->targetcode = 'A1 code';
$badge->save_alignment($alignment);
$alignment->targetname = 'Alignment 2';
$alignment->targeturl = 'http://a2-target-url.domain.co.nz';
$alignment->targetdescription = 'A2 target description';
$alignment->targetframework = 'A2 framework';
$alignment->targetcode = 'A2 code';
$badge->save_alignment($alignment);
// Now a course badge.
$badge->id = null;
$badge->name = "Test badge course";
$badge->description = "Testing badges course";
$badge->type = BADGE_TYPE_COURSE;
$badge->courseid = $this->course->id;
$coursebadgeid = $DB->insert_record('badge', $badge, true);
$badge = new \badge($coursebadgeid);
$badge->issue($this->student->id, true);
// Hack the database to adjust the time each badge was issued.
$DB->set_field('badge_issued', 'dateissued', $now - 10, array('userid' => $this->student->id, 'badgeid' => $coursebadgeid));
// Make the site badge a related badge.
$badge->add_related_badges(array($badgeid));
}
use external_helper;
/**
* Test get user badges.
* These is a basic test since the badges_get_my_user_badges used by the external function already has unit tests.
*
* @covers \core_badges_external::get_user_badges
*/
public function test_get_my_user_badges(): void {
$data = $this->prepare_test_data();
$this->setUser($this->student);
$badges = (array) badges_get_user_badges($this->student->id);
$expectedbadges = array();
$coursebadge = null;
foreach ($badges as $badge) {
$context = ($badge->type == BADGE_TYPE_SITE) ? \context_system::instance() : \context_course::instance($badge->courseid);
$badge->badgeurl = \moodle_url::make_webservice_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/',
'f3')->out(false);
// Get the endorsement, alignments and related badges.
$badgeinstance = new \badge($badge->id);
$endorsement = $badgeinstance->get_endorsement();
$alignments = $badgeinstance->get_alignments();
$relatedbadges = $badgeinstance->get_related_badges();
$badge->alignment = array();
$badge->relatedbadges = array();
if ($endorsement) {
$badge->endorsement = (array) $endorsement;
}
if (!empty($alignments)) {
foreach ($alignments as $alignment) {
// Students cannot see some fields of the alignments.
unset($alignment->targetname);
unset($alignment->targeturl);
unset($alignment->targetdescription);
unset($alignment->targetframework);
unset($alignment->targetcode);
$badge->alignment[] = (array) $alignment;
}
}
if (!empty($relatedbadges)) {
foreach ($relatedbadges as $relatedbadge) {
// Students cannot see some fields of the related badges.
unset($relatedbadge->version);
unset($relatedbadge->language);
unset($relatedbadge->type);
$badge->relatedbadges[] = (array) $relatedbadge;
}
}
$expectedbadges[] = (array) $badge;
if (isset($badge->courseid)) {
// Save the course badge to be able to compare it in our tests.
$coursebadge = (array) $badge;
}
}
$this->setUser($data['student']);
$result = core_badges_external::get_user_badges();
$result = external_api::clean_returnvalue(core_badges_external::get_user_badges_returns(), $result);
$this->assertEquals($expectedbadges, $result['badges']);
$this->assertCount(2, $result['badges']);
$this->assert_issued_badge($data['coursebadge'], $result['badges'][0], true, false);
$this->assert_issued_badge($data['sitebadge'], $result['badges'][1], true, false);
// Pagination and filtering.
$result = core_badges_external::get_user_badges(0, $this->course->id, 0, 1, '', true);
$result = core_badges_external::get_user_badges(0, 0, 0, 1, '', true);
$result = external_api::clean_returnvalue(core_badges_external::get_user_badges_returns(), $result);
$this->assertCount(1, $result['badges']);
$this->assertEquals($coursebadge, $result['badges'][0]);
$this->assert_issued_badge($data['coursebadge'], $result['badges'][0], true, false);
}
/**
* Test get user badges.
*
* @covers \core_badges_external::get_user_badges
*/
public function test_get_other_user_badges(): void {
$data = $this->prepare_test_data();
$this->setUser($this->teacher);
$result = core_badges_external::get_user_badges($this->student->id);
// User with "moodle/badges:configuredetails" capability.
$this->setAdminUser();
$result = core_badges_external::get_user_badges($data['student']->id);
$result = external_api::clean_returnvalue(core_badges_external::get_user_badges_returns(), $result);
$this->assertCount(2, $result['badges']);
$this->assert_issued_badge($data['coursebadge'], $result['badges'][0], false, true);
$this->assert_issued_badge($data['sitebadge'], $result['badges'][1], false, true);
// Check that we don't have permissions for view the complete information for site badges.
foreach ($result['badges'] as $badge) {
if (isset($badge['type']) and $badge['type'] == BADGE_TYPE_COURSE) {
$this->assertTrue(isset($badge['message']));
// Check that we have permissions to see all the data in alignments and related badges.
foreach ($badge['alignment'] as $alignment) {
$this->assertTrue(isset($alignment['targetdescription']));
}
foreach ($badge['relatedbadges'] as $relatedbadge) {
$this->assertTrue(isset($relatedbadge['type']));
}
} else {
$this->assertFalse(isset($badge['message']));
}
}
// User without "moodle/badges:configuredetails" capability.
$this->setUser($this->getDataGenerator()->create_user());
$result = core_badges_external::get_user_badges($data['student']->id);
$result = external_api::clean_returnvalue(core_badges_external::get_user_badges_returns(), $result);
$this->assertCount(2, $result['badges']);
$this->assert_issued_badge($data['coursebadge'], $result['badges'][0], false, false);
$this->assert_issued_badge($data['sitebadge'], $result['badges'][1], false, false);
}
/**
* Test get_user_badges where issuername contains text to be filtered
*
* @covers \core_badges_external::get_user_badges
*/
public function test_get_user_badges_filter_issuername(): void {
global $DB;
$data = $this->prepare_test_data();
filter_set_global_state('multilang', TEXTFILTER_ON);
filter_set_applies_to_strings('multilang', true);
@@ -273,7 +119,7 @@ final class external_test extends externallib_advanced_testcase {
$DB->set_field('badge', 'issuername', $issuername, ['name' => 'Test badge site']);
// Retrieve student badges.
$result = core_badges_external::get_user_badges($this->student->id);
$result = core_badges_external::get_user_badges($data['student']->id);
$result = external_api::clean_returnvalue(core_badges_external::get_user_badges_returns(), $result);
// Site badge will be last, because it has the earlier issued date.
+38 -187
View File
@@ -16,6 +16,7 @@
namespace core_badges\external;
use core_badges\tests\external_helper;
use externallib_advanced_testcase;
defined('MOODLE_INTERNAL') || die();
@@ -23,7 +24,6 @@ defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
require_once($CFG->libdir . '/badgeslib.php');
/**
* Tests for external function get_user_badge_by_hash.
@@ -36,141 +36,7 @@ require_once($CFG->libdir . '/badgeslib.php');
* @coversDefaultClass \core_badges\external\get_user_badge_by_hash
*/
final class get_user_badge_by_hash_test extends externallib_advanced_testcase {
/**
* Prepare the test.
*
* @return array
*/
private function prepare_test_data(): array {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Setup test data.
$course = $this->getDataGenerator()->create_course();
// Create users and enrolments.
$student1 = $this->getDataGenerator()->create_and_enrol($course);
$student2 = $this->getDataGenerator()->create_and_enrol($course);
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
// Mock up a site badge.
$now = time();
$badge = new \stdClass();
$badge->id = null;
$badge->name = "Test badge site";
$badge->description = "Testing badges site";
$badge->timecreated = $now;
$badge->timemodified = $now;
$badge->usercreated = (int) $teacher->id;
$badge->usermodified = (int) $teacher->id;
$badge->expiredate = null;
$badge->expireperiod = null;
$badge->type = BADGE_TYPE_SITE;
$badge->courseid = null;
$badge->messagesubject = "Test message subject for badge";
$badge->message = "Test message body for badge";
$badge->attachment = 1;
$badge->notification = 0;
$badge->status = BADGE_STATUS_ACTIVE;
$badge->version = '1';
$badge->language = 'en';
$badge->imageauthorname = 'Image author';
$badge->imageauthoremail = 'imageauthor@example.com';
$badge->imageauthorurl = 'http://image-author-url.domain.co.nz';
$badge->imagecaption = 'Caption';
$badgeid = $DB->insert_record('badge', $badge, true);
$badge->id = $badgeid;
$sitebadge = new \badge($badgeid);
$sitebadge->issue($student1->id, true);
$siteissuedbadge = $DB->get_record('badge_issued', [ 'badgeid' => $badge->id ]);
$badge->issuername = $sitebadge->issuername;
$badge->issuercontact = $sitebadge->issuercontact;
$badge->issuerurl = $sitebadge->issuerurl;
$badge->nextcron = $sitebadge->nextcron;
$badge->issuedid = (int) $siteissuedbadge->id;
$badge->uniquehash = $siteissuedbadge->uniquehash;
$badge->dateissued = (int) $siteissuedbadge->dateissued;
$badge->dateexpire = $siteissuedbadge->dateexpire;
$badge->visible = (int) $siteissuedbadge->visible;
$badge->email = $student1->email;
$context = \context_system::instance();
$badge->badgeurl = \moodle_url::make_webservice_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/',
'f3')->out(false);
$badge->status = BADGE_STATUS_ACTIVE_LOCKED;
// Add an endorsement for the badge.
$endorsement = new \stdClass();
$endorsement->badgeid = $badgeid;
$endorsement->issuername = 'Issuer name';
$endorsement->issuerurl = 'http://endorsement-issuer-url.domain.co.nz';
$endorsement->issueremail = 'endorsementissuer@example.com';
$endorsement->claimid = 'http://claim-url.domain.co.nz';
$endorsement->claimcomment = 'Claim comment';
$endorsement->dateissued = $now;
$endorsement->id = $sitebadge->save_endorsement($endorsement);
$badge->endorsement = (array) $endorsement;
// Add 2 alignments.
$alignment = new \stdClass();
$alignment->badgeid = $badgeid;
$alignment->id = $sitebadge->save_alignment($alignment);
$badge->alignment[] = (array) $alignment;
$alignment->id = $sitebadge->save_alignment($alignment);
$badge->alignment[] = (array) $alignment;
$badge->relatedbadges = [];
$usersitebadge[] = (array) $badge;
// Now a course badge.
$badge->id = null;
$badge->name = "Test badge course";
$badge->description = "Testing badges course";
$badge->type = BADGE_TYPE_COURSE;
$badge->courseid = (int) $course->id;
$badge->id = $DB->insert_record('badge', $badge, true);
$coursebadge = new \badge($badge->id );
$coursebadge->issue($student1->id, true);
$courseissuedbadge = $DB->get_record('badge_issued', [ 'badgeid' => $badge->id ]);
$badge->issuername = $coursebadge->issuername;
$badge->issuercontact = $coursebadge->issuercontact;
$badge->issuerurl = $coursebadge->issuerurl;
$badge->nextcron = $coursebadge->nextcron;
$badge->issuedid = (int) $courseissuedbadge->id;
$badge->uniquehash = $courseissuedbadge->uniquehash;
$badge->dateissued = (int) $courseissuedbadge->dateissued;
$badge->dateexpire = $courseissuedbadge->dateexpire;
$badge->visible = (int) $courseissuedbadge->visible;
$badge->email = $student1->email;
$context = \context_course::instance($badge->courseid);
$badge->badgeurl = \moodle_url::make_webservice_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id , '/',
'f3')->out(false);
unset($badge->endorsement);
$badge->alignment = [];
$usercoursebadge[] = (array) $badge;
// Make the site badge a related badge.
$sitebadge->add_related_badges([$badge->id]);
$usersitebadge[0]['relatedbadges'][0] = [
'id' => (int) $coursebadge->id,
'name' => $coursebadge->name
];
$usercoursebadge[0]['relatedbadges'][0] = [
'id' => (int) $sitebadge->id,
'name' => $sitebadge->name
];
return [
'coursebadge' => $usercoursebadge,
'sitebadge' => $usersitebadge,
'student1' => $student1,
'student2' => $student2
];
}
use external_helper;
/**
* Test get user badge by hash.
@@ -179,18 +45,47 @@ final class get_user_badge_by_hash_test extends externallib_advanced_testcase {
*/
public function test_get_user_badge_by_hash(): void {
$data = $this->prepare_test_data();
$this->setUser($data['student1']);
// Site badge.
$result = get_user_badge_by_hash::execute($data['sitebadge'][0]['uniquehash']);
// Site badge fetched by recipient.
$this->setUser($data['student']);
$result = get_user_badge_by_hash::execute($data['sitebadge']['uniquehash']);
$result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result);
$this->assertEquals($data['sitebadge'][0]['uniquehash'], $result['badge'][0]['uniquehash']);
$this->assert_issued_badge($data['sitebadge'], $result['badge'][0], true, false);
$this->assertEmpty($result['warnings']);
// Course badge.
$result = get_user_badge_by_hash::execute($data['coursebadge'][0]['uniquehash']);
// Site badge fetched by user without "moodle/badges:configuredetails" capability.
$this->setGuestUser();
$result = get_user_badge_by_hash::execute($data['sitebadge']['uniquehash']);
$result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result);
$this->assertEquals($data['coursebadge'][0]['uniquehash'], $result['badge'][0]['uniquehash']);
$this->assert_issued_badge($data['sitebadge'], $result['badge'][0], false, false);
$this->assertEmpty($result['warnings']);
// Site badge fetched by user with "moodle/badges:configuredetails" capability.
$this->setAdminUser();
$result = get_user_badge_by_hash::execute($data['sitebadge']['uniquehash']);
$result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result);
$this->assert_issued_badge($data['sitebadge'], $result['badge'][0], false, true);
$this->assertEmpty($result['warnings']);
// Course badge fetched by recipient.
$this->setUser($data['student']);
$result = get_user_badge_by_hash::execute($data['coursebadge']['uniquehash']);
$result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result);
$this->assert_issued_badge($data['coursebadge'], $result['badge'][0], true, false);
$this->assertEmpty($result['warnings']);
// Course badge fetched by user without "moodle/badges:configuredetails" capability.
$this->setGuestUser();
$result = get_user_badge_by_hash::execute($data['coursebadge']['uniquehash']);
$result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result);
$this->assert_issued_badge($data['coursebadge'], $result['badge'][0], false, false);
$this->assertEmpty($result['warnings']);
// Course badge fetched by user with "moodle/badges:configuredetails" capability.
$this->setAdminUser();
$result = get_user_badge_by_hash::execute($data['coursebadge']['uniquehash']);
$result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result);
$this->assert_issued_badge($data['coursebadge'], $result['badge'][0], false, true);
$this->assertEmpty($result['warnings']);
// Wrong hash.
@@ -200,48 +95,4 @@ final class get_user_badge_by_hash_test extends externallib_advanced_testcase {
$this->assertNotEmpty($result['warnings']);
$this->assertEquals('badgeawardnotfound', $result['warnings'][0]['warningcode']);
}
/**
* Test get user badge by hash with restrictions.
* @covers ::execute
*/
public function test_get_user_badge_by_hash_with_restrictions(): void {
$data = $this->prepare_test_data();
$this->setUser($data['student2']);
// Site badge.
$result = get_user_badge_by_hash::execute($data['sitebadge'][0]['uniquehash']);
$result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result);
$this->assertNotEmpty($result['badge']);
$this->assertEmpty($result['warnings']);
// Check that we don't have permissions for view the complete information for site badges.
if (isset($result['badge'][0]['type']) && $result['badge'][0]['type'] == BADGE_TYPE_SITE) {
$this->assertFalse(isset($result['badge'][0]['message']));
// Check that we have permissions to see all the data in alignments and related badges.
foreach ($result['badge'][0]['alignment'] as $alignment) {
$this->assertTrue(isset($alignment['id']));
}
foreach ($result['badge'][0]['relatedbadges'] as $relatedbadge) {
$this->assertTrue(isset($relatedbadge['id']));
}
} else {
$this->assertTrue(isset($result['badge'][0]['message']));
}
// Course badge.
$result = get_user_badge_by_hash::execute($data['coursebadge'][0]['uniquehash']);
$result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result);
$this->assertNotEmpty($result['badge']);
$this->assertEmpty($result['warnings']);
// Check that we don't have permissions for view the complete information for course badges.
if (isset($result['badge'][0]['type']) && $result['badge'][0]['type'] == BADGE_TYPE_COURSE) {
$this->assertFalse(isset($result['badge'][0]['message']));
} else {
$this->assertTrue(isset($result['badge'][0]['message']));
}
}
}
+16 -6
View File
@@ -15,7 +15,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Script to let a user edit the properties of a particular RSS feed.
* Script to let a user view the output of a particular RSS feed.
*
* @package block_rss_client
* @copyright 2009 Tim Hunt
@@ -26,9 +26,6 @@ require_once(__DIR__ . '/../../config.php');
require_once($CFG->libdir .'/simplepie/moodle_simplepie.php');
require_login();
if (isguestuser()) {
throw new \moodle_exception('guestsarenotallowed');
}
$returnurl = optional_param('returnurl', '', PARAM_LOCALURL);
$courseid = optional_param('courseid', 0, PARAM_INT);
@@ -46,6 +43,11 @@ if ($courseid) {
$PAGE->set_context($context);
}
$managesharedfeeds = has_capability('block/rss_client:manageanyfeeds', $context);
if (!$managesharedfeeds) {
require_capability('block/rss_client:manageownfeeds', $context);
}
$urlparams = array('rssid' => $rssid);
if ($courseid) {
$urlparams['courseid'] = $courseid;
@@ -56,10 +58,18 @@ if ($returnurl) {
$PAGE->set_url('/blocks/rss_client/viewfeed.php', $urlparams);
$PAGE->set_pagelayout('popup');
$rssrecord = $DB->get_record('block_rss_client', array('id' => $rssid), '*', MUST_EXIST);
if ($managesharedfeeds) {
$select = 'id = :id AND (userid = :userid OR shared = 1)';
} else {
$select = 'id = :id AND userid = :userid';
}
$rssrecord = $DB->get_record_select('block_rss_client', $select, [
'id' => $rssid,
'userid' => $USER->id,
], '*', MUST_EXIST);
$rss = new moodle_simplepie($rssrecord->url);
if ($rss->error()) {
debugging($rss->error());
throw new \moodle_exception('errorfetchingrssfeed');
+26 -17
View File
@@ -34,15 +34,21 @@ $PAGE->set_url(new moodle_url('/blog/external_blogs.php'));
require_capability('moodle/blog:manageexternal', $context);
$delete = optional_param('delete', null, PARAM_INT);
$confirm = optional_param('confirm', false, PARAM_BOOL);
$strexternalblogs = get_string('externalblogs', 'blog');
$straddnewexternalblog = get_string('addnewexternalblog', 'blog');
$strblogs = get_string('blogs', 'blog');
$message = null;
if ($delete && confirm_sesskey()) {
$externalblog = $DB->get_record('blog_external', array('id' => $delete));
if ($externalblog->userid == $USER->id) {
$PAGE->set_title("{$strblogs}: {$strexternalblogs}");
$PAGE->set_pagelayout('standard');
if ($delete) {
$externalblog = $DB->get_record('blog_external', ['id' => $delete, 'userid' => $USER->id], '*', MUST_EXIST);
if ($confirm) {
require_sesskey();
// Delete the external blog.
$DB->delete_records('blog_external', array('id' => $delete));
@@ -60,23 +66,28 @@ if ($delete && confirm_sesskey()) {
$event = \core\event\blog_external_removed::create($eventparms);
$event->add_record_snapshot('blog_external', $externalblog);
$event->trigger();
$message = get_string('externalblogdeleted', 'blog');
redirect($PAGE->url, get_string('externalblogdeleted', 'blog'));
} else {
echo $OUTPUT->header();
echo $OUTPUT->heading("{$strexternalblogs}: " . s($externalblog->name), 2);
echo $OUTPUT->confirm(
get_string('deleteexternalblog', 'blog'),
new moodle_url($PAGE->url->out_omit_querystring(), ['delete' => $delete, 'confirm' => 1]),
$PAGE->url,
);
echo $OUTPUT->footer();
die;
}
}
$blogs = $DB->get_records('blog_external', array('userid' => $USER->id));
$PAGE->set_heading(fullname($USER));
$PAGE->set_title("$strblogs: $strexternalblogs");
$PAGE->set_pagelayout('standard');
echo $OUTPUT->header();
echo $OUTPUT->heading($strexternalblogs, 2);
if (!empty($message)) {
echo $OUTPUT->notification($message);
}
echo $OUTPUT->box_start('generalbox boxaligncenter');
if (!empty($blogs)) {
@@ -99,10 +110,8 @@ if (!empty($blogs)) {
$editurl = new moodle_url('/blog/external_blog_edit.php', array('id' => $blog->id));
$editicon = $OUTPUT->action_icon($editurl, new pix_icon('t/edit', get_string('editexternalblog', 'blog')));
$deletelink = new moodle_url('/blog/external_blogs.php', array('delete' => $blog->id, 'sesskey' => sesskey()));
$action = new confirm_action(get_string('externalblogdeleteconfirm', 'blog'));
$deleteicon = $OUTPUT->action_icon($deletelink, new pix_icon('t/delete', get_string('deleteexternalblog', 'blog')),
$action);
$deletelink = new moodle_url('/blog/external_blogs.php', ['delete' => $blog->id]);
$deleteicon = $OUTPUT->action_icon($deletelink, new pix_icon('t/delete', get_string('deleteexternalblog', 'blog')));
$table->data[] = new html_table_row(array($blog->name,
$blog->url,
+3 -3
View File
@@ -915,10 +915,10 @@ class core_calendar_external extends external_api {
$formoptions['eventtypes'] = $allowedeeventtypes;
if ($courseid) {
require_once($CFG->libdir . '/grouplib.php');
$groupcoursedata = groups_get_course_data($courseid);
if (!empty($groupcoursedata->groups)) {
$groupcoursedata = groups_get_all_groups($courseid);
if (!empty($groupcoursedata)) {
$formoptions['groups'] = [];
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
foreach ($groupcoursedata as $groupid => $groupdata) {
$formoptions['groups'][$groupid] = $groupdata->name;
}
}
+4 -4
View File
@@ -3639,9 +3639,9 @@ function calendar_output_fragment_event_form($args) {
if (is_null($eventid)) {
if (!empty($courseid)) {
$groupcoursedata = groups_get_course_data($courseid);
$groupcoursedata = groups_get_all_groups($courseid);
$formoptions['groups'] = [];
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
foreach ($groupcoursedata as $groupid => $groupdata) {
$formoptions['groups'][$groupid] = $groupdata->name;
}
}
@@ -3683,9 +3683,9 @@ function calendar_output_fragment_event_form($args) {
$formoptions['event'] = $event;
if (!empty($event->courseid)) {
$groupcoursedata = groups_get_course_data($event->courseid);
$groupcoursedata = groups_get_all_groups($event->courseid);
$formoptions['groups'] = [];
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
foreach ($groupcoursedata as $groupid => $groupdata) {
$formoptions['groups'][$groupid] = $groupdata->name;
}
}
+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
*/
@@ -55,11 +55,11 @@ class cohorts extends system_report {
"{$entitymainalias}.component");
// Check if report needs to show a specific category.
$contextid = $this->get_parameter('contextid', 0, PARAM_INT);
$showall = $this->get_parameter('showall', true, PARAM_BOOL);
if (!$showall) {
if (!$this->get_context() instanceof context_system || !$this->get_parameter('showall', false, PARAM_BOOL)) {
$paramcontextid = database::generate_param_name();
$this->add_base_condition_sql("{$entitymainalias}.contextid = :$paramcontextid", [$paramcontextid => $contextid]);
$this->add_base_condition_sql("{$entitymainalias}.contextid = :{$paramcontextid}", [
$paramcontextid => $this->get_context()->id,
]);
}
// Now we can call our helper methods to add the content we want to include in the report.
@@ -77,14 +77,7 @@ class cohorts extends system_report {
* @return bool
*/
protected function can_view(): bool {
$contextid = $this->get_parameter('contextid', 0, PARAM_INT);
if ($contextid) {
$context = context::instance_by_id($contextid, MUST_EXIST);
} else {
$context = context_system::instance();
}
return has_any_capability(['moodle/cohort:manage', 'moodle/cohort:view'], $context);
return has_any_capability(['moodle/cohort:manage', 'moodle/cohort:view'], $this->get_context());
}
/**
@@ -98,10 +91,8 @@ class cohorts extends system_report {
public function add_columns(cohort $cohortentity): void {
$entitymainalias = $cohortentity->get_table_alias('cohort');
$showall = $this->get_parameter('showall', false, PARAM_BOOL);
// Category column. An extra callback is appended in order to extend the current column formatting.
if ($showall) {
if ($this->get_context() instanceof context_system && $this->get_parameter('showall', false, PARAM_BOOL)) {
$this->add_column_from_entity('cohort:context')
->add_callback(static function(string $value, stdClass $cohort): string {
$context = context::instance_by_id($cohort->contextid);
@@ -195,10 +186,11 @@ class cohorts extends system_report {
*/
protected function add_actions(): void {
$contextid = $this->get_parameter('contextid', 0, PARAM_INT);
$showall = $this->get_parameter('showall', true, PARAM_BOOL);
$returnurl = (new moodle_url('/cohort/index.php',
['id' => ':id', 'contextid' => $contextid, 'showall' => $showall]))->out(false);
$returnurl = (new moodle_url('/cohort/index.php', [
'id' => ':id',
'contextid' => $this->get_context()->id,
'showall' => $this->get_parameter('showall', false, PARAM_BOOL),
]))->out(false);
// Hide action. It will be only shown if the property 'visible' is true and user has 'moodle/cohort:manage' capabillity.
$this->add_action((new action(
+6
View File
@@ -405,6 +405,12 @@ class core_cohort_external extends external_api {
$results = $results + cohort_get_available_cohorts($context, COHORT_ALL, $limitfrom, $limitnum, $query);
}
} else if ($includes == 'all') {
$contextsystem = context_system::instance();
if (!$context instanceof context_system &&
!has_any_capability(['moodle/cohort:view', 'moodle/cohort:manage'], $contextsystem)) {
throw new required_capability_exception($contextsystem, 'moodle/cohort:view', 'nopermissions', '');
}
$results = cohort_get_all_cohorts($limitfrom, $limitnum, $query);
$results = $results['cohorts'];
} else {
+1 -2
View File
@@ -104,8 +104,7 @@ if ($editcontrols = cohort_edit_controls($context, $baseurl)) {
echo $OUTPUT->render($editcontrols);
}
$reportparams = ['contextid' => $context->id, 'showall' => $showall];
$report = system_report_factory::create(cohorts::class, $context, '', '', 0, $reportparams);
$report = system_report_factory::create(cohorts::class, $context, '', '', 0, ['showall' => $showall]);
// Check if it needs to search by name.
if (!empty($searchquery)) {
+16 -6
View File
@@ -672,6 +672,7 @@ final class externallib_test extends externallib_advanced_testcase {
role_assign($userrole, $catuser->id, $catcontext->id);
// Enrol user in the course.
$this->getDataGenerator()->enrol_user($creator->id, $course->id);
$this->getDataGenerator()->enrol_user($courseuser->id, $course->id, 'courserole');
$syscontext = array('contextid' => \context_system::instance()->id);
@@ -758,18 +759,27 @@ final class externallib_test extends externallib_advanced_testcase {
$this->assertCount(4, $result['cohorts']);
// A user in the course context with the system cohort:view capability. Check that all the system cohorts are returned.
$this->setUser($courseuser);
$result = core_cohort_external::search_cohorts("Cohortsearch", $coursecontext, 'all');
$this->assertCount(2, $result['cohorts']);
$this->assertEquals('Cohortsearch 1', $result['cohorts'][$cohort1->id]->name);
$this->assertCount(4, $result['cohorts']);
// A user in the course context without the ability to view system cohorts.
$this->setUser($courseuser);
try {
$result = core_cohort_external::search_cohorts("Cohortsearch", $coursecontext, 'all');
$this->fail('Exception expected');
} catch (\Throwable $e) {
$this->assertInstanceOf(\required_capability_exception::class, $e);
$this->assertStringContainsString('(View site-wide cohorts)', $e->getMessage());
}
// Detect invalid parameter $includes.
$this->setUser($creator);
try {
$result = core_cohort_external::search_cohorts("Cohortsearch", $syscontext, 'invalid');
$this->fail('Invalid parameter includes');
} catch (\coding_exception $e) {
// All good.
$this->fail('Exception expected');
} catch (\Throwable $e) {
$this->assertInstanceOf(\coding_exception::class, $e);
$this->assertStringContainsString('Invalid parameter value for \'includes\'', $e->getMessage());
}
}
}
@@ -57,7 +57,13 @@ class behat_core_competency_generator extends behat_generator_base {
'singular' => 'plan',
'datagenerator' => 'plan',
'required' => ['name'],
'switchids' => ['user' => 'userid'],
'switchids' => ['user' => 'userid', 'status' => 'status'],
],
'plan_competencies' => [
'singular' => 'plan_competency',
'datagenerator' => 'plan_competency',
'required' => ['plan', 'competency'],
'switchids' => ['competency' => 'competencyid', 'plan' => 'planid'],
],
'related_competencies' => [
'singular' => 'related_competency',
@@ -83,14 +89,38 @@ class behat_core_competency_generator extends behat_generator_base {
'required' => ['plan', 'competency', 'user'],
'switchids' => ['plan' => 'planid', 'competency' => 'competencyid', 'user' => 'userid'],
],
'user_evidence' => [
'singular' => 'user_evidence',
'datagenerator' => 'user_evidence',
'required' => ['user', 'name'],
'switchids' => ['user' => 'userid'],
],
'user_evidence_competency' => [
'singular' => 'user_evidence_competency',
'datagenerator' => 'user_evidence_competency',
'required' => ['userevidence', 'competency'],
'switchids' => ['userevidence' => 'userevidenceid', 'competency' => 'competencyid'],
],
'templates' => [
'singular' => 'template',
'datagenerator' => 'template',
'required' => ['shortname'],
'switchids' => ['context' => 'contextid'],
],
'template_competencies' => [
'singular' => 'template_competency',
'datagenerator' => 'template_competency',
'required' => ['template', 'competency'],
'switchids' => ['template' => 'templateid', 'competency' => 'competencyid'],
],
];
}
/**
* Get the competecy framework id using an idnumber.
* Get the competency framework id using an idnumber.
*
* @param string $idnumber
* @return int The competecy framework id
* @return int The competency framework id
*/
protected function get_competencyframework_id(string $idnumber): int {
global $DB;
@@ -103,10 +133,10 @@ class behat_core_competency_generator extends behat_generator_base {
}
/**
* Get the competecy id using an idnumber.
* Get the competency id using an idnumber.
*
* @param string $idnumber
* @return int The competecy id
* @return int The competency id
*/
protected function get_competency_id(string $idnumber): int {
global $DB;
@@ -135,15 +165,31 @@ class behat_core_competency_generator extends behat_generator_base {
}
/**
* Get the related competecy id using an idnumber.
* Get the related competency id using an idnumber.
*
* @param string $idnumber
* @return int The related competecy id
* @return int The related competency id
*/
protected function get_relatedcompetency_id(string $idnumber): int {
return $this->get_competency_id($idnumber);
}
/**
* Get the template id by shortname.
*
* @param string $shortname The template name.
* @return int
*/
protected function get_template_id(string $shortname): int {
global $DB;
if (!$id = $DB->get_field('competency_template', 'id', ['shortname' => $shortname])) {
throw new Exception('The specified template with name "' . $shortname . '" could not be found.');
}
return $id;
}
/**
* Add a plan.
*
@@ -208,6 +254,8 @@ class behat_core_competency_generator extends behat_generator_base {
* @return array Processed data.
*/
protected function preprocess_plan(array $data): array {
global $DB, $USER;
if (isset($data['competencies'])) {
$competencies = array_map('trim', str_getcsv($data['competencies']));
$data['competencyids'] = array_map([$this, 'get_competency_id'], $competencies);
@@ -215,11 +263,20 @@ class behat_core_competency_generator extends behat_generator_base {
unset($data['competencies']);
}
global $USER;
if (isset($data['reviewer'])) {
if (is_number($data['reviewer'])) {
$data['reviewerid'] = $data['reviewer'];
} else {
if (!$userid = $DB->get_field('user', 'id', ['username' => $data['reviewer']])) {
throw new Exception('The specified user "' . $data['reviewer'] . '" could not be found.');
}
$data['reviewerid'] = $userid;
}
unset($data['reviewer']);
}
return $data + [
'userid' => $USER->id,
'status' => plan::STATUS_ACTIVE,
];
}
@@ -258,4 +315,81 @@ class behat_core_competency_generator extends behat_generator_base {
protected function get_data_generator(): core_competency_generator {
return $this->componentdatagenerator;
}
/**
* Get the user evidence id using a name.
*
* @param string $name User evidence name.
* @return int The user evidence id
*/
protected function get_userevidence_id(string $name): int {
global $DB;
if (!$id = $DB->get_field('competency_userevidence', 'id', ['name' => $name])) {
throw new Exception('The specified user evidence with name "' . $name . '" could not be found.');
}
return $id;
}
/**
* Get the template competency id using a name.
*
* @param string $name Template competency name.
* @return int The template competency id
*/
protected function get_templatecompetency_id(string $name): int {
global $DB;
if (!$id = $DB->get_field('competency_template', 'id', ['name' => $name])) {
throw new Exception('The specified template competency with name "' . $name . '" could not be found.');
}
return $id;
}
/**
* Get the context id using a contextid.
*
* @param string $contextid Context id.
* @return int The context id
*/
protected function get_context_id(string $contextid): int {
global $DB;
if (!$id = $DB->get_field('context', 'id', ['id' => $contextid])) {
throw new Exception('The specified context with id "' . $contextid . '" could not be found.');
}
return $id;
}
/**
* Get the status id by status name.
*
* @param string $name Status name.
* @return int
*/
protected function get_status_id(string $name): int {
switch ($name) {
case 'draft':
$status = plan::STATUS_DRAFT;
break;
case 'in review':
$status = plan::STATUS_IN_REVIEW;
break;
case 'waiting for review':
$status = plan::STATUS_WAITING_FOR_REVIEW;
break;
case 'complete':
$status = plan::STATUS_COMPLETE;
break;
default:
$status = plan::STATUS_ACTIVE;
break;
}
return $status;
}
}
@@ -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?"
+1 -1
View File
@@ -272,7 +272,7 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
if (!$coursecat) {
// Course category not found.
if ($strictness == MUST_EXIST) {
throw new moodle_exception('unknowncategory');
throw new moodle_exception('unknowncategory', a: $id);
}
$coursecat = null;
} else if (!$alwaysreturnhidden && !$coursecat->is_uservisible($user)) {
+35 -10
View File
@@ -941,6 +941,8 @@ class core_course_external extends external_api {
$params = self::validate_parameters(self::create_courses_parameters(),
array('courses' => $courses));
$courseconfig = get_config('moodlecourse');
$availablethemes = core_component::get_plugin_list('theme');
$availablelangs = get_string_manager()->get_list_of_translations();
@@ -967,6 +969,14 @@ class core_course_external extends external_api {
throw new moodle_exception('errorinvalidparam', 'webservice', '', 'shortname');
}
// Make sure start/end date are correctly set.
if (!array_key_exists('startdate', $course)) {
$course['startdate'] = usergetmidnight(time());
}
if (!array_key_exists('enddate', $course) && $courseconfig->courseenddateenabled) {
$course['enddate'] = $course['startdate'] + $courseconfig->courseduration;
}
// Make sure lang is valid
if (array_key_exists('lang', $course)) {
if (empty($availablelangs[$course['lang']])) {
@@ -988,6 +998,8 @@ class core_course_external extends external_api {
}
}
$course['showactivitydates'] = $courseconfig->showactivitydates;
//force visibility if ws user doesn't have the permission to set it
$category = $DB->get_record('course_categories', array('id' => $course['categoryid']));
if (!has_capability('moodle/course:visibility', $context)) {
@@ -995,7 +1007,6 @@ class core_course_external extends external_api {
}
//set default value for completion
$courseconfig = get_config('moodlecourse');
if (completion_info::is_enabled_for_site()) {
if (!array_key_exists('enablecompletion', $course)) {
$course['enablecompletion'] = $courseconfig->enablecompletion;
@@ -4252,7 +4263,7 @@ class core_course_external extends external_api {
self::validate_context($usercontext);
if ($userid != $USER->id and !has_capability('moodle/user:viewdetails', $usercontext)) {
if ($userid != $USER->id && !has_capability('moodle/user:viewalldetails', $usercontext)) {
return array();
}
@@ -4302,22 +4313,28 @@ class core_course_external extends external_api {
* @param int $groupid Group id from which the users will be obtained
* @param bool $onlyactive Whether to return only the active enrolled users or all enrolled users in the course.
* @return array List of users
* @throws invalid_parameter_exception
*/
public static function get_enrolled_users_by_cmid(int $cmid, int $groupid = 0, bool $onlyactive = false) {
global $PAGE;
global $PAGE;
$warnings = [];
self::validate_parameters(self::get_enrolled_users_by_cmid_parameters(), [
'cmid' => $cmid,
'groupid' => $groupid,
'onlyactive' => $onlyactive,
[
'cmid' => $cmid,
'groupid' => $groupid,
'onlyactive' => $onlyactive,
] = self::validate_parameters(self::get_enrolled_users_by_cmid_parameters(), [
'cmid' => $cmid,
'groupid' => $groupid,
'onlyactive' => $onlyactive,
]);
list($course, $cm) = get_course_and_cm_from_cmid($cmid);
$coursecontext = context_course::instance($course->id);
self::validate_context($coursecontext);
course_require_view_participants($coursecontext);
$enrolledusers = get_enrolled_users($coursecontext, '', $groupid, 'u.*', null, 0, 0, $onlyactive);
$users = array_map(function ($user) use ($PAGE) {
@@ -4596,9 +4613,17 @@ class core_course_external extends external_api {
$coursecontext = context_course::instance($courseid);
self::validate_context($coursecontext);
$activeplugin = get_config('core', 'activitychooseractivefooter');
// The active plugin must be set, and be present on the site.
$activeplugin = clean_param(
get_config('core', 'activitychooseractivefooter'),
PARAM_COMPONENT,
);
if ($activeplugin !== COURSE_CHOOSER_FOOTER_NONE) {
if (
$activeplugin !== COURSE_CHOOSER_FOOTER_NONE &&
!empty($activeplugin) &&
core_component::get_component_directory($activeplugin) !== null
) {
$footerdata = component_callback($activeplugin, 'custom_chooser_footer', [$courseid, $sectionid]);
return [
'footer' => true,
File diff suppressed because one or more lines are too long

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