Compare commits

..

286 Commits

Author SHA1 Message Date
Huong Nguyen ac802ad804 Moodle release 5.1.2 2026-02-06 09:16:30 +07:00
Huong Nguyen a379081818 NOBUG: Add upgrade notes 2026-02-06 09:16:30 +07:00
AMOS bot 550afb2a72 Automatically generated installer lang files 2026-02-06 00:07:49 +00:00
Mihail Geshoski e99b368c60 weekly release 5.1.1+ 2026-02-05 20:23:16 +08:00
Mihail Geshoski 70d026f0f7 NOBUG: Add upgrade notes 2026-02-05 20:23:16 +08:00
Mihail Geshoski 833f55cf11 Merge branch 'install_501_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_501_STABLE 2026-02-05 20:23:05 +08:00
Paul Holden f36fd442e8 MDL-87870 admin: tighten up validation of colour picker values. 2026-02-05 17:25:37 +07:00
Paul Holden 60564c02ab MDL-87870 filter_tex: escape background argument content. 2026-02-05 17:25:37 +07:00
Mihail Geshoski b39f3bb7f9 Merge branch 'MDL-86444_501_STABLE' of https://github.com/marxjohnson/moodle into MOODLE_501_STABLE 2026-02-05 17:25:35 +07:00
Huong Nguyen 31a3e0eaa9 Merge branch 'MDL-87352-501' of https://github.com/andimendunia/moodle into MOODLE_501_STABLE 2026-02-05 17:24:53 +07:00
Mihail Geshoski aac49cd591 Merge branch 'MDL-87258-501' of https://github.com/rjnl/moodle into MOODLE_501_STABLE 2026-02-05 17:24:53 +07:00
Jun Pataleta 557432b764 Merge branch 'MDL-87757-501' of https://github.com/muhammadarnaldo/moodle into MOODLE_501_STABLE 2026-02-05 17:24:53 +07:00
yusufwib01 8683b4a049 MDL-86785 tiny_equation: Add timeout handling for mimetex execution 2026-02-05 11:45:53 +08:00
Adrian Greeve 49b29449af Merge branch 'MDL-87559-501' of https://github.com/abias/moodle into MOODLE_501_STABLE 2026-02-05 11:45:53 +08:00
Mihail Geshoski a80b692453 Merge branch 'MDL-86437-501' of https://github.com/snake/moodle into MOODLE_501_STABLE 2026-02-05 11:45:53 +08:00
Huong Nguyen 5100441bba MDL-87843 filter_tex: Switch to use Colour picker setting
This will allow users to easily choose the background colour
and prevent invalid input values
2026-02-05 11:45:53 +08:00
Paul Holden ff5c46a912 MDL-87612 repository: safer unserializing of file source data. 2026-02-05 11:45:53 +08:00
Mihail Geshoski 860feade62 Merge branch 'MDL-87789-501' of https://github.com/snake/moodle into MOODLE_501_STABLE 2026-02-05 11:45:53 +08:00
AMOS bot e193924db6 Automatically generated installer lang files 2026-02-05 00:07:50 +00:00
Muhammad Arnaldo 364a762fa7 MDL-87757 block_myoverview: Remove navigation role and behat update
- Removal of the navigation role on main and zero-state template
- Add accessibility extra tests for the block_overview
2026-02-04 10:11:41 +07:00
Muhammad Arnaldo fa810327f7 MDL-87757 block_myoverview: fix the heading-order 2026-02-04 10:11:41 +07:00
Andi Permana b25ed2333f MDL-87352 admin: Add MoodleNet removal warnings 2026-02-04 10:07:30 +07:00
AMOS bot 6803fd0988 Automatically generated installer lang files 2026-02-04 00:07:44 +00:00
Alexander Bias 03341aea9f MDL-87559 core: Set $SCRIPT even outside public/ 2026-02-03 10:39:40 +01:00
Jun Pataleta a7c5fad12d Merge branch 'MDL-83984_501' of https://github.com/santoshndev/moodle into MOODLE_501_STABLE 2026-02-03 17:06:49 +08:00
Jake Dallimore 3132cc9d4f Merge branch 'MDL-85535-501' of https://github.com/junpataleta/moodle into MOODLE_501_STABLE 2026-02-03 16:59:16 +08:00
Andrew Nicols 2eb32285d7 Merge branch 'MDL-87199-501' of https://github.com/abias/moodle into MOODLE_501_STABLE 2026-02-03 16:51:07 +08:00
Jun Pataleta 274d5ccc62 Merge branch 'MDL-87185-501' of https://github.com/durenadev/moodle into MOODLE_501_STABLE 2026-02-03 16:50:14 +08:00
Jun Pataleta a105edc53f Merge branch 'MDL-87593-501-enfix' of https://github.com/mudrd8mz/moodle into MOODLE_501_STABLE 2026-02-03 16:47:47 +08:00
Jun Pataleta 582f2e673a Merge branch 'MDL-77162-filepicker-a11y-tab-order-repository-keyboard-selection-M5.1' of https://github.com/stopfstedt/moodle into MOODLE_501_STABLE 2026-02-03 16:44:14 +08:00
Jun Pataleta e9e6e0921d Merge branch 'MDL-87694-course-participant-profile-missing-heading-M5.1' of https://github.com/stopfstedt/moodle into MOODLE_501_STABLE 2026-02-03 15:18:21 +08:00
Rajneel Totaram 0a580ed98d MDL-87258 mod_lti: request access for PNA
Since Chrome 142, public servers accessing resources on local networks
gets blocked by default. Users need to allow access for such requests
to proceed.

This patch introduces a new config setting `ltiallowlocalnetwork`.
This determines if the local-network-access flag should be added to LTI
iframes so that Chrome users get prompted when a local network access
is being made.
2026-02-03 18:49:16 +12:00
Mihail Geshoski bb13942ace Merge branch 'MDL-87606_501_STABLE' of https://github.com/marxjohnson/moodle into MOODLE_501_STABLE 2026-02-03 14:21:57 +08:00
Mihail Geshoski 3c681534da Merge branch 'MDL-86609_501_STABLE' of https://github.com/marxjohnson/moodle into MOODLE_501_STABLE 2026-02-03 14:10:25 +08:00
Mihail Geshoski 036fc9ef9a Merge branch 'MOODLE_501_STABLE_MDL-84966' of https://github.com/niko-hoogeveen/moodle into MOODLE_501_STABLE 2026-02-03 13:44:14 +08:00
Mihail Geshoski 03a901b1fb Merge branch 'MDL-85653_501_STABLE' of https://github.com/marxjohnson/moodle into MOODLE_501_STABLE 2026-02-03 13:36:13 +08:00
Mihail Geshoski 6390af2f48 Merge branch 'MDL-87029-501' of https://github.com/cameron1729/moodle into MOODLE_501_STABLE 2026-02-03 13:26:28 +08:00
Huong Nguyen 078308b04a Merge branch 'MDL-87334-501' of https://github.com/paulholden/moodle into MOODLE_501_STABLE 2026-02-03 11:07:41 +07:00
Huong Nguyen c708bb044e Merge branch 'MDL-87289-501' of https://github.com/raortegar/moodle into MOODLE_501_STABLE 2026-02-03 10:43:48 +07:00
Huong Nguyen e001ebc339 Merge branch 'MDL-87396-501' of https://github.com/izendegi/moodle into MOODLE_501_STABLE 2026-02-03 10:37:06 +07:00
Huong Nguyen f9a69ed13c Merge branch 'MDL-87520-501-branch-number' of https://github.com/mudrd8mz/moodle into MOODLE_501_STABLE 2026-02-03 10:32:34 +07:00
Huong Nguyen f1f1738ca8 Merge branch 'MDL-86905-501' of https://github.com/junpataleta/moodle into MOODLE_501_STABLE 2026-02-03 10:27:03 +07:00
Huong Nguyen 8bdec47d8e Merge branch 'MDL-87675-501' of https://github.com/nvhai248/moodle into MOODLE_501_STABLE 2026-02-03 10:23:32 +07:00
AMOS bot dc662df705 Automatically generated installer lang files 2026-02-03 00:07:58 +00:00
Stefan Topfstedt d5f21323ce MDL-87694 core: prevent empty heading in context header. 2026-02-02 10:35:44 -08:00
Stefan Topfstedt 608266dace MDL-87694 profile: change page title to "Login required".
This realigns the page title with public/user/view.php.
2026-02-02 10:20:40 -08:00
Stefan Topfstedt f8349fb154 MDL-87694 profile: set top level heading when accessed as guest user. 2026-02-02 10:12:45 -08:00
Jun Pataleta 3408c2366d MDL-87694 core: headinglevel display option for $OUTPUT->confirm()
`\core\output\core_renderer::confirm()`'s `$displayoptions` parameter
now also accepts a `headinglevel` option that developers can use to
specify the heading level of the confirmation's heading. If not
specified, the confirmation heading will be rendered in an `h4` tag.
2026-02-02 09:18:50 -08:00
cescobedo 6d9979bd1b Merge branch 'MDL-87761-501' of https://github.com/sarjona/moodle into MOODLE_501_STABLE 2026-02-02 11:16:10 +01:00
Jun Pataleta 9360d678e5 MDL-86905 core: Use proper link markup for email links 2026-02-02 18:04:10 +08:00
Huong Nguyen 5182889f53 Merge branch 'MDL-78342-501' of https://github.com/davewoloszyn/moodle into MOODLE_501_STABLE 2026-02-02 08:49:53 +07:00
Safat 918cc4f6c4 Merge branch 'MDL-86601-adhoc-index-MOODLE_501_STABLE' of https://github.com/bwalkerl/moodle into MOODLE_501_STABLE 2026-02-02 12:12:43 +11:00
Cameron Ball 6862165efa MDL-87029 quiz: Refetch attempts in overdue task to avoid stale submits
The update_overdue_attempts scheduled task iterated attempts from a recordset
and could act on stale attempt data if the web request finished an attempt
before cron processed it, causing duplicate submissions.

Change get_list_of_overdue_attempts() to return only attempt ids plus computed user
timeclose/timelimit, and refetch each attempt by id inside the task loop before
constructing quiz_attempt. This keeps quiz_attempt APIs unchanged and ensures cron
operates on current state
2026-01-31 15:38:34 +08:00
AMOS bot 052cf0a509 Automatically generated installer lang files 2026-01-31 00:07:50 +00:00
David Mudrák 9556da6637 MDL-87520 core: Fix interpretation of CFG->branch in update API client
Starting in version 3.10, Moodle started to use a different format for
the $CFG->branch that has not been fixed here. This is to make sure that
we request the correct version of the plugin.
2026-01-30 14:18:16 +01:00
Benjamin Walker 5af16f7c62 MDL-86601 core_task: Add index for scheduled task lastruntime 2026-01-30 22:52:31 +10:00
Benjamin Walker 223e070e9c MDL-86601 core_task: Add index for large adhoc task queues 2026-01-30 22:52:31 +10:00
Sara Arjona 3e49f34657 MDL-87761 course: Add warning for phasing out subsection descriptions
The Description field within the Subsection edit form has been deprecated since Moodle 5.2 onwards (see MDL-87279).
To ensure a smooth transition and avoid data loss for our users, the XD team have prepared a solution to notify them
of this upcoming change in stable versions.
2026-01-30 12:25:11 +01:00
Iñigo Zendegi a73abacee5 MDL-87396 mod_assign: Add availability check to notifications 2026-01-30 11:41:12 +01:00
Paul Holden 604a1e0cdd MDL-87334 course: looser check for empty return field editing course.
When the course format switcher reloads the form, the `$returnto`
parameter is of type string: "0", rather of type integer per initial
load.

In this case the `empty(...)` check is verifying all of empty string,
0 (integer) and "0" (string). Hurrah for PHP type checking.
2026-01-30 10:37:51 +00:00
Huong Nguyen 50e44173c8 weekly release 5.1.1+ 2026-01-30 15:39:13 +07:00
Huong Nguyen 8084242767 NOBUG: Add upgrade notes 2026-01-30 15:39:13 +07:00
Huong Nguyen e05e7093c9 Merge branch 'install_501_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_501_STABLE 2026-01-30 15:39:02 +07:00
Andrew Nicols ebf862e0d3 NOBUG: Bump composer.lock 2026-01-30 13:06:41 +08:00
Niko Hoogeveen c94a1593e3 MDL-84966 core_question: Updated question bank tag filter
Fixes a bug where filtering questions by tag the 'NONE'
option was not being respected. Automated testing was also added
to cover tag filtering conditions in the question bank
2026-01-29 09:41:57 -05:00
Daniel Ureña d5cad3ef39 MDL-87185 block_site_main_menu: Add required library 2026-01-29 13:14:55 +01:00
fgo 65da222994 MDL-86437 enrol_lti: Cast scoreGiven to float for LTI compliance
scoreGiven was being passed as a string (e.g. "8.0"), which caused
validation errors in some LMS implementations such as Rustici Engine.
According to the LTI specification, scoreGiven must be numeric if
present.

Co-authored-by: Jake Dallimore <jake@moodle.com>
2026-01-29 14:58:43 +08:00
Jake Dallimore 3382df5406 MDL-87789 libraries: patch lti1p3 for certification support
https://github.com/packbackbooks/lti-1-3-php-library/issues/169 blocks
us from using the new build.1edtech certification tooling. This issue
commits that fix as a library customisation/hack for stables, so the
suite can be used for annual/bi-annual re-certification.
2026-01-29 13:24:56 +08:00
Huong Nguyen f9280e786b Merge branch 'MDL-87521_501' of https://github.com/timhunt/moodle into MOODLE_501_STABLE 2026-01-29 11:03:56 +07:00
Huong Nguyen 8d4df26b01 Merge branch 'MDL-87087-501' of https://github.com/lameze/moodle into MOODLE_501_STABLE 2026-01-29 11:01:29 +07:00
Huong Nguyen 884a9b436c Merge branch 'MDL-86994-501' of https://github.com/mioyu-ubiq/moodle into MOODLE_501_STABLE 2026-01-29 10:58:20 +07:00
Huong Nguyen bada40d4a4 Merge branch 'MDL-87018-501' of https://github.com/bhect0/moodle into MOODLE_501_STABLE 2026-01-29 10:55:02 +07:00
Huong Nguyen b0130619d6 Merge branch 'MDL-87104-501' of https://github.com/paulholden/moodle into MOODLE_501_STABLE 2026-01-29 10:46:28 +07:00
Huong Nguyen 207f222e80 Merge branch 'MDL-87323-501' of https://github.com/paulholden/moodle into MOODLE_501_STABLE 2026-01-29 10:41:30 +07:00
Huong Nguyen 8e959ef40d Merge branch 'MDL-86988-501' of https://github.com/paulholden/moodle into MOODLE_501_STABLE 2026-01-29 10:39:09 +07:00
Angelia Dela Cruz 846ddd3979 MDL-87087 tool_dataprivacy: Test data request by authorized user 2026-01-29 11:08:26 +08:00
AMOS bot 5370fa4d1d Automatically generated installer lang files 2026-01-29 00:07:47 +00:00
David Mudrák b23182d82d MDL-87593 lang: Fix strings order 2026-01-28 21:44:45 +01:00
Paul Holden 8c49fdeb1d MDL-87714 customfield_number: upgrade note for recalculate parameters. 2026-01-28 16:34:04 +00:00
Paul Holden 37ec33f56c Merge branch 'MDL-87714-501' of https://github.com/sebastsg/moodle into MOODLE_501_STABLE 2026-01-28 16:07:12 +00:00
Sebastian Gundersen e4c3310295 MDL-87714 customfield: Add component/area to field number provider
Fixes ambiguous instances when using shared custom fields.
The component and area parameters are now used to get the proper handler.
2026-01-28 16:24:35 +01:00
Tim Hunt 915c263266 MDL-87521 behat: better error reporting if an adhoc task fails 2026-01-28 14:56:24 +00:00
Paul Holden 0f332fb0ff Merge branch 'MDL-87736-501' of https://github.com/yerairogo/moodle into MOODLE_501_STABLE 2026-01-28 14:45:07 +00:00
Paul Holden dd8a1647c3 MDL-87059 customfield: upgrade note regarding shortname uniqueness. 2026-01-28 14:39:48 +00:00
Paul Holden b56a9953cf Merge branch 'MDL-87059-501' of https://github.com/dravek/moodle into MOODLE_501_STABLE 2026-01-28 14:26:14 +00:00
Paul Holden 5cb2949d50 Merge branch 'MDL-87437-501' of https://github.com/tasiobg/moodle into MOODLE_501_STABLE 2026-01-28 14:16:27 +00:00
Yerai Rodríguez 75ad63dd97 MDL-87736 customfield: Retrieve instances data using component and area 2026-01-28 13:22:30 +01:00
Jun Pataleta 2448dbd55f Merge branch 'MDL-86639-501' of https://github.com/yusufwib01/moodle into MOODLE_501_STABLE 2026-01-28 16:17:30 +08:00
mioyu-ubiq 0222576f3f MDL-86994 mod_assign: Behat to test permitted users can edit submission 2026-01-28 13:47:22 +08:00
Hai Nguyen Van 002e281598 MDL-87675 core: Add delay for displaying the YUI alert dialogue message
* Add a slight delay for displaying the message content of the YUI
alert dialogue's ARIA live container to allow screen readers to
announce the alert message accordingly.
2026-01-28 03:19:03 +00:00
David Mudrák 17f2636bc2 MDL-87593 lang: Use the fixed strings in tests 2026-01-27 21:40:36 +01:00
Helen Foster cff7c05eb8 MDL-87593 lang: Import fixed English strings (en_fix) 2026-01-27 21:40:05 +01:00
Mark Johnson 3f9e294f21 MDL-87606 backup: Include child questions in backups
Since MDL-41924, because we are not always including the whole category
of each question being used in a backup, we have been missing some child
questions.

This expands the query selecting questions belonging to categories
where only annotated questions are being included, to also select any
children of those questions.
2026-01-27 14:52:13 +00:00
Paul Holden e80b3cf70a MDL-87104 message: remove border from contact request sent profile info. 2026-01-27 10:26:04 +00:00
Raquel Ortega 86de0a0c8d MDL-87289 mod_forum: Disable mediaplugin filter in emails 2026-01-26 21:33:40 +01:00
Huong Nguyen 59d0bb149d Merge branch 'MDL-84862-501-test' of https://github.com/yusufwib01/moodle into MOODLE_501_STABLE 2026-01-26 15:32:28 +07:00
David Carrillo 7b93ccde37 MDL-87059 customfield: Check if custom field shortname is unique
- If is a shared field check that shortname does not exist anywhere.
- If is an entity field check that the shortname does not exist
within the same entity fields or in the shared fields.
2026-01-26 08:02:39 +01:00
Huong Nguyen 96dd901bea weekly release 5.1.1+ 2026-01-26 10:38:54 +07:00
Huong Nguyen 316507507b Merge branch 'install_501_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_501_STABLE 2026-01-26 10:38:43 +07:00
Héctor Benedicte e11305428c MDL-87018 qtype_essay: Add unit tests for un_summarise_response(). 2026-01-23 19:29:22 +01:00
AMOS bot 489eb64b4a Automatically generated installer lang files 2026-01-23 00:08:05 +00:00
Mihail Geshoski 555eef58af MDL-87287 qbank_tagquestion: Fix version bump 2026-01-22 14:25:06 +07:00
Safat 2e011c622f Merge branch 'MDL-86527-501' of https://github.com/HuongNV13/moodle into MOODLE_501_STABLE 2026-01-22 15:39:43 +11:00
Huong Nguyen a686188165 Merge branch 'MDL-87188-501' of https://github.com/paulholden/moodle into MOODLE_501_STABLE 2026-01-22 11:02:45 +07:00
Huong Nguyen 880314b565 Merge branch 'MDL-87317-501' of https://github.com/lucaboesch/moodle into MOODLE_501_STABLE 2026-01-22 10:52:55 +07:00
Huong Nguyen 638de36275 Merge branch 'MDL-87318-501' of https://github.com/lucaboesch/moodle into MOODLE_501_STABLE 2026-01-22 10:46:19 +07:00
Huong Nguyen bc72e1c107 Merge branch 'MDL-86797-501' of https://github.com/paulholden/moodle into MOODLE_501_STABLE 2026-01-22 10:44:18 +07:00
Huong Nguyen 23a8fdc812 Merge branch 'MDL-87092-501' of https://github.com/Kredelirious/moodle into MOODLE_501_STABLE 2026-01-22 10:36:45 +07:00
Huong Nguyen 6511e183f5 Merge branch 'MDL-87090-501' of https://github.com/lameze/moodle into MOODLE_501_STABLE 2026-01-22 10:32:28 +07:00
Jun Pataleta 775858e46a Merge branch 'MDL-86136_501_STABLE' of https://github.com/marxjohnson/moodle into MOODLE_501_STABLE 2026-01-22 08:32:47 +08:00
Jun Pataleta ff9fb97d72 Merge branch 'MDL-87478_501' of https://github.com/mohdtahseen1/moodle into MOODLE_501_STABLE 2026-01-22 08:22:46 +08:00
Mark Johnson c00b4eda45 MDL-86136 question: Amend version restore test
Restoring a whole course will now restore the backed-up questions to a
new qbank instance. This means the test to restore versions was not
restoring the deleted versions to the original question bank entires,
but to a new question bank entry in the new qbank.

Updating the test to just backup and restore the quiz resolves this.
2026-01-21 09:30:31 +00:00
Mark Johnson ca12b96b77 MDL-86136 qbank: Restore entire activity when it is in the backup
When restoring a backup containing a question bank, we were always
trying to match questions to an existing category on the target course
if once existed. This meant that when duplicating a qbank on the same
course, or importing the same qbank to another course multiple times,
only one of the restored qbanks would end up with questions in it.

This adds a mechanism to record any activities in the backup which
support `FEATURE_PUBLISHES_QUESTIONS`. Then when we restore the
questions, we check if we will be restoring the original activity they
belong to, and skip trying to match them to an existing category. This
means a new copy of each question will be created, and moved to the new
copy of the qbank once it is created.
2026-01-21 09:30:31 +00:00
Muhammad Arnaldo 2bc458c4f9 MDL-86527 behat: Avoid element-click-intercepted failures
Adjusts the window size to accommodate wide tables.
This ensures that columns which were previously clipped
are now visible and accessible to the Behat driver.
2026-01-21 15:05:05 +07:00
Muhammad Arnaldo 6945ab9db5 MDL-86527 behat: Fix random failures when toggling edit mode 2026-01-21 15:05:05 +07:00
Adrian Greeve 3768fa2928 Merge branch 'MDL-87625-501' of https://github.com/andrewnicols/moodle into MOODLE_501_STABLE 2026-01-21 14:19:15 +08:00
Adrian Greeve ad9a1630a1 Merge branch 'MDL-87659-501' of https://github.com/andrewnicols/moodle into MOODLE_501_STABLE 2026-01-21 13:13:35 +08:00
Andrew Nicols 3252c42592 MDL-87659 core: Hide "Details" section of checks Table when there are none 2026-01-21 13:01:41 +08:00
Huong Nguyen f41ee92e8e Merge branch 'MDL-79221-MOODLE_501_STABLE' of https://github.com/PhMemmel/moodle into MOODLE_501_STABLE 2026-01-21 10:14:12 +07:00
Huong Nguyen 77937f2359 Merge branch 'MDL-51447-501' of https://github.com/AnupamaSarjoshi/moodle into MOODLE_501_STABLE 2026-01-21 10:10:40 +07:00
AMOS bot e3ccaace4a Automatically generated installer lang files 2026-01-21 00:08:12 +00:00
Yusuf Wibisono b612105230 MDL-86639 groups: Wrap group names in grade report group selector 2026-01-21 03:29:16 +07:00
Ilya Tregubov 157a8eb014 Merge branch 'MDL-76975-501' of https://github.com/yerairogo/moodle into MOODLE_501_STABLE 2026-01-20 19:45:50 +02:00
Paul Holden 7d620f8fdc MDL-86797 courseformat: account for hidden delegated section info. 2026-01-20 09:56:34 +00:00
Kim Jared Lucas ba75b60c85 MDL-87092 tool_uploaduser: suspend enrolments via CSV upload 2026-01-20 11:17:44 +08:00
Mihail Geshoski d70681c2f5 Merge branch 'MDL-87287_501_STABLE' of https://github.com/marxjohnson/moodle into MOODLE_501_STABLE 2026-01-20 10:35:32 +08:00
AMOS bot 1bc6eba524 Automatically generated installer lang files 2026-01-20 00:08:12 +00:00
Anupama Sarjoshi 6200a07627 MDL-51447 block_completionstatus: Show only user-visible activities
This change ensures that the completion details page displays only the
activities that are visible to the user on the course home page.
Activities that are hidden, unavailable or  located in hidden sections
are no longer shown in the completion details view.

Co-authored by: ewallah <info@ewallah.net>
2026-01-19 17:56:56 +05:30
Anupama Sarjoshi ccdacb8443 MDL-51447 core_completion: Get visible activities with completion on 2026-01-19 17:56:56 +05:30
Simey Lameze a84be34f83 MDL-87090 behat: improve new test 2026-01-19 14:12:25 +08:00
mioyu-ubiq 5ec89b95ec MDL-87090 mod_assign : Teacher can override assignment deadlines 2026-01-19 14:12:25 +08:00
David Woloszyn 189be87f2e MDL-78342 core_table: Remove line break conversion in format_text 2026-01-19 13:08:29 +11:00
Mark Johnson 61868b9510 MDL-86609 qbank_bulkmove: Select the current category by default 2026-01-16 14:26:37 +00:00
Nicolas Dunand 13317f2598 MDL-86609 qbank_bulkmove: Target qbank categoryid fix. 2026-01-16 14:25:36 +00:00
Luca Bösch 54dfeb3d11 MDL-87318 gradereport_grader: No over-thick table borders with BS 5. 2026-01-16 11:01:22 +01:00
Luca Bösch 908784bca9 MDL-87317 report_progress: Striped hovering table rows with BS 5. 2026-01-16 11:00:27 +01:00
Jake Dallimore 28c9a52455 weekly release 5.1.1+ 2026-01-16 16:38:17 +08:00
Jake Dallimore 86ee7eef07 NOBUG: Add upgrade notes 2026-01-16 16:38:17 +08:00
Jake Dallimore b4ed92a701 Merge branch 'install_501_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_501_STABLE 2026-01-16 16:38:11 +08:00
Paul Holden 2ae416b6f5 MDL-85300 grade: account for tooltip size in bulk move dialogue.
Now that it actually works, it obscures some elements required in
Behat scenarios. Resize the icon, and place it to the right.
2026-01-16 15:23:29 +08:00
Jake Dallimore c5faeaee4c Merge branch 'MDL-87634-501' of https://github.com/paulholden/moodle into MOODLE_501_STABLE 2026-01-16 15:23:29 +08:00
Jayce Birrell d3c9ccba3c MDL-86802 core: make CURLOPT_USERPWD optional for put()
Microsoft does not always allow an Authorization header when using PUT but the put() method sets one
by default - https://learn.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#remarks.
I've included an optional argument to prevent this, but the default
value will maintain the pre-existing logic to avoid other regressions.
2026-01-16 15:23:29 +08:00
Jake Dallimore 72c6f8882c Merge branch 'MDL-86423-501' of https://github.com/martygilbert/moodle into MOODLE_501_STABLE 2026-01-16 15:23:29 +08:00
Jake Dallimore ec07626137 Merge branch 'MDL-87635-501' of https://github.com/paulholden/moodle into MOODLE_501_STABLE 2026-01-16 15:23:29 +08:00
Jake Dallimore 28f156895a Merge branch 'MDL-86869_501-single-view-overload' of https://github.com/alexkeel/moodle into MOODLE_501_STABLE 2026-01-16 15:23:29 +08:00
Paul Holden 7adddedf4f MDL-50364 forms: consistent access to language strings from JS. 2026-01-16 15:23:29 +08:00
Paul Holden 6ba03f474a Merge branch 'MDL-50364-501' of https://github.com/Jayce0808/moodle into MOODLE_501_STABLE 2026-01-16 15:23:29 +08:00
Ilya Tregubov 44fbef243d Merge branch 'MDL-85300-501' of https://github.com/paulholden/moodle into MOODLE_501_STABLE 2026-01-16 15:23:29 +08:00
Andrew Nicols 7c19447345 Merge branch 'MDL-87608-501' of https://github.com/paulholden/moodle into MOODLE_501_STABLE 2026-01-16 15:23:29 +08:00
Mi Jia Looi 5e946c1f4f MDL-86017 mod_folder: Cast customdata to object in folder_cm_info_view() 2026-01-16 15:23:29 +08:00
Huong Nguyen f793334a44 Merge branch 'MDL-87070-501' of https://github.com/andelacruz/moodle into MOODLE_501_STABLE 2026-01-16 15:23:29 +08:00
Huong Nguyen 4c47af3dee Merge branch 'MDL-85431_501_STABLE' of https://github.com/marxjohnson/moodle into MOODLE_501_STABLE 2026-01-16 15:23:29 +08:00
Huong Nguyen 3ab3c72aa7 Merge branch 'MDL-87443-501' of https://github.com/paulholden/moodle into MOODLE_501_STABLE 2026-01-16 15:23:29 +08:00
Huong Nguyen efee80655a Merge branch 'MDL-87229-501' of https://github.com/lucaboesch/moodle into MOODLE_501_STABLE 2026-01-16 15:23:28 +08:00
Huong Nguyen a4da1b6b4e Merge branch 'MDL-87056-501' of https://github.com/paulholden/moodle into MOODLE_501_STABLE 2026-01-16 15:23:28 +08:00
Huong Nguyen 36186e9af7 Merge branch 'MDL-86565-501' of https://github.com/paulholden/moodle into MOODLE_501_STABLE 2026-01-16 15:23:28 +08:00
Huong Nguyen 37c54215d1 Merge branch 'MDL-87120-501' of https://github.com/paulholden/moodle into MOODLE_501_STABLE 2026-01-16 15:23:27 +08:00
AMOS bot 92788c9c31 Automatically generated installer lang files 2026-01-15 00:08:05 +00:00
Mark Johnson 42cb48db5c MDL-85653 qbank_editquestion: Display static status when not editable 2026-01-14 15:41:39 +00:00
Mark Johnson 3bb83daec9 MDL-86444 questions: Fix question categories with incorrect top parents 2026-01-14 10:28:15 +00:00
Paul Holden 34db25536e MDL-87634 core: exclude fixtures from Github actions "one-by-one" job.
This is consistent with the PHPUnit general configuration in 20aa9d05.
2026-01-14 09:25:27 +00:00
Yerai Rodríguez ab57c200c5 MDL-76975 admin: Move AMD dependencies to data attribute to fix overflow
Since there is a 1024 length limit on arguments passed through
js_call_amd(), we pass it through a HTML element dataset to avoid
this limit.
2026-01-14 08:28:48 +01:00
Guest 3b72da69af MDL-87478 mod_quiz: Make pre-create attempts work with previews 2026-01-13 15:12:52 +05:30
AMOS bot 358dd1dc29 Automatically generated installer lang files 2026-01-13 00:08:07 +00:00
Andrew Nicols 3f436bb599 MDL-87625 core: Use the /r.php basepath to guess routes 2026-01-13 08:06:18 +08:00
Paul Holden d7431b36a2 MDL-87635 backup: ensure required test libraries are loaded. 2026-01-12 17:22:16 +00:00
Simey Lameze 159a155062 MDL-87088 behat: add missing @javascript tags to required tests 2026-01-12 13:37:44 +07:00
Angelia Dela Cruz 0c0b4b69c9 MDL-87070 mod_glossary: Behat to confirm glossary autolinking works 2026-01-12 11:08:07 +08:00
Huong Nguyen a4b47235d6 Merge branch 'MDL-87088-501' of https://github.com/andelacruz/moodle into MOODLE_501_STABLE 2026-01-12 09:53:15 +07:00
dustinhuynh 90e25dfc45 MDL-86058 core_task: enable disable tasks with a pending upgrade 2026-01-12 09:44:05 +07:00
Stefan Topfstedt 3c7933178d MDL-77162 filepicker: default to selecting first repository available. 2026-01-09 08:34:59 -08:00
Paul Holden 738fb91dec MDL-87443 reportbuilder: use Toast selector in Behat scenarios. 2026-01-09 12:08:02 +00:00
Paul Holden badca8a618 MDL-87443 behat: define new Toast message named selector. 2026-01-09 12:08:02 +00:00
Mark Johnson 0fa3a9d6aa MDL-87287 qbank_deletequestion: Update behat test for tag deletion
Now that deleting a tagged question correctly deletes it tag instances,
the tag is no longer available for re-selection after all the tagged
questions are deleted.
2026-01-09 10:40:50 +00:00
Mark Johnson 8b0edbf074 MDL-87287 mod_qbank: De-duplicate tags when migrating questions
In the migration from old-style question banks to mod_qbank, if a
question had the same tag from multiple contexts, when it was moved to
its new question bank context, the migration tried to set multiple tag
instances for the same tag to the same context, which caused a unique
key violation.

This add some checking to the
`question_move_question_tags_to_new_context` function to identify any
tag instances to be moved that will trigger this key violation, and
delte any duplicates before the move so we end up with a single instance
of the tag in the new context. This is done in a transaction that will
only be committed once the remaining tags have been moved.

This required adding the `tiuserid` field to the data returned by
`core_tag_tag::get_items_tag` so we can assemble the unique key without
having to do additional queries.

This also extends the test coverage for the `transfer_questions` task
to ensure tags are moved to the new contexts, and tags on stale questions
are deleted.
2026-01-09 10:40:29 +00:00
Mark Johnson 27db562110 MDL-87287 qbank_tagquestion: Remove all tags from deleted questions
When we deleted a question, we were including the userid of the user who
performed the delete when checking for question tags. This means that if
user A tagged the question, and user B deleted the question, it might
leave the tags behind.

This fixes the issue by deleting tags regardless of the user, and adds
an upgrade step to find and remove orphaned tag instances.
2026-01-09 10:40:27 +00:00
Tasio de4bcb1b4c MDL-87437 course: Make course cached image entries domain-agnostic 2026-01-09 10:12:28 +01:00
Luca Bösch 47ce3408a1 MDL-87229 course: Show nice "Other users" table in courses. 2026-01-09 07:12:35 +01:00
Jayce Birrell 1e2abd1466 MDL-50364 forms: calendar header renderer uses translated months 2026-01-09 13:53:07 +10:30
Angelia Dela Cruz 1e5e225a86 MDL-87088 core_grades: Behat to view grades from user menu 2026-01-09 10:26:11 +08:00
Huong Nguyen 774796cf77 weekly release 5.1.1+ 2026-01-09 08:41:18 +07:00
Huong Nguyen 9e72cc3998 NOBUG: Add upgrade notes 2026-01-09 08:41:17 +07:00
Huong Nguyen 928b6719dc Merge branch 'install_501_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_501_STABLE 2026-01-09 08:41:06 +07:00
Jun Pataleta 1c4022b77d MDL-85535 forum: Render discussion navigation at the bottom only 2026-01-08 11:34:49 +08:00
Jun Pataleta 158bff3dcb MDL-85535 forum: Label discussion navigation 2026-01-08 11:34:49 +08:00
Marty 8642510801 MDL-86423 mod_assign: Reduce duesoon/overdue notifications.
Students shouldn't receive overdue notifications if there is nothing to
submit in Moodle.

Students shouldn't receive due soon notifications if they a) already
have a grade or b) meet activity completion status.
2026-01-07 12:24:20 -05:00
Paul Holden 36a0b0572f MDL-87120 course: observe activity dates display in course requests.
Upgrade step to ensure the default value for this field in the schema
matches the default value used in the interface itself.
2026-01-07 16:43:22 +00:00
Paul Holden 0fd0462bc6 MDL-87608 javascript: fix public paths in generated jsconfig file.
This partly reverts changes from 31335206, removing the double public/
directory component of the file paths.
2026-01-07 14:30:40 +00:00
Safat 362c9de6ad Merge branch 'MDL-87406-501' of https://github.com/ericmerrill/moodle into MOODLE_501_STABLE 2026-01-07 16:27:32 +11:00
Jun Pataleta 8d8941166c Merge branch 'MDL-86436-navigation-accessibility-page-heading-menu-visible-on-larger-screens-even-if-empty-M5.1' of https://github.com/stopfstedt/moodle into MOODLE_501_STABLE 2026-01-07 11:18:15 +08:00
Jun Pataleta 0d16e46646 Merge branch 'MDL-87416-glossary-entry-page-title-missing-M5.1' of https://github.com/stopfstedt/moodle into MOODLE_501_STABLE 2026-01-07 11:14:03 +08:00
Santosh Nagargoje 4d04135186 MDL-83984 core: Fixed subpanel show/hide issue 2026-01-06 16:20:20 +05:30
Adrian Greeve cb4e4c8d45 Merge branch 'MDL-87526-501' of https://github.com/andrewnicols/moodle into MOODLE_501_STABLE 2026-01-06 09:08:39 +08:00
Stefan Topfstedt bf58e69516 MDL-87416 glossary: set page title on glossary entry details page. 2026-01-05 11:30:37 -08:00
Mark Johnson cf51659f5a MDL-85431 questions: Fix access checks on Question banks page
Users who could not manage activities on a course, but did have
permission to access particular question banks, could not access the
Question banks page.

This modifies the permission checks on that page to allow access if the
user can either manage activities, or has access to at least one
activity that publishes questions. Some additional changes to the output
were required to prevent non-editing users adding banks.

It also modifies the navigation hook to add the Question banks
navigation node if the user can access any activitiy that publishes
questions on the course. This is all based on cached modinfo and
permissions data so should remain performant.
2026-01-05 15:21:05 +00:00
Paul Holden 4a7f57ee8e Merge branch 'MDL-87529-501' of https://github.com/yerairogo/moodle into MOODLE_501_STABLE 2026-01-05 14:24:31 +00:00
Paul Holden 1cc0453ccd Merge branch 'MDL-87288-501' of https://github.com/ilyatregubov/moodle into MOODLE_501_STABLE 2026-01-05 09:46:52 +00:00
AMOS bot 9f589703a6 Automatically generated installer lang files 2026-01-03 00:08:06 +00:00
Huong Nguyen ed4f0da1c9 Merge branch 'MDL-81882-501' of https://github.com/yusufwib01/moodle into MOODLE_501_STABLE 2025-12-29 13:51:38 +07:00
Matt Porritt 923eb86a8c MDL-84862 AI: Stop provider instance creataion when no provider plugins
Prevents calls to action to create an AI provider instance from
being displayed to users when there are no AI provider plugins
installed in the instance.

Co-Authored-By: Yusuf Wibisono <yusuf.wibisono@moodle.com>
2025-12-29 12:00:49 +07:00
Huong Nguyen 5886683a33 Merge branch 'MDL-87433-501' of https://github.com/muhammadarnaldo/moodle into MOODLE_501_STABLE 2025-12-29 09:54:37 +07:00
yusufwib01 5ef0a36d3d MDL-81882 core: Optimize block instance deletion with batch processing
Improve upgrade execution by gathering block instance IDs upfront
and processing deletions of related records in context, block_positions, user_preferences, and block_instances tables in batches.
2025-12-29 09:33:14 +07:00
AMOS bot 07e4f55804 Automatically generated installer lang files 2025-12-27 00:08:08 +00:00
AMOS bot dda4552e79 Automatically generated installer lang files 2025-12-26 00:08:05 +00:00
Muhammad Arnaldo c04a71b431 MDL-87433 course: Add sorting to get_nearest_editable_subcategory
Add ORDER BY to the query to ensure categories are returned in
consistent order. This fixes random test failures, especially
on PostgreSQL.
2025-12-24 16:14:19 +07:00
Yerai Rodríguez 120beb8aaf MDL-87529 core_customfield: reset CF handler cache after form save 2025-12-23 14:01:49 +01:00
Alex Keel d08fd3c2af MDL-86869 gradereport_singleview: Raises memory limit 2025-12-23 12:25:49 +00:00
Alex Keel 2b94d55524 MDL-86869 gradereport_singleview: Lower the max per page limit 2025-12-23 12:23:48 +00:00
Mihail Geshoski 057c314edc Merge branch 'MDL-86798_501_STABLE' of https://github.com/marxjohnson/moodle into MOODLE_501_STABLE 2025-12-23 16:09:55 +08:00
Andrew Nicols d4690ea232 MDL-87526 core: Specify composer project type 2025-12-23 11:49:15 +08:00
Andrew Nicols 88763ab043 MDL-87526 core: Moodle provides moodle/lms 2025-12-23 11:49:02 +08:00
Mark Johnson 547ea9cfbf MDL-86798 questions: Check original context is on the same site
When checking if the original context exists during a restore, we need
to check that we're looking at the context on the same site, not one on
a different site that happens to have the same ID.
2025-12-19 09:31:14 +00:00
Mark Johnson cfaccbf953 MDL-86798 questions: Track next version for question bank entries
This adds a new "nextversion" field to `question_bank_entries` to track
which version number the next version of the question should have. This
ensures that version numbers are not re-used if the latest version is
deleted.

To use this field, you must call
`\core_question\versions::get_next_version()`. This will initialise the
field to the correct value if it is currently null.

If you create a new version using this value, you must then call
`\core_question\versions::increment_next_version()` to increment the
counter.
2025-12-19 09:31:14 +00:00
Mark Johnson 11d0802ca6 MDL-86798 questions: Restore backups containing deleted versions
If the latest version of a question is deleted, and then a new one
created, the new one will currently re-use the version number of the
deleted version. If the original version was backed up before it was
deleted, then restored later, we end up with two questions in one
question bank entry that have the same version.

To resolve this, when restoring a question, we check if there is already
a version with the same version number, then bump it and any higher
versions up by 1. We also check for any references specifically using
these versions and bump those up by 1. Any references using "always
latest" will be fine, as the latest version will still be the latest.

Additionally, if deleting a version made it hidden, and the backup
contains the same version in "ready" state, restoring the backup will
restore it to "ready" state.
2025-12-19 09:29:03 +00:00
Huong Nguyen 1cd17816c5 weekly release 5.1.1+ 2025-12-19 09:04:16 +07:00
Ilya Tregubov 8bcf6f016d MDL-87288 output: Fix pagination in small containers. 2025-12-18 09:37:45 +01:00
Huong Nguyen bd39c5df46 Merge branch 'MDL-87340-501' of https://github.com/lameze/moodle into MOODLE_501_STABLE 2025-12-18 11:44:37 +08:00
Huong Nguyen 85f08eccd7 Merge branch 'MDL-86420-501' of https://github.com/muhammadarnaldo/moodle into MOODLE_501_STABLE 2025-12-18 11:44:36 +08:00
Huong Nguyen e35c3f18e5 Merge branch 'MDL-86086-501' of https://github.com/andimendunia/moodle into MOODLE_501_STABLE 2025-12-18 11:44:36 +08:00
Adrian Greeve 700ffe79a3 Merge branch 'MDL-87461-501' of https://github.com/andrewnicols/moodle into MOODLE_501_STABLE 2025-12-18 11:44:36 +08:00
Huong Nguyen 0bfc7f5abd Merge branch 'MDL-87094-501' of https://github.com/andelacruz/moodle into MOODLE_501_STABLE 2025-12-18 11:44:36 +08:00
Mihail Geshoski 39a16ed55a Merge branch 'MDL-72833_501_STABLE' of https://github.com/marxjohnson/moodle into MOODLE_501_STABLE 2025-12-18 11:44:36 +08:00
Andi Permana 7414e469b8 MDL-86086 theme_boost: Add visual distinction for readonly form inputs 2025-12-17 19:07:45 +07:00
Andrew Nicols a55b9cfc70 MDL-87461 core: Update composer lock file 2025-12-17 16:14:52 +08:00
Andrew Nicols f4187d49da MDL-87461 core: Add Composer config to indicate the use of public dir 2025-12-17 16:14:51 +08:00
Mihail Geshoski cacdd7e63a Merge branch 'MDL-86416-501' of https://github.com/junpataleta/moodle into MOODLE_501_STABLE 2025-12-17 12:14:29 +08:00
Paul Holden 08d6834166 MDL-85300 output: ensure Bootstrap tooltips load for dynamic content.
Prior to this change, tooltips would only load for elements in the
initial page DOM without considering those elements subsequently added
dynamically (e.g. via AJAX).

Co-authored-by: Sebastian Gundersen <sebastian@sgundersen.com>
2025-12-16 13:30:11 +00:00
Amaia Anabitarte 740c3dafcc Merge branch 'MDL-87436-501' of https://github.com/laurentdavid/moodle into MOODLE_501_STABLE 2025-12-15 13:41:16 +01:00
Mark Johnson b50f58ec0d MDL-72833 quiz: Respect locale in maxmark setting
Editing maxmarks in quiz slots was not respecting the decimal separator
from the set language's locale, meaning if your language formats numbers
like 1,10 editing didn't work correctly.

This sets the correct parameter to ensure the locale is respected.
2025-12-15 11:47:24 +00:00
Jun Pataleta 3fd9808fc6 Merge branch 'MDL-87341_501' of https://github.com/TomoTsuyuki/moodle into MOODLE_501_STABLE 2025-12-15 18:48:45 +08:00
Angelia Dela Cruz 9155d32f36 MDL-87094 core_course: Behat to restrict available activities/resources 2025-12-15 12:00:38 +08:00
Huong Nguyen e6d7fcb2e0 Merge branch 'MOODLE_501_STABLE_MDL-73396' of https://github.com/niko-hoogeveen/moodle into MOODLE_501_STABLE 2025-12-15 08:55:35 +07:00
Huong Nguyen c5b69126d7 Merge branch 'MDL-86679-501' of https://github.com/andimendunia/moodle into MOODLE_501_STABLE 2025-12-15 08:46:48 +07:00
Tomo Tsuyuki 0f0904243d MDL-87341 javascript: Fix aria label issue for navigation bar 2025-12-15 10:24:46 +11:00
Stefan Topfstedt 95d96c926b MDL-86436 navigation: hide empty page heading menu.
Co-authored-by: Jun Pataleta <jun@moodle.com>
2025-12-12 18:06:09 -08:00
Stefan Topfstedt e39521934c MDL-87415 profile: set page title when accessed as guest user. 2025-12-12 13:42:38 -08:00
Philipp Memmel c629f23da8 MDL-79221 core_grade: Fix fetching context for disabled mod plugins
Activity plugins that are disabled on the site are not included in
the modinfo cache. That's why their context must not be fetched via
modinfo cache.
2025-12-12 06:42:56 +01:00
Philipp Memmel 3f5b13cd49 MDL-79221 core_completion: Do not update state for disabled plugins 2025-12-12 06:42:56 +01:00
Shamim Rezaie 019e03af94 weekly release 5.1.1+ 2025-12-12 14:06:13 +11:00
Shamim Rezaie 8e477788f8 Merge branch 'install_501_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_501_STABLE 2025-12-12 14:06:02 +11:00
Mihail Geshoski b32b09d584 MDL-87412 qbank_managecategories: Use single quotes in scenario name 2025-12-12 09:45:21 +07:00
Huong Nguyen 1a1033c601 Merge branch 'MDL-84736-501' of https://github.com/lameze/moodle into MOODLE_501_STABLE 2025-12-12 09:45:21 +07:00
Huong Nguyen fcde530bc4 Merge branch 'MDL-86830-501' of https://github.com/andimendunia/moodle into MOODLE_501_STABLE 2025-12-12 09:45:20 +07:00
Huong Nguyen 8117d73b98 Merge branch 'MDL-86808-501' of https://github.com/muhammadarnaldo/moodle into MOODLE_501_STABLE 2025-12-12 09:45:20 +07:00
Mihail Geshoski 6ca58162c6 Merge branch 'MDL-87182-501' of https://github.com/srobotta/moodle into MOODLE_501_STABLE 2025-12-12 09:45:20 +07:00
Mihail Geshoski 7b348fa333 Merge branch 'MDL-87121_501' of https://github.com/santoshndev/moodle into MOODLE_501_STABLE 2025-12-12 09:45:20 +07:00
Mihail Geshoski a6587c7986 Merge branch 'MDL-87412_501_STABLE' of https://github.com/marxjohnson/moodle into MOODLE_501_STABLE 2025-12-12 09:45:20 +07:00
Mihail Geshoski 19b193784b Merge branch 'MDL-87142-501' of https://github.com/lucaboesch/moodle into MOODLE_501_STABLE 2025-12-12 09:45:20 +07:00
Jun Pataleta 6f44b00e42 Merge branch 'MDL-86807-501' of https://github.com/muhammadarnaldo/moodle into MOODLE_501_STABLE 2025-12-12 09:45:20 +07:00
Jun Pataleta 90eb487eec Merge branch 'MDL-86805-501' of https://github.com/andimendunia/moodle into MOODLE_501_STABLE 2025-12-12 09:45:20 +07:00
Huong Nguyen 2d111226fe Merge branch 'MDL-86593-501' of https://github.com/christianabila/moodle into MOODLE_501_STABLE 2025-12-12 09:45:20 +07:00
Muhammad Arnaldo 832acc2639 MDL-86420 enrol_database: avoid duplicate key violations 2025-12-11 10:25:18 +07:00
AMOS bot 7eb5190921 Automatically generated installer lang files 2025-12-11 00:08:10 +00:00
Niko Hoogeveen 2e1e274ed4 MDL-73396 auth_oauth2: link non-suspended account on oauth login
Added checks to link non-suspended user accounts to OAuth2, when logging
in. Also added an additional check to prevent users from using OAuth2
to login if their account is suspended in Moodle.
2025-12-10 14:22:52 -05:00
Niko Hoogeveen a217af7ae5 MDL-73396 core_user: prioritize active users when retrieving by email
Updated get_users_by_email() to return active users at the
start of the returned array, suspended users last.
2025-12-10 14:22:52 -05:00
Laurent David 1e893d4b21 MDL-87436 core_courseformat: Fix id collision when testing
* When testing using phpunit/sqlsrv the ids of sections and cm are colliding
so we need to make sure we separate them as a reference to a cm can be the
same as a reference to a section
2025-12-10 12:10:41 +01:00
Simey Lameze de2ab7ccb7 MDL-84736 behat: add custom step to edit a criterion
This commit also improves the existing tests and add a new scenario to
verify the 'Do not mark for regrade' behavior.
2025-12-09 17:21:40 +08:00
Angelia Dela Cruz f6a31e8fdc MDL-84736 gradingform : Behat for editing guide with graded submissions 2025-12-09 17:21:40 +08:00
Stefan Topfstedt 7cf1291cc2 MDL-86416 navigation: Render the more menu only when it's not empty
Co-authored-by: Jun Pataleta <jun@moodle.com>
2025-12-09 11:17:38 +08:00
Huong Nguyen f380071cb5 Merge branch 'MDL-87379_501' of https://github.com/santoshndev/moodle into MOODLE_501_STABLE 2025-12-09 09:28:18 +07:00
Mihail Geshoski 3055d89c46 Merge branch 'MDL-82281-501' of https://github.com/lucaboesch/moodle into MOODLE_501_STABLE 2025-12-09 09:45:32 +08:00
Mihail Geshoski 4ef04a9d94 Merge branch 'MDL-86677-501' of https://github.com/lucaboesch/moodle into MOODLE_501_STABLE 2025-12-09 09:28:08 +08:00
Mihail Geshoski a633683bb3 Merge branch 'mdl-87356_501' of https://github.com/james-cnz/moodle into MOODLE_501_STABLE 2025-12-09 09:17:16 +08:00
Simey Lameze 0c4b848a48 MDL-87340 mod_quiz: make test_question_shuffle more robust
This commit improves test_question_shuffle by retrying if questions
remains in the same order after shuffling.
2025-12-09 07:34:04 +08:00
Stephan Robotta 010daf38da MDL-87182 quiz: no CBM metric when no data available 2025-12-08 08:46:06 +01:00
Muhammad Arnaldo 038963316f MDL-86807 core_courseformat: RTL chevron in move activity modal 2025-12-08 11:50:31 +07:00
Andi Permana 94a34a82f7 MDL-86830 calendar: Fix double encoding in event title attributes 2025-12-08 10:42:42 +07:00
Luca Bösch d8f2f5e6a0 MDL-87142 question: dropdown-toggle class for question format select 2025-12-07 20:12:35 +01:00
Luca Bösch bdcc11503d MDL-86677 mod_quiz: let the summary of attempt table be striped 2025-12-07 20:11:41 +01:00
Luca Bösch 35ca47e57a MDL-82281 quiz: Amend test to circumvent modinfo cache fails. 2025-12-07 20:10:52 +01:00
Andi Permana 840c045bc1 MDL-86805 courseindex: Fix RTL keyboard navigation in tree 2025-12-06 01:09:20 +07:00
Mark Johnson 7ca1574c28 MDL-87412 qbank_managecategories: Fix "new child" drop target
This fixes the CSS rule that reveals the "as new child of" drop target
shown when dragging a category over another category which has no
children.

We can't behat test the actual drag-drop reordering behaviour with behat
as reactive DragDrop components aren't testable, but we can trigger the
CSS to add a regression test for the drop target's appearance.
2025-12-05 14:11:06 +00:00
Santosh Nagargoje bae9d77604 MDL-87379 theme_classic: Fixed mobile responsiveness issue 2025-12-05 15:31:36 +05:30
Muhammad Arnaldo 91bdc56a5d MDL-86808 theme_boost: misaligned footer popover in RTL
Limit the fallback placement vertically.
2025-12-05 16:27:54 +07:00
Andi Permana 00cd643c33 MDL-86679 core: Fix MS field mappings in upgrade script 2025-12-05 15:44:10 +07:00
Andi Permana 73118e777f MDL-86679 oauth2: Support MS personal and work account names 2025-12-05 15:42:33 +07:00
Eric Merrill eb50526c35 MDL-87406 customfield: Use recordset for processing upgrade 2025-12-04 23:59:52 -05:00
Santosh Nagargoje 8e952904e8 MDL-87121 quiz: Fixed random question performance issue 2025-12-04 17:26:22 +05:30
James C 0df4a0d4ce MDL-87356 qtype_random: orphaned random questions should be deleted
This patch reinstates the task from MDL-63260 and MDL-66273.
2025-12-04 23:01:50 +13:00
Christian Abila 56e76851cc MDL-86593 Course completion: Disabled modules not returned as criteria
- Filter out disabled modules
- Update get_criteria method signature
- Fix code style
2025-12-04 07:04:11 +01:00
Paul Holden 6fd62e9bfc MDL-87323 report_outline: fix report viewed event URL. 2025-11-27 17:28:43 +00:00
Paul Holden 8d07409ff8 MDL-86988 mod_resource: ensure required library always present.
The `resourcelib` library is explicitly required by the modules
own local library, and neither the add/update methods use it
directly.
2025-11-26 15:10:59 +00:00
Paul Holden 7c6aaf9d4b MDL-87188 gradereport_grader: prevent overlapping collapsed columns. 2025-11-26 11:40:27 +00:00
Paul Holden e06a6ee9c8 MDL-86565 courseformat: don't link to sections user cannot see. 2025-11-18 10:31:58 +00:00
Alexander Bias 92a8d0e9c0 MDL-87199 core: In public/config.php, use require_once() 2025-11-17 07:44:03 +01:00
Paul Holden 26a16e4e27 MDL-87056 enrol: update instance modified date when toggling status. 2025-11-11 12:36:18 +00:00
300 changed files with 6701 additions and 754 deletions
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
chunk=$(((($count % $chunks)) + 1))
echo "$testname $testfile" >> ./chunk_$chunk.txt
done < <(grep "function test_" "${testfile}" | sed -r "s/^.*function (test_[a-zA-Z0-9_]+).*/\1/")
done < <(find . -name "*_test.php")
done < <(find . -name "*_test.php" -not -path "*/fixtures/*")
# Generate the matrix to run tests.
echo "matrix=$(ls -1 chunk_*.txt | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT
echo "$count individual tests collected in $chunks files"
+2 -2
View File
@@ -43,8 +43,8 @@ module.exports = (grunt) => {
const componentData = fetchComponentData().components;
for (const [thisPath, component] of Object.entries(componentData)) {
jsconfigData.compilerOptions.paths[`${component}/*`] = [`public/${thisPath}/amd/src/*`];
jsconfigData.include.push(`public/${thisPath}/amd/src/**/*`);
jsconfigData.compilerOptions.paths[`${component}/*`] = [`${thisPath}/amd/src/*`];
jsconfigData.include.push(`${thisPath}/amd/src/**/*`);
}
grunt.file.write('jsconfig.json', JSON.stringify(jsconfigData, null, " ") + "\n");
+45
View File
@@ -6,6 +6,51 @@ 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.1.2
### core
#### Added
- 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)
#### Changed
- `\core\output\core_renderer::confirm()`'s `$displayoptions` parameter now also accepts a `headinglevel` option that developers can use to specify the heading level of the confirmation's heading. If not specified, the confirmation heading will be rendered in an `h4` tag.
For more information see [MDL-87694](https://tracker.moodle.org/browse/MDL-87694)
### core_customfield
#### Added
- Added new `\core_customfield\api::is_shortname_unique(...)` method to determine whether a shortname is available for use inside a given handler
For more information see [MDL-87059](https://tracker.moodle.org/browse/MDL-87059)
### core_question
#### Fixed
- In order to prevent re-use of question version numbers after a version is deleted, the `nextversion` column was added to `question_bank_entries`. This serves as a counter incremented each time a version is created.
Do not query this field directly. Instead use `core_question\versions::get_next_version()` to read the value, which will initialise it based on the existing versions if it is not set yet. By default, it will increment the version number automatically, unless you pass `increment: false`. Because of this, it is advisable to call it inside a transaction, that is only committed after the version number is used in a `question_versions` record.
For more information see [MDL-86798](https://tracker.moodle.org/browse/MDL-86798)
### customfield_number
#### Changed
- In order to fully support shared custom field categories, additional component/area/itemid parameters have been added to the following:
* The `customfield_number_recalculate_value` external method
* The abstract `\customfield_number\provider_base::recalculate()` method
* The `\customfield_number\task\recalculate` helpers for queueing task instances
For more information see [MDL-87714](https://tracker.moodle.org/browse/MDL-87714)
## 5.1.1
### core
+5 -5
View File
@@ -125,11 +125,6 @@ if ($options['list']) {
exit(0);
}
if (moodle_needs_upgrading()) {
mtrace("Moodle upgrade pending, cannot manage tasks.");
exit(1);
}
if ($disable = $options['disable']) {
if (!$task = \core\task\manager::get_scheduled_task($disable)) {
mtrace("Task '$disable' not found");
@@ -157,6 +152,11 @@ if ($disable = $options['disable']) {
exit(1);
}
} else if ($execute = $options['execute']) {
if (moodle_needs_upgrading()) {
mtrace("Moodle upgrade pending, cannot execute tasks.");
exit(1);
}
if (!$task = \core\task\manager::get_scheduled_task($execute)) {
mtrace("Task '$execute' not found");
exit(1);
+7 -1
View File
@@ -2,8 +2,11 @@
"name": "moodle/moodle",
"license": "GPL-3.0-or-later",
"description": "Moodle - the world's open source learning platform",
"type": "project",
"type": "moodle-core",
"homepage": "https://moodle.org",
"provide": {
"moodle/lms": "5.1"
},
"require-dev": {
"phpunit/phpunit": "^11",
"mikey179/vfsstream": "1.6.*",
@@ -26,6 +29,9 @@
"core_phpunit\\": "public/lib/phpunit/classes/"
}
},
"extra": {
"haspublicdir": true
},
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
Generated
+2 -2
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6dea3d23fae938df252056489715c946",
"content-hash": "dbc988c9feb49514ee1ac7b206929260",
"packages": [],
"packages-dev": [
{
@@ -4701,5 +4701,5 @@
"ext-sodium": "*"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}
+2 -2
View File
@@ -119,8 +119,8 @@ foreach ($settingspage->children as $childpage) {
$outputhtml .= html_writer::tag('div', '<!-- -->', array('class' => 'clearer'));
$outputhtml .= $setting->output_html($data);
if ($childpage->has_dependencies()) {
$opts = ['dependencies' => $childpage->get_dependencies_for_javascript()];
$PAGE->requires->js_call_amd('core/showhidesettings', 'init', [$opts]);
$context = ['dependencies' => json_encode($childpage->get_dependencies_for_javascript())];
echo $OUTPUT->render_from_template('core_admin/settings_showhide', $context);
}
}
$outputhtml .= html_writer::end_tag('fieldset');
@@ -0,0 +1,74 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_admin\admin;
use admin_setting;
/**
* Render a notification as part of other admin settings.
*
* @package core_admin
* @subpackage admin
* @copyright 2025 Matt Porritt <matt.porritt@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class admin_setting_notification extends admin_setting {
/**
* Constructor.
*
* @param string $name The name of the setting.
* @param string $notification The notification to display.
* @param string $type The type of notification.
* @param bool $cancelable Whether the notification can be canceled.
*/
public function __construct(
string $name,
/** @var string The notification to display. */
protected string $notification,
/** @var string The type of notification. */
protected string $type = 'info',
/** @var bool Whether the notification can be canceled. */
protected bool $cancelable = false
) {
$this->nosave = true;
parent::__construct($name, '', '', '');
}
#[\Override]
public function get_setting(): bool {
return true;
}
#[\Override]
public function get_defaultsetting(): bool {
return true;
}
#[\Override]
public function write_setting($data): string {
// Do not write any setting.
return '';
}
#[\Override]
public function output_html($data, $query = ''): string {
global $OUTPUT;
return $OUTPUT->notification($this->notification, $this->type, $this->cancelable);
}
}
+9 -1
View File
@@ -990,6 +990,13 @@ $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');
@@ -1016,5 +1023,6 @@ echo $output->admin_notifications_page(
$showcampaigncontent,
$showfeedbackencouragement,
$servicesandsupportcontent,
$xmlrpcwarning
$xmlrpcwarning,
$moodlenetwarning
);
+17 -1
View File
@@ -279,6 +279,7 @@ 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.
*/
@@ -288,7 +289,7 @@ class core_admin_renderer extends plugin_renderer_base {
$themedesignermode = false, $devlibdir = false, $mobileconfigured = false,
$overridetossl = false, $invalidforgottenpasswordurl = false, $croninfrequent = false,
$showcampaigncontent = false, bool $showfeedbackencouragement = false, bool $showservicesandsupport = false,
$xmlrpcwarning = '') {
$xmlrpcwarning = '', $moodlenetwarning = '') {
global $CFG;
$output = '';
@@ -313,6 +314,7 @@ 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->userfeedback_encouragement($showfeedbackencouragement);
$output .= $this->services_and_support_content($showservicesandsupport);
$output .= $this->campaign_content($showcampaigncontent);
@@ -2326,6 +2328,20 @@ class core_admin_renderer extends plugin_renderer_base {
return $this->warning($xmlrpcwarning);
}
/**
* 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 '';
}
return $this->warning($moodlenetwarning);
}
/**
* Renders the theme selector list.
*
+2 -4
View File
@@ -165,10 +165,8 @@ if (empty($SITE->fullname)) {
$PAGE->requires->js_call_amd('core_form/changechecker', 'watchFormById', ['adminsettings']);
if ($settingspage->has_dependencies()) {
$opts = [
'dependencies' => $settingspage->get_dependencies_for_javascript()
];
$PAGE->requires->js_call_amd('core/showhidesettings', 'init', [$opts]);
$context = ['dependencies' => json_encode($settingspage->get_dependencies_for_javascript())];
echo $OUTPUT->render_from_template('core_admin/settings_showhide', $context);
}
echo $OUTPUT->footer();
+18 -8
View File
@@ -32,19 +32,29 @@ if ($hassiteconfig) {
$providers->add(new admin_setting_heading('availableproviders',
get_string('availableproviders', 'core_ai'),
get_string('availableproviders_desc', 'core_ai')));
// Add call to action to add a new provider.
$providers->add(new \core_admin\admin\admin_setting_template_render(
name: 'addnewprovider',
templatename: 'core_ai/admin_add_provider',
context: ['addnewproviderurl' => new moodle_url('/ai/configure.php')]
));
$providers->add(new \core_ai\admin\admin_setting_provider_manager(
if (!empty(core_plugin_manager::instance()->get_plugins_of_type("aiprovider"))) {
// Add call to action to add a new provider.
$providers->add(new \core_admin\admin\admin_setting_template_render(
name: 'addnewprovider',
templatename: 'core_ai/admin_add_provider',
context: ['addnewproviderurl' => new moodle_url('/ai/configure.php')]
));
$providers->add(new \core_ai\admin\admin_setting_provider_manager(
'aiprovider',
\core_ai\table\aiprovider_management_table::class,
'manageaiproviders',
new lang_string('manageaiproviders', 'core_ai'),
));
));
} else {
$providers->add(new \core_admin\admin\admin_setting_notification(
name:'noproviderplugins',
notification: get_string('noproviderplugins', 'core_ai'),
type: 'danger'
));
}
$ADMIN->add('ai', $providers);
// Add settings page for AI placement settings.
@@ -0,0 +1,51 @@
{{!
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 core_admin/settings_showhide
Passes the necessary data to show/hide dependant settings.
Context variables required for this template:
* dependencies - JSON-serialized data structure describing the dependencies between settings
Example context (json):
{
"dependencies": {
"s__setting1":{
"eq":[
[
"s__setting0"
]
]
},
"s__setting2":{
"eq":[
[
"s__setting1"
]
]
}
}
}
}}
<span id="settings-showhide-{{uniqid}}" data-dependencies="{{dependencies}}"></span>
{{#js}}
require(['core/showhidesettings'], function(ShowHideSettings) {
ShowHideSettings.init("settings-showhide-{{uniqid}}");
});
{{/js}}
@@ -0,0 +1,100 @@
@tool @tool_dataprivacy
Feature: Authorized users can request others personal data
In order to export or access another users data
As a designated role
I need the correct permissions
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| user1 | User1 | One | user1@example.com |
| user2 | User2 | Two | user2@example.com |
| officer1 | Officer1 | One | officer1@example.com |
# Create Privacy Officer Role.
And the following "role" exists:
| shortname | privacyofficer |
| name | Privacy Officer |
| context_system | 1 |
| tool/dataprivacy:managedataregistry | allow |
| tool/dataprivacy:managedatarequests | allow |
| tool/dataprivacy:makedatarequestsforchildren | allow |
| moodle/site:configview | allow |
| moodle/category:viewhiddencategories | allow |
| moodle/course:viewhiddencourses | allow |
| moodle/course:viewhiddenactivities | allow |
| moodle/course:view | allow |
# Create Parent Role.
And the following "role" exists:
| shortname | parentrole |
| name | Parent Role |
| context_user | 1 |
| moodle/user:viewdetails | allow |
| moodle/user:viewalldetails | allow |
| moodle/user:readuserblogs | allow |
| moodle/user:readuserposts | allow |
| moodle/user:viewuseractivitiesreport | allow |
| moodle/user:editprofile | allow |
| tool/policy:acceptbehalf | allow |
| tool/dataprivacy:makedatarequestsforchildren | allow |
# Add permission to allow parent to make requests on behalf of child user.
And the following config values are set as admin:
| contactdataprotectionofficer | 1 | tool_dataprivacy |
And I log in as "admin"
@javascript
Scenario: Privacy officer can request for other user's personal data
Given I navigate to "Users > Permissions > Assign system roles" in site administration
# Assign Privacy Officer role to officer1.
And I follow "Privacy Officer"
And I set the field "addselect_searchtext" to "Officer1"
And I set the field "addselect" to "Officer1 One (officer1@example.com)"
And I press "Add"
# Navigate to home in order to navigate properly to Privacy settings.
And I am on site homepage
# Select Privacy officer in the Orivacy officer role mapping setting.
And I navigate to "Users > Privacy and policies > Privacy settings" in site administration
And I click on "Privacy Officer" "checkbox"
And I press "Save changes"
And I log in as "officer1"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
# Create a new request as the designated privacy officer.
When I follow "New request"
And I set the field "User" to "User1 One"
And I set the field "Comment" to "User One data"
And I press "Save changes"
# Confirm that the new data request is successfully created for selected user with status "Awaiting approval".
Then the following should exist in the "generaltable" table:
| Type | User | Requested by | Status | Message |
| Export | User1 One | Officer1 One | Awaiting approval | User One data |
@javascript
Scenario: Parent user can request data on behalf of child user
Given I navigate to "Users > Accounts > Browse list of users" in site administration
And I follow "User1 One"
And I click on "Preferences" "link" in the ".profile_tree" "css_element"
# Assign user2 as parent for user1.
And I follow "Assign roles relative to this user"
And I follow "Parent"
And I set the field "Potential users" to "User2 Two (user2@example.com)"
And I click on "Add" "button" in the "#page-content" "css_element"
And I log in as "user2"
And I follow "Profile" in the user menu
And I follow "Data requests"
# As parent, create a data request for a child user.
And I follow "New request"
And I click on "User" "field"
When I type "User1 One"
# Confirm that only the parent's child users can be searched and selected.
Then I should see "User1 One"
And I type "User2 Two"
And I should see "No suggestions"
And I type "Officer1 One"
And I should see "No suggestions"
And I set the field "Search" to "User1"
And I set the field "Comment" to "This is a comment"
And I press "Save changes"
# Confirm that data request was successfully made by parent on behalf of child user.
And I should see "Your request has been submitted to the privacy officer"
And the following should exist in the "generaltable" table:
| Type | Requested by | Status | Message |
| Export all of my personal data (User1 One) | User2 Two | Awaiting approval | This is a comment |
@@ -38,6 +38,9 @@ $string['defaultmoodlenet_desc'] = 'The URL of the MoodleNet instance available
$string['defaultmoodlenetname'] = "MoodleNet instance name";
$string['defaultmoodlenetnamevalue'] = 'MoodleNet Central';
$string['defaultmoodlenetname_desc'] = 'The name of the MoodleNet instance available via the activity chooser.';
$string['removalwarning_feature'] = 'If you need to continue using MoodleNet, contact your site administrator about setting up a self-hosted MoodleNet instance.';
$string['removalwarning_service'] = 'After the date, you will no longer be able to browse or add content from MoodleNet Central.';
$string['removalwarning_title'] = 'The MoodleNet service will be shut down on 20 April 2026.';
$string['enablemoodlenet'] = 'Enable MoodleNet integration (inbound)';
$string['enablemoodlenet_desc'] = 'If enabled, a user with the capability to create and manage activities can browse MoodleNet via the activity chooser and import MoodleNet resources into their course. In addition, a user with the capability to restore backups can select a backup file on MoodleNet and restore it into Moodle.';
$string['errorduringdownload'] = 'An error occurred while downloading the file: {$a}';
@@ -27,8 +27,16 @@
<div class="content text-start mb-5 px-5 py-4" data-region="chooser-option-summary-content-container">
<div data-region="moodle-net">
<div class="overlay-icon-container z-index-1 d-none" data-region="spinner"></div>
<img class="w-25 mb-4" aria-hidden="true" src="{{{img}}}">
<img class="w-25 mb-4" alt="" aria-hidden="true" src="{{{img}}}">
<p>{{#str}} instancedescription, tool_moodlenet {{/str}}</p>
{{! Removal warning - always visible when MoodleNet integration is enabled }}
<div class="alert alert-warning w-75 mx-auto mb-3" role="alert">
<strong>{{#str}}removalwarning_title, tool_moodlenet{{/str}}</strong>
<p class="mb-1">{{#str}}removalwarning_service, tool_moodlenet{{/str}}</p>
<p class="mb-0">{{#str}}removalwarning_feature, tool_moodlenet{{/str}}</p>
</div>
<p class="w-75 mx-auto mb-1 mt-5">{{#str}} connectandbrowse, tool_moodlenet {{/str}}</p>
<a class="btn btn-secondary d-block w-75 mx-auto mb-4"
data-action="browse"
@@ -61,7 +61,8 @@ Feature: Basic OAuth2 functionality
And I should see "device_authorization_endpoint"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Configure user field mappings" "link" in the "Testing service" "table_row"
And I should see "firstname" in the "givenname" "table_row"
And I should see "firstname" in the "given_name" "table_row"
And I should see "lastname" in the "family_name" "table_row"
And I should see "idnumber" in the "sub" "table_row"
And I should see "email" in the "email" "table_row"
And I should see "lang" in the "locale" "table_row"
@@ -0,0 +1,41 @@
@tool @tool_uploaduser @_file_upload
Feature: Admin can suspend user course enrolment via CSV upload
In order to manage enrolments in bulk
As an administrator
I need to be able to enrol and suspend users using CSV upload
Background:
Given the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
| Course 2 | C2 |
| Course 3 | C3 |
@javascript
Scenario: Admin uploads enrol and suspend CSVs and verifies enrolment status
Given I log in as "admin"
And I navigate to "Users > Accounts > Upload users" in site administration
When I upload "lib/tests/fixtures/QA_user_enrol.txt" file to "File" filemanager
And I press "Upload users"
And I press "Upload users"
And I press "Continue"
And I upload "lib/tests/fixtures/QA_user_suspend.txt" file to "File" filemanager
And I press "Upload users"
And I set the field "Upload type" to "Update existing users only"
And I press "Upload users"
And I press "Continue"
And I am on the "Course 1" "enrolled users" page
Then the following should exist in the "participants" table:
| First name | Status |
| Learner One | Active |
| Learner Two | Active |
And I am on the "Course 2" "enrolled users" page
And the following should exist in the "participants" table:
| First name | Status |
| Learner One | Active |
| Learner Two | Suspended |
And I am on the "Course 3" "enrolled users" page
And the following should exist in the "participants" table:
| First name | Status |
| Learner One | Suspended |
| Learner Two | Active |
+6
View File
@@ -72,6 +72,12 @@ $PAGE->set_pagelayout('admin');
$PAGE->set_title($title);
$PAGE->set_heading($title);
// Explode if there are no provider plugins installed.
$plugins = core_plugin_manager::instance()->get_plugins_of_type('aiprovider');
if (empty($plugins)) {
throw new moodle_exception('noproviderplugins', 'core_ai');
}
// Provider instance form processing.
$mform = new \core_ai\form\ai_provider_form(customdata: $data);
if ($mform->is_cancelled()) {
+2 -6
View File
@@ -197,9 +197,7 @@ class api {
$data->link = $confirmationurl->out(false);
$message = get_string('confirmlinkedloginemail', 'auth_oauth2', $data);
$data->link = $confirmationurl->out();
$messagehtml = text_to_html(get_string('confirmlinkedloginemail', 'auth_oauth2', $data), false, false, true);
$messagehtml = text_to_html(get_string('confirmlinkedloginemail', 'auth_oauth2', $data), false, false);
$user->mailformat = 1; // Always send HTML version as well.
@@ -339,9 +337,7 @@ class api {
$data->link = $confirmationurl->out(false);
$message = get_string('confirmaccountemail', 'auth_oauth2', $data);
$data->link = $confirmationurl->out();
$messagehtml = text_to_html(get_string('confirmaccountemail', 'auth_oauth2', $data), false, false, true);
$messagehtml = text_to_html(get_string('confirmaccountemail', 'auth_oauth2', $data), false, false);
$user->mailformat = 1; // Always send HTML version as well.
+42 -14
View File
@@ -467,18 +467,31 @@ class auth extends \auth_plugin_base {
$mappeduser = get_complete_user_data('id', $linkedlogin->get('userid'));
if ($mappeduser && $mappeduser->suspended) {
$failurereason = AUTH_LOGIN_SUSPENDED;
$event = \core\event\user_login_failed::create([
'userid' => $mappeduser->id,
'other' => [
'username' => $userinfo['username'],
'reason' => $failurereason
]
]);
$event->trigger();
$SESSION->loginerrormsg = get_string('invalidlogin');
$client->log_out();
redirect(new moodle_url('/login/index.php'));
// Check if there's another user with the same email that is not suspended.
$moodleuser = \core_user::get_user_by_email($userinfo['email'], '*', null, IGNORE_MULTIPLE);
if ($moodleuser->id == $mappeduser->id) {
$failurereason = AUTH_LOGIN_SUSPENDED;
$event = \core\event\user_login_failed::create([
'userid' => $mappeduser->id,
'other' => [
'username' => $userinfo['username'],
'reason' => $failurereason,
],
]);
$event->trigger();
$SESSION->loginerrormsg = get_string('invalidlogin');
$client->log_out();
redirect(new moodle_url('/login/index.php'));
} else if ($moodleuser && !$moodleuser->suspended) {
// Update the OAuth2 linked login to point to the active user account.
$linkedlogin->set('userid', $moodleuser->id);
$linkedlogin->set('timemodified', time());
$linkedlogin->update();
// Update user fields and continue with login.
$userinfo = $this->update_user($userinfo, $moodleuser);
$userwasmapped = true;
}
} else if ($mappeduser && ($mappeduser->confirmed || !$issuer->get('requireconfirmation'))) {
// Update user fields.
$userinfo = $this->update_user($userinfo, $mappeduser);
@@ -508,7 +521,6 @@ class auth extends \auth_plugin_base {
redirect(new moodle_url('/login/index.php'));
}
if (!$issuer->is_valid_login_domain($oauthemail)) {
// Trigger login failed event.
$failurereason = AUTH_LOGIN_UNAUTHORISED;
@@ -524,8 +536,24 @@ class auth extends \auth_plugin_base {
if (!$userwasmapped) {
// No defined mapping - we need to see if there is an existing account with the same email.
$moodleuser = \core_user::get_user_by_email($userinfo['email'], '*', null, IGNORE_MULTIPLE);
// Ensure we don't link a login for a suspended user.
if (!empty($moodleuser) && $moodleuser->suspended) {
$failurereason = AUTH_LOGIN_SUSPENDED;
$event = \core\event\user_login_failed::create([
'userid' => $moodleuser->id,
'other' => [
'username' => $userinfo['email'],
'reason' => $failurereason,
],
]);
$event->trigger();
$SESSION->loginerrormsg = get_string('invalidlogin');
$client->log_out();
redirect(new moodle_url('/login/index.php'));
}
$moodleuser = \core_user::get_user_by_email($userinfo['email']);
if (!empty($moodleuser)) {
if ($issuer->get('requireconfirmation')) {
$PAGE->set_url('/auth/oauth2/confirm-link-login.php');
+7 -16
View File
@@ -27,19 +27,14 @@ $string['auth_oauth2description'] = 'OAuth 2 standards based authentication';
$string['auth_oauth2settings'] = 'OAuth 2 authentication settings.';
$string['confirmaccountemail'] = 'Hi {$a->firstname},
A new account has been requested at \'{$a->sitename}\'
using your email address.
A new account has been requested at \'{$a->sitename}\' using your email address.
To confirm your new account, please go to this web address:
To confirm your new account, please click the link below:
{$a->link}
<a href="{$a->link}">Confirm your account</a>
In most mail programs, this should appear as a blue link
which you can just click on. If that doesn\'t work,
then cut and paste the address into the address
line at the top of your web browser window.
If you need help, please contact the site administrator,
If you need help, please contact the site administrator.
{$a->admin}
If you did not do this, someone else could be trying to compromise your account.
@@ -53,16 +48,12 @@ A request has been made to link the {$a->issuername} login
{$a->linkedemail} to your account at \'{$a->sitename}\'
using your email address.
To confirm this request and link these logins, please go to this web address:
To confirm this request and link these logins, please click the link below:
{$a->link}
<a href="{$a->link}">Link your accounts</a>
In most mail programs, this should appear as a blue link
which you can just click on. If that doesn\'t work,
then cut and paste the address into the address
line at the top of your web browser window.
If you need help, please contact the site administrator,
If you need help, please contact the site administrator.
{$a->admin}
If you did not do this, someone else could be trying to compromise your account.
@@ -32,7 +32,7 @@ Feature: Confirm that availability_completion works with previous activity setti
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
And I click on "Displayed if student doesn't meet this condition Click to hide" "link"
And I click on "Item name displayed with access restriction information if student doesn't meet this condition Click to hide" "link"
And I set the field "Activity or resource" to "Previous activity with completion"
And I press "Save and return to course"
Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
@@ -58,7 +58,7 @@ Feature: Confirm that availability_completion works with previous activity setti
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
And I click on "Displayed if student doesn't meet this condition Click to hide" "link"
And I click on "Item name displayed with access restriction information if student doesn't meet this condition Click to hide" "link"
And I set the field "Activity or resource" to "Previous activity with completion"
And I press "Save and return to course"
Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
@@ -83,7 +83,7 @@ Feature: Confirm that availability_completion works with previous activity setti
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
And I click on "Displayed if student doesn't meet this condition Click to hide" "link"
And I click on "Item name displayed with access restriction information if student doesn't meet this condition Click to hide" "link"
And I set the field "Activity or resource" to "Previous activity with completion"
And I press "Save and return to course"
Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
@@ -112,7 +112,7 @@ Feature: Confirm that availability_completion works with previous activity setti
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
And I click on "Displayed if student doesn't meet this condition Click to hide" "link"
And I click on "Item name displayed with access restriction information if student doesn't meet this condition Click to hide" "link"
And I set the field "Activity or resource" to "Previous activity with completion"
And I press "Save changes"
Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
@@ -138,7 +138,7 @@ Feature: Confirm that availability_completion works with previous activity setti
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
And I click on "Displayed if student doesn't meet this condition Click to hide" "link"
And I click on "Item name displayed with access restriction information if student doesn't meet this condition Click to hide" "link"
Then the "Activity or resource" select box should not contain "Previous activity with completion"
# Set Page2 restriction to Previous Activity with completion and delete Page1.
@@ -148,7 +148,7 @@ Feature: Confirm that availability_completion works with previous activity setti
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
And I click on "Displayed if student doesn't meet this condition Click to hide" "link"
And I click on "Item name displayed with access restriction information if student doesn't meet this condition Click to hide" "link"
And I set the field "Activity or resource" to "Previous activity with completion"
And I press "Save and return to course"
Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
@@ -170,7 +170,7 @@ Feature: Confirm that availability_completion works with previous activity setti
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
And I click on "Displayed if student doesn't meet this condition Click to hide" "link"
And I click on "Item name displayed with access restriction information if student doesn't meet this condition Click to hide" "link"
And I set the field "Activity or resource" to "Previous activity with completion"
And I press "Save changes"
Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
@@ -184,7 +184,7 @@ Feature: Confirm that availability_completion works with previous activity setti
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
And I click on "Displayed if student doesn't meet this condition Click to hide" "link"
And I click on "Item name displayed with access restriction information if student doesn't meet this condition Click to hide" "link"
And I set the field "Activity or resource" to "Previous activity with completion"
And I press "Save changes"
And I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
@@ -180,4 +180,4 @@ Feature: Display availability for activities and sections
And I should not see "Date" in the "Restrict access" "fieldset"
And I press "Add restriction..."
And I click on "Grade" "button" in the "Add restriction..." "dialogue"
And the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Displayed if student"
And the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Item name displayed"
@@ -89,13 +89,13 @@ Feature: edit_availability
And I should see "Date" in the "Restrict access" "fieldset"
And ".availability-item .availability-eye img" "css_element" should be visible
And ".availability-item .availability-delete img" "css_element" should be visible
And the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Displayed if student"
And the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Item name displayed"
# Toggle the eye icon.
When I click on ".availability-item .availability-eye img" "css_element"
Then the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Hidden entirely"
When I click on ".availability-item .availability-eye img" "css_element"
Then the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Displayed if student"
Then the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Item name displayed"
# Click the delete button.
When I click on ".availability-item .availability-delete img" "css_element"
+6 -2
View File
@@ -2649,6 +2649,7 @@ class backup_questions_structure_step extends backup_structure_step {
'questioncategoryid',
'idnumber',
'ownerid',
'nextversion',
]);
$questionversions = new backup_nested_element('question_version');
@@ -2725,7 +2726,7 @@ class backup_questions_structure_step extends backup_structure_step {
WHERE bi.backupid = ?
AND bi.itemname = 'question_categoryfinal'", [backup::VAR_BACKUPID]);
// Add all question bank entries from "complete" categories, plus annotated question bank entires
// Add all question bank entries from "complete" categories, plus annotated question bank entires and their children
// from "partial" categories.
$questionbankentry->set_source_sql(
"
@@ -2737,8 +2738,11 @@ class backup_questions_structure_step extends backup_structure_step {
UNION
SELECT qbe.*
FROM {question_bank_entries} qbe
JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id
JOIN {question} q ON q.id = qv.questionid
LEFT JOIN {question_versions} parentqv ON parentqv.questionid = q.parent
JOIN {question_category_partial_temp} qcp ON qcp.itemid = qbe.questioncategoryid
JOIN {backup_ids_temp} biq ON biq.itemid = qbe.id
JOIN {backup_ids_temp} biq ON biq.itemid = qbe.id OR biq.itemid = parentqv.questionbankentryid
WHERE qcp.itemid = ?
AND qcp.backupid = ?
AND biq.backupid = ?
+76 -2
View File
@@ -27,6 +27,9 @@
defined('MOODLE_INTERNAL') || die();
use core_question\local\bank\question_version_status;
use core_question\versions;
/**
* delete old directories and conditionally create backup_temp_ids table
*/
@@ -5289,6 +5292,15 @@ class restore_create_categories_and_questions extends restore_structure_step {
$this->set_mapping('question_bank_entry', $this->latestqbe->oldid, $this->latestqbe->newid);
}
if (
($data->qtype === 'random')
&& ($this->latestversion->status == \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN)
) {
// Ensure that this newly created question is considered by
// \qtype_random\task\remove_unused_questions.
$this->latestversion->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_DRAFT;
}
// Now store the question.
$newitemid = $DB->insert_record('question', $data);
$this->set_mapping('question', $oldid, $newitemid);
@@ -5301,9 +5313,55 @@ class restore_create_categories_and_questions extends restore_structure_step {
$oldqvid = $this->latestversion->id;
$this->latestversion->questionbankentryid = $this->latestqbe->newid;
$this->latestversion->questionid = $newitemid;
// In case the backed up version was deleted and a new one created in its place, increase the version numbers of
// conflicting versions to make room for this one.
$transaction = $DB->start_delegated_transaction();
if (
$DB->record_exists(
'question_versions',
[
'questionbankentryid' => $this->latestversion->questionbankentryid,
'version' => $this->latestversion->version,
],
)
) {
// We'll update each higher version and any references one-at-a-time, starting with the highest, to avoid
// creating a duplicate questionbankentryid-version combination in question_versions.
$moveversions = $DB->get_records_select(
'question_versions',
'questionbankentryid = :questionbankentryid AND version >= :oldversion',
[
'questionbankentryid' => $this->latestversion->questionbankentryid,
'oldversion' => $this->latestversion->version,
],
'version DESC',
);
foreach ($moveversions as $moveversion) {
$DB->set_field(
'question_versions',
'version',
$moveversion->version + 1,
[
'questionbankentryid' => $moveversion->questionbankentryid,
'version' => $moveversion->version,
]
);
$DB->set_field(
'question_references',
'version',
$moveversion->version + 1,
[
'questionbankentryid' => $moveversion->questionbankentryid,
'version' => $moveversion->version,
]
);
}
// Ensure the nextversion value has been initialised, and increment it to account for the additional version.
versions::get_next_version($this->latestversion->questionbankentryid);
}
$newqvid = $DB->insert_record('question_versions', $this->latestversion);
$this->set_mapping('question_versions', $oldqvid, $newqvid);
$transaction->allow_commit();
} else {
// By performing this set_mapping() we make get_old/new_parentid() to work for all the
// children elements of the 'question' one (so qtype plugins will know the question they belong to).
@@ -5311,6 +5369,18 @@ class restore_create_categories_and_questions extends restore_structure_step {
// Also create the question_bank_entry and version mappings, if required.
$newquestionversion = $DB->get_record('question_versions', ['questionid' => $questionmapping->newitemid]);
// Restore the version to ready state if it has been hidden.
if (
$newquestionversion->status == question_version_status::QUESTION_STATUS_HIDDEN
&& $this->latestversion->status == question_version_status::QUESTION_STATUS_READY
) {
$DB->set_field(
'question_versions',
'status',
question_version_status::QUESTION_STATUS_READY,
['questionid' => $questionmapping->newitemid],
);
}
$this->set_mapping('question_versions', $this->latestversion->id, $newquestionversion->id);
if (empty($this->latestqbe->newid)) {
$this->latestqbe->oldid = $this->latestqbe->id;
@@ -5485,7 +5555,11 @@ class restore_move_module_questions_categories extends restore_execution_step {
// but if that context still exists on the site and the user has access then point question references
// to the originals.
$originalcontext = context::instance_by_id($contextid, IGNORE_MISSING);
if ($originalcontext && has_capability('mod/qbank:view', $originalcontext)) {
if (
$this->task->is_samesite()
&& $originalcontext
&& has_capability('mod/qbank:view', $originalcontext)
) {
$originalquestions = get_questions_category(question_get_top_category($contextid), false);
$targetcoursecontext = context_course::instance($this->get_courseid());
foreach ($originalquestions as $originalquestion) {
@@ -32,6 +32,14 @@ use core_backup\hook\fixtures\copy_course_hook_callbacks;
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class copy_course_hook_test extends advanced_testcase {
/**
* Load required test libraries
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once("{$CFG->dirroot}/backup/util/includes/backup_includes.php");
parent::setUpBeforeClass();
}
/**
* Test the hook.
@@ -464,6 +464,39 @@ abstract class restore_dbops {
$xmlparser->process();
}
/**
* Store ids associated with any activity in the backup that supports FEATURE_PUBLISHES_QUESTIONS.
*
* @param string $restoreid The restore ID.
* @param string $activitiespath The path to the `activities` folder in the backup being restored.
*/
public static function load_questionbanks_to_tempids(string $restoreid, string $activitiespath): void {
if (!is_dir($activitiespath)) {
return;
}
// Get modules that publish questions.
$qmodules = array_filter(
array_keys(core\component::get_all_plugins_list('mod')),
fn($module) => plugin_supports('mod', $module, FEATURE_PUBLISHES_QUESTIONS),
);
foreach (scandir($activitiespath) as $activitydir) {
[$modname] = explode('_', $activitydir);
if (!in_array($modname, $qmodules)) {
continue;
}
$activityfile = "{$activitiespath}/{$activitydir}/{$modname}.xml";
if (!file_exists($activityfile)) { // Shouldn't happen ever, but...
throw new backup_helper_exception('missing_moodle_backup_xml_file', $activityfile);
}
// Parse each activity's file, storing the relevant data in the database.
$xmlparser = new progressive_parser();
$xmlparser->set_file($activityfile);
$xmlprocessor = new restore_questionbanks_parser_processor($restoreid);
$xmlparser->set_processor($xmlprocessor);
$xmlparser->process();
}
}
/**
* Check all the included categories and questions, deciding the action to perform
* for each one (mapping / creation) and returning one array of problems in case
@@ -609,9 +642,18 @@ abstract class restore_dbops {
$topcats = 0;
// get categories in context (bank)
$categories = self::restore_get_question_categories($restoreid, $contextid, $contextlevel);
// cache permissions if $targetcontext is found
if ($targetcontext = self::restore_find_best_target_context($categories, $courseid, $contextlevel)) {
if (
$contextlevel == \core\context\module::LEVEL
&& self::get_backup_ids_record($restoreid, 'questionbank', $contextid)
) {
// Don't look for an existing module context, we have the original context in the backup,
// so we'll put the categories in the course context for now and move them once the activity is restored.
$targetcontext = core\context\course::instance($courseid);
} else {
$targetcontext = self::restore_find_best_target_context($categories, $courseid, $contextlevel);
}
if ($targetcontext) {
// Cache permissions if $targetcontext is found.
$canmanagecategory = has_capability('moodle/question:managecategory', $targetcontext, $userid);
$canadd = has_capability('moodle/question:add', $targetcontext, $userid);
}
@@ -173,6 +173,7 @@ abstract class restore_prechecks_helper {
$progress->progress($majorstep++);
// Check we are able to restore and the categories and questions
restore_dbops::load_questionbanks_to_tempids($restoreid, $controller->get_plan()->get_basepath() . '/activities');
$file = $controller->get_plan()->get_basepath() . '/questions.xml';
restore_dbops::load_categories_and_questions_to_tempids($restoreid, $file);
if ($problems = restore_dbops::precheck_categories_and_questions($restoreid, $courseid, $userid, $samesite)) {
@@ -0,0 +1,59 @@
<?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/>.
defined('MOODLE_INTERNAL' || die());
require_once($CFG->dirroot . '/backup/util/xml/parser/processors/grouped_parser_processor.class.php');
/**
* Parse and store activity data for activities that publish questions.
*
* @package core
* @copyright 2025 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class restore_questionbanks_parser_processor extends grouped_parser_processor {
/**
* Store the restore ID and register paths.
*
* @param string $restoreid ID of the backup being restored.
*/
public function __construct(
/** @var string ID of the backup being restored */
protected string $restoreid,
) {
parent::__construct();
$this->add_path('/activity');
}
#[\Override]
protected function dispatch_chunk($data): void {
// Recieved one chunk, store the context ID as that's what we will match question categories against.
$itemid = $data['tags']['contextid'];
restore_dbops::set_backup_ids_record($this->restoreid, 'questionbank', $itemid);
}
#[\Override]
protected function notify_path_start($path) {
// Nothing to do.
}
#[\Override]
protected function notify_path_end($path) {
// Nothing to do.
}
}
@@ -39,6 +39,7 @@ require_once($CFG->dirroot . '/backup/util/helper/backup_anonymizer_helper.class
require_once($CFG->dirroot . '/backup/util/helper/backup_file_manager.class.php');
require_once($CFG->dirroot . '/backup/util/helper/copy_helper.class.php');
require_once($CFG->dirroot . '/backup/util/helper/restore_prechecks_helper.class.php');
require_once($CFG->dirroot . '/backup/util/helper/restore_questionbanks_parser_processor.php');
require_once($CFG->dirroot . '/backup/util/helper/restore_moodlexml_parser_processor.class.php');
require_once($CFG->dirroot . '/backup/util/helper/restore_inforef_parser_processor.class.php');
require_once($CFG->dirroot . '/backup/util/helper/restore_users_parser_processor.class.php');
@@ -104,6 +104,9 @@ class block_completionstatus extends block_base {
// Flag to set if current completion data is inconsistent with what is stored in the database.
$pending_update = false;
// Get activities visible to the user that have completion enabled.
$visibleactivities = $info->get_user_activities_with_completion($USER->id);
// Loop through course criteria.
foreach ($completions as $completion) {
$criteria = $completion->get_criteria();
@@ -115,6 +118,11 @@ class block_completionstatus extends block_base {
// Activities are a special case, so cache them and leave them till last.
if ($criteria->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
// Only include activities that are visible to the user.
if (!isset($visibleactivities[$criteria->moduleinstance])) {
continue;
}
$activities[$criteria->moduleinstance] = $complete;
if ($complete) {
@@ -119,10 +119,21 @@ $pendingupdate = false;
// Load criteria to display.
$completions = $info->get_completions($user->id);
// Get activities visible to the user that have completion enabled.
$visibleactivities = $info->get_user_activities_with_completion($user->id);
// Loop through course criteria.
foreach ($completions as $completion) {
$criteria = $completion->get_criteria();
// Skip display of activity completion criteria for activities the user cannot see.
if (
$criteria->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY &&
!isset($visibleactivities[$criteria->moduleinstance])
) {
continue;
}
if (!$pendingupdate && $criteria->is_pending($completion)) {
$pendingupdate = true;
}
@@ -6,9 +6,10 @@ Feature: Enable Block Completion in a course using activity completion
Background:
Given the following "users" exist:
| username | firstname | lastname | email | idnumber |
| teacher1 | Teacher | 1 | teacher1@example.com | T1 |
| student1 | Student | 1 | student1@example.com | S1 |
| username | firstname | lastname | email | idnumber |
| teacher1 | Teacher | 1 | teacher1@example.com | T1 |
| student1 | Student | 1 | student1@example.com | S1 |
| student2 | Student | 2 | student2@example.com | S2 |
And the following "courses" exist:
| fullname | shortname | category | enablecompletion |
| Course 1 | C1 | 0 | 1 |
@@ -16,6 +17,7 @@ Feature: Enable Block Completion in a course using activity completion
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
And the following "activities" exist:
| activity | course | idnumber | name | gradepass | completion | completionview | completionusegrade | completionpassgrade |
| page | C1 | page1 | Test page name | | 2 | 1 | 0 | 0 |
@@ -23,10 +25,12 @@ Feature: Enable Block Completion in a course using activity completion
And the following "blocks" exist:
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
| completionstatus | Course | C1 | course-view-* | side-pre |
And I am on the "Course 1" course page logged in as teacher1
And I change window size to "large"
And I turn editing mode on
Scenario: Completion status block when student has not started any activities
Given I am on the "Course 1" course page logged in as teacher1
And I navigate to "Course completion" in current page administration
Given I navigate to "Course completion" in current page administration
And I expand all fieldsets
And I set the following fields to these values:
| Test page name | 1 |
@@ -36,8 +40,7 @@ Feature: Enable Block Completion in a course using activity completion
And I should see "0 of 1" in the "Activity completion" "table_row"
Scenario: Completion status block when student has completed a page
Given I am on the "Course 1" course page logged in as teacher1
And I navigate to "Course completion" in current page administration
Given I navigate to "Course completion" in current page administration
And I expand all fieldsets
And I set the following fields to these values:
| Test page name | 1 |
@@ -50,8 +53,7 @@ Feature: Enable Block Completion in a course using activity completion
And I should see "Yes" in the "Activity completion" "table_row"
Scenario: Completion status block with items with passing grade
Given I am on the "Course 1" course page logged in as teacher1
And I navigate to "Course completion" in current page administration
Given I navigate to "Course completion" in current page administration
And I expand all fieldsets
And I set the following fields to these values:
| Test assign name | 1 |
@@ -69,8 +71,7 @@ Feature: Enable Block Completion in a course using activity completion
And I should see "Yes" in the "Activity completion" "table_row"
Scenario: Completion status block with items with failing grade
Given I am on the "Course 1" course page logged in as teacher1
And the following "grade grades" exist:
Given the following "grade grades" exist:
| gradeitem | user | grade |
| Test assign name | student1 | 49 |
And I navigate to "Course completion" in current page administration
@@ -86,3 +87,265 @@ Feature: Enable Block Completion in a course using activity completion
And I follow "More details"
And I should see "Achieving grade, Achieving passing grade" in the "Activity completion" "table_row"
And I should see "No" in the "Activity completion" "table_row"
@javascript
Scenario: Student visibility respects combined activity and section restrictions with progressive completion
Given the following "activities" exist:
| activity | name | intro | course | idnumber | section | visible | completion | completionview |
| page | task A | page description | C1 | page1 | 0 | 1 | 2 | 1 |
| page | task B | page description | C1 | page2 | 1 | 1 | 2 | 1 |
| assign | task C | assignment description | C1 | assign1 | 1 | 1 | 2 | 1 |
And I navigate to "Course completion" in current page administration
And I expand all fieldsets
And I set the following fields to these values:
| Page - task A | 1 |
| Page - task B | 1 |
| Assignment - task C | 1 |
And I press "Save changes"
# Add conditionally visible restriction (open eye) to section 1 requiring task A completion.
And I turn editing mode on
And I edit the section "1"
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
And I set the field "Activity or resource" to "task A"
And I press "Save changes"
# Add conditionally hidden restriction (closed eye) to task C requiring task A completion.
And I am on the "task C" "assign activity editing" page
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
And I click on "Item name displayed with access restriction information if student doesn't meet this condition Click to hide" "link"
And I set the field "Activity or resource" to "task A"
And I press "Save and return to course"
And I log out
# Initial state: Only unrestricted visible activities appear.
When I am on the "Course 1" course page logged in as student1
Then I should see "Status: Not yet started"
And I should see "0 of 1" in the "Activity completion" "table_row"
And I follow "More details"
And I should see "task A" in the "criteriastatus" "table"
And I should not see "task B" in the "criteriastatus" "table"
And I should not see "task C" in the "criteriastatus" "table"
# After completing task A: Section 2 activities become visible.
And I click on "task A" "link"
And I am on the "Course 1" course page logged in as student1
And I should see "Status: In progress"
And I should see "1 of 3" in the "Activity completion" "table_row"
And I follow "More details"
And I should see "task A" in the "criteriastatus" "table"
And I should see "task B" in the "criteriastatus" "table"
And I should see "task C" in the "criteriastatus" "table"
@javascript
Scenario: Student completion view shows only accessible activities considering all activity restrictions
Given the following "activities" exist:
| activity | name | intro | course | idnumber | section | visible | completion | completionview |
| page | task A | page description | C1 | page1 | 0 | 1 | 2 | 1 |
| page | task B | page description | C1 | assign1 | 1 | 1 | 2 | 1 |
| assign | task C | assignment description | C1 | assign2 | 1 | 1 | 2 | 1 |
And I navigate to "Course completion" in current page administration
And I expand all fieldsets
# Set completion of the activities.
And I set the following fields to these values:
| Page - task A | 1 |
| Page - task B | 1 |
| Assignment - task C | 1 |
And I press "Save changes"
# Add conditionally visible restriction (open eye) to "task B".
And I am on the "task B" "page activity editing" page
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
And I set the field "Activity or resource" to "task A"
And I press "Save and return to course"
# Add conditionally hidden restriction (closed eye) to "task C".
And I am on the "task C" "assign activity editing" page
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
And I click on "Item name displayed with access restriction information if student doesn't meet this condition Click to hide" "link"
And I set the field "Activity or resource" to "task A"
And I press "Save and return to course"
When I am on the "Course 1" course page logged in as student1
And I should see "Status: Not yet started"
And I should see "0 of 2" in the "Activity completion" "table_row"
And I follow "More details"
Then I should see "task A" in the "criteriastatus" "table"
And I should see "task B" in the "criteriastatus" "table"
And I should not see "task C" in the "criteriastatus" "table"
And I click on "task A" "link"
# Complete task A to make task C visible.
And I am on the "Course 1" course page logged in as student1
And I should see "Status: In progress"
And I should see "1 of 3" in the "Activity completion" "table_row"
And I follow "More details"
And I should see "task A" in the "criteriastatus" "table"
And I should see "task B" in the "criteriastatus" "table"
And I should see "task C" in the "criteriastatus" "table"
@javascript
Scenario: Hidden activities do not appear in the completion status block
Given the following "activities" exist:
| activity | name | intro | course | idnumber | section | visible | completion | completionview |
| page | task A | page description | C1 | page1 | 0 | 1 | 2 | 1 |
| page | task B | page description | C1 | page2 | 0 | 0 | 2 | 1 |
And I navigate to "Course completion" in current page administration
And I expand all fieldsets
# Set completion of the activities.
And I set the following fields to these values:
| Page - task A | 1 |
| Page - task B | 1 |
And I press "Save changes"
When I am on the "Course 1" course page logged in as student1
And I should see "Status: Not yet started"
And I should see "0 of 1" in the "Activity completion" "table_row"
And I follow "More details"
And I should see "task A" in the "criteriastatus" "table"
Then I should not see "task B" in the "criteriastatus" "table"
@javascript
Scenario: Activities in the hidden section do not appear in the completion status block
Given the following "activities" exist:
| activity | name | intro | course | idnumber | section | visible | completion | completionview |
| page | task A | page description | C1 | page1 | 0 | 1 | 2 | 1 |
| page | task B | page description | C1 | page2 | 1 | 1 | 2 | 1 |
| assign | task C | page description | C1 | page3 | 2 | 1 | 2 | 1 |
| assign | task D | page description | C1 | page4 | 3 | 1 | 2 | 1 |
And I navigate to "Course completion" in current page administration
And I expand all fieldsets
# Set completion of the activities.
And I set the following fields to these values:
| Page - task A | 1 |
| Page - task B | 1 |
| Assignment - task C | 1 |
| Assignment - task D | 1 |
And I press "Save changes"
# Hide section 1 to make sure book activities are not visible to the student.
And I hide section "1"
# Add conditionally visible restriction to section 2.
And I edit the section "2"
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
And I set the field "Activity or resource" to "task A"
And I press "Save changes"
# Add conditionally hidden restriction to section 3.
And I am on the "Course 1" course page
And I edit the section "3"
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
And I set the field "Activity or resource" to "task A"
And I click on "Item name displayed with access restriction information if student doesn't meet this condition Click to hide" "link"
And I press "Save changes"
And I log out
When I am on the "Course 1" course page logged in as student1
And I should see "Status: Not yet started"
And I should see "0 of 1" in the "Activity completion" "table_row"
And I follow "More details"
Then I should see "task A" in the "criteriastatus" "table"
And I should not see "task B" in the "criteriastatus" "table"
And I should not see "task C" in the "criteriastatus" "table"
And I should not see "task D" in the "criteriastatus" "table"
# Complete task A to make other activities visible.
And I click on "task A" "link"
And I am on the "Course 1" course page logged in as student1
And I should see "Status: In progress"
And I should see "1 of 3" in the "Activity completion" "table_row"
And I follow "More details"
And I should see "task A" in the "criteriastatus" "table"
And I should not see "task B" in the "criteriastatus" "table"
And I should see "task C" in the "criteriastatus" "table"
And I should see "task D" in the "criteriastatus" "table"
@javascript
Scenario: Activities with disabled completion tracking are omitted from the completion view
Given the following "activities" exist:
| activity | name | intro | course | idnumber | section | visible | completion | completionview |
| page | task A | page description | C1 | page1 | 0 | 1 | 2 | 1 |
| page | task B | page description | C1 | page2 | 1 | 1 | 2 | 1 |
And I navigate to "Course completion" in current page administration
And I expand all fieldsets
# Set completion of the activities.
And I set the following fields to these values:
| Page - task A | 1 |
| Page - task B | 1 |
And I press "Save changes"
# Disable completion tracking for "task A".
And I am on the "task A" "page activity editing" page
And I expand all fieldsets
And I set the following fields to these values:
| completion | 0 |
And I press "Save and return to course"
When I am on the "Course 1" course page logged in as student1
And I should see "Status: Not yet started"
And I should see "0 of 1" in the "Activity completion" "table_row"
And I follow "More details"
Then I should not see "task A" in the "criteriastatus" "table"
And I should see "task B" in the "criteriastatus" "table"
@javascript
Scenario: Activities with group or grouping restrictions are omitted from the completion view
Given the following "activities" exist:
| activity | name | intro | course | idnumber | section | visible | completion | completionview |
| page | task A | page description | C1 | page1 | 0 | 1 | 2 | 1 |
| page | task B | page description | C1 | page2 | 1 | 1 | 2 | 1 |
| page | task C | page description | C1 | page3 | 2 | 1 | 2 | 1 |
And I navigate to "Course completion" in current page administration
And I expand all fieldsets
# Set completion of the activities.
And I set the following fields to these values:
| Page - task A | 1 |
| Page - task B | 1 |
| Page - task C | 1 |
And I press "Save changes"
# Add groups and groupings.
And the following "groups" exist:
| name | course | idnumber |
| G1 | C1 | GI1 |
| G2 | C1 | GI2 |
And the following "groupings" exist:
| name | course | idnumber |
| Grouping 1 | C1 | GG1 |
| Grouping 2 | C1 | GG2 |
And the following "grouping groups" exist:
| grouping | group |
| GG1 | GI1 |
| GG2 | GI2 |
# Add students to groups.
And the following "group members" exist:
| user | group |
| student1 | GI1 |
| student2 | GI2 |
# Add group restriction to "task B".
And I am on the "task B" "page activity editing" page
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Group" "button" in the "Add restriction..." "dialogue"
And I set the field "Group" to "G1"
And I click on "Save and return to course" "button"
# Add Grouping and 'Activity or resource' restriction to "task C".
And I am on the "task C" "page activity editing" page
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Grouping" "button" in the "Add restriction..." "dialogue"
And I set the field "Grouping" to "Grouping 2"
And I click on "Add restriction..." "button"
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
And I set the field "Activity or resource" to "task A"
And I click on "Save and return to course" "button"
When I am on the "Course 1" course page logged in as student1
And I should see "Status: Not yet started"
And I should see "0 of 2" in the "Activity completion" "table_row"
And I follow "More details"
Then I should see "task A" in the "criteriastatus" "table"
And I should see "task B" in the "criteriastatus" "table"
And I should not see "task C" in the "criteriastatus" "table"
And I am on the "Course 1" course page logged in as student2
And I should see "Status: Not yet started"
And I should see "0 of 2" in the "Activity completion" "table_row"
And I follow "More details"
And I should see "task A" in the "criteriastatus" "table"
And I should not see "task B" in the "criteriastatus" "table"
And I should see "task C" in the "criteriastatus" "table"
@@ -23,7 +23,7 @@
{}
}}
<div id="block-myoverview-{{uniqid}}" class="block-myoverview block-cards" data-region="myoverview" role="navigation">
<div id="block-myoverview-{{uniqid}}" class="block-myoverview block-cards" data-region="myoverview">
<div role="search" data-region="filter" class="d-flex align-items-center my-2" aria-label="{{#str}} aria:controls, block_myoverview {{/str}}">
<div class="d-flex flex-wrap gap-1 align-items-center w-100">
{{> block_myoverview/nav-grouping-selector }}
@@ -44,14 +44,14 @@
}
}}
<div class="block-myoverview block-cards" data-region="myoverview" role="navigation">
<div class="block-myoverview block-cards" data-region="myoverview">
<div class="container-fluid p-0">
<div class="text-xs-center text-center mt-3" data-region="empty-message">
<img class="empty-placeholder-image-lg mt-1"
src="{{nocoursesimg}}"
alt=""
>
<h5 class="h5 mt-3 mb-0">{{{ title }}}</h5>
<h4 class="h4 mt-3 mb-0">{{{ title }}}</h4>
<p class="mt-3 mb-0">{{{ intro }}}</p>
<div class="mt-5 mb-0 whitebutton" id="action_bar">
@@ -314,6 +314,6 @@ Feature: The my overview block allows users to easily access their courses
Then I should not see "Category 1" in the "Course overview" "block"
@accessibility
Scenario: The dashboard page must have sufficient colour contrast
Scenario: The My courses page must meet accessibility standards
When I am on the "My courses" page logged in as "student1"
Then the page should meet "wcag143" accessibility standards
Then the page should meet accessibility standards with "best-practice" extra tests
@@ -88,4 +88,4 @@ Feature: Zero state on my overview block
@javascript @accessibility
Scenario: Evaluate the accessibility of the My courses (zero state)
When I am on the "My courses" page logged in as "manager"
Then the page should meet accessibility standards
Then the page should meet accessibility standards with "best-practice" extra tests
@@ -15,6 +15,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/course/lib.php');
require_once($CFG->dirroot. '/course/format/lib.php');
/**
+1 -1
View File
@@ -70,7 +70,7 @@ class event_exporter_base extends exporter {
$data = new \stdClass();
$data->id = $event->get_id();
$data->name = $event->get_name();
$data->name = clean_string($event->get_name());
$data->description = file_rewrite_pluginfile_urls(
$event->get_description()->get_value(),
'pluginfile.php',
+17 -13
View File
@@ -15,11 +15,11 @@ Feature: Perform basic calendar functionality
| name | category | idnumber |
| CatA | 0 | cata |
And the following "courses" exist:
| fullname | shortname | format | category |
| Course 1 | C1 | topics | cata |
| Course 2 | C2 | topics | cata |
| Course 3 | C3 | topics | cata |
| Course 4 | C4 | topics | cata |
| fullname | shortname | format | category | startdate |
| Course 1 | C1 | topics | cata | ##first day of last month## |
| Course 2 | C2 | topics | cata | ##first day of last month## |
| Course 3 | C3 | topics | cata | ##first day of last month## |
| Course 4 | C4 | topics | cata | ##first day of last month## |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
@@ -58,20 +58,24 @@ Feature: Perform basic calendar functionality
Scenario: Create a course event
Given I log in as "teacher1"
And I create a calendar event with form data:
| Type of event | course |
| Course | Course 1 |
| Event title | Really awesome event! |
| Description | Come join this awesome event, sucka! |
| Type of event | course |
| Course | Course 1 |
| timestart[day] | 1 |
| Event title | Really awesome event! & < > " ' |
| Description | Come join this awesome event, sucka! |
And I log out
When I am on the "Course 1" course page logged in as student1
And I follow "Course calendar"
And I click on "Really awesome event!" "link"
And "Course 1" "link" should exist in the "Really awesome event!" "dialogue"
And I click on "Close" "button" in the "Really awesome event!" "dialogue"
And I hover over day "1" of this month in the full calendar page
And I should see "Really awesome event! & < > \" '"
And I click on "Really awesome event! & < > \" '" "link"
And I should see "Really awesome event! & < > \" '" in the ".modal-title" "css_element"
And I should see "Course 1" in the ".modal-body" "css_element"
And I click on "Close" "button" in the ".modal-content" "css_element"
And I log out
And I log in as "student2"
And I follow "Full calendar"
Then I should not see "Really awesome event!"
Then I should not see "Really awesome event! & < > \" '"
@javascript
Scenario: Create a group event
+1 -1
View File
@@ -28,4 +28,4 @@ if (!file_exists($configfile)) {
die;
}
require($configfile);
require_once($configfile);
+8 -5
View File
@@ -53,21 +53,24 @@ class course_image implements data_source_interface {
* Loads the data for the key provided ready formatted for caching.
*
* @param string|int $key The key to load.
* @return string|bool Returns course image url as a string or false if the image is not exist
* @return string|null The course image URL path, or false if the image does not exist.
*/
public function load_for_cache($key) {
// We should use get_course() instead of get_fast_modinfo() for better performance.
$course = get_course($key);
return $this->get_image_url_from_overview_files($course);
$url = $this->get_image_url_from_overview_files($course);
// Return only the path.
return $url?->out_as_local_url();
}
/**
* Returns image URL from course overview files.
*
* @param \stdClass $course Course object.
* @return null|string Image URL or null if it's not exists.
* @return null|moodle_url Image URL or null if the image does not exist.
*/
protected function get_image_url_from_overview_files(\stdClass $course): ?string {
protected function get_image_url_from_overview_files(\stdClass $course): moodle_url|null {
$courseinlist = new core_course_list_element($course);
foreach ($courseinlist->get_course_overviewfiles() as $file) {
if ($file->is_valid_image()) {
@@ -78,7 +81,7 @@ class course_image implements data_source_interface {
null,
$file->get_filepath(),
$file->get_filename()
)->out();
);
}
}
+1
View File
@@ -3263,6 +3263,7 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
ON uc.roleid = rc.roleid
AND ( $eqpaths OR $likechild )
WHERE (ra.id IS NOT NULL OR uc.upath IS NOT NULL)
ORDER BY cc.sortorder, cc.id
";
$params = [
+4 -2
View File
@@ -16,6 +16,7 @@
namespace core_course;
use core\{clock, di};
use core\context;
use core\context\course as context_course;
use core\context\coursecat as context_coursecat;
@@ -324,6 +325,7 @@ class course_request {
$data->newsitems = $courseconfig->newsitems;
$data->showgrades = $courseconfig->showgrades;
$data->showreports = $courseconfig->showreports;
$data->showactivitydates = $courseconfig->showactivitydates;
$data->maxbytes = $courseconfig->maxbytes;
$data->groupmode = $courseconfig->groupmode;
$data->groupmodeforce = $courseconfig->groupmodeforce;
@@ -332,9 +334,9 @@ class course_request {
$data->lang = $courseconfig->lang;
$data->enablecompletion = $courseconfig->enablecompletion;
$data->numsections = $courseconfig->numsections;
$data->startdate = usergetmidnight(time());
$data->startdate = usergetmidnight(di::get(clock::class)->time());
if ($courseconfig->courseenddateenabled) {
$data->enddate = usergetmidnight(time()) + $courseconfig->courseduration;
$data->enddate = $data->startdate + $courseconfig->courseduration;
}
[$data->fullname, $data->shortname] = restore_dbops::calculate_course_names(0, $data->fullname, $data->shortname);
+3 -2
View File
@@ -186,10 +186,11 @@ class course_summary_exporter extends \core\external\exporter {
$image = \cache::make('core', 'course_image')->get($course->id);
if (is_null($image)) {
$image = false;
return false;
}
return $image;
$url = new moodle_url($image);
return $url->out();
}
/**
+1 -1
View File
@@ -440,7 +440,7 @@ class course_edit_form extends moodleform {
// When two elements we need a group.
$buttonarray = array();
$classarray = array('class' => 'form-submit');
if ($returnto !== 0) {
if (!empty($returnto)) {
$buttonarray[] = &$mform->createElement('submit', 'saveandreturn', get_string('savechangesandreturn'), $classarray);
}
$buttonarray[] = &$mform->createElement('submit', 'saveanddisplay', get_string('savechangesanddisplay'), $classarray);
+12 -1
View File
@@ -39,7 +39,18 @@ class editsection_form extends moodleform {
$mform->setDefault('name', $sectioninfo->name);
$mform->addRule('name', get_string('maximumchars', '', 1333), 'maxlength', 1333, 'client');
/// Prepare course and the editor
// Prepare course and the editor.
if ($sectioninfo->is_delegated()) {
// Show a warning that subsection descriptions will be discontinued.
$warning = $OUTPUT->notification(
message: get_string('subsectiondescriptionwarning', 'course'),
type: \core\output\notification::NOTIFY_WARNING,
closebutton: false,
titleicon: 'i/circleinfo',
);
$mform->addElement('static', 'subsectionwarning', '', $warning);
}
$mform->addElement('editor', 'summary_editor', get_string('description'), null, $this->_customdata['editoroptions']);
$mform->setType('summary_editor', PARAM_RAW);
@@ -8,6 +8,6 @@ define("core_courseformat/local/courseeditor/exporter",["exports"],(function(_ex
* @copyright 2021 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class{constructor(reactive){this.reactive=reactive,this.COMPLETIONS=["incomplete","complete","complete","fail"]}course(state){var _state$course$highlig;const data={sections:[],editmode:this.reactive.isEditing,highlighted:null!==(_state$course$highlig=state.course.highlighted)&&void 0!==_state$course$highlig?_state$course$highlig:""};return this.listedSectionIds(state).forEach((sectionid=>{var _state$section$get;const sectioninfo=null!==(_state$section$get=state.section.get(sectionid))&&void 0!==_state$section$get?_state$section$get:{},section=this.section(state,sectioninfo);data.sections.push(section)})),data.hassections=0!=data.sections.length,data}listedSectionIds(state){var _state$course$section;return(null!==(_state$course$section=state.course.sectionlist)&&void 0!==_state$course$section?_state$course$section:[]).filter((sectionid=>{var _state$section$get2;return null===(null!==(_state$section$get2=state.section.get(sectionid))&&void 0!==_state$section$get2?_state$section$get2:{}).component}))}section(state,sectioninfo){var _state$course$highlig2,_sectioninfo$cmlist;const section={...sectioninfo,highlighted:null!==(_state$course$highlig2=state.course.highlighted)&&void 0!==_state$course$highlig2?_state$course$highlig2:"",cms:[]};return(null!==(_sectioninfo$cmlist=sectioninfo.cmlist)&&void 0!==_sectioninfo$cmlist?_sectioninfo$cmlist:[]).forEach((cmid=>{const cminfo=state.cm.get(cmid),cm=this.cm(state,cminfo);section.cms.push(cm)})),section.hascms=0!=section.cms.length,section}cm(state,cminfo){const cm={...cminfo,isactive:!1,sectioninfo:!1};if(cminfo.hasdelegatedsection){const sectioninfo=state.section.get(cminfo.delegatesectionid);cm.sectioninfo=this.section(state,sectioninfo)}return cm}cmDraggableData(state,cmid){const cminfo=state.cm.get(cmid);if(!cminfo)return null;let nextcmid;const section=state.section.get(cminfo.sectionid),currentindex=null==section?void 0:section.cmlist.indexOf(cminfo.id);return void 0!==currentindex&&(nextcmid=null==section?void 0:section.cmlist[currentindex+1]),{type:"cm",id:cminfo.id,name:cminfo.name,sectionid:cminfo.sectionid,hasdelegatedsection:cminfo.hasdelegatedsection,nextcmid:nextcmid}}sectionDraggableData(state,sectionid){const sectioninfo=state.section.get(sectionid);return sectioninfo?{type:"section",id:sectioninfo.id,name:sectioninfo.name,number:sectioninfo.number}:null}fileDraggableData(state,dataTransfer){var _dataTransfer$files;const files=[];return(null===(_dataTransfer$files=dataTransfer.files)||void 0===_dataTransfer$files?void 0:_dataTransfer$files.length)>0&&dataTransfer.files.forEach((file=>{files.push(file)})),{type:"files",files:files}}cmCompletion(state,cminfo){const data={statename:"",state:"NaN"};if(void 0!==cminfo.completionstate){var _this$COMPLETIONS$cmi;data.state=cminfo.completionstate,data.hasstate=!0;let statename=null!==(_this$COMPLETIONS$cmi=this.COMPLETIONS[cminfo.completionstate])&&void 0!==_this$COMPLETIONS$cmi?_this$COMPLETIONS$cmi:"NaN";void 0!==cminfo.isoverallcomplete&&!0===cminfo.isoverallcomplete&&(statename="complete"),data["is".concat(statename)]=!0}return data}allItemsArray(state){var _state$course$section2;const items=[];return(null!==(_state$course$section2=state.course.sectionlist)&&void 0!==_state$course$section2?_state$course$section2:[]).forEach((sectionid=>{var _sectioninfo$cmlist2;const sectioninfo=state.section.get(sectionid);if(null!==sectioninfo.component)return;items.push({type:"section",id:sectioninfo.id,url:sectioninfo.sectionurl});(null!==(_sectioninfo$cmlist2=sectioninfo.cmlist)&&void 0!==_sectioninfo$cmlist2?_sectioninfo$cmlist2:[]).forEach((cmid=>{const cmInfo=state.cm.get(cmid);items.push(...this.cmItemsArray(state,cmInfo))}))})),items}cmItemsArray(state,cmInfo){if(cmInfo.hasdelegatedsection){var _delegatedsection$cml;const items=[],delegatedsection=state.section.get(cmInfo.delegatesectionid);items.push({type:"section",id:delegatedsection.id,url:delegatedsection.sectionurl});return(null!==(_delegatedsection$cml=delegatedsection.cmlist)&&void 0!==_delegatedsection$cml?_delegatedsection$cml:[]).forEach((cmid=>{const cmInfo=state.cm.get(cmid);items.push({type:"cm",id:cmInfo.id,url:cmInfo.url})})),items}return[{type:"cm",id:cmInfo.id,url:cmInfo.url}]}canUseStealth(state,cmIds){return cmIds.some((cmId=>{var _cminfo$allowstealth;const cminfo=state.cm.get(cmId);return null!==(_cminfo$allowstealth=null==cminfo?void 0:cminfo.allowstealth)&&void 0!==_cminfo$allowstealth&&_cminfo$allowstealth}))}},_exports.default}));
class{constructor(reactive){this.reactive=reactive,this.COMPLETIONS=["incomplete","complete","complete","fail"]}course(state){var _state$course$highlig;const data={sections:[],editmode:this.reactive.isEditing,highlighted:null!==(_state$course$highlig=state.course.highlighted)&&void 0!==_state$course$highlig?_state$course$highlig:""};return this.listedSectionIds(state).forEach((sectionid=>{var _state$section$get;const sectioninfo=null!==(_state$section$get=state.section.get(sectionid))&&void 0!==_state$section$get?_state$section$get:{},section=this.section(state,sectioninfo);data.sections.push(section)})),data.hassections=0!=data.sections.length,data}listedSectionIds(state){var _state$course$section;return(null!==(_state$course$section=state.course.sectionlist)&&void 0!==_state$course$section?_state$course$section:[]).filter((sectionid=>{var _state$section$get2;return null===(null!==(_state$section$get2=state.section.get(sectionid))&&void 0!==_state$section$get2?_state$section$get2:{}).component}))}section(state,sectioninfo){var _state$course$highlig2,_sectioninfo$cmlist;const section={...sectioninfo,highlighted:null!==(_state$course$highlig2=state.course.highlighted)&&void 0!==_state$course$highlig2?_state$course$highlig2:"",cms:[]};return(null!==(_sectioninfo$cmlist=sectioninfo.cmlist)&&void 0!==_sectioninfo$cmlist?_sectioninfo$cmlist:[]).forEach((cmid=>{const cminfo=state.cm.get(cmid),cm=this.cm(state,cminfo);section.cms.push(cm)})),section.hascms=0!=section.cms.length,section}cm(state,cminfo){const cm={...cminfo,isactive:!1,sectioninfo:!1};if(cminfo.hasdelegatedsection){var _state$section$get3;const sectioninfo=null!==(_state$section$get3=state.section.get(cminfo.delegatesectionid))&&void 0!==_state$section$get3?_state$section$get3:{};cm.sectioninfo=this.section(state,sectioninfo)}return cm}cmDraggableData(state,cmid){const cminfo=state.cm.get(cmid);if(!cminfo)return null;let nextcmid;const section=state.section.get(cminfo.sectionid),currentindex=null==section?void 0:section.cmlist.indexOf(cminfo.id);return void 0!==currentindex&&(nextcmid=null==section?void 0:section.cmlist[currentindex+1]),{type:"cm",id:cminfo.id,name:cminfo.name,sectionid:cminfo.sectionid,hasdelegatedsection:cminfo.hasdelegatedsection,nextcmid:nextcmid}}sectionDraggableData(state,sectionid){const sectioninfo=state.section.get(sectionid);return sectioninfo?{type:"section",id:sectioninfo.id,name:sectioninfo.name,number:sectioninfo.number}:null}fileDraggableData(state,dataTransfer){var _dataTransfer$files;const files=[];return(null===(_dataTransfer$files=dataTransfer.files)||void 0===_dataTransfer$files?void 0:_dataTransfer$files.length)>0&&dataTransfer.files.forEach((file=>{files.push(file)})),{type:"files",files:files}}cmCompletion(state,cminfo){const data={statename:"",state:"NaN"};if(void 0!==cminfo.completionstate){var _this$COMPLETIONS$cmi;data.state=cminfo.completionstate,data.hasstate=!0;let statename=null!==(_this$COMPLETIONS$cmi=this.COMPLETIONS[cminfo.completionstate])&&void 0!==_this$COMPLETIONS$cmi?_this$COMPLETIONS$cmi:"NaN";void 0!==cminfo.isoverallcomplete&&!0===cminfo.isoverallcomplete&&(statename="complete"),data["is".concat(statename)]=!0}return data}allItemsArray(state){var _state$course$section2;const items=[];return(null!==(_state$course$section2=state.course.sectionlist)&&void 0!==_state$course$section2?_state$course$section2:[]).forEach((sectionid=>{var _sectioninfo$cmlist2;const sectioninfo=state.section.get(sectionid);if(null!==sectioninfo.component)return;items.push({type:"section",id:sectioninfo.id,url:sectioninfo.sectionurl});(null!==(_sectioninfo$cmlist2=sectioninfo.cmlist)&&void 0!==_sectioninfo$cmlist2?_sectioninfo$cmlist2:[]).forEach((cmid=>{const cmInfo=state.cm.get(cmid);items.push(...this.cmItemsArray(state,cmInfo))}))})),items}cmItemsArray(state,cmInfo){if(cmInfo.hasdelegatedsection){const items=[],delegatedsection=state.section.get(cmInfo.delegatesectionid);if(delegatedsection){var _delegatedsection$cml;items.push({type:"section",id:delegatedsection.id,url:delegatedsection.sectionurl});(null!==(_delegatedsection$cml=delegatedsection.cmlist)&&void 0!==_delegatedsection$cml?_delegatedsection$cml:[]).forEach((cmid=>{const cmInfo=state.cm.get(cmid);items.push({type:"cm",id:cmInfo.id,url:cmInfo.url})}))}return items}return[{type:"cm",id:cmInfo.id,url:cmInfo.url}]}canUseStealth(state,cmIds){return cmIds.some((cmId=>{var _cminfo$allowstealth;const cminfo=state.cm.get(cmId);return null!==(_cminfo$allowstealth=null==cminfo?void 0:cminfo.allowstealth)&&void 0!==_cminfo$allowstealth&&_cminfo$allowstealth}))}},_exports.default}));
//# sourceMappingURL=exporter.min.js.map
File diff suppressed because one or more lines are too long
@@ -8,6 +8,6 @@ define("core_courseformat/local/courseindex/section",["exports","core_courseform
* @class core_courseformat/local/courseindex/section
* @copyright 2021 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_sectiontitle=_interopRequireDefault(_sectiontitle),_dndsection=_interopRequireDefault(_dndsection),_log=_interopRequireDefault(_log);class Component extends _dndsection.default{create(){this.name="courseindex_section",this.selectors={SECTION:"[data-for='section']",SECTION_ITEM:"[data-for='section_item']",SECTION_TITLE:"[data-for='section_title']",CM_LAST:'[data-for="cm"]:last-child',DND_ALLOWED:"[data-courseindexdndallowed='true']"},this.classes={SECTIONHIDDEN:"dimmed",SECTIONCURRENT:"current",LOCKED:"editinprogress",RESTRICTIONS:"restrictions",PAGEITEM:"pageitem",OVERLAYBORDERS:"overlay-preview-borders"},this.id=this.element.dataset.id,this.isPageItem=!1}static init(target,selectors){let element=document.querySelector(target);return element||(_log.default.debug("Init component with id is deprecated, use a query selector instead."),element=document.getElementById(target)),new this({element:element,selectors:selectors})}stateReady(state){var _section$sectionurl;this.configState(state);const sectionItem=this.getElement(this.selectors.SECTION_ITEM);if(this.reactive.isEditing&&this.reactive.supportComponents&&document.querySelector(this.selectors.DND_ALLOWED)){const titleitem=new _sectiontitle.default({...this,element:sectionItem,fullregion:this.element});this.configDragDrop(titleitem)}const section=state.section.get(this.id);window.location.href==(null===(_section$sectionurl=section.sectionurl)||void 0===_section$sectionurl?void 0:_section$sectionurl.replace(/&amp;/g,"&"))&&(this.reactive.dispatch("setPageItem","section",this.id),sectionItem.scrollIntoView())}getWatchers(){return[{watch:"section[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"section[".concat(this.id,"]:updated"),handler:this._refreshSection},{watch:"course.pageItem:updated",handler:this._refreshPageItem}]}getLastCm(){return this.getElement(this.selectors.CM_LAST)}_refreshSection(_ref){var _element$hasrestricti,_element$dragging,_element$locked;let{element:element}=_ref;const sectionItem=this.getElement(this.selectors.SECTION_ITEM);sectionItem.classList.toggle(this.classes.SECTIONHIDDEN,!element.visible),sectionItem.classList.toggle(this.classes.RESTRICTIONS,null!==(_element$hasrestricti=element.hasrestrictions)&&void 0!==_element$hasrestricti&&_element$hasrestricti),this.element.classList.toggle(this.classes.SECTIONCURRENT,element.current),this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.locked=element.locked,this.getElement(this.selectors.SECTION_TITLE).innerHTML=element.title}_refreshPageItem(_ref2){var _element$pageItem,_this$pageItem;let{element:element,state:state}=_ref2;if(!element.pageItem)return;const containsPageItem=this._isPageItemInThisSection(element.pageItem);if(!containsPageItem||this._isParentSectionIndexCollapsed(state))return this.pageItem=!1,void this.getElement(this.selectors.SECTION_ITEM).classList.remove(this.classes.PAGEITEM);!state.section.get(this.id).indexcollapsed||null!==(_element$pageItem=element.pageItem)&&void 0!==_element$pageItem&&_element$pageItem.isStatic?this.pageItem="section"==element.pageItem.type&&element.pageItem.id==this.id:this.pageItem=containsPageItem;this.getElement(this.selectors.SECTION_ITEM).classList.toggle(this.classes.PAGEITEM,null!==(_this$pageItem=this.pageItem)&&void 0!==_this$pageItem&&_this$pageItem),this.pageItem&&!this.reactive.isEditing&&this.element.scrollIntoView({block:"nearest"})}_isPageItemInThisSection(pageItem){if(pageItem.sectionId==this.id)return!0;return!!this.element.querySelector("".concat(this.selectors.SECTION,'[data-id="').concat(pageItem.sectionId,'"]'))}_isParentSectionIndexCollapsed(state){const parentElement=this.element.parentElement.closest(this.selectors.SECTION);if(!parentElement||!parentElement.dataset.id)return null;return!!state.section.get(parentElement.dataset.id).indexcollapsed}async addOverlay(){this.element.classList.add(this.classes.OVERLAYBORDERS)}removeOverlay(){this.element.classList.remove(this.classes.OVERLAYBORDERS)}}return _exports.default=Component,_exports.default}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_sectiontitle=_interopRequireDefault(_sectiontitle),_dndsection=_interopRequireDefault(_dndsection),_log=_interopRequireDefault(_log);class Component extends _dndsection.default{create(){this.name="courseindex_section",this.selectors={SECTION:"[data-for='section']",SECTION_ITEM:"[data-for='section_item']",SECTION_TITLE:"[data-for='section_title']",CM_LAST:'[data-for="cm"]:last-child',DND_ALLOWED:"[data-courseindexdndallowed='true']"},this.classes={SECTIONHIDDEN:"dimmed",SECTIONCURRENT:"current",LOCKED:"editinprogress",RESTRICTIONS:"restrictions",PAGEITEM:"pageitem",OVERLAYBORDERS:"overlay-preview-borders"},this.id=this.element.dataset.id,this.isPageItem=!1}static init(target,selectors){let element=document.querySelector(target);return element||(_log.default.debug("Init component with id is deprecated, use a query selector instead."),element=document.getElementById(target)),new this({element:element,selectors:selectors})}stateReady(state){var _section$sectionurl;this.configState(state);const sectionItem=this.getElement(this.selectors.SECTION_ITEM);if(this.reactive.isEditing&&this.reactive.supportComponents&&document.querySelector(this.selectors.DND_ALLOWED)){const titleitem=new _sectiontitle.default({...this,element:sectionItem,fullregion:this.element});this.configDragDrop(titleitem)}const section=state.section.get(this.id);window.location.href==(null==section||null===(_section$sectionurl=section.sectionurl)||void 0===_section$sectionurl?void 0:_section$sectionurl.replace(/&amp;/g,"&"))&&(this.reactive.dispatch("setPageItem","section",this.id),sectionItem.scrollIntoView())}getWatchers(){return[{watch:"section[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"section[".concat(this.id,"]:updated"),handler:this._refreshSection},{watch:"course.pageItem:updated",handler:this._refreshPageItem}]}getLastCm(){return this.getElement(this.selectors.CM_LAST)}_refreshSection(_ref){var _element$hasrestricti,_element$dragging,_element$locked;let{element:element}=_ref;const sectionItem=this.getElement(this.selectors.SECTION_ITEM);sectionItem.classList.toggle(this.classes.SECTIONHIDDEN,!element.visible),sectionItem.classList.toggle(this.classes.RESTRICTIONS,null!==(_element$hasrestricti=element.hasrestrictions)&&void 0!==_element$hasrestricti&&_element$hasrestricti),this.element.classList.toggle(this.classes.SECTIONCURRENT,element.current),this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.locked=element.locked,this.getElement(this.selectors.SECTION_TITLE).innerHTML=element.title}_refreshPageItem(_ref2){var _element$pageItem,_this$pageItem;let{element:element,state:state}=_ref2;if(!element.pageItem)return;const containsPageItem=this._isPageItemInThisSection(element.pageItem);if(!containsPageItem||this._isParentSectionIndexCollapsed(state))return this.pageItem=!1,void this.getElement(this.selectors.SECTION_ITEM).classList.remove(this.classes.PAGEITEM);!state.section.get(this.id).indexcollapsed||null!==(_element$pageItem=element.pageItem)&&void 0!==_element$pageItem&&_element$pageItem.isStatic?this.pageItem="section"==element.pageItem.type&&element.pageItem.id==this.id:this.pageItem=containsPageItem;this.getElement(this.selectors.SECTION_ITEM).classList.toggle(this.classes.PAGEITEM,null!==(_this$pageItem=this.pageItem)&&void 0!==_this$pageItem&&_this$pageItem),this.pageItem&&!this.reactive.isEditing&&this.element.scrollIntoView({block:"nearest"})}_isPageItemInThisSection(pageItem){if(pageItem.sectionId==this.id)return!0;return!!this.element.querySelector("".concat(this.selectors.SECTION,'[data-id="').concat(pageItem.sectionId,'"]'))}_isParentSectionIndexCollapsed(state){const parentElement=this.element.parentElement.closest(this.selectors.SECTION);if(!parentElement||!parentElement.dataset.id)return null;return!!state.section.get(parentElement.dataset.id).indexcollapsed}async addOverlay(){this.element.classList.add(this.classes.OVERLAYBORDERS)}removeOverlay(){this.element.classList.remove(this.classes.OVERLAYBORDERS)}}return _exports.default=Component,_exports.default}));
//# sourceMappingURL=section.min.js.map
File diff suppressed because one or more lines are too long
@@ -124,7 +124,7 @@ export default class {
sectioninfo: false, // Init to false to prevent mustache recursion loops.
};
if (cminfo.hasdelegatedsection) {
const sectioninfo = state.section.get(cminfo.delegatesectionid);
const sectioninfo = state.section.get(cminfo.delegatesectionid) ?? {};
cm.sectioninfo = this.section(state, sectioninfo);
}
return cm;
@@ -279,20 +279,22 @@ export default class {
if (cmInfo.hasdelegatedsection) {
const items = [];
const delegatedsection = state.section.get(cmInfo.delegatesectionid);
items.push({
type: 'section',
id: delegatedsection.id,
url: delegatedsection.sectionurl
});
const delegatedCmlist = delegatedsection.cmlist ?? [];
delegatedCmlist.forEach(cmid => {
const cmInfo = state.cm.get(cmid);
if (delegatedsection) {
items.push({
type: 'cm',
id: cmInfo.id,
url: cmInfo.url
type: 'section',
id: delegatedsection.id,
url: delegatedsection.sectionurl
});
});
const delegatedCmlist = delegatedsection.cmlist ?? [];
delegatedCmlist.forEach(cmid => {
const cmInfo = state.cm.get(cmid);
items.push({
type: 'cm',
id: cmInfo.id,
url: cmInfo.url
});
});
}
return items;
}
@@ -100,7 +100,7 @@ export default class Component extends DndSection {
// Check if the current url is the section url.
const section = state.section.get(this.id);
// Sections urls may be null.
if (window.location.href == section.sectionurl?.replace(/&amp;/g, "&")) {
if (window.location.href == section?.sectionurl?.replace(/&amp;/g, "&")) {
this.reactive.dispatch('setPageItem', 'section', this.id);
sectionItem.scrollIntoView();
}
@@ -84,7 +84,7 @@ class header implements named_templatable, renderable {
} else {
if (is_null($format->get_sectionid()) || $format->get_sectionid() != $section->id) {
// All sections are displayed.
if (!$data->editing) {
if (!$data->editing && $section->uservisible) {
$data->title = $output->section_title($section, $course);
} else {
$data->title = $output->section_title_without_link($section, $course);
@@ -83,7 +83,6 @@ class section implements renderable {
'rawtitle' => $section->name,
'cmlist' => [],
'visible' => !empty($section->visible),
'sectionurl' => course_get_url($course, $section->section, ['navigation' => true])?->out(false),
'current' => $format->is_section_current($section),
'indexcollapsed' => $indexcollapsed,
'contentcollapsed' => $contentcollapsed,
@@ -94,6 +93,10 @@ class section implements renderable {
'parentsectionid' => $section->get_component_instance()?->get_parent_section()?->id,
];
if ($section->uservisible) {
$data->sectionurl = course_get_url($course, $section->section, ['navigation' => true])?->out(false);
}
if (empty($modinfo->sections[$section->section])) {
return $data;
}
@@ -73,7 +73,8 @@
class="collapse-list-link icons-collapse-expand collapsed"
>
<span class="collapsed-icon icon-no-margin me-1" data-bs-toggle="tooltip" title="{{#str}} expand, core {{/str}}">
{{#pix}} t/collapsedchevron, core {{/pix}}
<span class="dir-rtl-hide">{{#pix}} t/collapsedchevron, core {{/pix}}</span>
<span class="dir-ltr-hide">{{#pix}} t/collapsedchevron_rtl, core {{/pix}}</span>
<span class="visually-hidden">{{#str}} expand, core {{/str}}</span>
</span>
<span class="expanded-icon icon-no-margin me-1" data-bs-toggle="tooltip" title="{{#str}} collapse, core {{/str}}">
@@ -1529,7 +1529,7 @@ final class stateactions_test extends \advanced_testcase {
$modinfo = get_fast_modinfo($course); // Get refreshed version.
$allsections = $modinfo->get_listed_section_info_all();
$cmidstoref = array_flip($references);
$cmidstoref = array_flip(array_filter($references, fn($key) => str_starts_with($key, 'cm'), ARRAY_FILTER_USE_KEY));
foreach ($allsections as $sectioninfo) {
$sectionkey = 'section' . $sectioninfo->sectionnum;
$coursetree[$sectionkey] = [];
@@ -17,6 +17,7 @@ Feature: Restrict activities availability
And the following "activities" exist:
| activity | course | name |
| assign | C1 | Test assign name |
And I enable "bigbluebuttonbn" "mod" plugin
Scenario: Activities can be added with the default permissions
Given I log in as "teacher1"
@@ -27,17 +28,34 @@ Feature: Restrict activities availability
And I should see "Test assign name"
@javascript @skip_chrome_zerosize
Scenario: Activities can not be added when the admin restricts the permissions
Scenario Outline: Activities can not be added when the admin restricts the permissions
Given the following "role capability" exists:
| role | editingteacher |
| mod/assign:addinstance | prohibit |
And I log in as "admin"
And I am on the "Course 1" "permissions" page
And I override the system permissions of "Teacher" role with:
| mod/glossary:addinstance | Prohibit |
And I log out
| role | editingteacher |
| <activityrole> | prohibit |
And I log in as "teacher1"
When I am on "Course 1" course homepage with editing mode on
And I open the activity chooser
Then "Add a new Assignment" "link" should not exist in the "Add an activity or resource" "dialogue"
Then "Add a new Glossary" "link" should not exist in the "Add an activity or resource" "dialogue"
Then "Add a new <activitytype>" "link" should not exist in the "Add an activity or resource" "dialogue"
Examples:
| activityrole | activitytype |
| mod/assign:addinstance | Assignment |
| mod/bigbluebuttonbn:addinstance | BigBlueButton |
| mod/book:addinstance | Book |
| mod/choice:addinstance | Choice |
| mod/data:addinstance | Database |
| mod/feedback:addinstance | Feedback |
| mod/folder:addinstance | Folder |
| mod/forum:addinstance | Forum |
| mod/glossary:addinstance | Glossary |
| mod/h5pactivity:addinstance | H5P |
| mod/imscp:addinstance | IMS content package |
| mod/label:addinstance | Text and media area |
| mod/lesson:addinstance | Lesson |
| mod/page:addinstance | Page |
| mod/quiz:addinstance | Quiz |
| mod/resource:addinstance | File |
| mod/scorm:addinstance | SCORM package |
| mod/url:addinstance | URL |
| mod/wiki:addinstance | Wiki |
| mod/workshop:addinstance | Workshop |
@@ -77,9 +77,12 @@ final class course_request_test extends \advanced_testcase {
public function test_approve_request(): void {
global $DB;
$this->resetAfterTest(true);
$this->preventResetByRollback();
$this->mock_clock_with_frozen(1622502000);
$defaultcategory = $DB->get_field_select('course_categories', "MIN(id)", "parent=0");
set_config('enablecourserequests', 1);
set_config('lockrequestcategory', 1);
@@ -116,6 +119,7 @@ final class course_request_test extends \advanced_testcase {
$id = $cr->approve();
$this->assertCount(1, $sink->get_messages_by_component_and_type('core', 'courserequestapproved'));
$sink->close();
$course = $DB->get_record('course', ['id' => $id]);
$this->assertEquals($data->fullname, $course->fullname);
$this->assertEquals($data->shortname, $course->shortname);
@@ -136,8 +140,49 @@ final class course_request_test extends \advanced_testcase {
$id = $cr->approve();
$this->assertCount(1, $sink->get_messages_by_component_and_type('core', 'courserequestapproved'));
$sink->close();
$course = $DB->get_record('course', ['id' => $id]);
$this->assertEquals($data->category, $course->category);
// Test with default course settings.
set_config('format', 'weeks', 'moodlecourse');
set_config('newsitems', 2, 'moodlecourse');
set_config('showgrades', false, 'moodlecourse');
set_config('showreports', true, 'moodlecourse');
set_config('showactivitydates', 0, 'moodlecourse');
set_config('maxbytes', 10240, 'moodlecourse');
set_config('groupmode', SEPARATEGROUPS, 'moodlecourse');
set_config('groupmodeforce', true, 'moodlecourse');
set_config('visible', false, 'moodlecourse');
set_config('lang', 'es', 'moodlecourse');
set_config('enablecompletion', false, 'moodlecourse');
set_config('courseenddateenabled', true, 'moodlecourse');
set_config('courseduration', WEEKSECS * 4, 'moodlecourse');
$data->shortname .= ' 3rd';
$this->setUser($requester);
$cr = course_request::create($data);
$this->setAdminUser();
$sink = $this->redirectMessages();
$id = $cr->approve();
$this->assertCount(1, $sink->get_messages_by_component_and_type('core', 'courserequestapproved'));
$sink->close();
$course = $DB->get_record('course', ['id' => $id]);
$this->assertEquals('weeks', $course->format);
$this->assertEquals(2, $course->newsitems);
$this->assertEquals(0, $course->showgrades);
$this->assertEquals(1, $course->showreports);
$this->assertEquals(0, $course->showactivitydates);
$this->assertEquals(10240, $course->maxbytes);
$this->assertEquals(SEPARATEGROUPS, $course->groupmode);
$this->assertEquals(1, $course->groupmodeforce);
$this->assertEquals(0, $course->visible);
$this->assertEquals('es', $course->lang);
$this->assertEquals(0, $course->enablecompletion);
$this->assertEquals(1622476800, $course->startdate);
$this->assertEquals($course->startdate + (WEEKSECS * 4), $course->enddate);
}
public function test_reject_request(): void {
+8
View File
@@ -1,5 +1,13 @@
# core_customfield (subsystem) Upgrade notes
## 5.1.2
### Added
- Added new `\core_customfield\api::is_shortname_unique(...)` method to determine whether a shortname is available for use inside a given handler
For more information see [MDL-87059](https://tracker.moodle.org/browse/MDL-87059)
## 5.1
### Changed
+56
View File
@@ -487,4 +487,60 @@ class api {
];
return shared::record_exists_select($sql, $params);
}
/**
* Checks if the given shortname is unique for the handler's component-area-itemid combination and shared fields.
*
* @param handler $handler
* @param string $shortname
* @param int $fieldid
* @return bool
*/
public static function is_shortname_unique(handler $handler, string $shortname, int $fieldid): bool {
global $DB;
if ($handler->get_component() === 'core_customfield' && $handler->get_area() === 'shared') {
// If it's a shared field, just check the shortname is unique among all fields.
$params = [
'shortname' => $shortname,
'fieldid' => $fieldid,
];
return !$DB->record_exists_select('customfield_field', 'shortname = :shortname AND id <> :fieldid', $params);
} else {
// Check the shortname is unique for this component-area-itemid combination and shared fields.
$query = "
SELECT 1
FROM {customfield_field} f
JOIN {customfield_category} c ON c.id = f.categoryid
WHERE f.shortname = :shortname
AND (
(
f.id <> :fieldid
AND c.component = :component
AND c.area = :area
AND c.itemid = :itemid
)
OR
(
c.component = :sharedcomponent
AND c.area = :sharedarea
AND c.itemid = :shareditemid
)
)
";
$params = [
'shortname' => $shortname,
'fieldid' => $fieldid,
'component' => $handler->get_component(),
'area' => $handler->get_area(),
'itemid' => $handler->get_itemid(),
'sharedcomponent' => 'core_customfield',
'sharedarea' => 'shared',
'shareditemid' => 0,
];
return !$DB->record_exists_sql($query, $params);
}
}
}
@@ -118,15 +118,11 @@ class field_config_form extends \core_form\dynamic_form {
$field = $this->get_field();
$handler = $field->get_handler();
// Check the shortname is specified and is unique for this component-area-itemid combination.
// Check the shortname is specified and is unique for this component-area-itemid combination and shared fields.
if (!preg_match('/^[a-z0-9_]+$/', $data['shortname'])) {
// Check allowed pattern (numbers, letters and underscore).
$errors['shortname'] = get_string('invalidshortnameerror', 'core_customfield');
} else if ($DB->record_exists_sql('SELECT 1 FROM {customfield_field} f ' .
'JOIN {customfield_category} c ON c.id = f.categoryid ' .
'WHERE f.shortname = ? AND f.id <> ? AND c.component = ? AND c.area = ? AND c.itemid = ?',
[$data['shortname'], $data['id'],
$handler->get_component(), $handler->get_area(), $handler->get_itemid()])) {
} else if (!\core_customfield\api::is_shortname_unique($handler, $data['shortname'], $field->get('id'))) {
$errors['shortname'] = get_string('formfieldcheckshortname', 'core_customfield');
}
+8 -1
View File
@@ -447,7 +447,13 @@ abstract class handler {
* In the last case data_controller::get_value() and export_value() functions will return default values.
*/
public function get_instances_data(array $instanceids, bool $returnall = false): array {
$result = api::get_instances_fields_data($this->get_fields(), $instanceids);
$result = api::get_instances_fields_data(
fields: $this->get_fields(),
instanceids: $instanceids,
component: $this->get_component(),
area: $this->get_area(),
itemid: $this->get_itemid(),
);
if (!$returnall) {
// Filter only by visible fields (list of visible fields may be different for each instance).
@@ -706,6 +712,7 @@ abstract class handler {
}
$data->instance_form_save($instance);
}
$this->clear_configuration_cache();
}
/**
@@ -1,5 +1,17 @@
# customfield_number Upgrade notes
## 5.1.2
### Changed
- In order to fully support shared custom field categories, additional component/area/itemid parameters have been added to the following:
* The `customfield_number_recalculate_value` external method
* The abstract `\customfield_number\provider_base::recalculate()` method
* The `\customfield_number\task\recalculate` helpers for queueing task instances
For more information see [MDL-87714](https://tracker.moodle.org/browse/MDL-87714)
## 4.5
### Added
@@ -6,6 +6,6 @@ define("customfield_number/recalculate",["exports","core/ajax","core/notificatio
* @author 2024 Marina Glancy
* @copyright 2024 Moodle Pty Ltd <support@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=function(){if(initialised)return;document.addEventListener("click",(e=>{const target=e.target.closest(SELECTORS_wrapper+" "+SELECTORS_link);if(!target)return;const el=target.closest(SELECTORS_wrapper).querySelector(SELECTORS_value);if(!el)return;e.preventDefault();const{fieldid:fieldid,instanceid:instanceid}=target.dataset,pendingPromise=new _pending.default("recalculate_customfield_number");(0,_loadingicon.addIconToContainer)(el).then((()=>_ajax.default.call([{methodname:"customfield_number_recalculate_value",args:{fieldid:fieldid,instanceid:instanceid}}])[0])).then((data=>(el.innerHTML=data.value,pendingPromise.resolve()))).catch(_notification.default.exception)})),initialised=!0},_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);const SELECTORS_wrapper='[data-fieldtype="wrapper"]',SELECTORS_value='[data-fieldtype="value"]',SELECTORS_link='[data-fieldtype="link"]';let initialised=!1}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=function(){if(initialised)return;document.addEventListener("click",(e=>{const target=e.target.closest(SELECTORS_wrapper+" "+SELECTORS_link);if(!target)return;const el=target.closest(SELECTORS_wrapper).querySelector(SELECTORS_value);if(!el)return;e.preventDefault();const{fieldid:fieldid,instanceid:instanceid,component:component,area:area,itemid:itemid}=target.dataset,pendingPromise=new _pending.default("recalculate_customfield_number");(0,_loadingicon.addIconToContainer)(el).then((()=>_ajax.default.call([{methodname:"customfield_number_recalculate_value",args:{fieldid:fieldid,instanceid:instanceid,component:component,area:area,itemid:itemid}}])[0])).then((data=>(el.innerHTML=data.value,pendingPromise.resolve()))).catch(_notification.default.exception)})),initialised=!0},_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);const SELECTORS_wrapper='[data-fieldtype="wrapper"]',SELECTORS_value='[data-fieldtype="value"]',SELECTORS_link='[data-fieldtype="link"]';let initialised=!1}));
//# sourceMappingURL=recalculate.min.js.map
@@ -1 +1 @@
{"version":3,"file":"recalculate.min.js","sources":["../src/recalculate.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 * Allows to recalculate a single value on demand\n *\n * @module customfield_number/recalculate\n * @author 2024 Marina Glancy\n * @copyright 2024 Moodle Pty Ltd <support@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\nimport {addIconToContainer} from 'core/loadingicon';\nimport Pending from 'core/pending';\n\nconst SELECTORS = {\n wrapper: '[data-fieldtype=\"wrapper\"]',\n value: '[data-fieldtype=\"value\"]',\n link: '[data-fieldtype=\"link\"]',\n};\n\nlet initialised = false;\n\n/**\n * Init\n */\nexport function init() {\n if (initialised) {\n return;\n }\n\n document.addEventListener('click', (e) => {\n const target = e.target.closest(SELECTORS.wrapper + \" \" + SELECTORS.link);\n if (!target) {\n return;\n }\n const el = target.closest(SELECTORS.wrapper).querySelector(SELECTORS.value);\n if (!el) {\n return;\n }\n e.preventDefault();\n\n const {fieldid, instanceid} = target.dataset;\n\n const pendingPromise = new Pending('recalculate_customfield_number');\n addIconToContainer(el).then(() => {\n return Ajax.call([{\n methodname: 'customfield_number_recalculate_value',\n args: {fieldid, instanceid}\n }])[0];\n }).then((data) => {\n el.innerHTML = data.value;\n return pendingPromise.resolve();\n }).catch(Notification.exception);\n });\n\n initialised = true;\n}\n"],"names":["initialised","document","addEventListener","e","target","closest","SELECTORS","el","querySelector","preventDefault","fieldid","instanceid","dataset","pendingPromise","Pending","then","Ajax","call","methodname","args","data","innerHTML","value","resolve","catch","Notification","exception"],"mappings":";;;;;;;;yFAyCQA,mBAIJC,SAASC,iBAAiB,SAAUC,UAC1BC,OAASD,EAAEC,OAAOC,QAAQC,kBAAoB,IAAMA,oBACrDF,oBAGCG,GAAKH,OAAOC,QAAQC,mBAAmBE,cAAcF,qBACtDC,UAGLJ,EAAEM,uBAEIC,QAACA,QAADC,WAAUA,YAAcP,OAAOQ,QAE/BC,eAAiB,IAAIC,iBAAQ,sEAChBP,IAAIQ,MAAK,IACjBC,cAAKC,KAAK,CAAC,CACdC,WAAY,uCACZC,KAAM,CAACT,QAAAA,QAASC,WAAAA,eAChB,KACLI,MAAMK,OACLb,GAAGc,UAAYD,KAAKE,MACbT,eAAeU,aACvBC,MAAMC,sBAAaC,cAG1B1B,aAAc,2IAzCZM,kBACO,6BADPA,gBAEK,2BAFLA,eAGI,8BAGNN,aAAc"}
{"version":3,"file":"recalculate.min.js","sources":["../src/recalculate.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 * Allows to recalculate a single value on demand\n *\n * @module customfield_number/recalculate\n * @author 2024 Marina Glancy\n * @copyright 2024 Moodle Pty Ltd <support@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\nimport {addIconToContainer} from 'core/loadingicon';\nimport Pending from 'core/pending';\n\nconst SELECTORS = {\n wrapper: '[data-fieldtype=\"wrapper\"]',\n value: '[data-fieldtype=\"value\"]',\n link: '[data-fieldtype=\"link\"]',\n};\n\nlet initialised = false;\n\n/**\n * Init\n */\nexport function init() {\n if (initialised) {\n return;\n }\n\n document.addEventListener('click', (e) => {\n const target = e.target.closest(SELECTORS.wrapper + \" \" + SELECTORS.link);\n if (!target) {\n return;\n }\n const el = target.closest(SELECTORS.wrapper).querySelector(SELECTORS.value);\n if (!el) {\n return;\n }\n e.preventDefault();\n\n const {fieldid, instanceid, component, area, itemid} = target.dataset;\n\n const pendingPromise = new Pending('recalculate_customfield_number');\n addIconToContainer(el).then(() => {\n return Ajax.call([{\n methodname: 'customfield_number_recalculate_value',\n args: {fieldid, instanceid, component, area, itemid}\n }])[0];\n }).then((data) => {\n el.innerHTML = data.value;\n return pendingPromise.resolve();\n }).catch(Notification.exception);\n });\n\n initialised = true;\n}\n"],"names":["initialised","document","addEventListener","e","target","closest","SELECTORS","el","querySelector","preventDefault","fieldid","instanceid","component","area","itemid","dataset","pendingPromise","Pending","then","Ajax","call","methodname","args","data","innerHTML","value","resolve","catch","Notification","exception"],"mappings":";;;;;;;;yFAyCQA,mBAIJC,SAASC,iBAAiB,SAAUC,UAC1BC,OAASD,EAAEC,OAAOC,QAAQC,kBAAoB,IAAMA,oBACrDF,oBAGCG,GAAKH,OAAOC,QAAQC,mBAAmBE,cAAcF,qBACtDC,UAGLJ,EAAEM,uBAEIC,QAACA,QAADC,WAAUA,WAAVC,UAAsBA,UAAtBC,KAAiCA,KAAjCC,OAAuCA,QAAUV,OAAOW,QAExDC,eAAiB,IAAIC,iBAAQ,sEAChBV,IAAIW,MAAK,IACjBC,cAAKC,KAAK,CAAC,CACdC,WAAY,uCACZC,KAAM,CAACZ,QAAAA,QAASC,WAAAA,WAAYC,UAAAA,UAAWC,KAAAA,KAAMC,OAAAA,WAC7C,KACLI,MAAMK,OACLhB,GAAGiB,UAAYD,KAAKE,MACbT,eAAeU,aACvBC,MAAMC,sBAAaC,cAG1B7B,aAAc,2IAzCZM,kBACO,6BADPA,gBAEK,2BAFLA,eAGI,8BAGNN,aAAc"}
@@ -54,13 +54,13 @@ export function init() {
}
e.preventDefault();
const {fieldid, instanceid} = target.dataset;
const {fieldid, instanceid, component, area, itemid} = target.dataset;
const pendingPromise = new Pending('recalculate_customfield_number');
addIconToContainer(el).then(() => {
return Ajax.call([{
methodname: 'customfield_number_recalculate_value',
args: {fieldid, instanceid}
args: {fieldid, instanceid, component, area, itemid}
}])[0];
}).then((data) => {
el.innerHTML = data.value;
@@ -52,7 +52,14 @@ class data_controller extends \core_customfield\data_controller {
$field = $this->get_field();
if (!$field->is_editable()) {
$instanceid = (int)$this->get('instanceid');
$data = ['value' => $this->export_value(), 'fieldid' => $field->get('id'), 'instanceid' => $instanceid];
$data = [
'value' => $this->export_value(),
'fieldid' => $field->get('id'),
'instanceid' => $instanceid,
'component' => $this->data->get('component'),
'area' => $this->data->get('area'),
'itemid' => $this->data->get('itemid'),
];
$value = $OUTPUT->render_from_template('customfield_number/staticvalue', $data);
$mform->addElement('static', $elementname . '_static', $this->get_field()->get_formatted_name(),
$value);
@@ -19,6 +19,7 @@ declare(strict_types=1);
namespace customfield_number\external;
use core\exception\invalid_parameter_exception;
use core_customfield\handler;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_api;
@@ -44,6 +45,9 @@ class recalculate extends external_api {
return new external_function_parameters([
'fieldid' => new external_value(PARAM_INT, 'Field id', VALUE_REQUIRED),
'instanceid' => new external_value(PARAM_INT, 'Instance id', VALUE_REQUIRED),
'component' => new external_value(PARAM_COMPONENT, 'Component related to instance id', VALUE_DEFAULT, 'core_course'),
'area' => new external_value(PARAM_AREA, 'Area related to instance id', VALUE_DEFAULT, 'course'),
'itemid' => new external_value(PARAM_INT, 'Item id', VALUE_DEFAULT, 0),
]);
}
@@ -52,16 +56,31 @@ class recalculate extends external_api {
*
* @param int $fieldid
* @param int $instanceid
* @param string $component
* @param string $area
* @param int $itemid
* @return array
*/
public static function execute(int $fieldid, int $instanceid): array {
public static function execute(
int $fieldid,
int $instanceid,
string $component = 'core_course',
string $area = 'course',
int $itemid = 0,
): array {
// Parameter validation.
[
'fieldid' => $fieldid,
'instanceid' => $instanceid,
'component' => $component,
'area' => $area,
'itemid' => $itemid,
] = self::validate_parameters(self::execute_parameters(), [
'fieldid' => $fieldid,
'instanceid' => $instanceid,
'component' => $component,
'area' => $area,
'itemid' => $itemid,
]);
// Access validation.
@@ -70,19 +89,25 @@ class recalculate extends external_api {
$field = \core_customfield\field_controller::create($fieldid);
$provider = provider_base::instance($field);
if (!$provider) {
if (!$provider || !$provider->is_available()) {
throw new invalid_parameter_exception('Invalid parameter');
}
$handler = $field->get_handler();
$handler = handler::get_handler($component, $area, $itemid);
if (!$handler->can_edit($field, $instanceid)) {
throw new \moodle_exception('nopermissions', '', '', get_string('update'));
}
$provider->recalculate($instanceid);
$provider->recalculate($instanceid, $component, $area, $itemid);
$data = $handler->get_instance_fields_data(
[$fieldid => $field], $instanceid)[$fieldid];
$datas = \core_customfield\api::get_instance_fields_data(
fields: [$fieldid => $field],
instanceid: $instanceid,
component: $component,
area: $area,
itemid: $itemid,
);
$data = $datas[$fieldid];
return ['value' => $data->export_value()];
}
@@ -19,6 +19,7 @@ declare(strict_types=1);
namespace customfield_number\local\numberproviders;
use context_course;
use core\exception\coding_exception;
use core_plugin_manager;
use customfield_number\data_controller;
use customfield_number\provider_base;
@@ -80,9 +81,21 @@ class nofactivities extends provider_base {
* Recalculate the number of activities in the course.
*
* @param int|null $instanceid
* @param string $component
* @param string $area
* @param int $itemid
*/
public function recalculate(?int $instanceid = null): void {
public function recalculate(
?int $instanceid = null,
string $component = 'core_course',
string $area = 'course',
int $itemid = 0,
): void {
global $DB;
if ($component !== 'core_course' || $area !== 'course') {
throw new coding_exception('Can only calculate number of activities for courses');
}
$types = $this->field->get_configdata_property('activitytypes');
$displaywhenzero = $this->field->get_configdata_property('displaywhenzero');
@@ -64,8 +64,16 @@ abstract class provider_base {
* Recalculate field value.
*
* @param int|null $instanceid
* @param string $component
* @param string $area
* @param int $itemid
*/
public function recalculate(?int $instanceid = null): void {
public function recalculate(
?int $instanceid = null,
string $component = 'core_course',
string $area = 'course',
int $itemid = 0,
): void {
}
/**
@@ -67,7 +67,7 @@ class cron extends scheduled_task {
$field = field_controller::create(0, $row, $category);
if ($provider = provider_base::instance($field)) {
if ($provider->is_available()) {
$provider->recalculate();
$provider->recalculate(null, $cat->component, $cat->area, $cat->itemid);
}
}
}
@@ -16,7 +16,6 @@
namespace customfield_number\task;
use coding_exception;
use core\task\adhoc_task;
use core_customfield\field_controller;
use customfield_number\provider_base;
@@ -58,9 +57,15 @@ class recalculate extends adhoc_task {
// Schedule recalculate for each field, checking component, area and the presense of provider.
$instanceid = clean_param($customdata->instanceid ?? null, PARAM_INT);
$component = clean_param($customdata->component ?? 'core_course', PARAM_COMPONENT);
$area = clean_param($customdata->area ?? 'course', PARAM_AREA);
$itemid = clean_param($customdata->itemid ?? 0, PARAM_INT);
foreach ($fields as $field) {
if ($this->field_is_scheduled($field) && ($provider = provider_base::instance($field))) {
$provider->recalculate($instanceid ?: null);
if ($this->field_is_scheduled($field)) {
$provider = provider_base::instance($field);
if ($provider?->is_available()) {
$provider->recalculate($instanceid ?: null, $component, $area, $itemid);
}
}
}
}
@@ -87,11 +92,26 @@ class recalculate extends adhoc_task {
*
* @param int $fieldid in of the custom field
* @param int|null $instanceid if specified, only recalculates for the given instance id
* @param string|null $component the component related to the instance
* @param string|null $area the area related to the instance
* @param int $itemid
* @return void
*/
public static function schedule_for_field(int $fieldid, ?int $instanceid = null) {
public static function schedule_for_field(
int $fieldid,
?int $instanceid = null,
?string $component = null,
?string $area = null,
int $itemid = 0,
) {
$task = new static();
$task->set_custom_data(['fieldid' => $fieldid, 'instanceid' => $instanceid]);
$task->set_custom_data([
'fieldid' => $fieldid,
'instanceid' => $instanceid,
'component' => $component,
'area' => $area,
'itemid' => $itemid,
]);
\core\task\manager::queue_adhoc_task($task, true);
}
@@ -102,16 +122,23 @@ class recalculate extends adhoc_task {
* @param string|null $component
* @param string|null $area
* @param int|null $instanceid
* @param int $itemid
* @return void
*/
public static function schedule_for_fieldtype(string $fieldtype, ?string $component = null, ?string $area = null,
?int $instanceid = null) {
public static function schedule_for_fieldtype(
string $fieldtype,
?string $component = null,
?string $area = null,
?int $instanceid = null,
int $itemid = 0,
) {
$task = new static();
$task->set_custom_data([
'fieldtype' => $fieldtype,
'component' => $component,
'area' => $area,
'instanceid' => $instanceid,
'itemid' => $itemid,
]);
\core\task\manager::queue_adhoc_task($task, true);
}
@@ -20,7 +20,10 @@
{
"value": 5,
"instanceid": 4,
"fieldid": 5
"fieldid": 5,
"component": "core_course",
"area": "course",
"itemid": 0
}
}}
<div class="customfield_number-recalculate-wrapper d-flex mt-2" data-fieldtype="wrapper">
@@ -28,7 +31,9 @@
{{#instanceid}}
<div class="customfield_number-recalculate-link ms-2">
<a href="#" data-customfield_number-action="recalculate" data-fieldtype="link"
data-fieldid="{{fieldid}}" data-instanceid="{{instanceid}}">{{#pix}}t/reload, core{{/pix}}{{#str}} update, core {{/str}}</a>
data-fieldid="{{fieldid}}" data-instanceid="{{instanceid}}"
data-component="{{component}}" data-area="{{area}}" data-itemid="{{itemid}}"
>{{#pix}}t/reload, core{{/pix}}{{#str}} update, core {{/str}}</a>
</div>
{{/instanceid}}
</div>
+55
View File
@@ -19,6 +19,7 @@ namespace core_customfield;
/**
* Functional test for class \core_customfield\api
*
* @covers \core_customfield\api
* @package core_customfield
* @category test
* @copyright 2018 Toni Barbera <toni@moodle.com>
@@ -271,4 +272,58 @@ final class api_test extends \advanced_testcase {
$this->assertCount(6, $DB->get_records_select(\core_customfield\field::TABLE, 'id '.$sql, $p));
$this->assertCount(6, $DB->get_records_select(\core_customfield\data::TABLE, 'fieldid '.$sql, $p));
}
/**
* Test for function api::is_shortname_unique()
*/
public function test_is_shortname_unique(): void {
$this->resetAfterTest();
$this->setAdminUser();
/** @var \core_customfield_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_customfield');
// Create shared category and fields.
$sharedcategory = $generator->create_category(['component' => 'core_customfield', 'area' => 'shared']);
$generator->create_field([
'categoryid' => $sharedcategory->get('id'),
'name' => 'My shared field',
'shortname' => 'mysharedfield',
'type' => 'text',
]);
$sharedfield2 = $generator->create_field([
'categoryid' => $sharedcategory->get('id'),
'name' => 'My shared field 2',
'shortname' => 'mysharedfield2',
'type' => 'text',
]);
// Create a course category and fields.
$coursecategory = $generator->create_category(['component' => 'core_course', 'area' => 'course']);
$generator->create_field([
'categoryid' => $coursecategory->get('id'),
'name' => 'My course field',
'shortname' => 'mycoursefield',
'type' => 'text',
]);
$coursefield2 = $generator->create_field([
'categoryid' => $coursecategory->get('id'),
'name' => 'My course field 2',
'shortname' => 'mycoursefield2',
'type' => 'text',
]);
$coursehandler = \core_course\customfield\course_handler::create();
$sharedhandler = \core_customfield\customfield\shared_handler::create();
$this->assertTrue(api::is_shortname_unique($coursehandler, 'otherfield', $coursefield2->get('id')));
$this->assertTrue(api::is_shortname_unique($coursehandler, 'mycoursefield2', $coursefield2->get('id')));
$this->assertFalse(api::is_shortname_unique($coursehandler, 'mycoursefield', $coursefield2->get('id')));
$this->assertFalse(api::is_shortname_unique($coursehandler, 'mysharedfield', $coursefield2->get('id')));
$this->assertTrue(api::is_shortname_unique($sharedhandler, 'otherfield', $sharedfield2->get('id')));
$this->assertTrue(api::is_shortname_unique($sharedhandler, 'mysharedfield2', $sharedfield2->get('id')));
$this->assertFalse(api::is_shortname_unique($sharedhandler, 'mycoursefield', $sharedfield2->get('id')));
$this->assertFalse(api::is_shortname_unique($sharedhandler, 'mysharedfield', $sharedfield2->get('id')));
}
}
@@ -6,8 +6,9 @@ Feature: Teachers can edit course custom fields
Background:
Given the following "custom field categories" exist:
| name | component | area | itemid |
| Category for test | core_course | course | 0 |
| name | component | area | itemid |
| Category for test | core_course | course | 0 |
| Shared category | core_customfield | shared | 0 |
And the following "custom fields" exist:
| name | category | type | shortname | description | configdata |
| Field 1 | Category for test | text | f1 | d1 | |
@@ -15,6 +16,7 @@ Feature: Teachers can edit course custom fields
| Field 3 | Category for test | checkbox | f3 | d3 | |
| Field 4 | Category for test | date | f4 | d4 | |
| Field 5 | Category for test | select | f5 | d5 | {"options":"a\nb\nc"} |
| Field 6 | Shared category | text | shf1 | shd1 | |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
@@ -110,5 +112,9 @@ Feature: Teachers can edit course custom fields
And I set the field "Short name" to "f1"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
Then I should see "Short name already exists" in the "Short name" "form_row"
# Let's try to use a shortname that exists in the Shared custom field.
And I set the field "Short name" to "shf1"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
Then I should see "Short name already exists" in the "Short name" "form_row"
And I click on "Cancel" "button" in the "Adding a new Short text" "dialogue"
And I log out
@@ -15,6 +15,37 @@ Feature: Create shared categories and fields
And I wait until the page is ready
And I wait until "Other fields" "text" does not exist
Scenario: Shared custom field short name must be unique across all instance fields
Given the following "custom field categories" exist:
| name | component | area | itemid |
| Category for course | core_course | course | 0 |
| Category for cohort | core_cohort | cohort | 0 |
| Shared category | core_customfield | shared | 0 |
And the following "custom fields" exist:
| name | category | type | shortname | description |
| Field 1 | Category for course | text | f1 | d1 |
| Field 2 | Category for cohort | text | f2 | d2 |
| Field 3 | Shared category | text | shf1 | shd1 |
When I log in as "admin"
And I navigate to "Custom fields > Shared custom fields" in site administration
And I click on "Add a new custom field" "link"
And I click on "Short text" "link"
And I set the following fields to these values:
| Name | Test field |
| Short name | shf1 |
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
Then I should see "Short name already exists" in the "Short name" "form_row"
And I set the field "Short name" to "f1"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
Then I should see "Short name already exists" in the "Short name" "form_row"
And I set the field "Short name" to "f2"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
Then I should see "Short name already exists" in the "Short name" "form_row"
And I set the field "Short name" to "f3"
And I click on "Save changes" "button" in the "Adding a new Short text" "dialogue"
And I should see "Add a new category"
And I should see "f3"
Scenario: Shared customfields are displayed in other entities
Given the following "custom field categories" exist:
| name | component | area | itemid |
+136
View File
@@ -48,4 +48,140 @@ final class handler_test extends advanced_testcase {
$this->expectExceptionMessage('Unable to find handler for custom fields for component core_blimey and area test');
handler::get_handler('core_blimey', 'test');
}
/**
* Test getting instances data
*/
public function test_get_instances_data(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$sharedcustomfieldcategory = $this->getDataGenerator()->create_custom_field_category([
'component' => 'core_customfield',
'area' => 'shared',
]);
$sharedfield = $this->getDataGenerator()->create_custom_field([
'categoryid' => $sharedcustomfieldcategory->get('id'),
'name' => 'Color',
'type' => 'text',
'shortname' => 'color',
]);
// Enable the shared custom field category for both courses and cohorts.
$shared = new shared(0, (object)[
'categoryid' => $sharedcustomfieldcategory->get('id'),
'component' => 'core_course',
'area' => 'course',
'itemid' => 0,
]);
$shared->create();
$shared = new shared(0, (object)[
'categoryid' => $sharedcustomfieldcategory->get('id'),
'component' => 'core_cohort',
'area' => 'cohort',
'itemid' => 0,
]);
$shared->create();
$course1 = $this->getDataGenerator()->create_course();
$course2 = $this->getDataGenerator()->create_course();
$sharedid1 = $course1->id;
$sharedid2 = $course2->id;
// Manually insert cohorts in the database to have both courses and cohorts using the same IDs.
$now = time();
$DB->insert_record_raw('cohort', [
'id' => $sharedid1,
'contextid' => 1,
'name' => 'Cohort 1',
'descriptionformat' => 0,
'visible' => 1,
'component' => '',
'timecreated' => $now,
'timemodified' => $now,
]);
$DB->insert_record_raw('cohort', [
'id' => $sharedid2,
'contextid' => 1,
'name' => 'Cohort 2',
'descriptionformat' => 0,
'visible' => 1,
'component' => '',
'timecreated' => $now,
'timemodified' => $now,
]);
$fieldid = $sharedfield->get('id');
// Manually insert customfield_data records as well.
$DB->insert_record_raw('customfield_data', [
'fieldid' => $fieldid,
'instanceid' => $sharedid1,
'charvalue' => 'orange',
'value' => 'orange',
'valueformat' => 0,
'valuetrust' => 0,
'timecreated' => $now,
'timemodified' => $now,
'component' => 'core_course',
'area' => 'course',
'itemid' => 0,
]);
$DB->insert_record_raw('customfield_data', [
'fieldid' => $fieldid,
'instanceid' => $sharedid2,
'charvalue' => 'red',
'value' => 'red',
'valueformat' => 0,
'valuetrust' => 0,
'timecreated' => $now,
'timemodified' => $now,
'component' => 'core_course',
'area' => 'course',
'itemid' => 0,
]);
$DB->insert_record_raw('customfield_data', [
'fieldid' => $fieldid,
'instanceid' => $sharedid1,
'charvalue' => 'blue',
'value' => 'blue',
'valueformat' => 0,
'valuetrust' => 0,
'timecreated' => $now,
'timemodified' => $now,
'component' => 'core_cohort',
'area' => 'cohort',
'itemid' => 0,
]);
$DB->insert_record_raw('customfield_data', [
'fieldid' => $fieldid,
'instanceid' => $sharedid2,
'charvalue' => 'green',
'value' => 'green',
'valueformat' => 0,
'valuetrust' => 0,
'timecreated' => $now,
'timemodified' => $now,
'component' => 'core_cohort',
'area' => 'cohort',
'itemid' => 0,
]);
// Verify courses retrieve their own custom field values despite sharing IDs with cohorts.
$coursehandler = handler::get_handler('core_course', 'course');
$coursedata = $coursehandler->get_instances_data([$sharedid1, $sharedid2], true);
$this->assertArrayHasKey($sharedid1, $coursedata);
$this->assertArrayHasKey($sharedid2, $coursedata);
$this->assertEquals('orange', $coursedata[$sharedid1][$fieldid]->get_value());
$this->assertEquals('red', $coursedata[$sharedid2][$fieldid]->get_value());
// Verify cohorts retrieve their own custom field values despite sharing IDs with courses.
$cohorthandler = handler::get_handler('core_cohort', 'cohort');
$cohortdata = $cohorthandler->get_instances_data([$sharedid1, $sharedid2], true);
$this->assertArrayHasKey($sharedid1, $cohortdata);
$this->assertArrayHasKey($sharedid2, $cohortdata);
$this->assertEquals('blue', $cohortdata[$sharedid1][$fieldid]->get_value());
$this->assertEquals('green', $cohortdata[$sharedid2][$fieldid]->get_value());
}
}
+6 -2
View File
@@ -58,15 +58,19 @@ function xmldb_enrol_database_upgrade($oldversion) {
// Migrate enrolments where possible.
// First, get the user enrolments that can be migrated.
// Only select the earliest (MIN id) user_enrolments per user to avoid
// duplicate key violations when a user has multiple enrolments across
// duplicate database enrol instances.
$migrateusers = $DB->get_records_sql(
"SELECT ue.id
"SELECT MIN(ue.id) AS id
FROM {user_enrolments} ue
WHERE ue.enrolid $insql
AND NOT EXISTS (
SELECT 1
FROM {user_enrolments} ue2
WHERE ue2.userid = ue.userid
AND ue2.enrolid = :idtokeep)",
AND ue2.enrolid = :idtokeep)
GROUP BY ue.userid",
array_merge($inparams, ['idtokeep' => $idtokeep]),
);
@@ -122,6 +122,7 @@ class sync_tool_grades extends adhoc_task {
mtrace("Skipping - Invalid grade $mtracecontent.");
continue;
}
$grade = floatval($grade); // Grade must be sent as a numeric value, not a string.
if (empty($grademax)) {
mtrace("Skipping - Invalid grademax $mtracecontent.");
+2 -2
View File
@@ -368,7 +368,7 @@ class course_enrolment_table extends html_table implements renderable {
$this->sort = optional_param(self::SORTVAR, self::DEFAULTSORT, PARAM_ALPHANUM);
$this->sortdirection = optional_param(self::SORTDIRECTIONVAR, self::DEFAULTSORTDIRECTION, PARAM_ALPHA);
$this->attributes = array('class' => 'userenrolment table-striped');
$this->attributes = ['class' => 'userenrolment table table-striped table-hover generaltable'];
if (!in_array($this->sort, self::$sortablefields)) {
$this->sort = self::DEFAULTSORT;
}
@@ -654,7 +654,7 @@ class course_enrolment_other_users_table extends course_enrolment_table {
*/
public function __construct(course_enrolment_manager $manager) {
parent::__construct($manager);
$this->attributes = array('class'=>'userenrolment otheruserenrolment');
$this->attributes['class'] .= ' otheruserenrolment';
}
/**
+1 -1
View File
@@ -143,7 +143,7 @@
// Run convert on document (.ps to .gif/.png) or run dvisvgm (.ps to .svg).
if ($background) {
$bg_opt = "-transparent \"$background\""; // Makes transparent background
$bg_opt = '-transparent ' . escapeshellarg($background); // Makes transparent background
} else {
$bg_opt = "";
}
+181 -2
View File
@@ -26,10 +26,22 @@
defined('MOODLE_INTERNAL') || die();
// Default timeout in seconds for mimetex command execution.
defined('FILTER_TEX_MIMETEX_TIMEOUT') || define('FILTER_TEX_MIMETEX_TIMEOUT', 5);
/**
* Check if the current operating system is Windows.
*
* @return bool True if running on Windows, false otherwise.
*/
function filter_tex_is_windows(): bool {
return (PHP_OS == "WINNT") || (PHP_OS == "WIN32") || (PHP_OS == "Windows");
}
function filter_tex_get_executable($debug=false) {
global $CFG;
if ((PHP_OS == "WINNT") || (PHP_OS == "WIN32") || (PHP_OS == "Windows")) {
if (filter_tex_is_windows()) {
return "$CFG->dirroot/filter/tex/mimetex.exe";
}
@@ -134,7 +146,7 @@ function filter_tex_get_cmd($pathname, $texexp) {
$texexp = escapeshellarg($texexp);
$executable = filter_tex_get_executable(false);
if ((PHP_OS == "WINNT") || (PHP_OS == "WIN32") || (PHP_OS == "Windows")) {
if (filter_tex_is_windows()) {
$executable = str_replace(' ', '^ ', $executable);
return "$executable ++ -e \"$pathname\" -- $texexp";
@@ -143,6 +155,173 @@ function filter_tex_get_cmd($pathname, $texexp) {
}
}
/**
* Run mimetex command with a timeout on Windows.
*
* @param string $cmd Command string to execute.
* @param int $timeoutmicros Timeout in microseconds.
* @return array Array with keys: code, timedout, status, errors.
*/
function filter_tex_exec_windows(string $cmd, int $timeoutmicros): array {
// Create temporary file for stderr.
$temperr = tempnam(sys_get_temp_dir(), 'err_');
$descriptors = [
0 => ['file', 'NUL', 'r'], // STDIN.
1 => ['file', 'NUL', 'w'], // STDOUT.
2 => ['file', $temperr, 'w'], // STDERR.
];
$process = proc_open($cmd, $descriptors, $pipes);
if (!is_resource($process)) {
unlink($temperr);
return [
'code' => 127, // Command not found.
'timedout' => false,
'status' => [],
'errors' => '',
];
}
$timedout = false;
while ($timeoutmicros > 0) {
$start = microtime(true);
$status = proc_get_status($process);
if (!$status['running']) {
break;
}
$timeoutmicros -= (microtime(true) - $start) * 1000000;
if ($timeoutmicros <= 0) {
$timedout = true;
$pid = (int)($status['pid'] ?? 0);
exec('taskkill /F /T /PID ' . $pid . ' 2>NUL');
break;
}
usleep(50000); // Sleep for 50ms.
}
$status = proc_get_status($process);
$code = proc_close($process);
// Capture stderr from temp file.
$errors = file_get_contents($temperr);
unlink($temperr);
return [
'code' => $code,
'timedout' => $timedout,
'status' => $status,
'errors' => $errors,
];
}
/**
* Run mimetex command with a timeout on Unix-like systems.
*
* @param string $cmd Command string to execute.
* @param int $timeoutmicros Timeout in microseconds.
* @return array Array with keys: code, timedout, status, errors.
*/
function filter_tex_exec_unix(string $cmd, int $timeoutmicros): array {
// File descriptors passed to the process.
$descriptors = [
0 => ['pipe', 'r'], // STDIN.
1 => ['pipe', 'w'], // STDOUT.
2 => ['pipe', 'w'], // STDERR.
];
$process = proc_open('exec ' . $cmd, $descriptors, $pipes);
if (!is_resource($process)) {
return [
'code' => 127, // Command not found.
'timedout' => false,
'status' => [],
'errors' => '',
];
}
fclose($pipes[0]);
stream_set_blocking($pipes[1], false);
stream_set_blocking($pipes[2], false);
$errors = '';
$timedout = false;
while ($timeoutmicros > 0) {
$start = microtime(true);
$read = [$pipes[1], $pipes[2]];
$other = [];
stream_select($read, $other, $other, 0, (int)$timeoutmicros);
$status = proc_get_status($process);
stream_get_contents($pipes[1]); // Discard stdout to prevent pipe blocking.
$errors .= stream_get_contents($pipes[2]);
if (!$status['running']) {
break;
}
$timeoutmicros -= (microtime(true) - $start) * 1000000;
if ($timeoutmicros <= 0) {
$timedout = true;
proc_terminate($process);
break;
}
usleep(50000); // Sleep for 50ms.
}
// Read any remaining data from pipes.
stream_get_contents($pipes[1]); // Discard remaining stdout.
$errors .= stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$status = proc_get_status($process);
$code = proc_close($process);
return [
'code' => $code,
'timedout' => $timedout,
'status' => $status,
'errors' => $errors,
];
}
/**
* Run mimetex command with a timeout.
*
* @param string $cmd Command string to execute.
* @param int|null &$code Exit code (passed by reference, set by function).
* @return void
*/
function filter_tex_exec(string $cmd, ?int &$code): void {
$timeoutmicros = FILTER_TEX_MIMETEX_TIMEOUT * 1000000;
if (filter_tex_is_windows()) {
$result = filter_tex_exec_windows($cmd, $timeoutmicros);
} else {
$result = filter_tex_exec_unix($cmd, $timeoutmicros);
}
if ($result['errors']) {
debugging('filter_tex_exec errors: ' . $result['errors'], DEBUG_DEVELOPER);
}
if ($result['timedout']) {
$code = 124;
} else if ($result['code'] === -1 && isset($result['status']['exitcode']) && $result['status']['exitcode'] !== -1) {
$code = $result['status']['exitcode'];
} else {
$code = $result['code'];
}
}
/**
* Purge all caches when settings changed.
*/
+1 -2
View File
@@ -18,7 +18,6 @@ define('NO_MOODLE_COOKIES', true); // Because it interferes with caching
require_once($CFG->dirroot.'/filter/tex/latex.php');
$cmd = ''; // Initialise these variables
$status = '';
$relativepath = get_file_argument();
@@ -58,7 +57,7 @@ define('NO_MOODLE_COOKIES', true); // Because it interferes with caching
$texexp = preg_replace('!\r\n?!', ' ', $texexp);
$texexp = '\Large '.$texexp;
$cmd = filter_tex_get_cmd($pathname, $texexp);
system($cmd, $status);
filter_tex_exec($cmd, $status);
}
}
}
+1 -1
View File
@@ -33,7 +33,7 @@ if ($ADMIN->fulltree) {
$items[] = new admin_setting_heading('filter_tex/latexheading', get_string('latexsettings', 'filter_tex'), '');
$items[] = new admin_setting_configtextarea('filter_tex/latexpreamble', get_string('latexpreamble','filter_tex'),
'', "\\usepackage[latin1]{inputenc}\n\\usepackage{amsmath}\n\\usepackage{amsfonts}\n\\RequirePackage{amsmath,amssymb,latexsym}\n");
$items[] = new admin_setting_configtext('filter_tex/latexbackground', get_string('backgroundcolour', 'admin'), '', '#FFFFFF');
$items[] = new admin_setting_configcolourpicker('filter_tex/latexbackground', get_string('backgroundcolour', 'admin'), '', '#FFFFFF');
$items[] = new admin_setting_configtext('filter_tex/density', get_string('density', 'admin'), '', '120', PARAM_INT);
$default_filter_tex_pathlatex = '';
+5 -1
View File
@@ -153,7 +153,7 @@
$texexp = '\Large '.$texexp;
$commandpath = filter_tex_get_executable(true);
$cmd = filter_tex_get_cmd($pathname, $texexp);
system($cmd, $status);
filter_tex_exec($cmd, $status);
if ($return) {
return $image;
@@ -172,6 +172,10 @@
echo "Status corresponds to bus error<br />\n";
} else if ($status == 22) {
echo "Status corresponds to abnormal termination<br />\n";
} else if ($status == 124) {
echo "Status corresponds to timeout<br />\n";
} else if ($status == 127) {
echo "Status corresponds to command not found<br />\n";
}
if (file_exists($commandpath)) {
echo "File size of mimetex executable $commandpath is " . filesize($commandpath) . "<br />";
@@ -97,6 +97,55 @@ class behat_gradingform_guide extends behat_base {
}
}
/**
* Edits an existing marking guide with the provided data.
*
* This method edits the marking guide of the marking guide definition
* form; the provided TableNode should contain one row for
* each field and each cell of the row should contain:
* | Field name | New value |
* | shortname | Updated Grade criterion |
*
* @When /^I edit the marking guide criterion "([^"]*)" with the following values:$/
* @param string $criterionname
* @param TableNode $fields
*/
public function i_edit_the_marking_guide_criterion_with_the_following_values(string $criterionname, TableNode $fields) {
if ($fieldvalues = $fields->getHash()) {
$criterionid = 0;
$locator = "//tr[contains(@class, 'criterion')]//div[@class='criterionname']"
. "//span[@class='textvalue'][text()='$criterionname']/ancestor::tr";
if ($criterionrow = $this->find('xpath', $locator)) {
$criterionid = str_replace('guide-criteria-', '', $criterionrow->getAttribute('id'));
}
if ($criterionid) {
$criterionroot = 'guide[criteria]' . '[' . $criterionid . ']';
foreach ($fieldvalues as $fieldvalue) {
// Make sure the fieldvalue array has 2 elements.
if (count($fieldvalue) != 2) {
throw new ExpectationException(
'The field definition should contain field name and new value. ' .
'Please follow this format: | Field name | New value |',
$this->getSession()
);
}
$fieldname = $fieldvalue['Field name'];
$newvalue = $fieldvalue['New value'];
$this->set_guide_field_value($criterionroot . "[$fieldname]", $newvalue);
}
} else {
throw new ExpectationException(
'Criterion with name "' . $criterionname . '" not found.',
$this->getSession()
);
}
}
}
/**
* Defines the marking guide with the provided data, following marking guide's definition grid cells.
*
@@ -0,0 +1,86 @@
@gradingform @gradingform_guide
Feature: Editing a marking guide already used for grading updates regrade state and student visible grades
In order to update marking guide details
As a teacher
I need to be able to grade a submission
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | One | teacher1@example.com |
| student1 | Student | One | student1@example.com |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "activities" exist:
| activity | course | name | advancedgradingmethod_submissions | assignsubmission_onlinetext_enabled |
| assign | C1 | Assign 1 | guide | 1 |
And the following "mod_assign > submissions" exist:
| assign | user | onlinetext |
| Assign 1 | student1 | Assign 1 submission |
And I am on the "Course 1" course page logged in as teacher1
And I go to "Assign 1" advanced grading definition page
And I set the following fields to these values:
| Name | Assign 1 marking guide |
| Description | Marking guide description |
And I define the following marking guide:
| Criterion name | Description for students | Description for markers | Maximum score |
| Grade criterion A | Grade 1 description for students | Grade 1 description for markers | 70 |
| Grade criterion B | Grade 2 description for students | Grade 2 description for markers | 30 |
And I press "Save marking guide and make it ready"
And I navigate to "Assignment" in current page administration
And I go to "Student One" "Assign 1" activity advanced grading page
And I grade by filling the marking guide with:
| Grade criterion A | 25 | Needs improvement |
| Grade criterion B | 20 | Excellent! |
And I press "Save changes"
And I am on "Course 1" course homepage
And I go to "Assign 1" advanced grading definition page
And I edit the marking guide criterion "Grade criterion A" with the following values:
| Field name | New value |
| shortname | Updated Grade criterion A |
| description | Updated description for students |
| descriptionmarkers | Updated description for markers |
| maxscore | 60 |
And I press "Save"
@javascript
Scenario: Teacher edits a used marking guide and mark it for regrade and student sees breakdown only after teacher regrades
# Set the marking guide to be "Mark for regrade".
Given I set the field "menuguideregrade" to "Mark for regrade"
And I should see "You are about to save changes to a marking guide that has already been used for grading. Please indicate if existing grades need to be reviewed. If you set this then the marking guide will be hidden from students until their item is regraded."
And I click on "Continue" "button"
And I am on the "Assign 1" "assign activity" page logged in as student1
# Student should not see the grade breakdown as the activity is marked for regrade.
And I should not see "Grade breakdown"
When I am on the "Assign 1" "assign activity" page logged in as teacher1
And I go to "Student One" "Assign 1" activity advanced grading page
And I grade by filling the marking guide with:
| Updated Grade criterion A | 50 | I changed my mind |
| Grade criterion B | 30 | It's all good now |
And I press "Save changes"
And I am on the "Assign 1" "assign activity" page logged in as student1
# Now the student should see the updated grade breakdown.
Then I should see "Grade breakdown"
And I should see the marking guide information displayed as:
| criteria | description | remark | maxscore | criteriascore |
| Updated Grade criterion A | Updated description for students | I changed my mind | 60 | 50 / 60 |
| Grade criterion B | Grade 2 description for students | It's all good now | 30 | 30 / 30 |
@javascript
Scenario: Teacher edits a used marking guide does not mark for regrade existing student grades remain visible and unchanged
# Set the marking guide to be "Do not mark for regrade".
Given I set the field "menuguideregrade" to "Do not mark for regrade"
And I should see "You are about to save changes to a marking guide that has already been used for grading. Please indicate if existing grades need to be reviewed. If you set this then the marking guide will be hidden from students until their item is regraded."
And I click on "Continue" "button"
When I am on the "Assign 1" "assign activity" page logged in as student1
# Student should see the updated grade breakdown as the activity was not marked for regrading.
Then I should see "Grade breakdown"
And I should see the marking guide information displayed as:
| criteria | description | remark | maxscore | criteriascore |
| Updated Grade criterion A | Updated description for students | Needs improvement | 60 | 25 / 60 |
| Grade criterion B | Grade 2 description for students | Excellent! | 30 | 20 / 30 |
+1 -1
View File
@@ -1297,7 +1297,7 @@ class grade_report_grader extends grade_report {
$html = '';
$fulltable = new html_table();
$fulltable->attributes['class'] = 'gradereport-grader-table d-none';
$fulltable->attributes['class'] = 'table gradereport-grader-table d-none';
$fulltable->id = 'user-grades';
$fulltable->caption = get_string('summarygrader', 'gradereport_grader');
$fulltable->captionhide = true;
@@ -26,7 +26,7 @@
"category": "Hitchhikers grade category"
}
}}
<li class="w-100 result-row form-check mb-1">
<li class="w-100 result-row form-check flex-shrink-0 mb-1">
<input class="form-check-input" data-collapse="{{name}}" type="checkbox" value="" id="check-collapse-{{name}}">
<label for="check-collapse-{{name}}" class="selected-option-info d-block pe-3 text-truncate form-check-label">
<span class="selected-option-text w-100 p-0">
@@ -88,7 +88,7 @@ abstract class screen {
protected $items;
/** @var int Maximum number of students that can be shown on one page */
protected static $maxperpage = 5000;
protected static $maxperpage = 1000;
/**
* List of allowed values for 'perpage' setting
@@ -129,7 +129,7 @@ abstract class screen {
$cache = \cache::make_from_params(\cache_store::MODE_SESSION, 'gradereport_singleview', 'perpage');
$perpage = optional_param('perpage', null, PARAM_INT);
if (!in_array($perpage, self::$validperpage) && ($perpage !== 0)) {
if ((!in_array($perpage, self::$validperpage) && $perpage != self::$maxperpage) && ($perpage !== 0)) {
// Get from cache.
$perpage = $cache->get(get_class($this));
} else {
@@ -435,7 +435,7 @@ abstract class screen {
sort($pagingoptions);
$pagingoptions = array_combine($pagingoptions, $pagingoptions);
if ($numusers > self::$maxperpage) {
$pagingoptions['0'] = self::$maxperpage;
$pagingoptions[self::$maxperpage] = self::$maxperpage;
} else {
$pagingoptions['0'] = get_string('all');
}
@@ -455,6 +455,6 @@ abstract class screen {
// The number of students per page is always limited even if it is claimed to be unlimited.
$this->perpage = $this->perpage ?: self::$maxperpage;
$perpagedata['pagingbar'] = $this->pager();
return $OUTPUT->render_from_template('gradereport_singleview/perpage', $perpagedata);;
return $OUTPUT->render_from_template('gradereport_singleview/perpage', $perpagedata);
}
}
+4
View File
@@ -29,6 +29,10 @@ require_once($CFG->dirroot.'/lib/gradelib.php');
require_once($CFG->dirroot.'/grade/lib.php');
require_once($CFG->dirroot.'/grade/report/lib.php');
// This report may require a lot of memory and time on large courses.
raise_memory_limit(MEMORY_HUGE);
set_time_limit(120);
$courseid = required_param('id', PARAM_INT);
$groupid = optional_param('group', null, PARAM_INT);
$reset = optional_param('reset', 0, PARAM_BOOL);
@@ -75,6 +75,61 @@ Feature: Singleview report pagination
And I should see "2" in the ".stickyfooter .pagination" "css_element"
And I should not see "3" in the ".stickyfooter .pagination" "css_element"
Scenario: Teachers can view all students on singleview report
Given "150" "users" exist with the following data:
| username | student[count] |
| firstname | Student |
| lastname | [count] |
| email | student[count]@example.com |
And "150" "course enrolments" exist with the following data:
| user | student[count] |
| course | C1 |
| role |student |
When I am on the "Course 1" "grades > Grader report > View" page logged in as "teacher1"
And I click on grade item menu "Test assignment one" of type "gradeitem" on "grader" page
And I choose "Single view for this item" in the open action menu
And I set the field "perpage" to "All"
# There is also 1 header row.
Then I should see "151" node occurrences of type "tr" in the "singleview-grades" "table"
And ".stickyfooter .pagination" "css_element" should not exist
Scenario: Max perpage is 1000 and All is not available
Given "1100" "users" exist with the following data:
| username | student[count] |
| firstname | Student |
| lastname | [count] |
| email | student[count]@example.com |
And "1100" "course enrolments" exist with the following data:
| user | student[count] |
| course | C1 |
| role | student |
When I am on the "Course 1" "grades > Grader report > View" page logged in as "teacher1"
And I click on grade item menu "Test assignment one" of type "gradeitem" on "grader" page
And I choose "Single view for this item" in the open action menu
Then the "perpage" select box should contain "1000"
And the "perpage" select box should not contain "All"
Scenario: Page loads correctly with Max perpage limit
Given "1100" "users" exist with the following data:
| username | student[count] |
| firstname | Student |
| lastname | [count] |
| email | student[count]@example.com |
And "1100" "course enrolments" exist with the following data:
| user | student[count] |
| course | C1 |
| role | student |
When I am on the "Course 1" "grades > Grader report > View" page logged in as "teacher1"
And I click on grade item menu "Test assignment one" of type "gradeitem" on "grader" page
And I choose "Single view for this item" in the open action menu
And I set the field "perpage" to "1000"
Then I should see "1001" node occurrences of type "tr" in the "singleview-grades" "table"
And I click on "2" "link" in the ".stickyfooter .pagination" "css_element"
And I should see "101" node occurrences of type "tr" in the "singleview-grades" "table"
And I should see "1" in the ".stickyfooter .pagination" "css_element"
And I should see "2" in the ".stickyfooter .pagination" "css_element"
And I should not see "3" in the ".stickyfooter .pagination" "css_element"
@javascript
Scenario: The pagination bar is only displayed when there is more than one page on singleview report
Given "21" "users" exist with the following data:
@@ -63,10 +63,10 @@
<li data-for="sectionnode" role="treeitem" aria-owns="movemodalcollapse{{id}}" data-type="category" data-id="{{id}}" data-selected="false" aria-expanded="true" data-collapsible="true">
<div class="collapse-list-item d-flex" id="movemodalsection{{id}}">
<a role="button" href="#movemodalcollapse{{id}}" aria-controls="movemodalcollapse{{id}}" class="collapse-list-link btn" tabindex="-1">
<span class="expanded-icon text-nowrap" data-bs-toggle="tooltip" title="{{#str}} collapse, core {{/str}}">
<span class="expanded-icon icon-size-3 text-nowrap" data-bs-toggle="tooltip" data-bs-placement="right" title="{{#str}} collapse, core {{/str}}">
{{#pix}} t/expandedchevron, core, {{#str}} collapse, core {{/str}} {{/pix}}
</span>
<span class="collapsed-icon text-nowrap" data-bs-toggle="tooltip" title="{{#str}} expand, core {{/str}}">
<span class="collapsed-icon icon-size-3 text-nowrap" data-bs-toggle="tooltip" data-bs-placement="right" title="{{#str}} expand, core {{/str}}">
<span class="dir-rtl-hide">{{#pix}} t/collapsedchevron, core, {{#str}} expand, core {{/str}} {{/pix}}</span>
<span class="dir-ltr-hide">{{#pix}} t/collapsedchevron_rtl, core, {{#str}} expand, core {{/str}} {{/pix}}</span>
</span>
@@ -250,7 +250,8 @@ Feature: We can use calculated grade totals
| Aggregation | Natural |
| Include outcomes in aggregation | 1 |
| Exclude empty grades | 0 |
And I change window size to "large"
# Change window size to ultra-wide to avoid 'element-click-intercepted' failures.
And I change window size to "5120x2160"
And I navigate to "View > Grader report" in the course gradebook
And I give the grade "Excellent" to the user "Student 1" for the grade item "Test outcome item one"
And I press "Save changes"
@@ -305,7 +306,8 @@ Feature: We can use calculated grade totals
And I set the following settings for grade item "Test outcome item one" of type "gradeitem" on "setup" page:
| Weight adjusted | 1 |
| aggregationcoef2 | 100 |
And I change window size to "large"
# Change window size to ultra-wide to avoid 'element-click-intercepted' failures.
And I change window size to "5120x2160"
And I navigate to "View > Grader report" in the course gradebook
And I give the grade "Excellent" to the user "Student 1" for the grade item "Test outcome item one"
And I press "Save changes"
+8 -1
View File
@@ -1,4 +1,4 @@
@core @core_grades @javascript
@core @core_grades
Feature: We can enter in grades and view reports from the gradebook
In order to check the expected results are displayed
As a teacher
@@ -46,6 +46,7 @@ Feature: We can enter in grades and view reports from the gradebook
And I give the grade "90.00" to the user "Student 1" for the grade item "Test assignment name 2"
And I press "Save changes"
@javascript
Scenario: Grade a grade item and ensure the results display correctly in the gradebook
When I navigate to "View > User report" in the course gradebook
And the "Gradebook navigation menu" select menu should contain "Grader report"
@@ -68,6 +69,7 @@ Feature: We can enter in grades and view reports from the gradebook
And "Course 1" row "Grade" column of "overview-grade" table should contain "170.00"
And "Course 1" row "Grade" column of "overview-grade" table should not contain "90.00"
@javascript
Scenario: We can add a weighting to a grade item and it is displayed properly in the user report
When I navigate to "Setup > Gradebook setup" in the course gradebook
And I set the following settings for grade item "Course 1" of type "course" on "setup" page:
@@ -92,3 +94,8 @@ Feature: We can enter in grades and view reports from the gradebook
| Test assignment name 1 | 0.72% | 0.72% |
| Test assignment name 2 | 1.00% | 1.00% |
| Course total | 1.00% | 1.00% |
Scenario: User not enrolled in any course should not see the grades report
Given I log in as "admin"
When I follow "Grades" in the user menu
Then I should see "You are not enrolled in, nor teaching any courses on this site."
@@ -30,11 +30,11 @@
}}
{{<core/local/comboboxsearch/resultitem }}
{{$content}}
<div class="pe-2 ps-1 w-25">
<div class="pe-3 ps-1 flex-shrink-0">
<img class="rounded-circle mx-auto img-fluid" src="{{groupimageurl}}" loading="lazy" alt=""/>
</div>
<div class="pe-3 w-75">
<span class="d-block w-100 p-0 text-truncate">
<div class="pe-3">
<span class="d-block w-100 p-0 text-wrap text-break">
{{name}}
</span>
{{^participation}}
+2
View File
@@ -69,6 +69,8 @@ $string['pathsunsecuredataroot'] = 'موضع مجلد البيانات الرئ
$string['pathswrongadmindir'] = 'مجلد المشرف غير موجود';
$string['phpextension'] = 'إمتداد PHP {$a}';
$string['phpversion'] = 'إصدار PHP';
$string['webserverconfigproblemdescription'] = 'إن مخدمك ليس مهيئًا لمنع الوصول إلى الملفات الواقعة خارج المجلد /public. يرجى الرجوع إلى <a href="https://moodledev.io/docs/5.1/guides/restructure">https://moodledev.io/docs/5.1/guides/restructure</a> للاطلاع على تفاصيل كيفية تهيئة مخدمك. بمجرد إعادة تهيئته، ترجى <a href="{$a}">زيارة مجلد الويب الأساسي</a>.';
$string['webservernotconfigured'] = 'مخدم الويب غير مهيأ';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'أنت تشاهد هذه الصفحة لأنك تمكنت بنجاح من تنصيب وإطلاق
حزمة <strong>{$a->packname} {$a->packversion}</strong> في حاسبتك. تهانينا!';

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