Compare commits

..

283 Commits

Author SHA1 Message Date
Huong Nguyen 71f4153316 weekly release 5.0.6+ 2026-03-27 15:40:52 +07:00
Huong Nguyen e0b5cab5f0 NOBUG: Add upgrade notes 2026-03-27 15:40:51 +07:00
Huong Nguyen a110695eaa Merge branch 'install_500_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_500_STABLE 2026-03-27 15:40:38 +07:00
Huong Nguyen 5db546886a Merge branch 'MDL-81241-500' of https://github.com/junpataleta/moodle into MOODLE_500_STABLE 2026-03-26 10:51:22 +07:00
Huong Nguyen 1ce9d9fc64 Merge branch 'MDL-88279-500' of https://github.com/meirzamoodle/moodle into MOODLE_500_STABLE 2026-03-26 07:49:36 +07:00
AMOS bot 7a07b77216 Automatically generated installer lang files 2026-03-26 00:07:45 +00:00
Huong Nguyen 978614b029 Merge branch 'MDL-77558-500' of https://github.com/davewoloszyn/moodle into MOODLE_500_STABLE 2026-03-25 14:38:06 +07:00
Huong Nguyen ad191cfbb8 Merge branch 'MDL-85451-500' of https://github.com/junpataleta/moodle into MOODLE_500_STABLE 2026-03-25 14:30:07 +07:00
Huong Nguyen 72d5be5541 Merge branch 'MDL-87560-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-03-25 14:27:36 +07:00
David Woloszyn 04aef8439d MDL-77558 mod_forum: Mark posts as read in forum grader 2026-03-25 17:38:48 +11:00
Huong Nguyen e41ba343d3 Merge branch 'MDL-87548-500' of https://github.com/andimendunia/moodle into MOODLE_500_STABLE 2026-03-25 10:50:31 +07:00
Huong Nguyen ca82ae7821 Merge branch 'MDL-88197-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-03-25 09:22:57 +07:00
Huong Nguyen a333518751 Merge branch 'MDL-88076-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-03-25 09:15:53 +07:00
Huong Nguyen c5c3a666aa Merge branch 'MDL-87626-500' of https://github.com/davewoloszyn/moodle into MOODLE_500_STABLE 2026-03-25 09:04:34 +07:00
Huong Nguyen 4f1c7bd20f Merge branch 'MDL-87803-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-03-25 08:59:33 +07:00
Huong Nguyen de6aebd83b Merge branch 'MDL-87580-task-autolinking-MOODLE_500_STABLE' of https://github.com/brendanheywood/moodle into MOODLE_500_STABLE 2026-03-25 08:48:37 +07:00
Paul Holden c378ea7409 MDL-87560 assignsubmission_onlinetext: indicate truncated summary.
Only when the submission has been truncated should we show the
"View more" link, otherwise it appears to not do anything for already
short text.
2026-03-24 12:48:08 +00:00
meirzamoodle 61be76a0cb MDL-88279 core_session: fix cluster test retry count assertion
Update session_redis_cluster_test to expect 3 connection attempts
instead of 5, matching the $maxretries default value in redis.php.
2026-03-24 14:39:44 +07:00
Huong Nguyen 636b899df7 Merge branch 'MDL-88070-500' of https://github.com/sh-csg/moodle into MOODLE_500_STABLE 2026-03-24 08:40:31 +07:00
Huong Nguyen c6c0fab232 Merge branch 'MDL-87748-500' of https://github.com/lucaboesch/moodle into MOODLE_500_STABLE 2026-03-24 08:37:53 +07:00
Huong Nguyen 1838b227de Merge branch 'MDL-51152-500' of https://github.com/christianabila/moodle into MOODLE_500_STABLE 2026-03-24 08:32:37 +07:00
Huong Nguyen bc3c9538a9 Merge branch 'MDL-66415-500' of https://github.com/raortegar/moodle into MOODLE_500_STABLE 2026-03-23 13:46:20 +07:00
Huong Nguyen 44e38356b1 Merge branch 'MDL-87941-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-03-23 13:41:49 +07:00
Huong Nguyen f90654eef6 Merge branch 'MDL-87438-500' of https://github.com/muhammadarnaldo/moodle into MOODLE_500_STABLE 2026-03-23 13:37:13 +07:00
David Woloszyn acad3aa672 MDL-87626 core_search: Fix typo in input value check
Co-authored-by: Uli Wessmann
2026-03-23 13:42:38 +11:00
AMOS bot cb1a01e6ff Automatically generated installer lang files 2026-03-21 00:07:49 +00:00
Luca Bösch 2d21be7a4f MDL-87748 assign: format checkbox with Bootstrap classes. 2026-03-20 08:56:02 +01:00
Mihail Geshoski 4f5d0ad9b0 Merge branch 'MDL-88122-navbar_empty-breadcrumb-M5.0' of https://github.com/stopfstedt/moodle into MOODLE_500_STABLE 2026-03-20 15:35:34 +08:00
Mihail Geshoski c7ef374f46 Merge branch 'MDL-87320-500' of https://github.com/lucaboesch/moodle into MOODLE_500_STABLE 2026-03-20 15:25:48 +08:00
Mihail Geshoski f6ef9913d8 Merge branch 'MDL-84780-500' of https://github.com/sh-csg/moodle into MOODLE_500_STABLE 2026-03-20 15:21:16 +08:00
Mihail Geshoski 7d23b29321 Merge branch 'MDL-83459_500_create_submission_switchids_cmid' of https://github.com/sharpchi/moodle into MOODLE_500_STABLE 2026-03-20 15:16:15 +08:00
Shamim Rezaie 961d7cea4f weekly release 5.0.6+ 2026-03-20 15:11:21 +11:00
Shamim Rezaie 941a996dfe NOBUG: Add upgrade notes 2026-03-20 15:11:20 +11:00
Shamim Rezaie 733db470a2 Merge branch 'install_500_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_500_STABLE 2026-03-20 15:11:04 +11:00
Stefan Hanauska 876a6ed947 MDL-84780 course: Disabled modules are not allowed 2026-03-19 20:47:24 +01:00
Luca Bösch de5ea7e700 MDL-87320 forum: Correctly escape group names. 2026-03-19 17:44:02 +01:00
Brendan Heywood a7ad739746 MDL-87580 task: Fix task output autolinking 2026-03-20 01:42:31 +11:00
Jun Pataleta f5f18cec27 MDL-81241 theme_boost: Accessibility test for the footer region links 2026-03-19 20:31:02 +08:00
Jun Pataleta bd236e173b MDL-81241 theme_boost: Ensure footer links have sufficient spacing 2026-03-19 20:31:01 +08:00
Safat 890b4aae57 MDL-87959 tool_installaddon: Fix behats for activity chooser footer 2026-03-19 12:14:54 +07:00
Mihail Geshoski 41c7b8d641 Merge branch 'MDL-87321-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-03-19 10:49:40 +08:00
Huong Nguyen 569efeb343 Merge branch 'MDL-88080-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-03-19 09:11:31 +07:00
Huong Nguyen d78ddaebc5 Merge branch 'MDL-88054-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-03-19 08:58:13 +07:00
Mihail Geshoski a234c3bea9 Merge branch 'MDL-87810-500' of https://github.com/junpataleta/moodle into MOODLE_500_STABLE 2026-03-19 09:50:13 +08:00
Huong Nguyen 2613dc5b8e Merge branch 'MDL-87935-500' of https://github.com/yusufwib01/moodle into MOODLE_500_STABLE 2026-03-19 08:23:43 +07:00
Huong Nguyen 5e4c29a948 Merge branch 'MDL-87592-500' of https://github.com/andimendunia/moodle into MOODLE_500_STABLE 2026-03-19 08:19:07 +07:00
Mark Sharp e5ef8781a9 MDL-83459 mod_assign: change switchids to use cmid 2026-03-18 15:45:07 +00:00
Ilya Tregubov 8dbd5bde8f Merge branch 'MDL-75067-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-03-18 14:02:12 +01:00
Mihail Geshoski 6dcd98ae8d Merge branch 'MDL-87096-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-03-18 17:36:49 +08:00
Andi Permana cd9aa203b3 MDL-87592 auth_email: preserve wantsurl through MFA email confirmation 2026-03-18 16:06:09 +07:00
Raquel Ortega 41cf49bee1 MDL-66415 core_message: Improve bulk messaging result handling 2026-03-18 09:35:49 +01:00
Jun Pataleta fb5e982a90 MDL-85451 block_timeline: Add accessibility Behat test coverage 2026-03-18 13:24:39 +08:00
Jun Pataleta adb609a38d MDL-85451 block_timeline: Group the due date filter options
To ensure that the filter options for the day filter menu matches
the menu pattern and pass accessibility checks, we need to:
* Group the due date filter options, labelled by the "Due date"
"heading".
* Set the "Due date" heading with a presentation role to prevent
assistive technologies from announcing it when the user goes through
the menu options.
2026-03-18 13:24:38 +08:00
Jun Pataleta 8189c4b741 MDL-85451 block_timeline: Fix heading levels 2026-03-18 13:24:38 +08:00
Paul Holden 6d519e67ba Merge branch 'MDL-88226-500' of https://github.com/dravek/moodle into MOODLE_500_STABLE 2026-03-17 10:29:46 +00:00
David Carrillo 5b1955c073 MDL-88226 phpunit: Fix fragile tests using adhoc task manager
- Reset task manager state when resetting all data
- mod_quiz: Fix fragile adhoc task manager tests
- mod_assign: Fix fragile adhoc task manager tests
2026-03-16 16:38:59 +01:00
Paul Holden 179a1604d9 MDL-88076 tool_mfa: improved parsing of redirect exclusions config.
Ensure all permutations of EOL markers are covered/trimmed.
2026-03-16 11:36:42 +00:00
Huong Nguyen eda03a322b Merge branch 'MDL-86839-MOODLE_500_STABLE' of https://github.com/PhMemmel/moodle into MOODLE_500_STABLE 2026-03-16 16:33:32 +07:00
Shamim Rezaie 4d0e4b7875 Merge branch 'MDL-82625-500' of https://github.com/cameron1729/moodle into MOODLE_500_STABLE 2026-03-16 18:28:19 +11:00
Huong Nguyen ac1d781d5e Merge branch 'MDL-87795-500' of https://github.com/albertgasset/moodle into MOODLE_500_STABLE 2026-03-16 09:46:06 +07:00
Huong Nguyen dff82484c8 Merge branch 'MDL-82439_500' of https://github.com/jonof/moodle into MOODLE_500_STABLE 2026-03-16 08:49:04 +07:00
Jake Dallimore bceaf184ba Merge branch 'MDL-83096-500' of https://github.com/jason-platts/moodle into MOODLE_500_STABLE 2026-03-16 09:43:13 +08:00
yusufwib01 7e9f571573 MDL-87935 aiprovider_ollama: fix incorrect error response handling 2026-03-14 02:48:18 +07:00
Sara Arjona 1768c5e66c Merge branch 'MDL-88211-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-03-13 15:25:02 +01:00
Paul Holden 4605729d45 MDL-88211 backup: remove redundant usage of task manager from tests.
The asynchronouse backup/restore tests are only concerned with testing
the implementation of the accompanying task class directly. When they
try to do so at a distance via the task manager API they introduce
random failures by failing to account for different queued tasks that
come from other components (e.g. Workplace).
2026-03-13 12:40:16 +00:00
Daniel Poggenpohl d446b2b831 MDL-83096 mod_lti: Register view completion when launching in new window 2026-03-13 09:02:54 +00:00
Mihail Geshoski 8ac692cab7 Merge branch 'MDL-87516-500' of https://github.com/NashTechOpenUniversity/moodle into MOODLE_500_STABLE 2026-03-13 15:36:10 +08:00
Huong Nguyen 520a9a99a9 Merge branch 'MDL-87959-500' of https://github.com/safatshahin/moodle into MOODLE_500_STABLE 2026-03-13 14:05:56 +07:00
Philipp Memmel 2bd5cb2531 MDL-86839 grunt: Make grunt watch respect --force option 2026-03-13 07:34:08 +01:00
Sara Arjona 27e17c32a6 Merge branch 'MDL-88109-500' of https://github.com/sarjona/moodle into MOODLE_500_STABLE 2026-03-13 06:45:34 +01:00
Safat 927bae14c6 MDL-87959 admin: Add marketplace availability in notifications 2026-03-13 15:55:44 +11:00
Safat 3d37413b84 MDL-87959 tool_installaddon: Add support for marketplace 2026-03-13 15:55:44 +11:00
Mihail Geshoski 8405e26f4e Merge branch 'MDL-81608-500-2' of https://github.com/junpataleta/moodle into MOODLE_500_STABLE 2026-03-13 09:54:19 +08:00
Huong Nguyen 59443e629b Merge branch 'MDL-87100-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-03-13 08:39:16 +07:00
Huong Nguyen d1aeafe3ba Merge branch 'MDL-86843-500' of https://github.com/lucaboesch/moodle into MOODLE_500_STABLE 2026-03-13 08:29:12 +07:00
Jonathon Fowler e247226574 MDL-82439 iplookup: catch and handle GeoIp2 exceptions 2026-03-13 07:42:19 +10:00
Albert Gasset 3486d93e2d MDL-87795 message_airnotifier: Handle 404 (unregistered token) responses 2026-03-12 11:51:54 +01:00
Paul Holden 195eb54c1d MDL-88197 customfield_number: observe required field configuration. 2026-03-12 09:19:38 +00:00
Huong Nguyen 449286a45a MDL-86843 tool_task: style tables correctly. 2026-03-12 09:51:35 +01:00
Andi Permana 5c1d657758 MDL-87548 repository_wikimedia: Handle HTTP 429 rate limiting 2026-03-12 15:29:37 +07:00
Huong Nguyen f04e80afcb Merge branch 'MDL-86616_500' of https://github.com/timhunt/moodle into MOODLE_500_STABLE 2026-03-12 14:43:53 +07:00
Mihail Geshoski 37aacf4078 Merge branch 'MDL-74519-500' of https://github.com/AnupamaSarjoshi/moodle into MOODLE_500_STABLE 2026-03-12 14:08:57 +08:00
Mihail Geshoski b569462cd3 Merge branch 'MDL-85637-500' of https://github.com/izendegi/moodle into MOODLE_500_STABLE 2026-03-12 14:03:56 +08:00
Safat 416cda978f Merge branch 'MDL-87708-500' of https://github.com/HuongNV13/moodle into MOODLE_500_STABLE 2026-03-12 15:14:46 +11:00
Huong Nguyen 208f519e18 MDL-87708 tool_moodlenet: Remove post install task 2026-03-12 10:47:50 +07:00
Huong Nguyen cb00b13745 MDL-87708 admin: Improve MoodleNet removal warnings message 2026-03-12 10:15:44 +07:00
Andi Permana 7afd9fa73a MDL-87708 tool_moodlenet: Clean up moodle.net configs in stable
Part of MDL-87351

- Discourage tool_moodlenet unless pointing to custom installation
- Clear activity chooser footer if set to MoodleNet
- Cleanup happens for: moodle.net URL, www.moodle.net, or empty config
- Preserves custom/local MoodleNet installations
2026-03-12 10:15:44 +07:00
hieuvu 59cdf4341d MDL-87516 core_question: update behaviour when update question attempt. 2026-03-12 09:17:41 +07:00
Stefan Topfstedt 0a9a626b65 MDL-88122 navigation: omit empty breadcrumb container in output. 2026-03-11 17:45:36 -07:00
Sara Arjona c08ab3ca54 MDL-88109 router: Use unencoded URL for location redirect 2026-03-11 16:46:22 +01:00
Hai Nguyen Van 2f1da3e9e8 MDL-86616 filepicker: link restrictions to input via aria-describedby 2026-03-11 11:03:08 +00:00
Iñigo Zendegi 8a27867a0d MDL-85637 question: Optimize max question version SQL query 2026-03-11 11:46:47 +01:00
Anupama Sarjoshi f89c4550fe MDL-74519 badges: Fix date validation in course completion criteria
Ensures that badge awarding correctly evaluates the configured by-date
condition in course and course set completion criteria.

Co-authored by: Diego Campos
2026-03-11 10:02:54 +05:30
Mihail Geshoski 1fedd03d02 Merge branch 'MDL-87955-500-2' of https://github.com/junpataleta/moodle into MOODLE_500_STABLE 2026-03-11 11:55:41 +08:00
AMOS bot da1991815a Automatically generated installer lang files 2026-03-11 00:09:28 +00:00
Jun Pataleta 974afeccd5 MDL-81608 quiz: Add Add menu spacing to prevent overlap 2026-03-10 17:58:58 +08:00
Jun Pataleta 8209259991 MDL-81608 quiz: Set more meaningful button labels
* Add/remove page break buttons
* Move question buttons
2026-03-10 17:58:57 +08:00
Jun Pataleta 9596fe1a83 MDL-81608 core: Add alt text to the YUI spinner image
The accessibility Behat test added to the quiz editing page uncovered
that the loading spinner icon does not have an appropriate alt text.
2026-03-10 17:58:57 +08:00
Jun Pataleta f2f26105d5 MDL-81608 quiz: Adjust size and spacing for the quiz edit page buttons
* In order to meet WCAG 2.2 criterion for minimum target size and make
it easier for users who have mobility limitations to use the quiz
editing page, we need to use the Bootstrap icon button styles and
increase the size and the spacing for the following buttons:
  * Add/remove page break button
  * Lock/unlock question dependency button
* This commit also makes the icons within the buttons as decorative and
moves the accessible name to the button elements themselves via the
aria-label attribute.
* Adjust Behat tests for checking the dependency buttons.
* Add accessibility Behat coverage for the quiz edit page.
2026-03-10 17:58:57 +08:00
Jun Pataleta d062f46eea MDL-81608 core: Label YUI dragdrop handles appropriately
The YUI dragdrop handles rely on the button's title attribute for its
accessible label. We should label it appropriately using the
aria-label attribute.
2026-03-10 17:58:56 +08:00
Huong Nguyen f71a7c928f Merge branch 'MDL-86698-500' of https://github.com/mickhawkins/moodle into MOODLE_500_STABLE 2026-03-10 08:26:26 +07:00
Paul Holden 84c053eca8 MDL-88080 admin: initially focus the form element failing validation.
This appears to replicate what the ancient admin_externalpage_print_header
method did prior to being deprecated in db785816, however its replacement
didn't support the same $focus argument - hence doing so manually now.
2026-03-09 11:58:05 +00:00
Huong Nguyen 854558c727 Merge branch 'MDL-88093-failed-task-lang-MOODLE_500_STABLE' of https://github.com/brendanheywood/moodle into MOODLE_500_STABLE 2026-03-09 09:11:09 +07:00
Huong Nguyen 681bc5f284 Merge branch 'MDL-87883-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-03-09 09:06:27 +07:00
Huong Nguyen 2802c659fe Merge branch 'MDL-79324-500' of https://github.com/lucaboesch/moodle into MOODLE_500_STABLE 2026-03-09 09:03:28 +07:00
Paul Holden 6c453d0f15 MDL-87803 blocks: set ARIA label to string content value.
Previously it was a string instance, which meant it wasn't encoded
correctly when used as part of the delete block confirmation.
2026-03-06 14:46:13 +00:00
Michael Hawkins 2dbbfd945f MDL-86698 core: Update security.txt expiry 2026-03-06 19:09:54 +08:00
Luca Bösch 557c0f54c4 MDL-79324 quiz: Don't show "Back" button in secure layout. 2026-03-06 11:07:09 +01:00
Jake Dallimore 2baad2002f weekly release 5.0.6+ 2026-03-06 12:10:19 +08:00
Jake Dallimore c9195c5ef7 NOBUG: Add upgrade notes 2026-03-06 12:10:19 +08:00
Jake Dallimore 20b33578e3 Merge branch 'install_500_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_500_STABLE 2026-03-06 12:10:14 +08:00
Brendan Heywood 63e1d9c227 MDL-88093 task: Send task failure messages in correct language 2026-03-05 22:04:52 +11:00
Muhammad Arnaldo 7299e4dcb6 MDL-87438 core: Fix multiselect saving with empty choices 2026-03-05 14:23:19 +07:00
Jake Dallimore af9d221360 Merge branch 'MDL-86154_500_STABLE' of https://github.com/marxjohnson/moodle into MOODLE_500_STABLE 2026-03-05 11:56:09 +08:00
Adrian Greeve 6b3619dab9 Merge branch 'MDL-88009-500' of https://github.com/andrewnicols/moodle into MOODLE_500_STABLE 2026-03-05 10:55:49 +08:00
Huong Nguyen 4ec4d94d95 Merge branch 'MDL-87657-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-03-05 09:22:54 +07:00
Mihail Geshoski 872f2c1f01 Merge branch 'MDL-86691_500_STABLE' of https://github.com/marxjohnson/moodle into MOODLE_500_STABLE 2026-03-05 09:44:32 +08:00
Jun Pataleta bfe8192363 MDL-87810 quiz: Accessibility Behat coverage for quiz attempt 2026-03-05 08:47:40 +08:00
Jun Pataleta 57fdeaedd7 MDL-87810 question: Fix colour contrast issues on question view
* Lighten the background for the question formulation container.
* Lighten the background for the question feedback container while
  slightly darkening the text font as well.
2026-03-05 08:47:39 +08:00
Jun Pataleta 475777e2dd Merge branch 'MDL-87949-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-03-04 21:26:09 +08:00
Jun Pataleta 7ddc61c4a3 MDL-87955 mod_forum: Manage subscribers accessibility improvements
* Replace the layout table with responsive Bootstrap columns.
* Add accessible labels for the existing and potential subscribers
  select boxes.
* Add accessibility Behat coverage.
* Remove unused CSS styles
2026-03-04 12:58:11 +08:00
Huong Nguyen 3b446e0dcc Merge branch 'MDL-85322-500' of https://github.com/durenadev/moodle into MOODLE_500_STABLE 2026-03-04 09:20:15 +07:00
Huong Nguyen 1c3d548e5c Merge branch 'MDL-86112_500' of https://github.com/jonof/moodle into MOODLE_500_STABLE 2026-03-04 08:55:16 +07:00
Huong Nguyen 0109cbd42d Merge branch 'MDL-87905-m500' of https://github.com/sammarshallou/moodle into MOODLE_500_STABLE 2026-03-04 08:50:09 +07:00
AMOS bot eda93c16c8 Automatically generated installer lang files 2026-03-04 00:07:47 +00:00
Daniel Ureña 0e397a294c MDL-85322 block_html: trust dashboard content in WS context 2026-03-03 11:52:39 +01:00
Sara Arjona d729e47c38 Merge branch 'MDL-87980-500' of https://github.com/laurentdavid/moodle into MOODLE_500_STABLE 2026-03-03 10:30:02 +01:00
Mihail Geshoski 3fe4bb91ac Merge branch 'MDL-87315_500_STABLE' of https://github.com/marxjohnson/moodle into MOODLE_500_STABLE 2026-03-03 16:37:37 +08:00
Safat 9bb22640b4 Merge branch 'MDL-88030-500' of https://github.com/HuongNV13/moodle into MOODLE_500_STABLE 2026-03-03 14:49:44 +11:00
Jonathon Fowler 0ceeee464b MDL-86112 courseformat: avoid a JS error modal when marking as done
When viewing an "available but not shown" cm with self-completion set,
then clicking the 'Mark as done' button in the activity header.
2026-03-03 11:40:45 +10:00
Huong Nguyen 0b99cbdf78 Merge branch 'MDL-87993-500' of https://github.com/junpataleta/moodle into MOODLE_500_STABLE 2026-03-03 08:36:22 +07:00
Huong Nguyen 1490960fd4 Merge branch 'MDL-87945-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-03-03 08:33:35 +07:00
Huong Nguyen 0d59764b12 Merge branch 'MDL-60912-500' of https://github.com/AnupamaSarjoshi/moodle into MOODLE_500_STABLE 2026-03-03 08:30:59 +07:00
Huong Nguyen 28e1177c9d Merge branch 'MDL-87710-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-03-03 08:28:33 +07:00
Paul Holden 824b1ed336 MDL-75067 behat: always interact with filepicker inside active dialogue.
This allows for tests to interact with multiple elements of this type
during scenarios, whereas previously Behat would always try to interact
with the first (even when referring to latter file managers).
2026-03-02 11:04:15 +00:00
Mark Johnson 541790a5d2 MDL-86154 questions: Delete unused questions after restore
After restoring an activity using shared questions without the question
bank they belong to, we may have left over question categories in the
target course context. These were being deleted, but any questions they
contained weren't.

This replaces the bulk delete of question category records with a call
to `question_category_delete_safe()` for each category, which will also
delete the questions they contain.
2026-02-27 14:36:19 +00:00
Mark Johnson 1890a0b94d MDL-87315 backup: Update set reference question category ID on restore.
When a question bank is restored along with a quiz that uses a random question from that bank,
the set reference for the random question should now point to the category in the restored bank,
rather than original.

This uses the question bank context IDs recorded using the method added in MDL-86136
to decide if the set reference's question context is being restored, and update it
accordingly.

This required adding a new restore step in the restore_root_task, since when a restore is
performed asynchronously, the temp IDs table is deleted after the prechecks are performed
when the task is queued. Adding this step allows the IDs to be recorded again when the restore
runs.
2026-02-27 13:44:30 +00:00
Laurent David 0eb03badc3 MDL-87980 mod_assign: Fix empty description display 2026-02-27 11:46:18 +01:00
Paul Holden 4ca9b8a9d5 MDL-88054 tool_mfa: ensure factor preference icons are circular.
They were once, but then weren't. Probably same cause as that fixed
in 0ff8f7b2 for all emoticons.
2026-02-27 09:57:39 +00:00
Ilya Tregubov d3d1fc39bf Merge branch 'MDL-87967-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-02-27 09:05:19 +01:00
Ilya Tregubov 60df7d8c30 Merge branch 'MDL-29421-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-02-27 08:52:53 +01:00
Huong Nguyen abfa0b8d72 weekly release 5.0.6+ 2026-02-27 09:39:01 +07:00
Huong Nguyen 0412afcc77 NOBUG: Add upgrade notes 2026-02-27 09:39:00 +07:00
Huong Nguyen da90a405af Merge branch 'install_500_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_500_STABLE 2026-02-27 09:38:48 +07:00
AMOS bot 9d0f61412e Automatically generated installer lang files 2026-02-27 00:07:39 +00:00
Mark Johnson b5fd7946f0 MDL-86691 questions: Fix incorrect set reference category contexts
During the upgrade to 5.x, question categories are moved to new
mod_qbank module contexts, and any set references using those categories
are updated using `move_question_set_references()`. However, this
function wasn't updating the `cat` arribute of the set reference's
filter condition, leaving it pointing at the original context ID.

This change update `move_question_set_references()` so that when a set
reference is moved (during the 5.x upgrade, or otherwise) the `cat`
parameter will be updated correctly.

For sites that have already gone through the 5.x upgrade, this fix also
adds a CLI script to find and fix incorrect question_set_reference
records. I considered adding this as an upgrade step, but it potentially
has a lot of records to check, and it's redundant if the site is being
upgraded to 5.x with this fix already in place.
2026-02-26 15:50:02 +00:00
sam marshall 643b66ede7 MDL-87905 Behat: pause incorrectly thinks terminal not interactive
When run from behat/cli/run.php, the pause command may fail because
it thinks the terminal is not interactive, even if the input and
output is passed through from an interactive terminal run.
2026-02-26 09:36:51 +00:00
Anupama Sarjoshi 48520cfc6a MDL-60912 core_completion: Fix course completion percentage calculation
This change ensures that course completion percentage calculations
include only completion activities visible to the user on the course
home page. Hidden or unavailable activities, and those in hidden
sections, are no longer included in the calculation.
2026-02-26 12:38:51 +05:30
Huong Nguyen 70aeabb6d6 Merge branch 'MDL-86866-500' of https://github.com/andrewnicols/moodle into MOODLE_500_STABLE 2026-02-26 09:16:34 +07:00
Huong Nguyen f3428e3933 Merge branch 'MDL-87588-500' of https://github.com/muhammadarnaldo/moodle into MOODLE_500_STABLE 2026-02-26 09:10:03 +07:00
Huong Nguyen 37ae90cf99 Merge branch 'MDL-88020-500' of https://github.com/rxdelacruz/moodle into MOODLE_500_STABLE 2026-02-26 09:03:25 +07:00
Huong Nguyen 61cfded4ec Merge branch 'MDL-88036-500' of https://github.com/mioyu-ubiq/moodle into MOODLE_500_STABLE 2026-02-26 09:01:40 +07:00
Mihail Geshoski 8ec5407f42 Merge branch 'MDL-85902-500' of https://github.com/junpataleta/moodle into MOODLE_500_STABLE 2026-02-26 09:57:37 +08:00
Mihail Geshoski bc793b7735 Merge branch 'MDL-86524_500_STABLE' of https://github.com/marxjohnson/moodle into MOODLE_500_STABLE 2026-02-26 09:44:41 +08:00
AMOS bot 81eb6b9787 Automatically generated installer lang files 2026-02-26 00:07:38 +00:00
Paul Holden 6cbc2c3437 MDL-29421 forms: return formatted frozen content for editor element.
This was partly addressed in 82d491ca, however formatted content and
embedded images were rendered incorrectly.
2026-02-25 17:08:51 +00:00
Paul Holden f53b072070 MDL-87949 gradingform_rubric: accessible "Click to edit" colour.
Darken it to meet the WCAG AA standards for accessible text. This
colour is also consistent with the accompanying buttons.
2026-02-25 16:25:47 +00:00
Ilya Tregubov f4115d8e93 Merge branch 'MDL-78112-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-02-25 12:07:34 +01:00
Stefan Hanauska df7d605f21 MDL-88070 javascript: Respect removeOnClose 2026-02-25 12:06:27 +01:00
Mark Johnson 3421b5b90d MDL-86524 quiz: Update restore test with current tag filter format
The test for restoring a random question from Moodle 3.9 using tags was
still testing for the pre-4.3 filtercondition format. This needed
updating to look for the current format.
2026-02-25 09:00:40 +00:00
Mark Johnson fe6c60dc3c MDL-86524 qbank_tagquestion: Map qtagids filter to restored tag IDs
When restoring a set reference containing a qtagids filter, check that
the tag ID still exists. If not, or we're restoring to a different site,
replace the ID in the filter with the mapped ID from the restore.

If a tag ID is not found, it is removed from the filter. If no matching
tags are found, the entire filter is removed.
2026-02-25 09:00:40 +00:00
Mark Johnson be0160ae03 MDL-86524 qbank_managecateories: Fix questionscontextid on restore
If a backup contains a set reference where the questionscontextid does
not match the context of the question category (as the category was
moved, but the reference was not updated correctly), then we might end
up restoring a quiz with a broken question.

This change ensures that we update the context to match the question
category during the restore.
2026-02-25 09:00:38 +00:00
rxdelacruz 0c88053c19 MDL-88020 enrol_self: Add Behat test for cohort restriction 2026-02-25 14:55:50 +08:00
mioyu-ubiq 97db9427a4 MDL-88036 mod_scorm: Fix duplicate scenario name 2026-02-25 14:34:59 +08:00
Huong Nguyen 230309cc82 Merge branch 'MDL-87190-500' of https://github.com/timhunt/moodle into MOODLE_500_STABLE 2026-02-25 10:58:13 +07:00
Mihail Geshoski 6063d99a54 Merge branch 'mdl-84045_500' of https://github.com/james-cnz/moodle into MOODLE_500_STABLE 2026-02-25 10:50:51 +08:00
Mihail Geshoski 783f804fb2 MDL-87492 mod_quiz: Fix improper version bump 2026-02-25 09:45:11 +08:00
Hai Nguyen Van 657ab15897 MDL-87190 filepicker: clarify error when dragging a folder into picker 2026-02-24 20:44:21 +00:00
Andrew Nicols 57ba40811f MDL-86866 core: Do not log route errors
Doing so leads to excessive noise in logs and unit tests.

At the moment we have no way to control these logs. This will change
when we bring in a PSR-4 logger and can use that instead, but at the
moment we do not have any such logger in core.
2026-02-24 22:14:20 +08:00
Andrew Nicols 9ae6a4d75e MDL-86866 router: Use response_aware handler for status code 2026-02-24 22:14:19 +08:00
Jun Pataleta e8389198e8 MDL-85902 course: Better labeling of activity navigation components
* The arrow icons produced by the `$OUTPUT->larrow()` and
`$OUTPUT->rarrow()` functions are announced by screen readers, and they
do not make sense when heard by screen reader users. To fix this,
we need to override the accessible names of the previous and next
activity links using the `aria-label` attribute and specify whether the
activity links point to the previous or next activities.
* The label for the jump menu "Jump to" is not informative enough to
convey the element's purpose. Let's improve it's accessible label to
"Jump to activity".
2026-02-24 22:04:18 +08:00
Jun Pataleta f0f0d51c66 MDL-85902 course: Convert activity navigation to a navigation landmark 2026-02-24 20:59:58 +08:00
Mihail Geshoski 0fc56b0ceb Merge branch 'MDL-87492_MOODLE_500_STABLE' of https://github.com/cwarwicker/moodle into MOODLE_500_STABLE 2026-02-24 14:33:58 +08:00
Mihail Geshoski dbc8211c14 Merge branch 'MDL-87452-500' of https://github.com/lucaboesch/moodle into MOODLE_500_STABLE 2026-02-24 14:17:37 +08:00
Huong Nguyen 9e54fcaabf Merge branch 'MDL-86809-500' of https://github.com/davewoloszyn/moodle into MOODLE_500_STABLE 2026-02-24 11:04:28 +07:00
Huong Nguyen 1e3ebca248 Merge branch 'MDL-87442-500' of https://github.com/andimendunia/moodle into MOODLE_500_STABLE 2026-02-24 10:44:55 +07:00
Huong Nguyen af0256f3d4 Merge branch 'MDL-87456-500' of https://github.com/lucaboesch/moodle into MOODLE_500_STABLE 2026-02-24 09:50:13 +07:00
AMOS bot a531684b78 Automatically generated installer lang files 2026-02-24 00:07:40 +00:00
Mark Johnson 310083d206 MDL-86524 questions: Update set reference context after category move
When a category is restored, it is first created in the course context,
them moved it its module context once that has been restored.

When this happens, we also need to update set references that refer to
that category to use the new context. However, this was using the legacy
category fields so was not updating them correctly. It now does.
2026-02-23 11:38:42 +00:00
Mark Johnson b82be2ab8c MDL-86524 questions: Delegate filter condition restore to plugins
The core backup code was taking sole responsibility for conversion and
mapping of IDs in set reference filter conditions during restores.
However, it was only actually processing category conditions, not tags
or any of the more recently added conditions.

This adds a new `restore_filtercondition` method to the
`core_question\local\bank\condition` class which can be overridden by
each condition to convert IDs and add mappings during the restore
process.

It also adds a new `maptags` option to
`convert_legacy_set_reference_filter_condition`, so that if we are
doing the conversion during a restore, we don't also create or map
the tags as they are converted. We record ID mappings of the tags
as the are restored, then these can be used by the
`qbank_tagquestions` plugins later to map the converted condition.
2026-02-23 11:38:42 +00:00
Huong Nguyen 4d4066a3c4 MDL-88030 library: Bump Behat and PHPUnit
- Bump Behat to 3.29.0
 - Bump PHPUnit to 11.5.55

Generated with PHP 8.2, following the instructions at:
https://moodledev.io/general/development/tools/composer
2026-02-23 16:22:04 +07:00
Luca Bösch 6359406461 MDL-87456 workshop: Amend action links to switch phase. 2026-02-20 20:47:40 +01:00
Luca Bösch fbad1615c8 MDL-87452 lesson: Align the checkboxes and labels with BS5. 2026-02-20 19:51:53 +01:00
Andi Permana f5a2c05b85 MDL-87442 theme_boost: Fix "With selected" select in mod_choice 2026-02-20 16:32:48 +07:00
Jun Pataleta 8feb15fa41 MDL-87993 core: Use visually hidden core/toast for the srlogger 2026-02-20 16:13:10 +08:00
Jun Pataleta a7e9ac30a9 MDL-87993 core: Allow toasts to be visually hidden 2026-02-20 16:13:10 +08:00
Juan Segarra Montesinos d0b6e9bdc1 MDL-86809 completion: Disable receiveagradegroup when marked as done
Co-authored-by: Yusuf Wibisono <yusuf.wibisono@moodle.com>
2026-02-20 15:12:58 +11:00
Mihail Geshoski 15557a5fc2 weekly release 5.0.6+ 2026-02-20 09:17:34 +08:00
Mihail Geshoski 10e7d8d63d Merge branch 'install_500_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_500_STABLE 2026-02-20 09:17:22 +08:00
james-cnz f2ab1fd1d0 MDL-84045 core_courseformat: Restore section return functionality 2026-02-19 22:35:28 +13:00
Andrew Nicols 10654bff5b MDL-88009 theme_boost: Disable input sanitisation for footer
The footer popover contains content that we have curated from a range of
places and which does not include user-provided content. It should be
safe to use the content unsanitised.
2026-02-19 15:37:30 +08:00
Andrew Nicols 843611eb27 MDL-86866 core: Web 404 responses should generate HTML not JSON 2026-02-19 12:17:22 +08:00
Huong Nguyen 85a3f34d07 Merge branch 'MDL-87441-500' of https://github.com/lucaboesch/moodle into MOODLE_500_STABLE 2026-02-19 08:54:38 +07:00
Huong Nguyen 94b89f4503 Merge branch 'mdl-87187-50' of https://github.com/matthewhilton/moodle into MOODLE_500_STABLE 2026-02-19 08:50:57 +07:00
Huong Nguyen ae59573047 Merge branch 'MDL-85301-500' of https://github.com/davewoloszyn/moodle into MOODLE_500_STABLE 2026-02-19 08:47:43 +07:00
AMOS bot 0b6ee85461 Automatically generated installer lang files 2026-02-19 00:07:39 +00:00
Huong Nguyen 9995c0edff Merge branch 'MDL-87482-500' of https://github.com/HuongNV13/moodle into MOODLE_500_STABLE 2026-02-18 15:40:14 +07:00
Mihail Geshoski 8ff8053bac Merge branch 'MDL-85111-500' of https://github.com/muhammadarnaldo/moodle into MOODLE_500_STABLE 2026-02-18 10:08:07 +08:00
AMOS bot d9c347382a Automatically generated installer lang files 2026-02-18 00:07:42 +00:00
Sara Arjona fa790afa94 Merge branch 'MDL-87919-500' of https://github.com/aanabit/moodle into MOODLE_500_STABLE 2026-02-17 16:54:59 +01:00
Amaia Anabitarte cdee3646dd MDL-87919 theme: Fix restricted selected subsections in course index 2026-02-17 15:27:41 +01:00
Luca Bösch e24d3713e1 MDL-87441 user: Align the "With selected users..." label with BS5. 2026-02-17 08:21:50 +01:00
Shamim Rezaie 2f9c784b34 weekly release 5.0.6+ 2026-02-17 12:56:01 +11:00
Shamim Rezaie 10fdde228d Merge branch 'install_500_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_500_STABLE 2026-02-17 12:55:49 +11:00
Matthew Hilton 946739dd23 MDL-87187 bigbluebuttonbn: Fix record time during backup/restore 2026-02-17 09:10:14 +10:00
Paul Holden 29afbfbbba MDL-87967 output: ensure disabled toggle elements show tooltip. 2026-02-16 14:37:15 +00:00
Muhammad Arnaldo 814b0640b4 MDL-87588 mod_bigbluebuttonbn: Fix missing recordings_deleted column 2026-02-16 17:47:53 +07:00
Paul Holden fc5e318864 MDL-78112 forms: support frozen filemanager form elements.
Replace editable content with a list of files stored in the file area.
2026-02-16 10:21:00 +00:00
Trisha Milan 4ac81d6a66 MDL-82625 navigation: Add disableactive flag to combobox widget
- Introduced disableactive flag to prevent setting active state on combobox items when a page redirect occurs.
This resolves the issue where incorrect checkmarks remain after using the browser's back button due to bfcache.
- Added Behat test cases to verify correct checkmark behaviour in tertiary navigation when navigating and using
the browser's back/forward buttons.
2026-02-16 16:51:38 +08:00
Mihail Geshoski 8166f959e5 Merge branch 'MDL-85658-500' of https://github.com/HuongNV13/moodle into MOODLE_500_STABLE 2026-02-16 13:47:27 +08:00
Mihail Geshoski 9633fcbf4b Merge branch 'MDL-87338-500' of https://github.com/lucaboesch/moodle into MOODLE_500_STABLE 2026-02-16 13:38:48 +08:00
Mihail Geshoski 5e8b19eee7 Merge branch 'MDL-87552-500' of https://github.com/lucaboesch/moodle into MOODLE_500_STABLE 2026-02-16 13:29:49 +08:00
Andre Krueger 07c33cd2c3 MDL-85301 aiprovider_ollama: Fix basic authentication 2026-02-16 08:07:19 +11:00
Jun Pataleta c7978215ec Merge branch 'MDL-87450-500' of https://github.com/junpataleta/moodle into MOODLE_500_STABLE 2026-02-13 20:34:43 +08:00
Huong Nguyen a7d7f41bff Merge branch 'MDL-87597-500' of https://github.com/yusufwib01/moodle into MOODLE_500_STABLE 2026-02-13 15:08:55 +07:00
Mihail Geshoski b88c7a1dbb Merge branch 'MDL-87600-500' of https://github.com/junpataleta/moodle into MOODLE_500_STABLE 2026-02-13 14:18:02 +08:00
Jun Pataleta f49005139a Merge branch 'MDL-76437-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-02-13 13:49:31 +08:00
Huong Nguyen 25ee88a4b7 Merge branch 'MDL-86985-500' of https://github.com/davewoloszyn/moodle into MOODLE_500_STABLE 2026-02-13 11:10:38 +07:00
Jun Pataleta 7ac8908b0c Merge branch 'MDL-87946-500' of https://github.com/andrewnicols/moodle into MOODLE_500_STABLE 2026-02-13 11:59:39 +08:00
Luca Bösch 2d517fbc74 MDL-86985 mod_data: Stripe and hover effect restored to presets table 2026-02-13 14:56:11 +11:00
Luca Bösch 72e41fa58a MDL-87450 forum: Remove Bootstrap 5 table borders. 2026-02-13 11:05:09 +08:00
Huong Nguyen 14c226b4a5 Merge branch 'MDL-87458-500' of https://github.com/jfederico/moodle into MOODLE_500_STABLE 2026-02-13 09:53:25 +07:00
Huong Nguyen 3e8ba76fc1 Merge branch 'MDL-87075-500' of https://github.com/raortegar/moodle into MOODLE_500_STABLE 2026-02-13 09:50:20 +07:00
Huong Nguyen c705fef94a Merge branch 'MDL-81441-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-02-13 09:46:02 +07:00
Huong Nguyen 8504e1facb Merge branch 'MDL-77209-500' of https://github.com/junpataleta/moodle into MOODLE_500_STABLE 2026-02-13 09:42:31 +07:00
Andrew Nicols 29f2b34e00 MDL-87946 github: Update GHA Database connections
Both the pgsql and mysql steps we were using use `docker:stable`, which
has been abandoned and is not a recommended approach. They are also both
abandoned.

This commit moves to using a service entry for postgres; and a manual
step for mysql.

We can't use the services entry for MySQL because GHA does not support
providing arguments to service containers.
2026-02-13 10:33:31 +08:00
Huong Nguyen 5f83a0a0ab MDL-87482 core: Better text comparison 2026-02-13 09:32:06 +07:00
Andi Permana 435ce7fbe1 MDL-87482 core_message: Fix compatibility with libxml2 >= 2.14.0 2026-02-13 09:32:06 +07:00
Muhammad Arnaldo 980a99c6d5 MDL-85111 qtype_essay: improve word counting logic
Improve handling of < (less-than) symbol when it's attached to a non-HTML word.

Co-authored-by: Tim Hunt <T.J.Hunt@open.ac.uk>
2026-02-13 09:14:40 +07:00
Huong Nguyen e4afd807a7 MDL-85658 core_h5p: Implement contentLoaded event handler
This improvement will help to avoid random errors
because H5P content needs time to be rendered.
2026-02-13 08:54:56 +07:00
Huong Nguyen 76934ad945 MDL-85658 behat: Wait until the iframe is completely loaded 2026-02-13 08:54:56 +07:00
Paul Holden a4e790fbaa MDL-87941 core: handle param validation of boolean type false. 2026-02-12 17:34:29 +00:00
Paul Holden bb0390c606 MDL-87657 cohort: fix permission check for viewing system cohorts.
The problem manifested itself in custom reports using the corresponding
cohort field filter, which didn't reload current form values due to
incorrect context tree comparison.
2026-02-12 17:01:27 +00:00
Paul Holden fa189b24da MDL-87945 webservice: handle invalid functions when adding to service.
Same thing as we did for API docs in 6e9e73aba0.
2026-02-12 16:05:25 +00:00
Christian Abila e8c7d8cd41 MDL-87075 core: Filter roles to include visibility permissions 2026-02-12 14:52:05 +01:00
Huong Nguyen 926a474613 Merge branch 'MDL-87095-500' of https://github.com/lameze/moodle into MOODLE_500_STABLE 2026-02-12 10:24:37 +07:00
Huong Nguyen da8b875b14 Merge branch 'MDL-83325_500' of https://github.com/timhunt/moodle into MOODLE_500_STABLE 2026-02-12 10:21:06 +07:00
Huong Nguyen f0069655b7 Merge branch 'MDL-87113-500' of https://github.com/timhunt/moodle into MOODLE_500_STABLE 2026-02-12 08:59:55 +07:00
Huong Nguyen d29c43c555 Merge branch 'MDL-87358-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-02-12 08:57:00 +07:00
Simey Lameze 4b6ffc5064 MDL-87095 behat: reorganize lesson question importing tests 2026-02-12 09:51:00 +08:00
Kim Jared Lucas ef2dde6f11 MDL-87095 mod_lesson: add behat test for importing questions into lesson 2026-02-12 09:49:35 +08:00
Huong Nguyen 77c1bd1458 Merge branch 'MDL-78343-500' of https://github.com/davewoloszyn/moodle into MOODLE_500_STABLE 2026-02-12 08:46:48 +07:00
AMOS bot 7eaa5573c4 Automatically generated installer lang files 2026-02-12 00:09:35 +00:00
jfedericobn 83ae101436 MDL-87458 bigbluebuttonbn: reset removes local recording references only 2026-02-11 16:40:41 +00:00
yusufwib01 757f0d1a44 MDL-87597 core_message: Fix double-click to reopen message drawer 2026-02-11 23:28:59 +07:00
Michael Kotlyar a8e1540a82 MDL-87492 mod_quiz: Add grade_submission task
This task simply calls $attempt->process_grade_submission for a quiz
attempt. The need for this task has arisen due to some instances of
moodle sites needing to process many and/or complex quiz attempts. This
change only utilises this task for the mod_quiz upgrade script.
2026-02-11 10:02:30 +00:00
Huong Nguyen 1b56e5c622 Merge branch 'MDL-86659-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-02-11 17:01:49 +07:00
Hai Nguyen Van c5b011bb9c MDL-87113 filepicker: fix progress stuck after invalid upload 2026-02-11 09:58:05 +00:00
Sara Arjona 07dd973f44 Merge branch 'MDL-87874-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-02-11 07:08:05 +01:00
Huong Nguyen 77753e861b Merge branch 'MDL-87448-500' of https://github.com/lucaboesch/moodle into MOODLE_500_STABLE 2026-02-11 12:54:12 +07:00
Huong Nguyen d91aed29c2 Merge branch 'MDL-87535-500_take2' of https://github.com/lucaboesch/moodle into MOODLE_500_STABLE 2026-02-11 11:48:04 +07:00
Huong Nguyen abdfe22bd0 Merge branch 'MDL-86714-500' of https://github.com/paulholden/moodle into MOODLE_500_STABLE 2026-02-11 11:45:53 +07:00
Huong Nguyen 1679f8b767 Merge branch 'MDL-68062-500' of https://github.com/sh-csg/moodle into MOODLE_500_STABLE 2026-02-11 11:24:50 +07:00
Huong Nguyen e6505da61a Moodle release 5.0.6 2026-02-11 08:44:56 +07:00
Huong Nguyen 55d43fe137 weekly release 5.0.5+ 2026-02-10 08:41:45 +07:00
Shamim Rezaie 8f07ef57be Merge branch 'MDL-87892-500' of https://github.com/HuongNV13/moodle into MOODLE_500_STABLE 2026-02-09 21:07:33 +11:00
Huong Nguyen 8fe771bb9f MDL-87892 admin: Fix MoodleNet check 2026-02-09 16:33:27 +07:00
Shamim Rezaie caaa3a4824 Merge branch 'MDL-87892-500' of https://github.com/HuongNV13/moodle into MOODLE_500_STABLE 2026-02-09 19:36:52 +11:00
Huong Nguyen fb3565e79b MDL-87892 admin: Fix MoodleNet removal warning
Instead of introducing a new optional parameter,
we will make all the changes in the Admin Renderer
2026-02-09 09:05:03 +07:00
Paul Holden 862bf7b443 MDL-87874 customfield: expand size of decimal data field type.
This change now makes the decimal field size equivalent to that of
the corresponding integer field size.
2026-02-06 15:57:31 +00:00
Paul Holden fe2572647f MDL-87874 customfield_number: validation for maximum size constraints. 2026-02-06 15:53:20 +00:00
Michael Kotlyar 1509236178 MDL-87492 mod_quiz: 'autosubmit' quiz attempt handling fix
Some quiz attempts throw an exception when they are being processed and
are stuck in a 'submitted' state. This change wraps the process in a
transaction so that there are no partial changes made in such cases.
2026-02-06 14:14:16 +00:00
Paul Holden cb590fdedc MDL-87883 core: correct license page link in project readme. 2026-02-06 09:09:10 +00:00
Jun Pataleta 55bf752d79 MDL-77209 core: Make More help links open on a new tab 2026-02-06 10:56:28 +08:00
David Woloszyn 306b3b897b MDL-78343 core_table: Improved HTML tag detection in format_text 2026-02-06 13:38:00 +11:00
Luca Bösch 69817cc74e MDL-87448 qbank: put margins to the end of the buttons to align better. 2026-02-05 17:41:16 +01:00
Luca Bösch 2dd0f54dbf MDL-87552 groupings: Striped hovering lines table with BS 5. 2026-02-05 17:25:22 +01:00
Luca Bösch 348498c0b7 MDL-87338 core_table: avoid string concatenation. 2026-02-05 17:21:21 +01:00
Luca Bösch ba5ab0ff06 MDL-87535 quiz: Quiz dates shouldn't show on secure page. 2026-02-05 07:52:21 +01:00
Jun Pataleta 68ab52d55d MDL-87600 gradereport_overview: Fix heading levels on the index page 2026-02-04 18:49:45 +08:00
Paul Holden 4bbd54b415 MDL-86714 mod_scorm: parse reference file post-restore/import.
Ensure the SCORM reference file is setup and ready to use after
restoring from backup file. Additional support for ancient 1.9
backups.
2026-02-02 10:29:01 +00:00
Paul Holden b0f3314cb1 MDL-76437 gradingform_rubric: consistent styles for add buttons.
Remove duplication/redundancy for same elements from the theme.
2026-02-02 10:28:09 +00:00
Paul Holden 630b762a0e MDL-87096 enrol_database: observe activity dates display in courses.
Co-authored-by: Owen Herbert <owenherbert@catalyst-au.net>
2026-02-02 10:27:16 +00:00
Paul Holden 708160cb4a MDL-87321 courseformat: explicit empty string check renaming module. 2026-01-30 10:36:50 +00:00
Tim Hunt 800a766aee MDL-83325 filepicker: tidy up display of restrictions 2026-01-28 20:13:44 +00:00
Paul Holden 09d17c943b MDL-87100 core: infer upgrade note issue number from current branch. 2026-01-27 10:55:05 +00:00
Paul Holden aab0e6cc57 MDL-87710 behat: move upgrade notes into the correct location.
They were put in the wrong location by MDL-87443 (my bad).
2026-01-27 10:45:56 +00:00
Paul Holden 64ef2fe631 MDL-86659 enrol_flatfile: remove spurious get_config parameters. 2026-01-27 10:40:26 +00:00
Paul Holden a2f8649326 MDL-81441 tiny_media: broaden content search for used draft files.
Changes in 47f9106c (upon which this code is based) assumed used files
were always enclosed within quote marks. However this isn't the case
for embedded H5P files which output the filepath directly and rely on
subsequent filtering to turn into appropriate markup.
2026-01-26 10:53:07 +00:00
Paul Holden 73c0ee2162 MDL-87358 h5plib_v127: add unicode modifier to title matching regex.
See https://github.com/h5p/h5p-php-library/issues/276

Co-authored-by: Фидан Сынбулатов
2026-01-15 11:05:04 +00:00
Christian Abila 426b0a0138 MDL-51152 Enrolments: clarify web service 2026-01-08 09:30:16 +01:00
Christian Abila 7e29055181 MDL-51152 Enrolments: remove unused parameter roleid 2026-01-08 09:30:16 +01:00
Stefan Hanauska b458318d13 MDL-68062 badges: Allow revoke by every user with the correct role
Co-authored-by: Rajneel Totaram <rjnlfj@yahoo.com>
2025-11-04 14:12:42 +01:00
367 changed files with 7538 additions and 1772 deletions
+21 -20
View File
@@ -56,6 +56,14 @@ jobs:
image: redis
ports:
- 6379:6379
postgres:
image: ${{ matrix.db == 'pgsql' && 'postgres:14' || '' }}
env:
POSTGRES_DB: test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- 5432:5432
strategy:
fail-fast: false
matrix:
@@ -71,27 +79,20 @@ jobs:
db: pgsql
steps:
- name: Setting up DB mysql
- name: Run MySQL Server
if: ${{ matrix.db == 'mysqli' }}
uses: moodlehq/mysql-action@v1
with:
collation server: utf8mb4_bin
mysql version: 8.4
mysql database: test
mysql user: test
mysql password: test
use tmpfs: true
tmpfs size: '1024M'
extra conf: --skip-log-bin
- name: Setting up DB pgsql
if: ${{ matrix.db == 'pgsql' }}
uses: m4nu56/postgresql-action@v1
with:
postgresql version: 14
postgresql db: test
postgresql user: test
postgresql password: test
run: |
docker run --rm \
-e MYSQL_DATABASE=test \
-e MYSQL_USER=test \
-e MYSQL_PASSWORD=test \
-e MYSQL_ROOT_PASSWORD=test \
-p 3306:3306 \
-d \
--tmpfs /var/lib/mysql:rw,noexec,nosuid,size=1024M \
mysql:8.4 \
--skip-log-bin \
--collation-server=utf8mb4_bin
- name: Configuring git vars
uses: rlespinasse/github-slug-action@v4
+2 -2
View File
@@ -18,7 +18,7 @@ import inquirer from 'inquirer';
import chalk from 'chalk';
import { createNote } from './note.mjs';
import { getInitialValues } from './helpers.mjs';
import { getInitialValues, getIssueNumberFromBranchName } from './helpers.mjs';
import * as Prompts from './prompts.mjs';
import logger from './logger.mjs';
@@ -29,7 +29,7 @@ export default async (options) => {
// Fetch information.
const messages = [];
const { issueNumber } = await inquirer.prompt([
Prompts.getIssuePrompt(),
Prompts.getIssuePrompt(getIssueNumberFromBranchName()),
], initialValues);
let selection = {};
+14
View File
@@ -14,6 +14,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
import chalk from 'chalk';
import branchName from 'current-git-branch';
import { isStandardComponent, isCommunityComponent, rewritePlugintypeAsSubsystem } from './components.mjs';
import { isValidNoteName } from './noteTypes.mjs';
import logger from './logger.mjs';
@@ -59,6 +60,19 @@ export const formatIssueNumber = (input) => {
return input;
};
/**
* Get the issue number, based off current git branch name
*
* @returns {string|null}
*/
export const getIssueNumberFromBranchName = () => {
const issueNumber = String(branchName()).match(/^[a-zA-Z]*-\d+/);
if (issueNumber) {
return formatIssueNumber(issueNumber[0]);
}
return null;
};
/**
* Validate a component.
*
+5 -1
View File
@@ -49,6 +49,10 @@ module.exports = grunt => {
// Spawn the task in a child process so that it doesn't kill this one
// if it failed.
var args = [task, filesOption];
if (grunt.option('force')) {
args.push('--force');
}
grunt.util.spawn(
{
// Spawn with the grunt bin.
@@ -58,7 +62,7 @@ module.exports = grunt => {
cwd: grunt.moodleEnv.fullRunDir,
stdio: 'inherit'
},
args: [task, filesOption]
args: args
},
function(err, res, code) {
if (code !== 0) {
@@ -0,0 +1,7 @@
issueNumber: MDL-51152
notes:
enrol_manual:
- message: >-
The unused parameter 'roleid' has been removed from the external
function `unenrol_users()`
type: removed
@@ -0,0 +1,8 @@
issueNumber: MDL-75067
notes:
tool_behat:
- message: >-
The `behat_session_trait::ensure_element_[does_not_]exists(...)` methods
now accept optional `$container` parameter to define the parent node to
look within
type: improved
@@ -0,0 +1,8 @@
issueNumber: MDL-85322
notes:
block_html:
- message: >-
Treat Dashboard (pagetype 'my-index') as trusted in web services so
get_content_for_external preserves embedded HTML (e.g. iframes) on user
Dashboard.
type: changed
@@ -0,0 +1,12 @@
issueNumber: MDL-86524
notes:
core_question:
- message: >-
During restore of a question_set_reference, mapping of IDs in the
filtercondition is now delegated to qbank plugins.
If your qbank plugin defines a filter condition that uses database
IDs, add an override of `restore_filtercondition()` to the `condition`
class, which checks the condition's data and replaces the IDs with
mapped values if required. See
`qbank_managecategories\category_condition` for an example.
type: improved
@@ -0,0 +1,10 @@
issueNumber: MDL-86839
notes:
core:
- message: >
"grunt watch" now accepts a force flag. Run "grunt watch -f" or "grunt
watch --force" to prevent grunt from cancelling builds when errors
occur. This is especially useful during development, because js build
files will be built even if, for example, jslint errors are still
present in the files.
type: improved
@@ -0,0 +1,7 @@
issueNumber: MDL-87100
notes:
core:
- message: >-
When creating upgrade notes, the issue number will be inferred from the
current Git branch name by default
type: improved
@@ -0,0 +1,7 @@
issueNumber: MDL-87795
notes:
core_user:
- message: >-
Added new optional parameter `userid` to the `user_remove_user_device`
function.
type: improved
@@ -0,0 +1,8 @@
issueNumber: MDL-87993
notes:
core:
- message: >-
The `core/toast` JS module now accepts a `visuallyHidden` configuration
parameter to render visually hidden toast messages for screen reader
users.
type: improved
+1 -1
View File
@@ -32,7 +32,7 @@ Moodle also offers hosting through both [MoodleCloud][11], and our [partner netw
## License
Moodle is provided freely as open source software, under version 3 of the GNU General Public License. For more information on our license see
Moodle is provided freely as open source software, under version 3 of the GNU General Public License. See our [license page][12] for more information.
[1]: https://moodle.org
[2]: https://moodle.com
+59
View File
@@ -6,6 +6,65 @@ More detailed information on key changes can be found in the [Developer update n
The format of this change log follows the advice given at [Keep a CHANGELOG](https://keepachangelog.com).
## 5.0.6+
### core
#### Added
- "grunt watch" now accepts a force flag. Run "grunt watch -f" or "grunt watch --force" to prevent grunt from cancelling builds when errors occur. This is especially useful during development, because js build files will be built even if, for example, jslint errors are still present in the files.
For more information see [MDL-86839](https://tracker.moodle.org/browse/MDL-86839)
- When creating upgrade notes, the issue number will be inferred from the current Git branch name by default
For more information see [MDL-87100](https://tracker.moodle.org/browse/MDL-87100)
- There is a new Behat `toast_message` named selector to more easily assert the presence of Toast messages on the page
For more information see [MDL-87443](https://tracker.moodle.org/browse/MDL-87443)
- The `core/toast` JS module now accepts a `visuallyHidden` configuration parameter to render visually hidden toast messages for screen reader users.
For more information see [MDL-87993](https://tracker.moodle.org/browse/MDL-87993)
### core_question
#### Added
- During restore of a question_set_reference, mapping of IDs in the filtercondition is now delegated to qbank plugins. If your qbank plugin defines a filter condition that uses database IDs, add an override of `restore_filtercondition()` to the `condition` class, which checks the condition's data and replaces the IDs with mapped values if required. See `qbank_managecategories\category_condition` for an example.
For more information see [MDL-86524](https://tracker.moodle.org/browse/MDL-86524)
### core_user
#### Added
- Added new optional parameter `userid` to the `user_remove_user_device` function.
For more information see [MDL-87795](https://tracker.moodle.org/browse/MDL-87795)
### block_html
#### Changed
- Treat Dashboard (pagetype 'my-index') as trusted in web services so get_content_for_external preserves embedded HTML (e.g. iframes) on user Dashboard.
For more information see [MDL-85322](https://tracker.moodle.org/browse/MDL-85322)
### enrol_manual
#### Removed
- The unused parameter 'roleid' has been removed from the external function `unenrol_users()`
For more information see [MDL-51152](https://tracker.moodle.org/browse/MDL-51152)
### tool_behat
#### Added
- The `behat_session_trait::ensure_element_[does_not_]exists(...)` methods now accept optional `$container` parameter to define the parent node to look within
For more information see [MDL-75067](https://tracker.moodle.org/browse/MDL-75067)
## 5.0.5
### core
+24 -13
View File
@@ -937,20 +937,31 @@ $result = new environment_results('custom_checks');
$result = check_xmlrpc_usage($result);
$xmlrpcwarning = !is_null($result) ? get_string($result->getFeedbackStr(), 'admin') : '';
// Check whether MoodleNet features are enabled and warn about removal.
$moodlenetwarning = '';
$moodlenetenabled = get_config('tool_moodlenet', 'enablemoodlenet');
if ($moodlenetenabled) {
$moodlenetwarning = get_string('moodlenetremovalwarning', 'admin');
}
admin_externalpage_setup('adminnotifications');
$output = $PAGE->get_renderer('core', 'admin');
echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, $cronoverdue, $dbproblems,
$maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
$registered, $cachewarnings, $eventshandlers, $themedesignermode, $devlibdir,
$mobileconfigured, $overridetossl, $invalidforgottenpasswordurl, $croninfrequent,
$showcampaigncontent, $showfeedbackencouragement, $servicesandsupportcontent,
$xmlrpcwarning, $moodlenetwarning);
echo $output->admin_notifications_page(
$maturity,
$insecuredataroot,
$errorsdisplayed,
$cronoverdue,
$dbproblems,
$maintenancemode,
$availableupdates,
$availableupdatesfetch,
$buggyiconvnomb,
$registered,
$cachewarnings,
$eventshandlers,
$themedesignermode,
$devlibdir,
$mobileconfigured,
$overridetossl,
$invalidforgottenpasswordurl,
$croninfrequent,
$showcampaigncontent,
$showfeedbackencouragement,
$servicesandsupportcontent,
$xmlrpcwarning
);
+44 -14
View File
@@ -279,17 +279,33 @@ class core_admin_renderer extends plugin_renderer_base {
* @param bool $showfeedbackencouragement Whether the feedback encouragement content should be displayed or not.
* @param bool $showservicesandsupport Whether the services and support content should be displayed or not.
* @param string $xmlrpcwarning XML-RPC deprecation warning message.
* @param string $moodlenetwarning MoodleNet removal warning message.
*
* @return string HTML to output.
*/
public function admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed,
$cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch,
$buggyiconvnomb, $registered, array $cachewarnings = array(), $eventshandlers = 0,
$themedesignermode = false, $devlibdir = false, $mobileconfigured = false,
$overridetossl = false, $invalidforgottenpasswordurl = false, $croninfrequent = false,
$showcampaigncontent = false, bool $showfeedbackencouragement = false, bool $showservicesandsupport = false,
$xmlrpcwarning = '', $moodlenetwarning = '') {
public function admin_notifications_page(
$maturity,
$insecuredataroot,
$errorsdisplayed,
$cronoverdue,
$dbproblems,
$maintenancemode,
$availableupdates,
$availableupdatesfetch,
$buggyiconvnomb,
$registered,
array $cachewarnings = [],
$eventshandlers = 0,
$themedesignermode = false,
$devlibdir = false,
$mobileconfigured = false,
$overridetossl = false,
$invalidforgottenpasswordurl = false,
$croninfrequent = false,
$showcampaigncontent = false,
bool $showfeedbackencouragement = false,
bool $showservicesandsupport = false,
$xmlrpcwarning = ''
) {
global $CFG;
$output = '';
@@ -314,7 +330,8 @@ class core_admin_renderer extends plugin_renderer_base {
$output .= $this->mobile_configuration_warning($mobileconfigured);
$output .= $this->forgotten_password_url_warning($invalidforgottenpasswordurl);
$output .= $this->mnet_deprecation_warning($xmlrpcwarning);
$output .= $this->moodlenet_removal_warning($moodlenetwarning);
$output .= $this->moodlenet_removal_warning();
$output .= $this->marketplace_integration_notice();
$output .= $this->userfeedback_encouragement($showfeedbackencouragement);
$output .= $this->services_and_support_content($showservicesandsupport);
$output .= $this->campaign_content($showcampaigncontent);
@@ -2331,15 +2348,28 @@ class core_admin_renderer extends plugin_renderer_base {
/**
* Display a warning about the removal of MoodleNet integration.
*
* @param string $moodlenetwarning The warning message
* @return string HTML to output.
*/
protected function moodlenet_removal_warning($moodlenetwarning) {
if (empty($moodlenetwarning)) {
return '';
protected function moodlenet_removal_warning(): string {
$moodlenetenabled = get_config('tool_moodlenet', 'enablemoodlenet');
if (!empty($moodlenetenabled)) {
$moodlenetwarning = get_string('moodlenetremovalwarning', 'admin');
return $this->warning($moodlenetwarning);
}
return $this->warning($moodlenetwarning);
return '';
}
/**
* Display a notice about Moodle Marketplace integration.
*
* @return string HTML to output.
*/
protected function marketplace_integration_notice(): string {
$installer = tool_installaddon_installer::instance();
$url = $installer->get_marketplace_url();
$notice = get_string('marketplaceavailablenotice', 'admin', $url->out());
return $this->warning($notice, 'info');
}
/**
+2 -3
View File
@@ -32,7 +32,6 @@ $adminroot = admin_get_root(); // need all settings here
$adminroot->search = $query; // So we can reference it in search boxes later in this invocation
$statusmsg = '';
$errormsg = '';
$focus = '';
// now we'll deal with the case that the admin has submitted the form with changed settings
if ($data = data_submitted() and confirm_sesskey() and isset($data->action) and $data->action == 'save-settings') {
@@ -41,7 +40,7 @@ if ($data = data_submitted() and confirm_sesskey() and isset($data->action) and
if (!empty($adminroot->errors)) {
$errormsg = get_string('errorwithsettings', 'admin');
$firsterror = reset($adminroot->errors);
$focus = $firsterror->id;
$PAGE->set_focuscontrol($firsterror->id);
} else {
// No errors. Did we change any setting? If so, then redirect with success.
if ($count) {
@@ -55,7 +54,7 @@ $PAGE->set_primary_active_tab('siteadminnode');
// and finally, if we get here, then there are matching settings and we have to print a form
// to modify them
echo $OUTPUT->header($focus);
echo $OUTPUT->header();
// Display a warning if site is not registered.
if (empty($query)) {
+1
View File
@@ -72,6 +72,7 @@ if ($data = data_submitted() and confirm_sesskey() and isset($data->action) and
} else {
$errormsg = get_string('errorwithsettings', 'admin');
$firsterror = reset($adminroot->errors);
$PAGE->set_focuscontrol($firsterror->id);
}
$settingspage = $adminroot->locate($section, true);
}
+8
View File
@@ -1,5 +1,13 @@
# tool_behat Upgrade notes
## 5.0.6+
### Added
- The `behat_session_trait::ensure_element_[does_not_]exists(...)` methods now accept optional `$container` parameter to define the parent node to look within
For more information see [MDL-75067](https://tracker.moodle.org/browse/MDL-75067)
## 5.0
### Added
+5 -1
View File
@@ -224,6 +224,11 @@ if (empty($parallelrun)) {
$cmds['singlerun'] = $runtestscommand;
echo "Running single behat site:" . PHP_EOL;
// The inner PHP process is not marked as having an interactive terminal even if it's passed
// through from this one which does, so we need to pass it through as an environment variable.
if (function_exists('posix_isatty') && posix_isatty(STDOUT)) {
putenv('MOODLE_BEHAT_RUNNING_IN_TTY=1');
}
passthru("php $runtestscommand", $status);
$exitcodes['singlerun'] = $status;
chdir($cwd);
@@ -525,4 +530,3 @@ function get_status_lines_from_run_op(Symfony\Component\Process\Process $process
return $statusstr;
}
+10
View File
@@ -0,0 +1,10 @@
define("tool_installaddon/footer",["exports","jquery","core_course/local/activitychooser/selectors"],(function(_exports,_jquery,_selectors){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Activity chooser footer handlers for tool_installaddon.
*
* @module tool_installaddon/footer
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.footerClickListener=void 0,_jquery=_interopRequireDefault(_jquery),_selectors=_interopRequireDefault(_selectors);_exports.footerClickListener=(e,footerData,modal)=>{const closeOption=e.target.closest(_selectors.default.actions.closeOption);if(!closeOption)return;const moduleName=closeOption.dataset.modname;if(!moduleName)return;const carousel=(0,_jquery.default)(modal.getBody()[0].querySelector(_selectors.default.regions.carousel));carousel.carousel("prev"),modal.setFooter(footerData.customfootertemplate),carousel.one("slid.bs.carousel",(()=>{const caller=modal.getBody()[0].querySelector(_selectors.default.regions.modules).querySelector(_selectors.default.regions.getModuleSelector(moduleName));caller&&caller.focus()}))}}));
//# sourceMappingURL=footer.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"footer.min.js","sources":["../src/footer.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Activity chooser footer handlers for tool_installaddon.\n *\n * @module tool_installaddon/footer\n * @copyright 2026 Safat Shahin <safat.shahin@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport selectors from 'core_course/local/activitychooser/selectors';\n\n/**\n * Handle clicks in the chooser footer when installaddon is the active footer plugin.\n *\n * @param {Event} e The event being triggered\n * @param {Object} footerData The footer data generated for the chooser\n * @param {Object} modal The chooser modal\n */\nexport const footerClickListener = (e, footerData, modal) => {\n const closeOption = e.target.closest(selectors.actions.closeOption);\n\n if (!closeOption) {\n return;\n }\n\n const moduleName = closeOption.dataset.modname;\n\n if (!moduleName) {\n return;\n }\n\n const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));\n\n // Trigger the transition between 'pages'.\n carousel.carousel('prev');\n modal.setFooter(footerData.customfootertemplate);\n carousel.one('slid.bs.carousel', () => {\n const allModules = modal.getBody()[0].querySelector(selectors.regions.modules);\n const caller = allModules.querySelector(selectors.regions.getModuleSelector(moduleName));\n if (caller) {\n caller.focus();\n }\n });\n};\n"],"names":["e","footerData","modal","closeOption","target","closest","selectors","actions","moduleName","dataset","modname","carousel","getBody","querySelector","regions","setFooter","customfootertemplate","one","caller","modules","getModuleSelector","focus"],"mappings":";;;;;;;oNAiCmC,CAACA,EAAGC,WAAYC,eACzCC,YAAcH,EAAEI,OAAOC,QAAQC,mBAAUC,QAAQJ,iBAElDA,yBAICK,WAAaL,YAAYM,QAAQC,YAElCF,wBAICG,UAAW,mBAAET,MAAMU,UAAU,GAAGC,cAAcP,mBAAUQ,QAAQH,WAGtEA,SAASA,SAAS,QAClBT,MAAMa,UAAUd,WAAWe,sBAC3BL,SAASM,IAAI,oBAAoB,WAEvBC,OADahB,MAAMU,UAAU,GAAGC,cAAcP,mBAAUQ,QAAQK,SAC5CN,cAAcP,mBAAUQ,QAAQM,kBAAkBZ,aACxEU,QACAA,OAAOG"}
+59
View File
@@ -0,0 +1,59 @@
// 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/>.
/**
* Activity chooser footer handlers for tool_installaddon.
*
* @module tool_installaddon/footer
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import $ from 'jquery';
import selectors from 'core_course/local/activitychooser/selectors';
/**
* Handle clicks in the chooser footer when installaddon is the active footer plugin.
*
* @param {Event} e The event being triggered
* @param {Object} footerData The footer data generated for the chooser
* @param {Object} modal The chooser modal
*/
export const footerClickListener = (e, footerData, modal) => {
const closeOption = e.target.closest(selectors.actions.closeOption);
if (!closeOption) {
return;
}
const moduleName = closeOption.dataset.modname;
if (!moduleName) {
return;
}
const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));
// Trigger the transition between 'pages'.
carousel.carousel('prev');
modal.setFooter(footerData.customfootertemplate);
carousel.one('slid.bs.carousel', () => {
const allModules = modal.getBody()[0].querySelector(selectors.regions.modules);
const caller = allModules.querySelector(selectors.regions.getModuleSelector(moduleName));
if (caller) {
caller.focus();
}
});
};
@@ -70,6 +70,25 @@ class tool_installaddon_installer {
$url = 'https://moodle.org/plugins/get.php';
}
return $this->get_external_service_url($url);
}
/**
* Returns URL to Moodle Marketplace.
*
* @return moodle_url
*/
public function get_marketplace_url(): moodle_url {
return $this->get_external_service_url('https://marketplace.moodle.com/');
}
/**
* Returns URL to external service with optional encoded site information.
*
* @param string $url The external service base URL.
* @return moodle_url
*/
protected function get_external_service_url(string $url): moodle_url {
if (!$this->should_send_site_info()) {
return new moodle_url($url);
}
@@ -16,20 +16,23 @@
declare(strict_types=1);
namespace tool_moodlenet\task;
namespace tool_installaddon\task;
/**
* Ad-hoc task to perform post install tasks.
* We use this to set the active activity chooser footer plugin to tool_moodlenet.
* We use this to set the active activity chooser footer plugin to tool_installaddon.
* We couldn't do this directly in install.php, because there is an admin_apply_default_settings() call after all plugins are
* installed and that would reset whatever value we had set earlier to 'hidden'.
*
* @package tool_moodlenet
* @copyright 2022 Shamim Rezaie <shamim@moodle.com>
* @package tool_installaddon
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class post_install extends \core\task\adhoc_task {
public function execute() {
set_config('activitychooseractivefooter', 'tool_moodlenet');
/**
* Sets the active activity chooser footer plugin after install defaults are applied.
*/
public function execute(): void {
set_config('activitychooseractivefooter', 'tool_installaddon');
}
}
+37
View File
@@ -0,0 +1,37 @@
<?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/>.
/**
* Install script for tool_installaddon.
*
* @package tool_installaddon
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Perform the post-install procedures.
*/
function xmldb_tool_installaddon_install(): void {
// Use an ad-hoc task to set the active activity chooser footer plugin to tool_installaddon.
// We couldn't do this in admin/settings/courses.php for 2 reasons:
// - First, because it would be a breach of component communications principles to do so there.
// - Second, because we can't call get_plugins_with_function() during install and upgrade (or it will return []).
// We couldn't do this directly here either, because there is an admin_apply_default_settings() call after all plugins are
// installed and that would reset whatever value we set here to 'hidden'.
$postinstall = new tool_installaddon\task\post_install();
core\task\manager::queue_adhoc_task($postinstall);
}
+41
View File
@@ -0,0 +1,41 @@
<?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/>.
/**
* Upgrade script for tool_installaddon.
*
* @package tool_installaddon
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Upgrade the plugin.
*
* @param int $oldversion
* @return bool always true
*/
function xmldb_tool_installaddon_upgrade(int $oldversion): bool {
if ($oldversion < 2025041401) {
// Set the activity chooser active footer to include marketplace regardless of the previous setting.
// We are deliberately setting this to increase awareness of marketplace.
set_config('activitychooseractivefooter', 'tool_installaddon');
upgrade_plugin_savepoint(true, 2025041401, 'tool', 'installaddon');
}
return true;
}
@@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die();
$string['acknowledgement'] = 'Acknowledgement';
$string['acknowledgementtext'] = 'I understand that it is my responsibility to have full backups of this site prior to installing additional plugins. I accept and understand that plugins (especially but not only those originating in unofficial sources) may contain security holes, can make the site unavailable, or cause private data leaks or loss.';
$string['activitychooserfootertext'] = 'Browse more activities on';
$string['featuredisabled'] = 'The plugin installer is disabled on this site.';
$string['installaddon'] = 'Install plugin!';
$string['installaddons'] = 'Install plugins';
@@ -44,6 +45,8 @@ $string['installfromzipsubmit'] = 'Install plugin from the ZIP file';
$string['installfromziptype'] = 'Plugin type';
$string['installfromziptype_help'] = 'For plugins that correctly declare their component name, the installer is able to detect the plugin type automatically. If the auto-detection fails, choose the correct type of plugin manually. Warning: The installation procedure can fail badly if an incorrect plugin type is specified.';
$string['installfromziptype_link'] = 'Development:Plugins';
$string['marketplaceadminlinktext'] = 'Browse new plugins';
$string['marketplacelink'] = 'Moodle Marketplace';
$string['permcheck'] = 'Make sure the plugin type root location is writable by the web server process.';
$string['permcheckerror'] = 'Error while checking for write permission';
$string['permcheckprogress'] = 'Checking for write permission ...';
+48
View File
@@ -0,0 +1,48 @@
<?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/>.
/**
* Public API for tool_installaddon.
*
* @package tool_installaddon
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core_course\local\entity\activity_chooser_footer;
/**
* Build activity chooser footer content for Marketplace.
*
* @param int $courseid The selected course id.
* @param int $sectionid The selected section id.
* @return activity_chooser_footer
*/
function tool_installaddon_custom_chooser_footer(int $courseid, int $sectionid): activity_chooser_footer {
global $OUTPUT;
$installer = tool_installaddon_installer::instance();
$marketplaceurl = $installer->get_marketplace_url();
$renderedfooter = $OUTPUT->render_from_template('tool_installaddon/chooser_footer', [
'url' => $marketplaceurl->out(false),
]);
return new activity_chooser_footer(
'tool_installaddon/footer',
$renderedfooter
);
}
+6
View File
@@ -26,6 +26,12 @@
defined('MOODLE_INTERNAL') || die();
if ($hassiteconfig and empty($CFG->disableupdateautodeploy)) {
$installer = tool_installaddon_installer::instance();
$ADMIN->add('modules', new admin_externalpage(
'tool_installaddon_marketplace',
get_string('marketplaceadminlinktext', 'tool_installaddon'),
$installer->get_marketplace_url()->out(false)
), 'modsettings');
$ADMIN->add('modules', new admin_externalpage('tool_installaddon_index',
get_string('installaddons', 'tool_installaddon'),
@@ -0,0 +1,30 @@
{{!
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/>.
}}
{{!
@template tool_installaddon/chooser_footer
Activity chooser footer link to Marketplace.
Example context (json):
{
"url": "https://marketplace.moodle.com/?site=hash"
}
}}
<div class="w-100 d-flex px-2">
<span class="my-auto me-1">{{#str}} activitychooserfootertext, tool_installaddon {{/str}}</span>
{{> tool_installaddon/marketplace_link }}
</div>
@@ -0,0 +1,36 @@
{{!
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/>.
}}
{{!
@template tool_installaddon/marketplace_link
Reusable link to Moodle Marketplace.
Example context (json):
{
"url": "https://marketplace.moodle.com/?site=hash"
}
}}
<a class="d-inline my-auto" href="{{url}}" target="_blank" rel="noopener noreferrer">
{{#str}} marketplacelink, tool_installaddon {{/str}}
<i
class="fa fa-external-link"
role="img"
aria-label="{{#str}} opensinnewwindow, core {{/str}}"
title="{{#str}} opensinnewwindow, core {{/str}}"
>
</i>
</a>
@@ -0,0 +1,37 @@
@tool @tool_installaddon @javascript
Feature: Marketplace activity chooser footer
In order to browse plugins from the activity chooser
As a teacher
I need to see the Marketplace footer link when installaddon is the active footer plugin
Background:
Given the following config values are set as admin:
| activitychooseractivefooter | tool_installaddon |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category | format |
| Course 1 | C1 | 0 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
Scenario: Activity chooser footer includes Marketplace link
Given I log in as "teacher1"
When I am on "Course 1" course homepage with editing mode on
And I open the activity chooser
Then "Browse more activities on" "text" should exist in the "Add an activity or resource" "dialogue"
And "Marketplace" "link" should exist in the "Add an activity or resource" "dialogue"
Scenario: Back button works from activity help when the Marketplace footer is active
Given I log in as "teacher1"
When I am on "Course 1" course homepage with editing mode on
And I open the activity chooser
And I click on "Information about the Assignment activity" "button" in the "Add an activity or resource" "dialogue"
Then "help" "core_course > Activity chooser screen" should be visible
When I click on "Back" "button" in the "Add an activity or resource" "dialogue"
Then "modules" "core_course > Activity chooser screen" should be visible
And "help" "core_course > Activity chooser screen" should not be visible
And "Marketplace" "link" should exist in the "Add an activity or resource" "dialogue"
And "Back" "button" should not exist in the "Add an activity or resource" "dialogue"
@@ -0,0 +1,46 @@
<?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/>.
/**
* Provides a mock testable_tool_installaddon_installer_without_site_info class.
*
* @package tool_installaddon
* @subpackage fixtures
* @category test
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/testable_installer.php');
/**
* Testable subclass with site-info sharing disabled.
*
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class testable_tool_installaddon_installer_without_site_info extends testable_tool_installaddon_installer {
/**
* Disable site info sharing.
*
* @return bool
*/
protected function should_send_site_info() {
return false;
}
}
@@ -18,12 +18,14 @@
namespace tool_installaddon;
use testable_tool_installaddon_installer;
use testable_tool_installaddon_installer_without_site_info;
use tool_installaddon_installer;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__.'/fixtures/testable_installer.php');
require_once(__DIR__ . '/fixtures/testable_installer.php');
require_once(__DIR__ . '/fixtures/testable_installer_without_site_info.php');
/**
* Unit tests for the {@link tool_installaddon_installer} class
@@ -32,16 +34,14 @@ require_once(__DIR__.'/fixtures/testable_installer.php');
* @category test
* @copyright 2013 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \tool_installaddon_installer
*/
final class installer_test extends \advanced_testcase {
public function test_get_addons_repository_url(): void {
$installer = testable_tool_installaddon_installer::instance();
$url = $installer->get_addons_repository_url();
$query = parse_url($url, PHP_URL_QUERY);
$this->assertEquals(1, preg_match('~^site=(.+)$~', $query, $matches));
$site = rawurldecode($matches[1]);
$site = json_decode(base64_decode($site), true);
$site = $this->decode_site_info_from_url($url);
$this->assertIsArray($site);
$this->assertEquals(3, count($site));
$this->assertSame('Nasty site', $site['fullname']);
@@ -49,6 +49,34 @@ final class installer_test extends \advanced_testcase {
$this->assertSame("2.5'; DROP TABLE mdl_user; --", $site['majorversion']);
}
/**
* Tests that marketplace URL includes expected host, scheme and site payload.
*/
public function test_get_marketplace_url(): void {
$installer = testable_tool_installaddon_installer::instance();
$addonsurl = $installer->get_addons_repository_url();
$marketplaceurl = $installer->get_marketplace_url();
$this->assertSame('marketplace.moodle.com', parse_url($marketplaceurl, PHP_URL_HOST));
$this->assertSame('https', parse_url($marketplaceurl, PHP_URL_SCHEME));
$this->assertSame(
$this->decode_site_info_from_url($addonsurl),
$this->decode_site_info_from_url($marketplaceurl),
);
}
/**
* Tests that marketplace URL has no query string when site info is unavailable.
*/
public function test_get_marketplace_url_without_site_info(): void {
$installer = testable_tool_installaddon_installer_without_site_info::instance();
$marketplaceurl = $installer->get_marketplace_url();
$this->assertSame('marketplace.moodle.com', parse_url($marketplaceurl, PHP_URL_HOST));
$this->assertSame('https', parse_url($marketplaceurl, PHP_URL_SCHEME));
$this->assertEmpty(parse_url($marketplaceurl, PHP_URL_QUERY));
}
public function test_decode_remote_request(): void {
$installer = testable_tool_installaddon_installer::instance();
@@ -132,7 +160,7 @@ $plugin->version = 2014121300;
$versionphp = file_get_contents($fixtures.'/github/moodle-repository_mahara-master/version.php');
$this->assertEquals('repository_mahara', $installer->testable_detect_plugin_component_from_versionphp($versionphp));
$versionphp = file_get_contents($fixtures.'/nocomponent/baz/version.php');
$versionphp = file_get_contents($fixtures . '/nocomponent/baz/version.php');
$this->assertFalse($installer->testable_detect_plugin_component_from_versionphp($versionphp));
}
@@ -143,15 +171,28 @@ $plugin->version = 2014121300;
$storage1 = $installer->make_installfromzip_storage();
$this->assertTrue(is_dir($storage1));
$this->assertTrue(is_writable($storage1));
file_put_contents($storage1.'/hello.txt', 'Find me if you can!');
file_put_contents($storage1 . '/hello.txt', 'Find me if you can!');
// Check we get unique directory on each call.
$storage2 = $installer->make_installfromzip_storage();
$this->assertTrue(is_dir($storage2));
$this->assertTrue(is_writable($storage2));
$this->assertFalse(file_exists($storage2.'/hello.txt'));
$this->assertFalse(file_exists($storage2 . '/hello.txt'));
// Check both are in the same parent directory.
$this->assertEquals(dirname($storage1), dirname($storage2));
}
/**
* Decodes the encoded 'site' URL query parameter.
*
* @param moodle_url $url
* @return array
*/
private function decode_site_info_from_url(\moodle_url $url): array {
$query = parse_url($url, PHP_URL_QUERY);
$this->assertEquals(1, preg_match('~^site=(.+)$~', $query, $matches));
$site = rawurldecode($matches[1]);
return json_decode(base64_decode($site), true);
}
}
@@ -0,0 +1,53 @@
<?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/>.
/**
* Unit tests for tool_installaddon lib.
*
* @package tool_installaddon
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_installaddon;
use core_course\local\entity\activity_chooser_footer;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/admin/tool/installaddon/lib.php');
/**
* Test installaddon lib functions.
*/
final class lib_test extends \advanced_testcase {
/**
* Tests chooser footer generation for marketplace link content.
*
* @covers ::tool_installaddon_custom_chooser_footer
*/
public function test_tool_installaddon_custom_chooser_footer(): void {
$this->resetAfterTest();
$footer = \tool_installaddon_custom_chooser_footer(1, 1);
$this->assertInstanceOf(activity_chooser_footer::class, $footer);
$this->assertSame('tool_installaddon/footer', $footer->get_footer_js_file());
$this->assertStringContainsString('https://marketplace.moodle.com/', $footer->get_footer_template());
$this->assertStringContainsString('site=', $footer->get_footer_template());
}
}
@@ -0,0 +1,47 @@
<?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/>.
declare(strict_types=1);
namespace tool_installaddon\task;
/**
* Unit tests for the post install task.
*
* @package tool_installaddon
* @category test
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \tool_installaddon\task\post_install
*/
final class post_install_test extends \advanced_testcase {
/**
* Tests that the task sets the activity chooser footer plugin to tool_installaddon.
*/
public function test_execute_sets_activitychooseractivefooter_config(): void {
$this->resetAfterTest();
set_config('activitychooseractivefooter', 'hidden');
$task = new post_install();
$task->execute();
$this->assertSame(
'tool_installaddon',
get_config('core', 'activitychooseractivefooter'),
);
}
}
+1 -1
View File
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'tool_installaddon';
$plugin->version = 2025041400;
$plugin->version = 2025041401;
$plugin->requires = 2025040800;
$plugin->maturity = MATURITY_STABLE;
+7 -2
View File
@@ -598,6 +598,10 @@ class manager {
$urls = [
new \moodle_url('/login/logout.php'),
new \moodle_url('/admin/tool/mfa/guide.php'),
// Allow email self-registration confirmation to complete so that
// auth_email can restore wantsurl from the auth_email_wantsurl user
// preference before MFA intercepts on the next request.
new \moodle_url('/login/confirm.php'),
];
foreach ($factors as $factor) {
$urls = array_merge($urls, $factor->get_no_redirect_urls());
@@ -605,8 +609,9 @@ class manager {
// Allow forced redirection exclusions.
if ($exclusions = get_config('tool_mfa', 'redir_exclusions')) {
foreach (explode("\n", $exclusions) as $exclusion) {
$urls[] = new \moodle_url($exclusion);
$exclusions = preg_split('/\n|\r/', $exclusions, -1, PREG_SPLIT_NO_EMPTY);
foreach ($exclusions as $exclusion) {
$urls[] = new \moodle_url(trim($exclusion));
}
}
+5
View File
@@ -38,3 +38,8 @@ input.tool-mfa-verification-code,
.mfa-verify-form .form-group.fitem > span > input {
width: 100%;
}
#mfalist .icon-circle .icon {
width: 64px;
height: 64px;
}
+48 -21
View File
@@ -20,6 +20,7 @@ namespace tool_mfa;
* Tests for MFA manager class.
*
* @package tool_mfa
* @covers \tool_mfa\manager
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -30,9 +31,6 @@ final class manager_test extends \advanced_testcase {
/**
* Tests getting the factor total weight
*
* @covers ::get_total_weight
* @covers ::setup_user_factor
*/
public function test_get_total_weight(): void {
$this->resetAfterTest(true);
@@ -76,8 +74,6 @@ final class manager_test extends \advanced_testcase {
/**
* Tests getting the factor status
*
* @covers ::get_status
*/
public function test_get_status(): void {
$this->resetAfterTest(true);
@@ -117,8 +113,6 @@ final class manager_test extends \advanced_testcase {
/**
* Tests checking if passed enough factors
*
* @covers ::passed_enough_factors
*/
public function test_passed_enough_factors(): void {
$this->resetAfterTest(true);
@@ -151,6 +145,30 @@ final class manager_test extends \advanced_testcase {
$this->assertEquals(true, \tool_mfa\manager::passed_enough_factors());
}
/**
* Test reading no redirect URLs configuration
*/
public function test_get_no_redirect_urls(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Ensure all EOL markers are covered.
$config = "/user/profile.php\r" .
"/course/index.php\n" .
"/admin/settings.php\r\n" .
"/mod/bar/view.php ";
set_config('redir_exclusions', $config, 'tool_mfa');
// Cast URLs to string for simpler comparison.
$noredirecturls = array_map(fn(\moodle_url $url) => (string) $url, manager::get_no_redirect_urls());
$this->assertContains((string) (new \moodle_url('/user/profile.php')), $noredirecturls);
$this->assertContains((string) (new \moodle_url('/course/index.php')), $noredirecturls);
$this->assertContains((string) (new \moodle_url('/admin/settings.php')), $noredirecturls);
$this->assertContains((string) (new \moodle_url('/mod/bar/view.php')), $noredirecturls);
}
/**
* The data provider for whether urls should be redirected or not
*
@@ -178,7 +196,6 @@ final class manager_test extends \advanced_testcase {
/**
* Tests whether it should require mfa
*
* @covers ::should_require_mfa
* @param string $urlstring
* @param string $webroot
* @param bool $status
@@ -197,8 +214,6 @@ final class manager_test extends \advanced_testcase {
/**
* Tests whether it should require the mfa checks
*
* @covers ::should_require_mfa
*/
public function test_should_require_mfa_checks(): void {
// Setup test and user.
@@ -285,8 +300,6 @@ final class manager_test extends \advanced_testcase {
/**
* Tests should require the mfa redirection loop
*
* @covers ::should_require_mfa
*/
public function test_should_require_mfa_redirection_loop(): void {
// Setup test and user.
@@ -341,9 +354,6 @@ final class manager_test extends \advanced_testcase {
/**
* Tests checking for possible setup factor
*
* @covers ::possible_factor_setup
* @covers ::setup_user_factor
*/
public function test_possible_factor_setup(): void {
// Setup test and user.
@@ -377,8 +387,6 @@ final class manager_test extends \advanced_testcase {
/**
* Tests checking if a factor is ready
*
* @covers ::is_ready
*/
public function test_is_ready(): void {
// Setup test and user.
@@ -419,9 +427,6 @@ final class manager_test extends \advanced_testcase {
/**
* Tests core hooks
*
* @covers ::mfa_config_hook_test
* @covers ::mfa_login_hook_test
*/
public function test_core_hooks(): void {
// Setup test and user.
@@ -438,9 +443,31 @@ final class manager_test extends \advanced_testcase {
}
/**
* Tests circular redirect auth
* Tests that /login/confirm.php is excluded from MFA redirection.
*
* When a user follows an email self-registration confirmation link, MFA must
* not intercept the request before auth_email::user_confirm() has had a chance
* to restore the wantsurl from the auth_email_wantsurl user preference.
*
* @covers ::should_require_mfa
* @covers ::get_no_redirect_urls
*/
public function test_confirm_url_no_redirect(): void {
$this->resetAfterTest(true);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$confirmurl = new \moodle_url('/login/confirm.php');
$this->assertEquals(
\tool_mfa\manager::NO_REDIRECT,
\tool_mfa\manager::should_require_mfa($confirmurl, false),
'/login/confirm.php must not trigger an MFA redirect so that auth_email can ' .
'restore wantsurl from the auth_email_wantsurl user preference first.'
);
}
/**
* Tests circular redirect auth
*/
public function test_circular_redirect_auth(): void {
// Setup test and user.
+1 -8
View File
@@ -28,12 +28,5 @@ declare(strict_types=1);
* Perform the post-install procedures.
*/
function xmldb_tool_moodlenet_install() {
// Use an ad-hoc task to set the active activity chooser footer plugin to tool_moodlenet.
// We couldn't do this in admin/settings/courses.php for 2 reasons:
// - First, because it would be a breach of component communications principles to do so there.
// - Second, because we can't call get_plugins_with_function() during install and upgrade (or it will return []).
// We couldn't do this directly here either, because there is an admin_apply_default_settings() call after all plugins are
// installed and that would reset whatever value we set here to 'hidden'.
$postinstall = new tool_moodlenet\task\post_install();
core\task\manager::queue_adhoc_task($postinstall);
}
+1 -1
View File
@@ -49,7 +49,7 @@ function tool_task_mtrace_wrapper(string $message, string $eol = ''): void {
// We autolink urls and emails here but can't use format_text as it does
// more than we need and has side effects which are not useful in this context.
$urlpattern = '/(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/';
$urlpattern = '~\b(?:https?|ftps?)://[a-z0-9-]+(?:\.[a-z0-9-]+)*(?::\d+)?(?:/[^\s<]*)?~i';
$message = preg_replace_callback($urlpattern, function($matches) {
$url = $matches[0];
return html_writer::link($url, $url, ['target' => '_blank']);
+2 -2
View File
@@ -58,7 +58,7 @@ class tool_task_renderer extends plugin_renderer_base {
get_string('nextruntime', 'tool_task'),
];
$table->attributes['class'] = 'admintable generaltable table table-hover';
$table->attributes['class'] = 'admintable generaltable table table-striped table-hover';
$table->colclasses = [];
// For each task entry (row) show action buttons/logs link depending on the user permissions.
@@ -388,7 +388,7 @@ class tool_task_renderer extends plugin_renderer_base {
get_string('default', 'tool_task'),
];
$table->attributes['class'] = 'admintable generaltable table table-hover';
$table->attributes['class'] = 'admintable generaltable table table-striped table-hover';
$table->colclasses = [];
if (!$showloglink) {
+82
View File
@@ -0,0 +1,82 @@
<?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 tool_task;
/**
* Test for the lib class.
*
* @package tool_task
* @copyright 2026 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class lib_test extends \advanced_testcase {
/**
* Data provider for mtrace
*
* @return array
*/
public static function tool_task_mtrace_wrapper_provider(): array {
return [
[
'A url http://moodle.com',
'A url <a target="_blank" href="http://moodle.com">http://moodle.com</a>',
],
[
'A url https://moodle.com',
'A url <a target="_blank" href="https://moodle.com">https://moodle.com</a>',
],
[
'A url https://moodle.com post text',
'A url <a target="_blank" href="https://moodle.com">https://moodle.com</a> post text',
],
[
'A url https://moodle.com. In a paragraph',
'A url <a target="_blank" href="https://moodle.com">https://moodle.com</a>. In a paragraph',
],
[
'A url https://localhost post text',
'A url <a target="_blank" href="https://localhost">https://localhost</a> post text',
],
[
'A url https://main.localhost post text',
'A url <a target="_blank" href="https://main.localhost">https://main.localhost</a> post text',
],
[
'email info@moodle.com after',
'email <a href="mailto:info@moodle.com">info@moodle.com</a> after',
],
[
'A sentence that ends in info@moodle.com. With another sentence.',
'A sentence that ends in <a href="mailto:info@moodle.com">info@moodle.com</a>. With another sentence.',
],
];
}
/**
* Test validations for minute field.
* @dataProvider tool_task_mtrace_wrapper_provider
* @param string $output task output
* @param string $expected html
* @covers ::tool_task_mtrace_wrapper
*/
public function test_tool_task_mtrace_wrapper(string $output, string $expected): void {
global $CFG;
require_once("{$CFG->dirroot}/{$CFG->admin}/tool/task/lib.php");
$this->expectOutputString($expected);
$result = tool_task_mtrace_wrapper($output);
}
}
+2 -4
View File
@@ -35,8 +35,6 @@ if (isset($newsettings['frontpagesettings'])) {
$newsettingshtml = implode($newsettings);
unset($newsettings);
$focus = '';
if (empty($adminroot->errors) and $newsettingshtml === '') {
// there must be either redirect without message or continue button or else upgrade would be sometimes broken
if ($return == 'site') {
@@ -48,12 +46,12 @@ if (empty($adminroot->errors) and $newsettingshtml === '') {
if (!empty($adminroot->errors)) {
$firsterror = reset($adminroot->errors);
$focus = $firsterror->id;
$PAGE->set_focuscontrol($firsterror->id);
}
// and finally, if we get here, then there are new settings and we have to print a form
// to modify them
echo $OUTPUT->header($focus);
echo $OUTPUT->header();
if (!empty($SITE->fullname) and !empty($SITE->shortname)) {
echo $OUTPUT->box(get_string('upgradesettingsintro','admin'), 'generalbox');
+6 -2
View File
@@ -196,8 +196,12 @@ class external_service_functions_form extends moodleform {
//we add the descriptions to the functions
foreach ($functions as $functionid => $functionname) {
//retrieve full function information (including the description)
$function = \core_external\external_api::external_function_info($functionname);
if (empty($function->deprecated)) {
try {
$function = \core_external\external_api::external_function_info($functionname);
} catch (Throwable $exception) {
$function = null;
}
if ($function !== null && empty($function->deprecated)) {
$functions[$functionid] = $function->name . ':' . $function->description;
} else {
// Exclude the deprecated ones.
@@ -152,7 +152,7 @@ abstract class abstract_processor extends process_base {
$responsearr['errormessage'] = $response->getReasonPhrase();
} else {
$bodyobj = json_decode($response->getBody()->getContents());
$responsearr['errormessage'] = $bodyobj->error->message;
$responsearr['errormessage'] = $bodyobj->error;
}
return $responsearr;
+1 -1
View File
@@ -55,7 +55,7 @@ class provider extends \core_ai\provider {
#[\Override]
public function add_authentication_headers(RequestInterface $request): RequestInterface {
if (empty($this->config['basicauthenabled'])) {
if (empty($this->config['enablebasicauth'])) {
return $request;
} else {
// Add the Authorization header for basic auth.
@@ -151,12 +151,21 @@ final class process_explain_text_test extends \advanced_testcase {
$responses = [
500 => new Response(500, ['Content-Type' => 'application/json']),
503 => new Response(503, ['Content-Type' => 'application/json']),
401 => new Response(401, ['Content-Type' => 'application/json'],
'{"error": {"message": "Invalid Authentication"}}'),
404 => new Response(404, ['Content-Type' => 'application/json'],
'{"error": {"message": "You must be a member of an organization to use the API"}}'),
429 => new Response(429, ['Content-Type' => 'application/json'],
'{"error": {"message": "Rate limit reached for requests"}}'),
401 => new Response(
401,
['Content-Type' => 'application/json'],
json_encode(['error' => 'Invalid Authentication']),
),
404 => new Response(
404,
['Content-Type' => 'application/json'],
json_encode(['error' => 'You must be a member of an organization to use the API']),
),
429 => new Response(
429,
['Content-Type' => 'application/json'],
json_encode(['error' => 'Rate limit reached for requests']),
),
];
$processor = new process_explain_text($this->provider, $this->action);
@@ -316,7 +325,7 @@ final class process_explain_text_test extends \advanced_testcase {
$mock->append(new Response(
401,
['Content-Type' => 'application/json'],
json_encode(['error' => ['message' => 'Invalid Authentication']]),
json_encode(['error' => 'Invalid Authentication']),
));
$processor = new process_explain_text($this->provider, $this->action);
@@ -153,17 +153,17 @@ final class process_generate_text_test extends \advanced_testcase {
401 => new Response(
401,
['Content-Type' => 'application/json'],
json_encode(['error' => ['message' => 'Invalid Authentication']]),
json_encode(['error' => 'Invalid Authentication']),
),
404 => new Response(
404,
['Content-Type' => 'application/json'],
json_encode(['error' => ['message' => 'You must be a member of an organization to use the API']]),
json_encode(['error' => 'You must be a member of an organization to use the API']),
),
429 => new Response(
429,
['Content-Type' => 'application/json'],
json_encode(['error' => ['message' => 'Rate limit reached for requests']]),
json_encode(['error' => 'Rate limit reached for requests']),
),
];
@@ -325,7 +325,7 @@ final class process_generate_text_test extends \advanced_testcase {
$mock->append(new Response(
401,
['Content-Type' => 'application/json'],
json_encode(['error' => ['message' => 'Invalid Authentication']]),
json_encode(['error' => 'Invalid Authentication']),
));
$processor = new process_generate_text($this->provider, $this->action);
@@ -151,12 +151,21 @@ final class process_summarise_text_test extends \advanced_testcase {
$responses = [
500 => new Response(500, ['Content-Type' => 'application/json']),
503 => new Response(503, ['Content-Type' => 'application/json']),
401 => new Response(401, ['Content-Type' => 'application/json'],
'{"error": {"message": "Invalid Authentication"}}'),
404 => new Response(404, ['Content-Type' => 'application/json'],
'{"error": {"message": "You must be a member of an organization to use the API"}}'),
429 => new Response(429, ['Content-Type' => 'application/json'],
'{"error": {"message": "Rate limit reached for requests"}}'),
401 => new Response(
401,
['Content-Type' => 'application/json'],
json_encode(['error' => 'Invalid Authentication']),
),
404 => new Response(
404,
['Content-Type' => 'application/json'],
json_encode(['error' => 'You must be a member of an organization to use the API']),
),
429 => new Response(
429,
['Content-Type' => 'application/json'],
json_encode(['error' => 'Rate limit reached for requests']),
),
];
$processor = new process_summarise_text($this->provider, $this->action);
@@ -316,7 +325,7 @@ final class process_summarise_text_test extends \advanced_testcase {
$mock->append(new Response(
401,
['Content-Type' => 'application/json'],
json_encode(['error' => ['message' => 'Invalid Authentication']]),
json_encode(['error' => 'Invalid Authentication']),
));
$processor = new process_summarise_text($this->provider, $this->action);
+6 -8
View File
@@ -171,7 +171,7 @@ class auth_plugin_email extends auth_plugin_base {
* @param string $confirmsecret
*/
function user_confirm($username, $confirmsecret) {
global $DB, $SESSION;
global $DB;
$user = get_complete_user_data('username', $username);
if (!empty($user)) {
@@ -179,17 +179,15 @@ class auth_plugin_email extends auth_plugin_base {
return AUTH_CONFIRM_ERROR;
} else if ($user->secret === $confirmsecret && $user->confirmed) {
// Clean up stale wantsurl preference if user clicks confirmation link again.
unset_user_preference('auth_email_wantsurl', $user);
return AUTH_CONFIRM_ALREADY;
} else if ($user->secret === $confirmsecret) { // They have provided the secret key to get in
$DB->set_field("user", "confirmed", 1, array("id"=>$user->id));
if ($wantsurl = get_user_preferences('auth_email_wantsurl', false, $user)) {
// Ensure user gets returned to page they were trying to access before signing up.
$SESSION->wantsurl = $wantsurl;
unset_user_preference('auth_email_wantsurl', $user);
}
// Clean up the wantsurl preference regardless of how confirmation was triggered
// (e.g. /login/confirm.php, admin single confirm, bulk confirm, web service).
unset_user_preference('auth_email_wantsurl', $user);
return AUTH_CONFIRM_OK;
}
} else {
+96
View File
@@ -0,0 +1,96 @@
<?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 auth_email;
/**
* Tests for email authentication plugin.
*
* @package auth_email
* @copyright 2026 Moodle Pty Ltd
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \auth_plugin_email
*/
final class auth_test extends \advanced_testcase {
/**
* Test that user_confirm() cleans up the auth_email_wantsurl preference
* when confirming a user for the first time (AUTH_CONFIRM_OK).
*/
public function test_user_confirm_cleans_up_wantsurl_preference(): void {
global $DB;
$this->resetAfterTest(true);
// Create an unconfirmed user with the email auth method.
$user = $this->getDataGenerator()->create_user([
'auth' => 'email',
'confirmed' => 0,
]);
$secret = random_string(15);
$DB->set_field('user', 'secret', $secret, ['id' => $user->id]);
// Simulate the wantsurl preference saved at signup time.
set_user_preference('auth_email_wantsurl', 'https://example.com/course/view.php?id=42', $user);
$this->assertTrue(
$DB->record_exists('user_preferences', ['userid' => $user->id, 'name' => 'auth_email_wantsurl']),
'Preference should exist in DB before confirmation.'
);
$auth = get_auth_plugin('email');
$result = $auth->user_confirm($user->username, $secret);
$this->assertEquals(AUTH_CONFIRM_OK, $result);
$this->assertFalse(
$DB->record_exists('user_preferences', ['userid' => $user->id, 'name' => 'auth_email_wantsurl']),
'auth_email_wantsurl preference must be removed from DB after successful confirmation.'
);
}
/**
* Test that user_confirm() cleans up the auth_email_wantsurl preference
* even when the user is already confirmed (AUTH_CONFIRM_ALREADY).
*
* This covers the edge case where a user clicks the confirmation link
* a second time — the stale preference should still be cleaned up.
*/
public function test_user_confirm_already_confirmed_cleans_up_wantsurl_preference(): void {
global $DB;
$this->resetAfterTest(true);
// Create an already-confirmed user with the email auth method.
$user = $this->getDataGenerator()->create_user([
'auth' => 'email',
'confirmed' => 1,
]);
$secret = random_string(15);
$DB->set_field('user', 'secret', $secret, ['id' => $user->id]);
// Simulate a stale wantsurl preference left over from signup.
set_user_preference('auth_email_wantsurl', 'https://example.com/course/view.php?id=42', $user);
$this->assertTrue(
$DB->record_exists('user_preferences', ['userid' => $user->id, 'name' => 'auth_email_wantsurl']),
'Preference should exist in DB before re-confirmation.'
);
$auth = get_auth_plugin('email');
$result = $auth->user_confirm($user->username, $secret);
$this->assertEquals(AUTH_CONFIRM_ALREADY, $result);
$this->assertFalse(
$DB->record_exists('user_preferences', ['userid' => $user->id, 'name' => 'auth_email_wantsurl']),
'auth_email_wantsurl preference must be removed from DB even when user is already confirmed.'
);
}
}
+4 -2
View File
@@ -34,11 +34,13 @@ Feature: Test if the login form provides the correct feedback
Scenario: Set logo for loginpage
Given I log in as "admin"
And I navigate to "Appearance > Logos" in site administration
And I upload "course/tests/fixtures/image.jpg" file to "Logo" filemanager
And I upload "lib/tests/fixtures/1.jpg" file to "Logo" filemanager
And I upload "lib/tests/fixtures/2.jpg" file to "Favicon" filemanager
And I press "Save changes"
And I log out
And I follow "Log in"
Then "//img[@id='logoimage']" "xpath_element" should exist
Then "//img[@id='logoimage' and contains(@src, '/1.jpg')]" "xpath_element" should exist
And "//link[@rel='shortcut icon' and contains(@href, '/2.jpg')]" "xpath_element" should exist
Scenario: Add a custom welcome message
Given the following config values are set as admin:
@@ -78,6 +78,9 @@ class restore_root_task extends restore_task {
// Unconditionally, load create all the needed outcomes
$this->add_step(new restore_outcomes_structure_step('create_scales', 'outcomes.xml'));
// If we haven't preloaded information, load all the question banks to temp_ids_table.
$this->add_step(new \core\backup\restore_load_questionbanks('load_questionbanks'));
// If we haven't preloaded information, load all the needed categories and questions (reduced) to temp_ids_table
$this->add_step(new restore_load_categories_and_questions('load_categories_and_questions'));
+56 -47
View File
@@ -5472,12 +5472,18 @@ class restore_create_categories_and_questions extends restore_structure_step {
}
$tagcontextid = $this->cachedcategory->contextid;
// Add the tag to the question.
core_tag_tag::add_item_tag('core_question',
$taginstanceid = core_tag_tag::add_item_tag(
'core_question',
'question',
$newquestion,
context::instance_by_id($tagcontextid),
$tagname
$tagname,
);
$tagid = $DB->get_field('tag_instance', 'tagid', ['id' => $taginstanceid]);
if ($tagid != $data->id) {
// The tag didn't exist already, map the new ID.
$this->set_mapping('tag', $data->id, $tagid);
}
}
}
@@ -5682,13 +5688,19 @@ class restore_move_module_questions_categories extends restore_execution_step {
// We need to check all the question_set_references belonging to this context_module.
$references = $DB->get_records('question_set_references', ['usingcontextid' => $newcontext->newitemid]);
foreach ($references as $reference) {
$filtercondition = json_decode($reference->filtercondition);
if (!empty($filtercondition->questioncategoryid) &&
in_array($filtercondition->questioncategoryid, $categoryids)) {
// This is one of ours, update the questionscontextid.
$DB->set_field('question_set_references',
'questionscontextid', $newcontext->newitemid,
['id' => $reference->id]);
$filtercondition = json_decode($reference->filtercondition, true);
if (!array_key_exists('filter', $filtercondition)) {
$filtercondition = \core_question\question_reference_manager::convert_legacy_set_reference_filter_condition(
$filtercondition,
);
}
$questioncategoryid = $filtercondition['filter']['category']['values'][0];
if (in_array($questioncategoryid, $categoryids)) {
// This is one of ours, update the questionscontextid and filtercondition fields.
$reference->questionscontextid = $newcontext->newitemid;
$filtercondition['cat'] = "{$questioncategoryid},{$newcontext->newitemid}";
$reference->filtercondition = json_encode($filtercondition);
$DB->update_record('question_set_references', $reference);
}
}
}
@@ -5703,20 +5715,20 @@ class restore_move_module_questions_categories extends restore_execution_step {
);
}
}
// Remove any remaining course-level question categories from the restored course.
// Remove any remaining course-level question categories and their questions from the restored course.
$coursecatsql = "
SELECT qc.id AS categoryid
SELECT qc.id AS id, qc.contextid AS contextid
FROM {question_categories} qc
JOIN {context} c ON c.id = qc.contextid
WHERE c.contextlevel = :courselevel AND c.instanceid = :courseid
";
$DB->delete_records_subquery(
'question_categories',
'id',
'categoryid',
$categories = $DB->get_records_sql(
$coursecatsql,
['courselevel' => context_course::LEVEL, 'courseid' => $this->task->get_courseid()]
['courselevel' => context_course::LEVEL, 'courseid' => $this->task->get_courseid()],
);
foreach ($categories as $category) {
question_category_delete_safe($category);
}
}
}
@@ -6495,50 +6507,47 @@ trait restore_question_set_reference_data_trait {
public function process_question_set_reference($data) {
global $DB;
$data = (object) $data;
$owncontext = $data->usingcontextid == $data->questionscontextid;
$data->usingcontextid = $this->get_mappingid('context', $data->usingcontextid);
$data->itemid = $this->get_new_parentid('quiz_question_instance');
$originalbankinbackup = (bool) restore_dbops::get_backup_ids_record(
$this->get_restoreid(),
'questionbank',
$data->questionscontextid,
);
if ($context = $this->get_mappingid('context', $data->questionscontextid)) {
$data->questionscontextid = $context;
} else {
$this->log(
"question_set_reference with old id {$data->id} referenced question context "
. "{$data->questionscontextid} which was not included in the backup. Therefore, this has been "
. "restored with the old questionscontextid.",
backup::LOG_WARNING,
);
}
$filtercondition = json_decode($data->filtercondition, true);
if (!isset($filtercondition['filter'])) {
// Pre-4.3, convert the old filtercondition format to the new format.
// Don't map tags to new IDs, the plugin will do that below.
$filtercondition = \core_question\question_reference_manager::convert_legacy_set_reference_filter_condition(
$filtercondition);
$filtercondition,
false,
);
}
// Map category id used for category filter condition and corresponding context id.
$oldcategoryid = $filtercondition['filter']['category']['values'][0];
// Decide if we're going to refer back to the original category, or to the new category.
// Are we restoring to a different site?
// Has the original context or category been deleted?
// Did the old category belong to the same context as the original set reference?
// Are we allowed to use its questions?
$questionscontext = context::instance_by_id($data->questionscontextid, IGNORE_MISSING);
if (
!$this->get_task()->is_samesite()
|| !$questionscontext
|| !$DB->record_exists('question_categories', ['id' => $oldcategoryid])
|| $owncontext
|| !has_capability('moodle/question:useall', $questionscontext)
) {
$newcategoryid = $this->get_mappingid('question_category', $oldcategoryid);
$filtercondition['filter']['category']['values'][0] = $newcategoryid;
$qbankfeatureclasses = \core\component::get_plugin_list_with_class('qbank', 'plugin_feature');
if ($context = $this->get_mappingid('context', $data->questionscontextid)) {
$data->questionscontextid = $context;
} else {
$this->log('question_set_reference with old id ' . $data->id .
' referenced question context ' . $data->questionscontextid .
' which was not included in the backup. Therefore, this has been ' .
' restored with the old questionscontextid.', backup::LOG_WARNING);
foreach ($qbankfeatureclasses as $qbankfeatureclass) {
$qbankfeature = new $qbankfeatureclass();
$filters = $qbankfeature->get_question_filters();
foreach ($filters as $filter) {
$filtercondition = $filter->restore_filtercondition($filtercondition, $data, $this, $originalbankinbackup);
}
}
$filtercondition['cat'] = implode(',', [
$filtercondition['filter']['category']['values'][0],
$data->questionscontextid,
]);
$data->filtercondition = json_encode($filtercondition);
$DB->insert_record('question_set_references', $data);
+3 -20
View File
@@ -107,17 +107,12 @@ final class async_backup_test extends \advanced_testcase {
$asynctask = new \core\task\asynchronous_backup_task();
$asynctask->set_custom_data(['backupid' => $backupid]);
$asynctask->set_userid($USER->id);
\core\task\manager::queue_adhoc_task($asynctask);
// We are expecting trace output during this test.
$this->expectOutputRegex("/$backupid/");
// Execute adhoc task.
$now = time();
$task = \core\task\manager::get_next_adhoc_task($now);
$this->assertInstanceOf('\\core\\task\\asynchronous_backup_task', $task);
$task->execute();
\core\task\manager::adhoc_task_complete($task);
$asynctask->execute();
$postbackuprec = $DB->get_record('backup_controllers', ['backupid' => $backupid]);
@@ -151,13 +146,9 @@ final class async_backup_test extends \advanced_testcase {
// Create the adhoc task.
$asynctask = new \core\task\asynchronous_backup_task();
$asynctask->set_custom_data(['backupid' => $backupid]);
\core\task\manager::queue_adhoc_task($asynctask);
// Execute adhoc task.
$now = time();
$task = \core\task\manager::get_next_adhoc_task($now);
$task->execute();
\core\task\manager::adhoc_task_complete($task);
$asynctask->execute();
$postbackuprec = $DB->get_record('backup_controllers', ['backupid' => $backupid]);
@@ -241,19 +232,11 @@ final class async_backup_test extends \advanced_testcase {
// Now queue an adhoc task and check it handles and completes gracefully.
$asynctask = new \core\task\asynchronous_backup_task();
$asynctask->set_custom_data(array('backupid' => $backupid));
\core\task\manager::queue_adhoc_task($asynctask);
// We are expecting a specific message output during this test.
$this->expectOutputRegex('/invalid controller/');
// Execute adhoc task.
$now = time();
$task = \core\task\manager::get_next_adhoc_task($now);
$this->assertInstanceOf('\\core\\task\\asynchronous_backup_task', $task);
$task->execute();
\core\task\manager::adhoc_task_complete($task);
// Check the task record is removed.
$this->assertEquals(0, $DB->count_records('task_adhoc'));
$asynctask->execute();
}
}
+2 -18
View File
@@ -223,20 +223,12 @@ final class async_restore_test extends \advanced_testcase {
// Create the adhoc task.
$asynctask = new \core\task\asynchronous_restore_task();
$asynctask->set_custom_data(['backupid' => $restoreid]);
\core\task\manager::queue_adhoc_task($asynctask);
// We are expecting a specific message output during this test.
$this->expectOutputRegex('/invalid controller/');
// Execute adhoc task.
$now = time();
$task = \core\task\manager::get_next_adhoc_task($now);
$this->assertInstanceOf('\\core\\task\\asynchronous_restore_task', $task);
$task->execute();
\core\task\manager::adhoc_task_complete($task);
// Check the task record is removed.
$this->assertEquals(0, $DB->count_records('task_adhoc'));
$asynctask->execute();
// Now delete the record and confirm an entirely missing controller is handled.
$DB->delete_records('backup_controllers');
@@ -244,19 +236,11 @@ final class async_restore_test extends \advanced_testcase {
// Create the adhoc task.
$asynctask = new \core\task\asynchronous_restore_task();
$asynctask->set_custom_data(['backupid' => $restoreid]);
\core\task\manager::queue_adhoc_task($asynctask);
// We are expecting a specific message output during this test.
$this->expectOutputRegex('/Unable to find restore controller/');
// Execute adhoc task.
$now = time();
$task = \core\task\manager::get_next_adhoc_task($now);
$this->assertInstanceOf('\\core\\task\\asynchronous_restore_task', $task);
$task->execute();
\core\task\manager::adhoc_task_complete($task);
// Check the task record is removed.
$this->assertEquals(0, $DB->count_records('task_adhoc'));
$asynctask->execute();
}
}
+1 -1
View File
@@ -411,7 +411,7 @@ abstract class backup_helper {
}
$selectmenu = new \core\output\select_menu('coursereusetype', $menuarray, $activeurl);
$selectmenu = new \core\output\select_menu('coursereusetype', $menuarray, $activeurl, true);
$selectmenu->set_label(get_string('coursereusenavigationmenu'), ['class' => 'visually-hidden']);
$options = \html_writer::tag(
'div',
+1 -1
View File
@@ -217,7 +217,7 @@ if ($award && data_submitted() && has_capability('moodle/badges:awardbadge', $co
$users = $existingselector->get_selected_users();
foreach ($users as $user) {
if (!process_manual_revoke($user->id, $USER->id, $issuerrole->roleid, $badgeid)) {
if (!process_manual_revoke($user->id, 0, $issuerrole->roleid, $badgeid)) {
echo $OUTPUT->error_text(get_string('error:cannotrevokebadge', 'badges'));
}
}
+1 -1
View File
@@ -234,7 +234,7 @@ class award_criteria_course extends award_criteria {
$params['courseid'] = $this->courseid;
// Add by date parameter.
if (isset($param['bydate'])) {
if (isset($coursecriteria['bydate'])) {
$where .= ' AND cc.timecompleted <= :completebydate';
$params['completebydate'] = $coursecriteria['bydate'];
}
+17 -2
View File
@@ -211,7 +211,10 @@ class award_criteria_courseset extends award_criteria {
$check_grade = ($grade->grade >= $param['grade']);
}
if (!$filtered && isset($param['bydate'])) {
// When the user list is not filtered, or the aggregation method is ANY,
// ensure the course completion date is before the bydate (if set).
$comparedates = !$filtered || $this->method == BADGE_CRITERIA_AGGREGATION_ANY;
if ($comparedates && isset($param['bydate'])) {
$cparams = array(
'userid' => $userid,
'course' => $course->id,
@@ -254,9 +257,16 @@ class award_criteria_courseset extends award_criteria {
$params = array();
if ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) {
$coursedata = [];
foreach ($this->params as $param) {
$coursedata[] = " cc.course = :completedcourse{$param['course']} ";
$conditionsql = " cc.course = :completedcourse{$param['course']} ";
$params["completedcourse{$param['course']}"] = $param['course'];
if (isset($param['bydate'])) {
$conditionsql .= " AND cc.timecompleted <= :completedbydate{$param['course']} ";
$params["completedbydate{$param['course']}"] = $param['bydate'];
}
$coursedata[] = "({$conditionsql})";
}
if (!empty($coursedata)) {
$extraon = implode(' OR ', $coursedata);
@@ -272,6 +282,11 @@ class award_criteria_courseset extends award_criteria {
cc{$param['course']}.timecompleted > 0 ";
$where .= " AND cc{$param['course']}.course IS NOT NULL ";
$params["completedcourse{$param['course']}"] = $param['course'];
if (isset($param['bydate'])) {
$where .= " AND cc{$param['course']}.timecompleted <= :completedbydate{$param['course']} ";
$params["completedbydate{$param['course']}"] = $param['bydate'];
}
}
return array($join, $where, $params);
}
+16 -16
View File
@@ -290,27 +290,27 @@ function process_manual_award($recipientid, $issuerid, $issuerrole, $badgeid) {
/**
* Manually revoke awarded badges.
*
* @param int $recipientid
* @param int $issuerid
* @param int $issuerrole
* @param int $badgeid
* @param int $recipientid User ID of the recipient
* @param int $issuerid User ID of the issuer (if 0, issuer will be ignored)
* @param int $issuerrole Role of the issuer
* @param int $badgeid ID of the badge
* @return bool
*/
function process_manual_revoke($recipientid, $issuerid, $issuerrole, $badgeid) {
global $DB;
$params = array(
'badgeid' => $badgeid,
'issuerid' => $issuerid,
'issuerrole' => $issuerrole,
'recipientid' => $recipientid
);
$params = [
'badgeid' => $badgeid,
'issuerrole' => $issuerrole,
'recipientid' => $recipientid,
];
if (!empty($issuerid)) {
$params['issuerid'] = $issuerid;
}
if ($DB->record_exists('badge_manual_award', $params)) {
if ($DB->delete_records('badge_manual_award', array('badgeid' => $badgeid,
'issuerid' => $issuerid,
'recipientid' => $recipientid))
&& $DB->delete_records('badge_issued', array('badgeid' => $badgeid,
'userid' => $recipientid))) {
if (
$DB->delete_records('badge_manual_award', $params) &&
$DB->delete_records('badge_issued', ['badgeid' => $badgeid, 'userid' => $recipientid])
) {
// Trigger event, badge revoked.
$badge = new \badge($badgeid);
$eventparams = array(
+16 -4
View File
@@ -11,11 +11,13 @@ Feature: Award badges
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| teacher2 | Teacher | 2 | teacher2@example.com |
| student1 | Student | 1 | student1@example.com |
| student2 | Student | 2 | student2@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| teacher2 | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
And the following "activity" exists:
@@ -371,14 +373,24 @@ Feature: Award badges
And I am on "Course 1" course homepage
And I navigate to "Badges" in current page administration
And I follow "Course Badge"
Then I should see "Recipients (2)"
And I select "Recipients (2)" from the "jump" singleselect
And I press "Award badge"
And I set the field "existingrecipients[]" to "Student 2 (student2@example.com)"
And I press "Revoke badge"
And I set the field "existingrecipients[]" to "Student 1 (student1@example.com)"
When I press "Revoke badge"
And I am on "Course 1" course homepage
And I navigate to "Badges" in current page administration
And I follow "Course Badge"
Then I should see "Recipients (0)"
Then I should see "Recipients (1)"
And I log out
# Now attempt to revoke a badge as another teacher.
And I am on the "Course 1" "course" page logged in as "teacher2"
And I navigate to "Badges" in current page administration
And I follow "Course Badge"
And I select "Recipients (1)" from the "jump" singleselect
And I press "Award badge"
And I set the field "existingrecipients[]" to "Student 1 (student1@example.com)"
And I press "Revoke badge"
And I am on "Course 1" course homepage
And I navigate to "Badges" in current page administration
And I follow "Course Badge"
And I should see "Recipients (0)"
@@ -0,0 +1,149 @@
@mod @mod_quiz @core_badges @core_completion @javascript
Feature: Award badges based on course completion
In order to ensure a student has learned the required material
As a teacher
I need a badge to be awarded only when the student completes the course or a set of courses.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
| student2 | Student | 2 | student2@example.com |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category | enablecompletion |
| Course 1 | C1 | 0 | 1 |
| Course 2 | C2 | 0 | 1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
| teacher1 | C2 | editingteacher |
| student1 | C2 | student |
| student2 | C2 | student |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
| Course | C2 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | First question | Answer the first question |
And the following "activities" exist:
| activity | name | course | idnumber | attempts | gradepass | completion | completionpassgrade | completionusegrade |
| quiz | Test quiz name C1 | C1 | quiz1 | 2 | 5.00 | 2 | 1 | 1 |
| quiz | Test quiz name C2 | C2 | quiz1 | 2 | 5.00 | 2 | 1 | 1 |
And quiz "Test quiz name C1" contains the following questions:
| question | page |
| First question | 1 |
And quiz "Test quiz name C2" contains the following questions:
| question | page |
| First question | 1 |
And the following "blocks" exist:
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
| completionstatus | Course | C1 | course-view-* | side-pre |
| completionstatus | Course | C2 | course-view-* | side-pre |
And I am on the "Course 1" course page logged in as teacher1
And I navigate to "Course completion" in current page administration
And I expand all fieldsets
And I set the field "Test quiz name C1" to "1"
And I press "Save changes"
And I am on the "Course 2" course page logged in as teacher1
And I navigate to "Course completion" in current page administration
And I expand all fieldsets
And I set the field "Test quiz name C2" to "1"
And I press "Save changes"
Scenario Outline: Badge awarded depending on the course completion by-date
Given I am on the "Course 1" course page
And the following "core_badges > Badge" exists:
| name | Course Badge 1 |
| status | 0 |
| type | 2 |
| course | C1 |
| description | Course badge 1 description |
| image | badges/tests/behat/badge.png |
And I navigate to "Badges" in current page administration
And I click on "Course Badge 1" "link"
And I select "Criteria" from the "jump" singleselect
And I set the field "type" to "Course completion"
And I set the field "Enable" to "1"
And I set the field "Day" to "1"
And I set the field "Month" to "January"
And I set the field "Year" to "<year>"
And I press "Save"
And I press "Enable access"
And I click on "Enable" "button" in the "Confirm" "dialogue"
And user "student1" has attempted "Test quiz name C1" with responses:
| slot | response |
| 1 | True |
# Completion cron won't mark the whole course completed unless the
# individual criteria was marked completed more than a second ago. So
# run it twice, first to mark the criteria and second for the course.
And I run the scheduled task "core\task\completion_regular_task"
And I wait "1" seconds
And I run the scheduled task "core\task\completion_regular_task"
When I run the scheduled task "core\task\badges_cron_task"
And I run all adhoc tasks
And I navigate to "Badges" in current page administration
And I click on "Course Badge 1" "link"
Then I should see "Recipients (<count>)"
And I select "Recipients (<count>)" from the "jump" singleselect
And I <student1_visibility> see "Student 1"
And I should not see "Student 2"
Examples:
| year | count | student1_visibility |
| ## +1 year ## %Y ## | 1 | should |
| ## -1 year ## %Y ## | 0 | should not |
Scenario Outline: Badge awarded depending on the courseset completion by-date
Given I am logged in as "admin"
And the following "core_badges > Badge" exists:
| name | Site Badge |
| status | 0 |
| description | Site badge description |
| image | badges/tests/behat/badge.png |
And I navigate to "Badges > Manage badges" in site administration
And I press "Edit" action in the "Site Badge" report row
And I select "Criteria" from the "jump" singleselect
And I set the field "type" to "Completing a set of courses"
And I set the field "courses" to "Course 1, Course 2"
And I press "Add courses"
And I expand all fieldsets
# Set a very high grade requirement to ensure that the course completion criteria won't be marked completed just by passing the quiz.
And I set the field with xpath "(//input[contains(@name, 'grade')])[1]" to "200"
And I set the field with xpath "(//input[contains(@name, 'enabled')])[2]" to "1"
And I set the field with xpath "(//select[contains(@name, 'day')])[2]" to "1"
And I set the field with xpath "(//select[contains(@name, 'month')])[2]" to "January"
And I set the field with xpath "(//select[contains(@name, 'year')])[2]" to "<year>"
And I click on "Any of the selected courses is complete" "radio"
And I press "Save"
And I press "Enable access"
And I click on "Enable" "button" in the "Confirm" "dialogue"
And user "student1" has attempted "Test quiz name C1" with responses:
| slot | response |
| 1 | True |
And user "student1" has attempted "Test quiz name C2" with responses:
| slot | response |
| 1 | True |
# Completion cron won't mark the whole course completed unless the
# individual criteria was marked completed more than a second ago. So
# run it twice, first to mark the criteria and second for the course.
And I run the scheduled task "core\task\completion_regular_task"
And I wait "1" seconds
And I run the scheduled task "core\task\completion_regular_task"
When I run the scheduled task "core\task\badges_cron_task"
And I run all adhoc tasks
And I navigate to "Badges > Manage badges" in site administration
And I follow "Site Badge"
Then I should see "Recipients (<count>)"
And I select "Recipients (<count>)" from the "jump" singleselect
And I <student1_visibility> see "Student 1"
And I should not see "Student 2"
Examples:
| year | count | student1_visibility |
| ## +1 year ## %Y ## | 1 | should |
| ## -1 year ## %Y ## | 0 | should not |
+9
View File
@@ -0,0 +1,9 @@
# block_html Upgrade notes
## 5.0.6+
### Changed
- Treat Dashboard (pagetype 'my-index') as trusted in web services so get_content_for_external preserves embedded HTML (e.g. iframes) on user Dashboard.
For more information see [MDL-85322](https://tracker.moodle.org/browse/MDL-85322)
+13 -3
View File
@@ -165,14 +165,24 @@ class block_html extends block_base {
}
function content_is_trusted() {
global $SCRIPT;
global $SCRIPT, $USER;
if (!$context = context::instance_by_id($this->instance->parentcontextid, IGNORE_MISSING)) {
return false;
}
//find out if this block is on the profile page
if ($context->contextlevel == CONTEXT_USER) {
if ($SCRIPT === '/my/index.php') {
$usersubpage = my_get_page($USER->id);
$usersubpage = $usersubpage->id ?? null;
if (
$SCRIPT === '/my/index.php' ||
(
defined('WS_SERVER') &&
WS_SERVER &&
!empty($this->page) &&
$this->page->pagetype === 'my-index' &&
$this->page->subpage === $usersubpage
)
) {
// this is exception - page is completely private, nobody else may see content there
// that is why we allow JS here
return true;
+2 -3
View File
@@ -240,9 +240,8 @@ class block_base {
$bc->title = $this->title;
}
if (empty($bc->title)) {
$bc->arialabel = new lang_string('pluginname', get_class($this));
$this->arialabel = $bc->arialabel;
if ((string) $bc->title === '') {
$this->arialabel = $bc->arialabel = get_string('pluginname', get_class($this));
}
if ($this->page->user_is_editing() && $this->instance_can_be_edited()) {
@@ -66,7 +66,7 @@
<div class="pb-2" data-region="event-list-wrapper">
{{#eventsbyday}}
<div class="mt-3" data-region="event-list-content-date" data-timestamp="{{dayTimestamp}}">
<h5 class="h6 d-inline {{^courseview}}fw-bold px-2{{/courseview}}">{{#userdate}} {{dayTimestamp}}, {{#str}} strftimedaydate, core_langconfig {{/str}} {{/userdate}}</h5>
<h4 class="h6 d-inline {{^courseview}}fw-bold px-2{{/courseview}}">{{#userdate}} {{dayTimestamp}}, {{#str}} strftimedaydate, core_langconfig {{/str}} {{/userdate}}</h4>
</div>
{{> block_timeline/event-list-items }}
{{/eventsbyday}}
@@ -66,13 +66,13 @@
</div>
<div class="event-name-container flex-grow-1 line-height-3 nowrap text-truncate">
<div class="d-flex">
<h6 class="event-name mb-0 pb-1 text-truncate">
<h5 class="h6 event-name mb-0 pb-1 text-truncate">
{{#overdue}}<span class="badge rounded-pill bg-danger text-white ms-1 float-end">{{#str}} overdue, block_timeline {{/str}}</span>{{/overdue}}
<a href="{{url}}"
title="{{name}}"
aria-label='{{#cleanstr}} ariaeventlistitem, block_timeline, { "name": {{#quote}}{{{activityname}}}{{/quote}}, "course": {{#quote}}{{{course.fullnamedisplay}}}{{/quote}}, "date": "{{#userdate}} {{timesort}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}}" } {{/cleanstr}}'>
{{{activityname}}}</a>
</h6>
</h5>
</div>
<small class="mb-0">
{{#courseview}}
@@ -86,7 +86,7 @@
</div>
{{#action.actionable}}
<div class="d-flex timeline-action-button">
<h6 class="event-action">
<h5 class="h6 event-action">
<a class="list-group-item-action btn btn-outline-secondary btn-sm text-nowrap"
href="{{action.url}}"
aria-label="{{action.name}}"
@@ -96,7 +96,7 @@
<span class="badge bg-secondary text-dark">{{action.itemcount}}</span>
{{/action.showitemcount}}
</a>
</h6>
</h5>
</div>
{{/action.actionable}}
</div>
@@ -60,54 +60,56 @@
{{#str}} overdue, block_timeline {{/str}}
</a>
<div class="dropdown-divider" role="separator"></div>
<h6 class="dropdown-header">{{#str}} duedate, block_timeline {{/str}}</h6>
<a
class="dropdown-item"
href="#"
data-from="0"
data-to="7"
data-filtername="next7days"
{{#next7days}}aria-current="true"{{/next7days}}
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next7days, block_timeline {{/str}}{{/str}}"
role="menuitem"
>
{{#str}} next7days, block_timeline {{/str}}
</a>
<a
class="dropdown-item"
href="#"
data-from="0"
data-to="30"
data-filtername="next30days"
{{#next30days}}aria-current="true"{{/next30days}}
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next30days, block_timeline {{/str}}{{/str}}"
role="menuitem"
>
{{#str}} next30days, block_timeline {{/str}}
</a>
<a
class="dropdown-item"
href="#"
data-from="0"
data-to="90"
data-filtername="next3months"
{{#next3months}}aria-current="true"{{/next3months}}
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next3months, block_timeline {{/str}}{{/str}}"
role="menuitem"
>
{{#str}} next3months, block_timeline {{/str}}
</a>
<a
class="dropdown-item"
href="#"
data-from="0"
data-to="180"
data-filtername="next6months"
{{#next6months}}aria-current="true"{{/next6months}}
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next6months, block_timeline {{/str}}{{/str}}"
role="menuitem"
>
{{#str}} next6months, block_timeline {{/str}}
</a>
<div role="group" aria-labelledby="duedatefiltergrouplabel">
<div class="h6 dropdown-header" role="presentation" id="duedatefiltergrouplabel">{{#str}} duedate, block_timeline {{/str}}</div>
<a
class="dropdown-item"
href="#"
data-from="0"
data-to="7"
data-filtername="next7days"
{{#next7days}}aria-current="true"{{/next7days}}
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next7days, block_timeline {{/str}}{{/str}}"
role="menuitem"
>
{{#str}} next7days, block_timeline {{/str}}
</a>
<a
class="dropdown-item"
href="#"
data-from="0"
data-to="30"
data-filtername="next30days"
{{#next30days}}aria-current="true"{{/next30days}}
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next30days, block_timeline {{/str}}{{/str}}"
role="menuitem"
>
{{#str}} next30days, block_timeline {{/str}}
</a>
<a
class="dropdown-item"
href="#"
data-from="0"
data-to="90"
data-filtername="next3months"
{{#next3months}}aria-current="true"{{/next3months}}
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next3months, block_timeline {{/str}}{{/str}}"
role="menuitem"
>
{{#str}} next3months, block_timeline {{/str}}
</a>
<a
class="dropdown-item"
href="#"
data-from="0"
data-to="180"
data-filtername="next6months"
{{#next6months}}aria-current="true"{{/next6months}}
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next6months, block_timeline {{/str}}{{/str}}"
role="menuitem"
>
{{#str}} next6months, block_timeline {{/str}}
</a>
</div>
</div>
</div>
@@ -37,8 +37,10 @@ Feature: The timeline block allows users to see upcoming courses
| student1 | C3 | student |
| student1 | C4 | student |
@accessibility
Scenario: Next 30 days in course view
Given I log in as "student1"
And the "Timeline" "block" should meet accessibility standards with "best-practice" extra tests
And I click on "Filter timeline by date" "button" in the "Timeline" "block"
And I click on "Next 30 days" "link" in the "Timeline" "block"
And I click on "Sort timeline items" "button" in the "Timeline" "block"
@@ -61,6 +63,7 @@ Feature: The timeline block allows users to see upcoming courses
And "Test feedback 2" "link" should not exist in the "Timeline" "block"
And "Test feedback 3" "link" should not exist in the "Timeline" "block"
And "Test assign 1" "link" should not exist in the "Timeline" "block"
And the "Timeline" "block" should meet accessibility standards with "best-practice" extra tests
Scenario: All in course view
Given I log in as "student1"
@@ -50,9 +50,11 @@ Feature: The timeline block allows users to see upcoming activities
And "Test assign 1" "link" should not exist in the "Timeline" "block"
And I should not see "Assignment is due · Course 1" in the "Timeline" "block"
@accessibility
Scenario: Overdue in date view
Given I log in as "student1"
And I click on "Filter timeline by date" "button" in the "Timeline" "block"
And the "Timeline" "block" should meet accessibility standards with "best-practice" extra tests
When I click on "Overdue" "link" in the "Timeline" "block"
Then "Test assign 1" "link" should exist in the "Timeline" "block"
And I should see "Assignment is due · Course 1" in the "Timeline" "block"
@@ -63,6 +65,7 @@ Feature: The timeline block allows users to see upcoming activities
And "Test choice 3" "link" should not exist in the "Timeline" "block"
And "Test feedback 3" "link" should not exist in the "Timeline" "block"
And "Test feedback 4" "link" should not exist in the "Timeline" "block"
And the "Timeline" "block" should meet accessibility standards with "best-practice" extra tests
Scenario: All in date view
Given I log in as "student1"
+1 -1
View File
@@ -363,7 +363,7 @@ function cohort_can_view_cohort($cohortorid, $currentcontext) {
$cohort = $cohortorid;
}
if ($cohort && in_array($cohort->contextid, $currentcontext->get_parent_context_ids())) {
if ($cohort && in_array($cohort->contextid, $currentcontext->get_parent_context_ids(true))) {
if ($cohort->visible) {
return true;
}
+15
View File
@@ -943,6 +943,21 @@ final class lib_test extends \advanced_testcase {
}
}
/**
* Test whether user can view cohort
*
* @covers ::cohort_can_view_cohort
*/
public function test_cohort_can_view_cohort(): void {
$this->resetAfterTest();
$this->setAdminUser();
$context = \context_system::instance();
$cohort = $this->getDataGenerator()->create_cohort(['contextid' => $context->id]);
$this->assertTrue(cohort_can_view_cohort($cohort, $context));
}
/**
* Test the behaviour of cohort_get_cohort().
*
+13
View File
@@ -475,6 +475,19 @@ trait form_trait {
// Don't use hardFreeze or checkbox value gets lost.
$mform->freeze($completionviewel);
}
$receivegradegroupel = 'receiveagradegroup' . $suffix;
if ($mform->elementExists($receivegradegroupel)) {
$groupelements = $mform->getElement($receivegradegroupel)->getElements();
foreach ($groupelements as $element) {
if ($element->_type == 'select') {
$element->updateAttributes(['disabled' => 'disabled']);
} else {
$element->freeze();
}
}
}
$completionusegradeel = 'completionusegrade' . $suffix;
if ($mform->elementExists($completionusegradeel)) {
$mform->freeze($completionusegradeel);
+2 -2
View File
@@ -70,14 +70,14 @@ class progress {
}
// Get the number of modules that support completion.
$modules = $completion->get_activities();
$modules = $completion->get_user_activities_with_completion($userid);
$count = count($modules);
if (!$count) {
return null;
}
// Get the number of modules that have been completed.
$totalcompleted = $completion->count_modules_completed($userid);
$totalcompleted = $completion->count_modules_completed($userid, array_keys($modules));
return ($totalcompleted / $count) * 100;
}
+292
View File
@@ -17,6 +17,8 @@
namespace core_completion;
use completion_completion;
use core_availability\tree;
use availability_date\condition;
/**
* Test completion progress API.
@@ -67,6 +69,8 @@ final class progress_test extends \advanced_testcase {
// Add an activity that does *not* use completion.
$this->getDataGenerator()->create_module('assign', array('course' => $course->id));
$this->setUser($user);
// Mark two of them as completed for a user.
$cmassign = get_coursemodule_from_id('assign', $assign->cmid);
$cmdata = get_coursemodule_from_id('data', $data->cmid);
@@ -283,4 +287,292 @@ final class progress_test extends \advanced_testcase {
// Check that the result is null now.
$this->assertNull(\core_completion\progress::get_course_progress_percentage($course, $user->id));
}
/**
* Tests course progress with hidden section.
*/
public function test_course_progress_percentage_with_hidden_section(): void {
global $DB;
// Create a course with completion enabled and two sections.
$course = $this->getDataGenerator()->create_course([
'enablecompletion' => 1,
'numsections' => 2,
'format' => 'topics',
]);
// Enrol a student.
$user = $this->getDataGenerator()->create_user();
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
$this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id);
/** @var \mod_assign_generator $assigngenerator */
$assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
// Add visible activities to section 0 and 1.
$activity1 = $assigngenerator->create_instance([
'course' => $course->id,
'section' => 0,
'completion' => COMPLETION_ENABLED,
]);
$activity2 = $assigngenerator->create_instance([
'course' => $course->id,
'section' => 1,
'completion' => COMPLETION_ENABLED,
]);
$this->setUser($user);
// Hide section 1.
set_section_visible($course->id, 1, 0);
$completion = new \completion_info($course);
// Complete the visible activity: activity1.
$cm = get_coursemodule_from_id('assign', $activity1->cmid);
$completion->update_state($cm, COMPLETION_COMPLETE, $user->id);
// Only the visible activity (activity1) counts toward course completion.
// Activities in hidden sections are not included in the calculation.
$this->assertEquals(100, \core_completion\progress::get_course_progress_percentage($course, $user->id));
// Now unhide section 1.
set_section_visible($course->id, 1, 1);
// Course completion: 1 of 2 visible activities complete; previously hidden activity now counted.
$this->assertEquals(50, \core_completion\progress::get_course_progress_percentage($course, $user->id));
}
/**
* Tests course progress with hidden activity.
*/
public function test_course_progress_percentage_with_hidden_activity(): void {
global $DB;
// Create a course with completion enabled and two sections.
$course = $this->getDataGenerator()->create_course([
'enablecompletion' => 1,
'numsections' => 3,
'format' => 'topics',
]);
// Enrol a student.
$user = $this->getDataGenerator()->create_user();
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
$this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id);
/** @var \mod_assign_generator $assigngenerator */
$assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
// Section 0: visible activity.
$activity1 = $assigngenerator->create_instance([
'course' => $course->id,
'section' => 0,
'completion' => COMPLETION_ENABLED,
]);
// Section 1: hidden activity.
$activity2 = $assigngenerator->create_instance([
'course' => $course->id,
'section' => 1,
'completion' => COMPLETION_ENABLED,
'visible' => 0,
]);
$completion = new \completion_info($course);
$this->setUser($user);
// Complete activity1 (visible).
$cm1 = get_coursemodule_from_id('assign', $activity1->cmid);
$completion->update_state($cm1, COMPLETION_COMPLETE, $user->id);
// Only 1 visible activity: course completion = 100%.
$this->assertEquals(100, \core_completion\progress::get_course_progress_percentage($course, $user->id));
$cm2 = get_coursemodule_from_id('assign', $activity2->cmid);
// Show activity2.
$DB->set_field('course_modules', 'visible', 1, ['id' => $cm2->id]);
$completion->update_state($cm2, COMPLETION_COMPLETE, $user->id);
rebuild_course_cache($course->id, true);
// Course completion: both activities are complete.
$this->assertEquals(100, \core_completion\progress::get_course_progress_percentage($course, $user->id));
// Hide activity1 so it is excluded from course progress.
$DB->set_field('course_modules', 'visible', 0, ['id' => $cm1->id]);
rebuild_course_cache($course->id, true);
// Course completion: activity1 is hidden and excluded from calculation.
// Only activity2 remains visible and complete, so progress stays at 100%.
$this->assertEquals(100, \core_completion\progress::get_course_progress_percentage($course, $user->id));
}
/**
* Tests course progress percentage with availability restrictions.
*/
public function test_course_progress_percentage_with_availability_restrictions(): void {
global $DB;
set_config('enableavailability', 1);
// Create a course with completion enabled.
$course = $this->getDataGenerator()->create_course([
'enablecompletion' => 1,
'format' => 'topics',
]);
// Create and enrol a student.
$user = $this->getDataGenerator()->create_user();
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
$this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id);
/** @var \mod_assign_generator $assigngenerator */
$assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
// Activity 1: no restrictions (always visible).
$assign['activity1'] = $assigngenerator->create_instance([
'course' => $course->id,
'completion' => COMPLETION_ENABLED,
]);
// Activity 2: date restriction from past (visible).
$assign['activity2'] = $assigngenerator->create_instance([
'course' => $course->id,
'completion' => COMPLETION_ENABLED,
]);
$availabilityjson = json_encode(tree::get_root_json(
[
condition::get_json(condition::DIRECTION_FROM, time() - 3600),
],
tree::OP_AND,
false,
));
$DB->set_field('course_modules', 'availability', $availabilityjson, ['id' => $assign['activity2']->cmid]);
// Activity 3: date restriction from future, shows info (open eye).
$assign['activity3'] = $assigngenerator->create_instance([
'course' => $course->id,
'completion' => COMPLETION_ENABLED,
]);
$availabilityjson = json_encode(tree::get_root_json(
[
condition::get_json(condition::DIRECTION_FROM, time() + 3600),
],
tree::OP_AND,
true,
));
$DB->set_field('course_modules', 'availability', $availabilityjson, ['id' => $assign['activity3']->cmid]);
// Activity 4: date restriction from future, hidden (closed eye).
$assign['activity4'] = $assigngenerator->create_instance([
'course' => $course->id,
'completion' => COMPLETION_ENABLED,
]);
$availabilityjson = json_encode(tree::get_root_json(
[
condition::get_json(condition::DIRECTION_FROM, time() + 7200),
],
tree::OP_AND,
false,
));
$DB->set_field('course_modules', 'availability', $availabilityjson, ['id' => $assign['activity4']->cmid]);
rebuild_course_cache($course->id, true);
// Set user context and get completion info.
$this->setUser($user);
$completion = new \completion_info($course);
// Initial completion: 0%.
$this->assertEquals(0, \core_completion\progress::get_course_progress_percentage($course, $user->id));
// Complete activity 1.
$cm1 = get_coursemodule_from_id('assign', $assign['activity1']->cmid);
$completion->update_state($cm1, COMPLETION_COMPLETE, $user->id);
// 1 of 3 user-visible activities is complete; activity4 is hidden and ignored.
$this->assertEquals(33.33, round(\core_completion\progress::get_course_progress_percentage($course, $user->id), 2));
// Complete activity 2 (available from past).
$cm2 = get_coursemodule_from_id('assign', $assign['activity2']->cmid);
$completion->update_state($cm2, COMPLETION_COMPLETE, $user->id);
// Now 2 of the 3 user-visible activities are complete.
$this->assertEquals(66.67, round(\core_completion\progress::get_course_progress_percentage($course, $user->id), 2));
// Mark the restricted activity (with open eye) as complete.
$cm3 = get_coursemodule_from_id('assign', $assign['activity3']->cmid);
$completion->update_state($cm3, COMPLETION_COMPLETE, $user->id);
// All 3 user-visible activities are now complete; activity4 remains hidden.
$this->assertEquals(100, round(\core_completion\progress::get_course_progress_percentage($course, $user->id), 2));
}
/**
* Tests course progress percentage with group restrictions.
*/
public function test_course_progress_percentage_with_group_restrictions(): void {
global $DB;
set_config('enableavailability', 1);
// Create a course with completion enabled.
$course = $this->getDataGenerator()->create_course([
'enablecompletion' => 1,
'format' => 'topics',
]);
// Create and enrol two students in the course.
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
$this->getDataGenerator()->enrol_user($user1->id, $course->id, $studentrole->id);
$this->getDataGenerator()->enrol_user($user2->id, $course->id, $studentrole->id);
// Create a group and add user1.
$group1 = $this->getDataGenerator()->create_group([
'courseid' => $course->id,
'name' => 'Group 1',
]);
$this->getDataGenerator()->create_group_member([
'groupid' => $group1->id,
'userid' => $user1->id,
]);
/** @var \mod_assign_generator $assigngenerator */
$assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
// Activity 1: restricted to group1.
$assign['activity1'] = $assigngenerator->create_instance([
'course' => $course->id,
'completion' => COMPLETION_ENABLED,
]);
$availabilityjson = json_encode(tree::get_root_json(
[
\availability_group\condition::get_json($group1->id),
],
tree::OP_AND,
false,
));
$DB->set_field('course_modules', 'availability', $availabilityjson, ['id' => $assign['activity1']->cmid]);
// Activity 2: Visible to all users.
$assign['activity2'] = $assigngenerator->create_instance([
'course' => $course->id,
'completion' => COMPLETION_ENABLED,
]);
rebuild_course_cache($course->id, true);
$cm1 = get_coursemodule_from_id('assign', $assign['activity1']->cmid);
$cm2 = get_coursemodule_from_id('assign', $assign['activity2']->cmid);
// Test user1 (in group): complete both the activities.
$this->setUser($user1);
$completion = new \completion_info($course);
$completion->update_state($cm1, COMPLETION_COMPLETE, $user1->id);
$this->assertEquals(50, \core_completion\progress::get_course_progress_percentage($course, $user1->id));
$completion->update_state($cm2, COMPLETION_COMPLETE, $user1->id);
$this->assertEquals(100, \core_completion\progress::get_course_progress_percentage($course, $user1->id));
// Test user2 (not in group1): completes only the visible activity.
$this->setUser($user2);
$completion = new \completion_info($course);
$completion->update_state($cm2, COMPLETION_COMPLETE, $user2->id);
$this->assertEquals(100, \core_completion\progress::get_course_progress_percentage($course, $user2->id));
}
}
Generated
+679 -430
View File
File diff suppressed because it is too large Load Diff
@@ -74,6 +74,7 @@ class activity_navigation implements renderable, templatable {
$attributes = [
'class' => 'btn btn-link',
'id' => 'prev-activity-link',
'aria-label' => get_string('activitynavigation:preva', 'course', $linkname),
];
$this->prevlink = new \action_link($linkurl, $OUTPUT->larrow() . ' ' . $linkname, null, $attributes);
}
@@ -89,15 +90,16 @@ class activity_navigation implements renderable, templatable {
$attributes = [
'class' => 'btn btn-link',
'id' => 'next-activity-link',
'aria-label' => get_string('activitynavigation:nexta', 'course', $linkname),
];
$this->nextlink = new \action_link($linkurl, $linkname . ' ' . $OUTPUT->rarrow(), null, $attributes);
}
// Render the activity list dropdown menu if available.
if (!empty($activitylist)) {
$select = new url_select($activitylist, '', array('' => get_string('jumpto')));
$select->set_label(get_string('jumpto'), ['class' => 'visually-hidden']);
$select->attributes = array('id' => 'jump-to-activity');
$select = new url_select($activitylist, '', ['' => get_string('activitynavigation:jumptoactivity', 'course')]);
$select->set_label(get_string('activitynavigation:jumptoactivity', 'course'), ['class' => 'visually-hidden']);
$select->attributes = ['id' => 'jump-to-activity'];
$this->activitylist = $select;
}
}
@@ -61,7 +61,8 @@ class completion_action_bar implements templatable, renderable {
$selectmenu = new select_menu(
'coursecompletionnavigation',
manager::get_available_completion_options($this->courseid),
$this->currenturl->out(false)
$this->currenturl->out(false),
true
);
$selectmenu->set_label(
get_string('coursecompletionnavigation', 'completion'),
-2
View File
@@ -217,8 +217,6 @@ class course_edit_form extends moodleform {
}
if (!empty($course->id) and !has_capability('moodle/course:changesummary', $coursecontext)) {
// Remove the description header it does not contain anything any more.
$mform->removeElement('descriptionhdr');
$mform->hardFreeze($summaryfields);
}
+19 -19
View File
@@ -903,32 +903,32 @@ abstract class base {
* 'expanded' (bool) if true the section will be shown expanded, true by default
* @return null|moodle_url
*/
public function get_view_url($section, $options = array()) {
public function get_view_url($section, $options = []) {
$course = $this->get_course();
$url = new moodle_url('/course/view.php', ['id' => $course->id]);
$section = (is_object($section) || is_null($section)) ? $section : $this->get_section($section, IGNORE_MISSING);
// Determine page.
if (array_key_exists('sr', $options)) {
$sectionno = $options['sr'];
} else if (is_object($section)) {
$sectionno = $section->section;
$pagesection = !is_null($options['sr']) ? $this->get_section($options['sr'], IGNORE_MISSING) : null;
} else if ($options['navigation'] ?? false) {
$pagesection = $section;
} else {
$sectionno = $section;
$pagesection = null;
}
if ((!empty($options['navigation']) || array_key_exists('sr', $options)) && $sectionno !== null) {
// Display section on separate page.
$sectioninfo = $this->get_section($sectionno);
return new moodle_url('/course/section.php', ['id' => $sectioninfo->id]);
// Base URL.
if (is_null($pagesection)) {
$url = new moodle_url('/course/view.php', ['id' => $course->id]);
} else {
$url = new moodle_url('/course/section.php', ['id' => $pagesection->id]);
}
if ($this->uses_sections() && $sectionno !== null) {
// The url includes the parameter to expand the section by default.
if (!array_key_exists('expanded', $options)) {
$options['expanded'] = true;
// Add details.
if ($this->uses_sections() && $section && ($section->id != $pagesection?->id)) {
if ($options['expanded'] ?? true) {
$url->param('expandsection', $section->section);
}
if ($options['expanded']) {
// This parameter is being set by default.
$url->param('expandsection', $sectionno);
}
$url->set_anchor('section-'.$sectionno);
$url->set_anchor('section-' . $section->section);
}
return $url;
+2 -2
View File
@@ -105,8 +105,8 @@ class get_state extends external_api {
}
foreach ($modinfo->cms as $cm) {
if ($cm->is_visible_on_course_page() && $cm->is_of_type_that_can_display()) {
// Only return this course module data if it's visible by current user on the course page.
if (($cm->is_visible_on_course_page() || $cm->is_stealth()) && $cm->is_of_type_that_can_display()) {
// Only return this course module data if it's accessible to the current user on the course page.
$section = $sections[$cm->sectionnum];
$cmstate = new $cmclass($courseformat, $section, $cm, istrackeduser: $istrackeduser);
$result->cm[] = $cmstate->export_for_template($renderer);
+3 -4
View File
@@ -76,9 +76,10 @@ class cmactions extends baseactions {
$paramcleaning = empty($CFG->formatstringstriptags) ? PARAM_CLEANHTML : PARAM_TEXT;
$name = clean_param($name, $paramcleaning);
if (empty($name)) {
if ($name === '') {
return false;
}
if (\core_text::strlen($name) > 255) {
throw new \moodle_exception('maximumchars', 'moodle', '', 255);
}
@@ -100,10 +101,8 @@ class cmactions extends baseactions {
'timemodified' => time(),
]
);
$cm->name = $name;
$fields = new \stdClass();
$fields->name = $name;
$cm->name = $name;
\core\event\course_module_updated::create_from_cm($cm)->trigger();
course_modinfo::purge_course_module_cache($cm->course, $cm->id);
@@ -120,12 +120,11 @@ class delegatedcontrolmenu extends basecontrolmenu {
return null;
}
$sectionreturn = $this->format->get_sectionnum();
$returnparams = !is_null($sectionreturn) ? ['sr' => $sectionreturn] : [];
$url = new url(
'/course/editsection.php',
[
'id' => $this->section->id,
'sr' => $this->section->sectionnum,
]
array_merge(['id' => $this->section->id], $returnparams)
);
return new link_secondary(
@@ -136,12 +136,11 @@ class controlmenu extends basecontrolmenu {
return null;
}
$sectionreturn = $this->format->get_sectionnum();
$returnparams = !is_null($sectionreturn) ? ['sr' => $sectionreturn] : [];
$url = new url(
'/course/editsection.php',
[
'id' => $this->section->id,
'sr' => $this->section->sectionnum,
]
array_merge(['id' => $this->section->id], $returnparams)
);
return new link_secondary(
@@ -39,23 +39,23 @@ Feature: Activity navigation in a single activity course
Scenario: Jump to a hidden activity as a teacher
Given I log in as "teacher1"
When I am on "Course 1" course homepage
Then "Jump to..." "field" should exist
Then "Jump to activity" "field" should exist
# The current activity (Forum 1) will not be listed.
And the "Jump to..." select box should not contain "Forum 1"
And the "Jump to activity" select box should not contain "Forum 1"
# Check drop down menu contents.
And the "Jump to..." select box should contain "Assignment 1 (hidden)"
And the "Jump to..." select box should contain "Lesson 1 (hidden)"
And the "Jump to activity" select box should contain "Assignment 1 (hidden)"
And the "Jump to activity" select box should contain "Lesson 1 (hidden)"
# Jump to a hidden activity somewhere in the middle.
When I select "Assignment 1 (hidden)" from the "Jump to..." singleselect
When I select "Assignment 1 (hidden)" from the "Jump to activity" singleselect
Then I should see "Assignment 1"
And I should see "Forum 1" in the "#prev-activity-link" "css_element"
And I should see "Lesson 1 (hidden)" in the "#next-activity-link" "css_element"
# Jump to the first activity.
And I select "Forum 1" from the "Jump to..." singleselect
And I select "Forum 1" from the "Jump to activity" singleselect
And I should see "Assignment 1 (hidden)" in the "#next-activity-link" "css_element"
But "#prev-activity-link" "css_element" should not exist
# Jump to the last activity.
And I select "Lesson 1 (hidden)" from the "Jump to..." singleselect
And I select "Lesson 1 (hidden)" from the "Jump to activity" singleselect
And I should see "Assignment 1 (hidden)" in the "#prev-activity-link" "css_element"
But "#next-activity-link" "css_element" should not exist
@@ -65,7 +65,7 @@ Feature: Activity navigation in a single activity course
# The first activity won't have the previous activity link.
Then "#prev-activity-link" "css_element" should not exist
And "#next-activity-link" "css_element" should not exist
And "Jump to..." "field" should not exist
And "Jump to activity" "field" should not exist
Scenario: The activity navigation asks for login to guest user
Given I log in as "guest"
+2 -15
View File
@@ -110,21 +110,8 @@ class format_topics extends core_courseformat\base {
* @return moodle_url
*/
public function get_view_url($section, $options = []) {
$course = $this->get_course();
if (array_key_exists('sr', $options) && !is_null($options['sr'])) {
$sectionno = $options['sr'];
} else if (is_object($section)) {
$sectionno = $section->section;
} else {
$sectionno = $section;
}
if ((!empty($options['navigation']) || array_key_exists('sr', $options)) && $sectionno !== null) {
// Display section on separate page.
$sectioninfo = $this->get_section($sectionno);
return new moodle_url('/course/section.php', ['id' => $sectioninfo->id]);
}
return new moodle_url('/course/view.php', ['id' => $course->id]);
$options['expanded'] = false;
return parent::get_view_url($section, $options);
}
/**
+3 -16
View File
@@ -126,22 +126,9 @@ class format_weeks extends core_courseformat\base {
* 'sr' (int) used by course formats to specify to which section to return
* @return moodle_url
*/
public function get_view_url($section, $options = array()) {
$course = $this->get_course();
if (array_key_exists('sr', $options) && !is_null($options['sr'])) {
$sectionno = $options['sr'];
} else if (is_object($section)) {
$sectionno = $section->section;
} else {
$sectionno = $section;
}
if ((!empty($options['navigation']) || array_key_exists('sr', $options)) && $sectionno !== null) {
// Display section on separate page.
$sectioninfo = $this->get_section($sectionno);
return new moodle_url('/course/section.php', ['id' => $sectioninfo->id]);
}
return new moodle_url('/course/view.php', ['id' => $course->id]);
public function get_view_url($section, $options = []) {
$options['expanded'] = false;
return parent::get_view_url($section, $options);
}
/**
+4
View File
@@ -1654,6 +1654,10 @@ function course_allowed_module($course, $modname, ?\stdClass $user = null) {
supports numeric module ids. Please update your code to pass the module name.');
}
if (!\core\plugininfo\mod::get_enabled_plugin($modname)) {
return false;
}
$capability = 'mod/' . $modname . ':addinstance';
if (!get_capability_info($capability)) {
// Debug warning that the capability does not exist, but no more than once per page.
@@ -56,7 +56,7 @@
"id": "url_select_test",
"action": "#",
"options": [
{"name": "Jump to...", "value": "#0"},
{"name": "Jump to activity", "value": "#0"},
{"name": "Activity A", "value": "#1"},
{"name": "Activity B", "value": "#2"},
{"name": "Activity C", "value": "#3"}
@@ -64,7 +64,7 @@
}
}
}}
<div class="mt-5 mb-1 activity-navigation container-fluid">
<nav class="mt-5 mb-1 activity-navigation container-fluid" aria-label="{{#str}}activitynavigation, course{{/str}}">
{{< core/columns-1to1to1}}
{{$column1}}
<div class="float-start">
@@ -82,4 +82,4 @@
</div>
{{/column3}}
{{/ core/columns-1to1to1}}
</div>
</nav>
+4 -4
View File
@@ -38,7 +38,7 @@ Feature: Display and choose from the available activities in course
Scenario: The teacher can choose to add an activity from the activity summary in the activity chooser
Given I open the activity chooser
When I click on "Information about the Assignment activity" "button" in the "Add an activity or resource" "dialogue"
When I click on "Add a new Assignment" "link" in the "help" "core_course > Activity chooser screen"
And I click on "Add a new Assignment" "link" in the ".modal-footer" "css_element"
Then I should see "New Assignment"
Scenario: Show summary
@@ -58,8 +58,8 @@ Feature: Display and choose from the available activities in course
Given I open the activity chooser
When I click on "Information about the Assignment activity" "button" in the "modules" "core_course > Activity chooser screen"
And I should see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback." in the "help" "core_course > Activity chooser screen"
And I should see "Back" in the "help" "core_course > Activity chooser screen"
When I click on "Back" "button" in the "help" "core_course > Activity chooser screen"
And I should see "Back" in the ".modal-footer" "css_element"
When I click on "Back" "button" in the ".modal-footer" "css_element"
Then "modules" "core_course > Activity chooser screen" should be visible
And "help" "core_course > Activity chooser screen" should not be visible
And "Back" "button" should not exist in the "modules" "core_course > Activity chooser screen"
@@ -69,7 +69,7 @@ Feature: Display and choose from the available activities in course
And I click on "Add content" "button" in the "13 January - 19 January" "section"
And I click on "Activity or resource" "button" in the "13 January - 19 January" "section"
And I click on "Information about the Assignment activity" "button" in the "Add an activity or resource" "dialogue"
And I click on "Back" "button" in the "help" "core_course > Activity chooser screen"
And I click on "Back" "button" in the ".modal-footer" "css_element"
And "modules" "core_course > Activity chooser screen" should be visible
And "help" "core_course > Activity chooser screen" should not be visible
And "Back" "button" should not exist in the "modules" "core_course > Activity chooser screen"
+48 -48
View File
@@ -178,47 +178,47 @@ Feature: Activity navigation
Given I log in as "teacher1"
When I am on "Course 1" course homepage
And I follow "Assignment 1"
Then "Jump to..." "field" should exist
Then "Jump to activity" "field" should exist
# The current activity will not be listed.
And the "Jump to..." select box should not contain "Assignment 1"
And the "Jump to activity" select box should not contain "Assignment 1"
# Stealth activities will not be listed.
And the "Jump to..." select box should not contain "Forum 1"
And the "Jump to activity" select box should not contain "Forum 1"
# Resources without view URL (e.g. labels) will not be listed.
And the "Jump to..." select box should not contain "Label 1"
And the "Jump to activity" select box should not contain "Label 1"
# Check drop down menu contents.
And the "Jump to..." select box should contain "Book 1"
And the "Jump to..." select box should contain "Choice 1"
And the "Jump to..." select box should contain "Database 1"
And the "Jump to..." select box should contain "Feedback 1"
And the "Jump to..." select box should contain "Folder 1"
And the "Jump to..." select box should contain "Imscp 1"
And the "Jump to..." select box should contain "Lesson 1"
And the "Jump to..." select box should contain "Lti 1"
And the "Jump to..." select box should contain "Page 1"
And the "Jump to..." select box should contain "Quiz 1"
And the "Jump to..." select box should contain "Url 1"
And the "Jump to..." select box should contain "Wiki 1"
And the "Jump to..." select box should contain "Workshop 1"
And the "Jump to activity" select box should contain "Book 1"
And the "Jump to activity" select box should contain "Choice 1"
And the "Jump to activity" select box should contain "Database 1"
And the "Jump to activity" select box should contain "Feedback 1"
And the "Jump to activity" select box should contain "Folder 1"
And the "Jump to activity" select box should contain "Imscp 1"
And the "Jump to activity" select box should contain "Lesson 1"
And the "Jump to activity" select box should contain "Lti 1"
And the "Jump to activity" select box should contain "Page 1"
And the "Jump to activity" select box should contain "Quiz 1"
And the "Jump to activity" select box should contain "Url 1"
And the "Jump to activity" select box should contain "Wiki 1"
And the "Jump to activity" select box should contain "Workshop 1"
# Hidden activities will be rendered with a '(hidden)' text.
And the "Jump to..." select box should contain "Glossary 1 (hidden)"
And the "Jump to activity" select box should contain "Glossary 1 (hidden)"
# Activities in hidden sections will be rendered with a '(hidden)' text.
And the "Jump to..." select box should contain "Resource 1 (hidden)"
And the "Jump to..." select box should contain "Scorm 1 (hidden)"
And the "Jump to activity" select box should contain "Resource 1 (hidden)"
And the "Jump to activity" select box should contain "Scorm 1 (hidden)"
# Jump to an activity somewhere in the middle.
When I select "Page 1" from the "Jump to..." singleselect
When I select "Page 1" from the "Jump to activity" singleselect
Then I should see "Page 1"
And I should see "Lti 1" in the "#prev-activity-link" "css_element"
And I should see "Quiz 1" in the "#next-activity-link" "css_element"
# Jump to the first activity.
And I select "Assignment 1" from the "Jump to..." singleselect
And I select "Assignment 1" from the "Jump to activity" singleselect
And I should see "Book 1" in the "#next-activity-link" "css_element"
But "#prev-activity-link" "css_element" should not exist
# Jump to the last activity.
And I select "Workshop 1" from the "Jump to..." singleselect
And I select "Workshop 1" from the "Jump to activity" singleselect
And I should see "Wiki 1" in the "#prev-activity-link" "css_element"
But "#next-activity-link" "css_element" should not exist
# Jump to a hidden activity.
And I select "Glossary 1" from the "Jump to..." singleselect
And I select "Glossary 1" from the "Jump to activity" singleselect
And I should see "Folder 1" in the "#prev-activity-link" "css_element"
And I should see "Imscp 1" in the "#next-activity-link" "css_element"
@@ -226,43 +226,43 @@ Feature: Activity navigation
Given I log in as "student1"
And I am on "Course 1" course homepage
And I follow "Assignment 1"
And "Jump to..." "field" should exist
And "Jump to activity" "field" should exist
# The current activity will not be listed.
And the "Jump to..." select box should not contain "Assignment 1"
And the "Jump to activity" select box should not contain "Assignment 1"
# Stealth activities will not be listed for students.
And the "Jump to..." select box should not contain "Forum 1"
And the "Jump to activity" select box should not contain "Forum 1"
# Resources without view URL (e.g. labels) will not be listed.
And the "Jump to..." select box should not contain "Label 1"
And the "Jump to activity" select box should not contain "Label 1"
# Hidden activities will not be listed for students.
And the "Jump to..." select box should not contain "Glossary 1"
And the "Jump to activity" select box should not contain "Glossary 1"
# Activities in hidden sections will not be listed for students.
And the "Jump to..." select box should not contain "Resource 1"
And the "Jump to..." select box should not contain "Scorm 1"
And the "Jump to activity" select box should not contain "Resource 1"
And the "Jump to activity" select box should not contain "Scorm 1"
# Only activities visible to students will be listed.
And the "Jump to..." select box should contain "Book 1"
And the "Jump to..." select box should contain "Choice 1"
And the "Jump to..." select box should contain "Database 1"
And the "Jump to..." select box should contain "Feedback 1"
And the "Jump to..." select box should contain "Folder 1"
And the "Jump to..." select box should contain "Imscp 1"
And the "Jump to..." select box should contain "Lesson 1"
And the "Jump to..." select box should contain "Lti 1"
And the "Jump to..." select box should contain "Page 1"
And the "Jump to..." select box should contain "Quiz 1"
And the "Jump to..." select box should contain "Url 1"
And the "Jump to..." select box should contain "Wiki 1"
And the "Jump to..." select box should contain "Workshop 1"
And the "Jump to activity" select box should contain "Book 1"
And the "Jump to activity" select box should contain "Choice 1"
And the "Jump to activity" select box should contain "Database 1"
And the "Jump to activity" select box should contain "Feedback 1"
And the "Jump to activity" select box should contain "Folder 1"
And the "Jump to activity" select box should contain "Imscp 1"
And the "Jump to activity" select box should contain "Lesson 1"
And the "Jump to activity" select box should contain "Lti 1"
And the "Jump to activity" select box should contain "Page 1"
And the "Jump to activity" select box should contain "Quiz 1"
And the "Jump to activity" select box should contain "Url 1"
And the "Jump to activity" select box should contain "Wiki 1"
And the "Jump to activity" select box should contain "Workshop 1"
# Jump to an activity somewhere in the middle.
When I select "Page 1" from the "Jump to..." singleselect
When I select "Page 1" from the "Jump to activity" singleselect
Then I should see "Page 1"
And I should see "Lti 1" in the "#prev-activity-link" "css_element"
And I should see "Quiz 1" in the "#next-activity-link" "css_element"
# Jump to the first activity.
And I select "Assignment 1" from the "Jump to..." singleselect
And I select "Assignment 1" from the "Jump to activity" singleselect
And I should see "Book 1" in the "#next-activity-link" "css_element"
But "#prev-activity-link" "css_element" should not exist
# Jump to the last activity.
And I select "Workshop 1" from the "Jump to..." singleselect
And I select "Workshop 1" from the "Jump to activity" singleselect
And I should see "Wiki 1" in the "#prev-activity-link" "css_element"
But "#next-activity-link" "css_element" should not exist
@@ -272,4 +272,4 @@ Feature: Activity navigation
And I follow "Assignment 1"
Then "#prev-activity-link" "css_element" should not exist
And "#next-activity-link" "css_element" should not exist
And "Jump to..." "field" should not exist
And "Jump to activity" "field" should not exist
@@ -48,13 +48,13 @@ Feature: Activity navigation involving activities with access restrictions
Given I am on the "Page 1" "page activity" page logged in as student1
Then I should see "Page 2" in the "#next-activity-link" "css_element"
# Activity that has access restriction should not show up in the dropdown.
And the "Jump to..." select box should not contain "Page 3"
And I select "Page 4" from the "Jump to..." singleselect
And the "Jump to activity" select box should not contain "Page 3"
And I select "Page 4" from the "Jump to activity" singleselect
# Page 2 should be shown in the previous link since Page 3 is not yet available.
And I should see "Page 2" in the "#prev-activity-link" "css_element"
And the "Jump to..." select box should not contain "Page 3"
And the "Jump to activity" select box should not contain "Page 3"
# Navigate to Page 2.
And I click on "Page 2" "link" in the "page-content" "region"
# Since Page 2 has now been viewed and deemed completed, Page 3 can now be accessed.
And I should see "Page 3" in the "#next-activity-link" "css_element"
And the "Jump to..." select box should contain "Page 3"
And the "Jump to activity" select box should contain "Page 3"
+6
View File
@@ -7404,6 +7404,12 @@ final class courselib_test extends advanced_testcase {
// Manager has permissions.
$this->assertTrue(course_allowed_module($course, 'assign', $manager));
// Disable the assign module.
$DB->set_field('modules', 'visible', 0, ['name' => 'assign']);
// Verify that disabled modules are not allowed.
$this->assertFalse(course_allowed_module($course, 'assign', $manager));
}
/**
+14 -2
View File
@@ -103,22 +103,34 @@ class data extends persistent {
/**
* For integer data field, persistent won't allow empty string, swap for null
*
* Ensure we constrain maximum value to that supported by database
*
* @param string|null $value
* @return self
*/
protected function set_intvalue(?string $value): self {
$value = (string) $value === '' ? null : (int) $value;
if ((string) $value === '') {
$value = null;
} else {
$value = min((int) $value, SQL_INT_MAX);
}
return $this->raw_set('intvalue', $value);
}
/**
* For decimal data field, persistent won't allow empty string, swap for null
*
* Ensure we constrain maximum value to that supported by database
*
* @param string|null $value
* @return self
*/
protected function set_decvalue(?string $value): self {
$value = (string) $value === '' ? null : (float) $value;
if ((string) $value === '') {
$value = null;
} else {
$value = min((float) $value, SQL_INT_MAX + .9999);
}
return $this->raw_set('decvalue', $value);
}
@@ -59,10 +59,24 @@ class data_controller extends \core_customfield\data_controller {
return;
}
// Required for subsequent input field comparison rule.
$mform->addElement('hidden', "{$elementname}_maximum", SQL_INT_MAX + 1);
$mform->setType("{$elementname}_maximum", PARAM_INT);
// Number input field.
$mform->addElement('float', $elementname, $this->get_field()->get_formatted_name());
$mform->addRule(
[$elementname, "{$elementname}_maximum"],
get_string('maximumvalueerror', 'customfield_number', SQL_INT_MAX),
'compare',
'lt',
);
if (!$this->get('id')) {
$mform->setDefault($elementname, $this->get_default_value());
}
if ($field->get_configdata_property('required')) {
$mform->addRule($elementname, null, 'required', null, 'client');
}
}
/**

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