Compare commits

..

272 Commits

Author SHA1 Message Date
Ilya Tregubov 76fe404dd5 Moodle release 4.1.4 2023-06-10 14:08:05 +08:00
Ilya Tregubov f3400f4fc5 weekly release 4.1.3+ 2023-06-09 13:14:27 +08:00
Sara Arjona c872490dcf Merge branch 'MDL-78415-401-enfix' of https://github.com/vmdef/moodle into MOODLE_401_STABLE 2023-06-08 20:33:14 +02:00
Helen Foster e518de15f3 MDL-78415 lang: Import fixed English strings (en_fix) 2023-06-08 12:02:34 +01:00
Sara Arjona 4fffa81e41 Merge branch 'MDL-77946-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-06-08 06:56:56 +02:00
Sara Arjona fcd946c862 Merge branch 'MDL-78308-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-06-08 06:48:40 +02:00
Andrew Nicols b272406c74 Merge branch 'MDL-75696_401_STABLE' of https://github.com/marxjohnson/moodle into MOODLE_401_STABLE 2023-06-08 12:31:16 +08:00
Andrew Nicols d0e0225a1a Merge branch 'MDL-78364-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-06-08 12:19:31 +08:00
Andrew Nicols 2649d1a01e Merge branch 'MDL-78008_401' of https://github.com/timhunt/moodle into MOODLE_401_STABLE 2023-06-08 12:10:42 +08:00
Andrew Nicols aa77a76c7a Merge branch 'MDL-77933-401' of https://github.com/snake/moodle into MOODLE_401_STABLE 2023-06-08 12:03:37 +08:00
Andrew Nicols 1b27cfd795 MDL-76838 core: Add unit test for invalid unicode sequences 2023-06-08 11:59:23 +08:00
Jake Dallimore da10ccd8d4 MDL-77933 enrol_lti: return site name and logo during registration
Instead of the Moodle icon and the 'moodle' lang string, return the
site fullname and compact logo during dynamic registration.
2023-06-08 11:56:31 +08:00
Andrew Nicols cc7ea9fecd Merge branch 'MDL-76838-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-06-08 11:54:42 +08:00
Jun Pataleta 2cf0da5dc6 Merge branch 'MDL-75898-41' of https://github.com/matthewhilton/moodle into MOODLE_401_STABLE 2023-06-08 11:43:06 +08:00
Andrew Nicols a6fde372ab Merge branch 'MDL-78026-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-06-08 11:36:54 +08:00
Andrew Nicols 7dc747ea99 Merge branch 'MDL-77264-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-06-08 11:32:46 +08:00
Andrew Nicols 5a8881ac37 Merge branch 'MDL-76821-401' of https://github.com/snake/moodle into MOODLE_401_STABLE 2023-06-08 11:30:09 +08:00
Andrew Nicols b4eb274d7a Merge branch 'MDL-77799-m401' of https://github.com/sammarshallou/moodle into MOODLE_401_STABLE 2023-06-08 11:21:35 +08:00
Andrew Nicols 21443080c8 Merge branch 'MDL-78408-401' of https://github.com/snake/moodle into MOODLE_401_STABLE 2023-06-08 11:05:34 +08:00
Jun Pataleta ea1a8336e5 Merge branch 'MDL-78036_401' of https://github.com/stronk7/moodle into MOODLE_401_STABLE 2023-06-08 11:03:24 +08:00
Andrew Nicols f68fedcf77 Merge branch 'wip_MDL-78400_m41' of https://github.com/gjb2048/moodle into MOODLE_401_STABLE 2023-06-08 10:51:50 +08:00
Andrew Nicols cf3ae5bc2a Merge branch 'MDL-76406-401' of https://github.com/safatshahin/moodle into MOODLE_401_STABLE 2023-06-08 10:38:58 +08:00
Jun Pataleta c8bb7a6911 Merge branch 'MDL-76842-401' of https://github.com/snake/moodle into MOODLE_401_STABLE 2023-06-08 10:34:17 +08:00
Jun Pataleta bf448de7aa Merge branch 'MDL-78229-401' of https://github.com/lucaboesch/moodle into MOODLE_401_STABLE 2023-06-08 10:30:23 +08:00
Jake Dallimore cbacf8ee58 MDL-76842 enrol_lti: fix user record updates in user_repository
Only call user_update_user when the relevant user data has changed,
preventing unnecessary user_updated events. This also removes the
line setting timemodified on the user since user_update_user already
handles this.
2023-06-08 10:29:01 +08:00
Jun Pataleta 37a29f7950 Merge branch 'MDL-78431-401' of https://github.com/andrewnicols/moodle into MOODLE_401_STABLE 2023-06-08 10:28:18 +08:00
Jake Dallimore b1dcb08e3f MDL-76842 enrol_lti: test confirming the erroneous user_updated events
This test will fail until the fix - only updating users when data has
changed - is put in place in the following commit.
2023-06-08 09:40:29 +08:00
Jake Dallimore 0ccf800f01 MDL-76842 auth_lti: don't update the user unless data has changed
When receiving data from the LTI launch, or service call, only update
the user record when we know something has changed. This prevents the
creation of many \core\event\user_updated events.
2023-06-08 09:40:28 +08:00
Jake Dallimore b097d38fb3 MDL-76842 auth_lti: don't update the user after membership creation
This isn't required and generates excess events. The only thing update
does, other than update the user fields, is to update the picture, but
in this case, that's unset in membership-based auths anyway, so it's
entirely safe to remove this.
2023-06-08 09:39:09 +08:00
Jake Dallimore 1858e29d23 MDL-76842 auth_lti: test confirming the erroneous user_updated events
This test will fail until the fix - only updating users when data has
changed - is put in place in the following commit.
2023-06-08 09:39:09 +08:00
Stevani Andolo bd5329b1a5 MDL-78215 core: Added support unit test 2023-06-08 09:37:02 +08:00
Stevani Andolo 69596a6bb2 MDL-78215 core: support checking if 0.0.0.0 is in subnet 2023-06-08 09:37:01 +08:00
Jun Pataleta 79510d5d73 Merge branch 'MDL-78378-401' of https://github.com/sarjona/moodle into MOODLE_401_STABLE 2023-06-07 23:25:19 +08:00
Jun Pataleta fce2d2f407 Merge branch 'MDL-78368-401' of https://github.com/andrewnicols/moodle into MOODLE_401_STABLE 2023-06-07 23:22:25 +08:00
Jun Pataleta 3b8e3ba59e Merge branch 'MDL-76745_401_STABLE' of https://github.com/marxjohnson/moodle into MOODLE_401_STABLE 2023-06-07 22:53:45 +08:00
Andrew Nicols 2279eee0ab MDL-78431 core: Abort after config for source maps 2023-06-07 21:07:52 +08:00
Sara Arjona 411f43dc5a Merge branch 'MDL-78322-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-06-07 12:43:08 +02:00
Sara Arjona ef2a553bd4 Merge branch 'MDL-78352-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-06-07 12:41:16 +02:00
Sara Arjona af1bd51b99 Merge branch 'MDL-78047-401' of https://github.com/stevandoMoodle/moodle into MOODLE_401_STABLE 2023-06-07 18:17:40 +08:00
Sara Arjona b89ece2484 Merge branch 'MDL-78010-401' of https://github.com/ferranrecio/moodle into MOODLE_401_STABLE 2023-06-07 18:17:40 +08:00
sam marshall 0c3b3c9b92 MDL-77799 webservice_rest: unhandled exception should go in server log 2023-06-07 10:04:07 +01:00
Jake Dallimore 7fd423acfc MDL-78408 core: fix restoration of anchor to wantsurl during login 2023-06-07 14:30:37 +08:00
Stevani Andolo abac304ba9 MDL-78047 core_courseformat: Wrapping added to text and media activity 2023-06-07 12:58:38 +08:00
Paul Holden 677b67ac05 MDL-77193 mnet: correct sorting of access control table. 2023-06-07 09:53:44 +08:00
Ferran Recio 5f30a47660 MDL-78010 mod_label: improve upgrade performance 2023-06-06 21:33:14 +02:00
Eloy Lafuente (stronk7) 7abeaf37ea MDL-78036 behat: Move all links from old docs to new counterparts 2023-06-06 12:39:21 +02:00
Petr Skoda 79981290d9 MDL-76683 core_group: fix template 2023-06-06 09:30:27 +00:00
Sara Arjona e96dab8594 Merge branch 'MDL-76936-401' of https://github.com/HirotoKagotani/moodle into MOODLE_401_STABLE 2023-06-06 11:22:15 +02:00
Paul Holden 6843ba0aad MDL-78364 reportbuilder: earlier calculation of schedule recipients. 2023-06-05 16:22:33 +01:00
Gareth Barnard 257e2b414a MDL-78400 JavaScript: Building JS puts build files in src dir on Win. 2023-06-05 14:52:16 +01:00
Sara Arjona b17d91c6f8 MDL-78378 survey: Display default introduction when description is empty
This is a regression when the activity pages were redessigned in Moodle
4.0. The default introduction should be displayed when the activity
description is empty.
2023-06-02 08:19:49 +02:00
Safat 7debcb85b4 MDL-76406 core: Add environment check for versions not supporting 8.2 2023-06-02 12:09:15 +10:00
Ilya Tregubov 03b605f2eb weekly release 4.1.3+ 2023-06-02 09:55:42 +08:00
Ilya Tregubov de0bab848d Merge branch 'install_401_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_401_STABLE 2023-06-02 09:55:39 +08:00
Mark Johnson dfdf8ba7c9 MDL-76745 cache: Implement cache_loader_with_locking in cache_disabled
If a plugin attempts to do something that would lock coursemodinfo cache
during install (such as creating a course module), this currently fails
as check_lock_state is not implemented in cache_disabled. Adding the
cache_loader_with_locking interface ensures that all lock methods are
implemented.
2023-06-01 15:03:33 +01:00
Andrew Nicols f1f87dbd6b Merge branch 'MDL-77749-unset-config-duration-MOODLE_401_STABLE' of https://github.com/brendanheywood/moodle into MOODLE_401_STABLE 2023-06-01 12:03:26 +08:00
Jun Pataleta 2885b47377 Merge branch 'MDL-78092-m401' of https://github.com/sammarshallou/moodle into MOODLE_401_STABLE 2023-06-01 11:36:21 +08:00
Andrew Nicols 036cb9a997 Merge branch 'MDL-78327_401' of https://github.com/timhunt/moodle into MOODLE_401_STABLE 2023-06-01 10:52:47 +08:00
Ilya Tregubov 732f27c770 Merge branch 'MDL-78196-401' of https://github.com/jleyva/moodle into MOODLE_401_STABLE 2023-06-01 10:40:36 +08:00
Andrew Nicols d6a8581abe Merge branch 'MDL-78262-401' of https://github.com/danghieu1407/moodle into MOODLE_401_STABLE 2023-06-01 09:57:03 +08:00
Ilya Tregubov d37efc5160 Merge branch 'MDL-77259-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-06-01 09:30:02 +08:00
Ilya Tregubov e0dc9aa297 Merge branch 'MDL-75961-401' of https://github.com/lameze/moodle into MOODLE_401_STABLE 2023-06-01 09:08:28 +08:00
AMOS bot e939cee281 Automatically generated installer lang files 2023-06-01 00:11:03 +00:00
Sara Arjona 17be7f62d6 Merge branch 'MDL-76693-401' of https://github.com/roland04/moodle into MOODLE_401_STABLE 2023-05-31 17:09:03 +02:00
Simey Lameze b512186fce MDL-77547 behat: run recalculate task before checking values 2023-05-31 15:04:56 +02:00
Tim Hunt ee3752e639 MDL-78327 question stats: store same timestamp for all question stats 2023-05-31 12:03:56 +01:00
Sara Arjona e47c5527f6 Merge branch 'MDL-77547-401' of https://github.com/andelacruz/moodle into MOODLE_401_STABLE 2023-05-31 12:51:34 +02:00
Sara Arjona 99a48299f0 Merge branch 'MDL-77907-401' of https://github.com/andelacruz/moodle into MOODLE_401_STABLE 2023-05-31 12:18:12 +02:00
Sara Arjona 5a84debc90 Merge branch 'MDL-78148-401' of https://github.com/ferranrecio/moodle into MOODLE_401_STABLE 2023-05-31 11:42:16 +02:00
Sara Arjona 70e6fb0a7d Merge branch 'MDL-73648-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-31 11:38:21 +02:00
Ilya Tregubov 95ec4161f5 Merge branch 'MDL-73138-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-30 12:54:24 +08:00
Ilya Tregubov bd41a5bbec Merge branch 'MDL-77723-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-30 12:44:54 +08:00
AMOS bot f02891c647 Automatically generated installer lang files 2023-05-30 00:10:36 +00:00
Jun Pataleta 2d99010fc9 Merge branch 'MDL-78314-401' of https://github.com/lucaboesch/moodle into MOODLE_401_STABLE 2023-05-29 17:35:04 +08:00
Luca Bösch 23ba0d96ef MDL-78314 quiz: style button fieldset in "Manually grade question". 2023-05-29 10:54:27 +02:00
Mikel Martín 11205ec5ef MDL-76693 core_course: Fix activity chooser summary overlap
In activity chooser modal, there is an overlap issue in the information "sub-page".
Changing some classes in the layout solves the problem.
2023-05-29 09:38:28 +02:00
Ferran Recio b01dd76331 MDL-78148 core_courseformat: reduce the use of uniqid in templates 2023-05-29 08:40:18 +02:00
Eloy Lafuente (stronk7) 278ce49b7f MDL-78036 behat: Fix php81 deprecation warning 2023-05-26 18:04:26 +02:00
Dani Palou 4cbe98f2e7 MDL-78368 airnotifier: Fix data encrypted several times
Backport of MDL-78054.
2023-05-26 21:46:35 +08:00
Paul Holden a22aabb29f MDL-78352 webservice: use default user context page heading. 2023-05-26 12:27:46 +01:00
Paul Holden 1ff279717e MDL-73648 badges: add Behat generator class for creating badges. 2023-05-26 12:19:27 +01:00
Paul Holden 017f8a9c4d MDL-73138 group: remove double encoding of group/role names. 2023-05-26 10:25:50 +01:00
Paul Holden 16c5835a80 MDL-73138 user: avoid double encoding participant filter values. 2023-05-26 10:25:50 +01:00
Sara Arjona 090baf556e weekly release 4.1.3+ 2023-05-26 09:28:53 +02:00
Sara Arjona 0b46994b0c Merge branch 'install_401_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_401_STABLE 2023-05-26 09:28:45 +02:00
Adrien Loison a229905760 MDL-78262 lib_spout: Update box/spout to address PHP 8.1 deprecation 2023-05-26 08:50:17 +07:00
Jun Pataleta 1c7f1e0bed Merge branch 'MDL-78346-401' of https://github.com/andrewnicols/moodle into MOODLE_401_STABLE 2023-05-25 16:56:14 +02:00
Jun Pataleta 59596ddedc Merge branch 'MDL-76065-401' of https://github.com/lameze/moodle into MOODLE_401_STABLE 2023-05-25 16:56:14 +02:00
Jun Pataleta 34dd7e7978 Merge branch 'MDL-78320-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-25 16:56:14 +02:00
Jun Pataleta e7d2f494c3 Merge branch 'MDL-78346-401' of https://github.com/andrewnicols/moodle into MOODLE_401_STABLE 2023-05-25 16:56:14 +02:00
Andrew Nicols 9c5febc47c MDL-78346 tool_langimport: Print any notifications before redirect 2023-05-25 14:55:01 +08:00
Andrew Nicols 4ddbcf2aec MDL-78346 tool_langimport: Redirect after action to prevent resubmission 2023-05-25 11:14:15 +08:00
Andrew Nicols 9dc72be2d4 MDL-78346 tool_langimport: Prevent uninstallation of invalid targets 2023-05-25 11:14:14 +08:00
Andrew Nicols 05a5ed676c Merge branch 'MDL-77995-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-25 09:50:47 +08:00
Paul Holden aaf17fc8b6 MDL-78320 quiz_overview: format group names in overview report. 2023-05-24 16:19:34 +01:00
Andrew Nicols b870bc0577 Merge branch 'MDL-77983-401' of https://github.com/rjnl/moodle into MOODLE_401_STABLE 2023-05-24 21:15:17 +08:00
Jun Pataleta 9e582a6099 Merge branch 'MDL-78072-401' of https://github.com/andrewnicols/moodle into MOODLE_401_STABLE 2023-05-24 17:56:27 +08:00
Jun Pataleta ff402c26e5 Merge branch 'MDL-78005-401' of https://github.com/davewoloszyn/moodle into MOODLE_401_STABLE 2023-05-24 12:06:17 +08:00
Simey Lameze b12f914708 MDL-75961 behat: review and enhance mod_imscp tests
This commit makes the following improvements to mod_imscp tests:
* Removes unnecessary @javascript and @_file_upload tags from non-JS tests.
* Removes user/enrol data generation from tests that can be performed as admin.
* Removes "I log out" and other unnecessary steps.
2023-05-24 10:41:41 +08:00
Angelia Dela Cruz e7d32b89ac MDL-75961 behat: IMSCP behat generators use UI
Replace steps that manually add IMSCP instances via the UI and use
Behat generators. This improves the speed of the Behat test runs.
2023-05-24 10:41:41 +08:00
Sara Arjona 22705fa4fb Merge branch 'MDL-78176_401' of https://github.com/AnupamaSarjoshi/moodle into MOODLE_401_STABLE 2023-05-23 17:45:57 +02:00
Sara Arjona 7c0954ccd1 Merge branch 'MDL-78242-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-23 17:37:00 +02:00
Jun Pataleta 0479c711c2 Merge branch 'MDL-76688/401' of https://github.com/skodak/moodle into MOODLE_401_STABLE 2023-05-23 15:22:04 +08:00
Angelia Dela Cruz de6c14ee71 MDL-77547 Behat: Coverage to check for bad multichoice questions 2023-05-23 15:17:43 +08:00
Simey Lameze 1936b30c88 MDL-76065 behat: Review and enhance mod_lti Behat tests
In this commit, the following improvements were made to the mod_lti Behat tests:

* Replaced manual steps with data generators to set completion.
* Eliminated unnecessary user and course enrolments data generation as some tests can be performed as an admin.
* Removed the @javascript tag from non-JS tests.
* Updated the LTI data generator to generate an internal Moodle URL in the toolurl field, enabling the use of XML files.
2023-05-23 12:23:26 +08:00
Angelia Dela Cruz f19f31736a MDL-76065 behat: LTI behat generators use UI
Replace steps that manually add LTI instances via the UI and use
Behat generators. This improves the speed of the Behat test runs.
2023-05-23 12:15:25 +08:00
Andrew Nicols 2b01af17be Merge branch 'MDL-78170-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-23 10:28:33 +08:00
AMOS bot 439f75e6e1 Automatically generated installer lang files 2023-05-23 00:10:27 +00:00
Paul Holden dec3d56032 MDL-76838 backup: remove invalid utf8 code sequences writing XML.
The surrogate blocks FFFE and FFFF are non-characters, specifically
disallowed in the character range for XML documents.
2023-05-22 21:04:12 +01:00
Paul Holden 452c7c72e0 MDL-78322 tool_cohortroles: don't assign roles to deleted users. 2023-05-22 20:14:39 +01:00
Tim Hunt 0d0868ee97 MDL-78008 qtype_multianswer: random guess score with missing subqs
Sometimes, due to other bugs, subquestions can get lost, so the code
needs to be robust to this.
2023-05-22 17:02:22 +01:00
Sara Arjona c98e7e7dbf Merge branch 'MDL-78211-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-22 17:41:48 +02:00
Sara Arjona 90e18e28a2 Merge branch 'MDL-78226-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-22 17:29:27 +02:00
Sara Arjona db7c650f8c Merge branch 'MDL-78225-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-22 12:04:19 +02:00
sam marshall 79d0e63b18 MDL-78092 Cache: Modinfo locking with Redis store does not work
The feature added in 4.1 to lock the modinfo cache does not work when
using Redis, because:

* The API to acquire a cache lock is confusing, and the code did not
  check that it successfully acquired a lock before going on to build
  the cache anyway.

* Unlike the other types of cache lock, the Redis store did not retry
  the lock for a timeout period before giving up and failing.

This change fixes both points.
2023-05-22 10:45:13 +01:00
Jun Pataleta 962c8ef477 Merge branch 'MDL-78157-401' of https://github.com/andrewnicols/moodle into MOODLE_401_STABLE 2023-05-22 15:29:30 +08:00
Andrew Nicols 2036f2439e MDL-78157 js: Be more careful about inserting missing module names
It is perfectly legitimate to create and/or use a method named `define`
in JS outside of RequireJS.

Unfortunately our requirejs.php wrapper is dumb and does not understand
this.

In the long term we need to stop doing this at all. We really should be
able to already, but every time I try to something prevents it.

In the interim, this change adds a secondary check to see if there is an
existing define which _does_ have the right name in it already.
2023-05-22 14:40:41 +08:00
Luca Bösch 6bb4351e8b MDL-78229 quiz: quiz editing page less narrow.
This is a backport of MDL-77872.
2023-05-19 18:13:07 +02:00
Anupama Sarjoshi 80fa181a26 MDL-78176 Question: Fix D&D onto image and D&D markers theme issue 2023-05-19 16:28:53 +01:00
Jun Pataleta 6ca70dd59f weekly release 4.1.3+ 2023-05-19 21:13:57 +08:00
Jun Pataleta 712737e955 Merge branch 'install_401_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_401_STABLE 2023-05-19 21:13:54 +08:00
Andrew Nicols 5e32d574c6 MDL-77733 behat: Remove extraneous no-axe option
There is no need for no-axe if we make axe a boolean value. We already
have automatic support for negated boolean flags.
2023-05-19 11:28:08 +08:00
Mark Johnson 5620e0059a MDL-75696 quiz: Fix restoring pre-4.0 quizzes with random questions
Restoring multiple quizzes from a pre-4.0 backup was broken when
the quizzes shared a random question. This is because after the
first quiz created a set reference in place of the random question,
it deleted the question record so it was not there for the second
quiz to use. This change tracks the IDs of random questions so
they can be deleted at the end.
2023-05-18 15:59:53 +01:00
Mark Johnson 85b1e992e9 MDL-75696 quiz: Set default value for includingsubcategories on restore 2023-05-18 15:56:02 +01:00
Mark Johnson 8d50911af5 MDL-75696 backup: Fix backup version checks
Several version checks were incorrectly using
restore_controller::info::moodle_release instead of moodle_version as a
version number. This replaces all of those checks with a common pair of
methods to make the checks clearer and more maintainable.
2023-05-18 15:56:02 +01:00
Paul Holden 5c871bff29 MDL-78308 output: better parsing of custom menu item configuration.
Previously certain values would cause deprecation notices in PHP8.1.
2023-05-18 10:19:18 +01:00
Jun Pataleta 096f1e67c8 Merge branch 'MDL-78116-401' of https://github.com/andrewnicols/moodle into MOODLE_401_STABLE 2023-05-18 12:11:31 +08:00
Andrew Nicols 21cc09fa2d Merge branch 'MDL-77451-401' of https://github.com/rezaies/moodle into MOODLE_401_STABLE 2023-05-18 11:56:33 +08:00
Jun Pataleta e2a7a44d3e Merge branch 'MDL-78135-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-18 11:54:46 +08:00
Andrew Nicols b28a28ef0f Merge branch 'MDL-78260_401' of https://github.com/timhunt/moodle into MOODLE_401_STABLE 2023-05-18 11:51:47 +08:00
Andrew Nicols 30e9810589 Merge branch 'MDL-78151-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-18 11:48:39 +08:00
Andrew Nicols cf83791269 Merge branch 'MDL-77733-401' of https://github.com/lameze/moodle into MOODLE_401_STABLE 2023-05-18 11:44:18 +08:00
David Woloszyn 3f07fc246f MDL-78005 editor_tiny: Declared vars to supress error logging 2023-05-18 13:03:04 +10:00
Tim Hunt 3d023db5b0 MDL-78260 quiz stats: fix View details for random questions 2023-05-17 09:04:49 +01:00
Rajneel Totaram 13b48a0ae8 MDL-77983 core_user: Avoid passing nulls to base64_decode 2023-05-17 19:39:52 +12:00
Ilya Tregubov 87223abc7a Merge branch 'MDL-78152-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-17 15:07:38 +08:00
AMOS bot e7458d017a Automatically generated installer lang files 2023-05-17 00:10:58 +00:00
Jun Pataleta d2b81dd116 Merge branch 'MDL-77439-401' of https://github.com/junpataleta/moodle into MOODLE_401_STABLE 2023-05-16 20:39:09 +08:00
Peter Mayer 361f0fa524 MDL-77439 lib_filebrowser: Splitup query to speedup get_courses 2023-05-16 20:37:57 +08:00
Paul Holden 7a23da59ce MDL-78026 group: format retrieved group names from external methods. 2023-05-16 10:10:06 +01:00
Paul Holden 01ae473823 MDL-77264 grade: define fallback name of abstract export event. 2023-05-16 09:54:40 +01:00
Paul Holden 0aff5ccd01 MDL-77259 tool_monitor: use core component API for event list.
The previous method of hardcoded filepaths, specifically for events
belonging to core, meant that any events belongs to core subsystems
were omitted (core_customfield, core_h5p, core_payment, etc).
2023-05-16 09:50:22 +01:00
Angelia Dela Cruz 7a511874b8 MDL-77907 Behat: Coverage for duplicating competency framework 2023-05-16 16:11:58 +08:00
Andrew Nicols a1b37845a8 MDL-78072 core: Skip mobile encryption if Sodium is not available 2023-05-16 12:51:14 +08:00
Jake Dallimore 071adb2fb1 Merge branch 'MDL-77930-401' of https://github.com/rjnl/moodle into MOODLE_401_STABLE 2023-05-16 10:50:04 +08:00
Jake Dallimore aa39ebbb99 Merge branch 'MDL-78120-401' of https://github.com/lucaboesch/moodle into MOODLE_401_STABLE 2023-05-16 10:45:27 +08:00
Jun Pataleta 4818dae2fc Merge branch 'MDL-75576_401' of https://github.com/timhunt/moodle into MOODLE_401_STABLE 2023-05-16 09:59:01 +08:00
Ilya Tregubov dc38a92ec6 Merge branch 'MDL-77840-401' of https://github.com/davewoloszyn/moodle into MOODLE_401_STABLE 2023-05-16 09:47:48 +08:00
Andrew Nicols 946405d44b Merge branch 'MDL-77432-401' of https://github.com/lucaboesch/moodle into MOODLE_401_STABLE 2023-05-15 22:12:44 +08:00
Andrew Nicols bdeda3ea9b Merge branch 'MDL-78149-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-15 22:07:13 +08:00
Andrew Nicols 01c04f4cf8 Merge branch 'MDL-78140-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-15 22:03:30 +08:00
Paul Holden 9ef8025e4c MDL-78242 roles: consistently check course contact config. 2023-05-15 13:04:48 +01:00
Tim Hunt efe895f1bb MDL-75576 quiz/question statistics: don't expire by time
Previously, a set of calculated quiz statistics would only 'last' for
15 minutes. Then they would be considered invalid and not used.

Now, computed statistics are kept indefinitely. Instead, when a new
batch of values are computed for a particular set of settings, older numbers
for the same settings are deleted first. Therefore,
question_stats_cleanup_task is no more.
2023-05-15 10:04:34 +01:00
Tim Hunt a532f407bb MDL-75576 quiz statistics: don't time-limit the use of cached values
This should have been done as part of  MDL-74762, but was missed.
2023-05-15 10:04:33 +01:00
Tim Hunt 86932a330b MDL-75576 quiz statistics: don't compute when a user views the qbank
Previously, when users viewed the question bank, if the quiz statistics
had not already been calculated, we would try to compute them there an then.
This could be very, very slow, leading to session lock problems.

Now, we never try to compute the statistics on the fly. Instead, we rely
on the existing \quiz_statistics\task\recalculate scheduled task to do it.
2023-05-15 10:04:33 +01:00
Tim Hunt db9a0e296a MDL-75576 question bank: bulk-load statistics for all questions at once
There is a new method on the column class, for the column to declare any
statistics it requires. The question then efficiently loads all those
statistics in one go, and makes them available as the columns are
rendererd, or do any subsequent processing.
2023-05-15 10:04:33 +01:00
Tim Hunt 9ff59e99ff MDL-75576 qbank_statistics: remove copy-pasted PHPdoc comments
These methods are already correctly documented on the base class.
2023-05-15 10:04:32 +01:00
Luca Bösch 997990f94f MDL-78120 login: handle empty wantsurl on login with alternateloginurl. 2023-05-15 10:19:00 +02:00
Paul Holden e5132eff2f MDL-77995 javascript: more precise path matching building JS. 2023-05-15 09:05:51 +01:00
Paul Holden 7093578baf MDL-77723 user: ensure identity refers to a valid custom profile field. 2023-05-15 09:03:40 +01:00
Jun Pataleta 28026b923a Merge branch 'MDL-76903-401' of https://github.com/laurentdavid/moodle into MOODLE_401_STABLE 2023-05-15 12:06:02 +08:00
Andrew Nicols 0e4426818a Merge branch 'MDL-78125-401' of https://github.com/junpataleta/moodle into MOODLE_401_STABLE 2023-05-15 11:58:59 +08:00
Jun Pataleta e9f6b9c2d6 MDL-78125 qtype_multianswer: Convert popover trigger to link
According to Boostrap Popovers documentation for popovers triggered
on focus:
"For proper cross-browser and cross-platform behavior, you must use the
<a> tag, not the <button> tag, and you also must include a tabindex
attribute."
2023-05-12 22:47:11 +08:00
Luca Bösch f1dd604fce MDL-77432 mod_assign: Confirm submission page standard and not incourse 2023-05-12 09:44:22 +02:00
Ilya Tregubov 017a3274fe weekly release 4.1.3+ 2023-05-12 08:56:50 +08:00
Ilya Tregubov 3b9bfc1d79 Merge branch 'install_401_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_401_STABLE 2023-05-12 08:56:48 +08:00
Cameron Ball 77eb2ed79b MDL-75898 assignfeedback_editpdf: Add randomness to PDF annotations
The behat step "I draw on the pdf" creates the exact same line every
time it is called. This can cause issues in a situation like the following:

- Student uploads PDF
- Teacher annotates page 1
- Student edits submission, adding another PDF
- Teacher annotates page 1 again
- When the student looks at their annotated PDF it appears the same as
  before they added the second PDF (i.e., they don't see extra pages for
  the second PDF)

Exactly why this happens isn't clear, and in practice this probably never
happens because the chances of drawing the exact same line twice are almost
0. However, when testing the functionality added in MDL-45580 it was causing
issues.
2023-05-12 09:14:03 +10:00
Cameron Ball 90350915c2 MDL-75898 assignfeedback_editpdf: Don't poll readonly pages
When requesting the readonly version of pages (which contain
teacher annotations), they should always be available - the
PHP side even throws an exception when they're not. This
means we don't need to worry about polling document converters
from the JS side and can just return the pages immediately.
2023-05-12 09:14:03 +10:00
Cameron Ball 825609ae08 MDL-75898 assignfeedback_editpdf: Improve page count mismatch logic
MDL-45580 introduced the readonlypages filearea, and when loading
page images for an attempt, the code would check if the pages existed
, creating them if not. The code inside this block also contained
a guard clause for the case where no readonly pages existed - which
is a situation that should not happen. Whenever readonly pages are
requested, they should exist.

MDL-66626 introduced a situation where page counts not matching would
also retrigger page generation. However this led to a situation where
the guard clause could be entered when requesting readonly pages.

This patch refactors the guard clause, and improves the logic to
regenerate pages.
2023-05-12 09:14:03 +10:00
Paul Holden 5b3d55def7 MDL-78226 contentbank: verify presence of bank heading element. 2023-05-11 23:16:33 +01:00
Paul Holden 67effa603e MDL-78225 output: add sesskey for post requests only in renderables. 2023-05-11 22:48:54 +01:00
Laurent David 20b4efad5c MDL-76903 mod_book: Fix activity completion
* Check only visibles pages to set completion status.
* Make sure we have consistent completion value in API and on the view page.
2023-05-11 12:09:24 +02:00
Paul Holden 11af0c5388 MDL-78140 analytics: improve context restrictions manager test.
This test apparently fails sporadically on Oracle, without obvious
causes. The failure however was hard to interpret, and the test
itself made a lot of assumptions about the system re. naming of
categories/courses. This could be prone to errors, so improve that.
2023-05-11 09:38:01 +01:00
Paul Holden 683840999d MDL-77946 access: fix category sort order of capability contexts. 2023-05-11 08:33:32 +01:00
Jun Pataleta f8537a9536 Merge branch 'MDL-76344-401-fix' of https://github.com/stevandoMoodle/moodle into MOODLE_401_STABLE 2023-05-11 14:35:30 +08:00
Stevani Andolo 7b1e0a6980 MDL-76344 course: Fixed course image 'non image file' layout 2023-05-11 14:11:16 +08:00
Jake Dallimore 838047106d Merge branch 'MDL-75552-401' of https://github.com/sarjona/moodle into MOODLE_401_STABLE 2023-05-11 11:23:49 +08:00
Sara Arjona 84175f399f Merge branch 'MDL-78040-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-10 19:45:18 +02:00
Paul Holden 9e0f8fc7bf MDL-78211 contentbank: fix context option when formatting categories. 2023-05-10 17:42:29 +01:00
Paul Holden c228866a46 MDL-78151 core: account for timezone differences in intl extension.
Recent additions to timezones from the IANA TZDB are not necessarily
reflected in those used by the intl extension and/or ICU zones:

 * America/Ciudad_Juarez
 * America/Nuuk
 * Europe/Kyiv
 * Pacific/Kanton

Create mapping to allow these zones to be used without errors.
2023-05-10 17:12:33 +01:00
Juan Leyva d82f68742b MDL-78196 user: Fix files used space check 2023-05-10 17:17:48 +02:00
Andrew Nicols da78523b15 Merge branch 'MDL-77592-401' of https://github.com/golenkovm/moodle into MOODLE_401_STABLE 2023-05-10 13:03:57 +08:00
Andrew Nicols 5ceb711355 Merge branch 'MDL-78034-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-10 13:00:30 +08:00
Ilya Tregubov 1d5ef6aa9a Merge branch 'MDL-75623-MOODLE_401_STABLE' of https://github.com/sh-csg/moodle into MOODLE_401_STABLE 2023-05-10 11:46:40 +08:00
Paul Holden d3b4e4a9a5 MDL-78170 rating: consistently calculate average rating aggregation.
Ensure it's always cast by the database as a float, to avoid loss of
precision.
2023-05-09 15:57:43 +01:00
Paul Holden d7ba3f656d MDL-78149 mod_data: ensure group selector is always present. 2023-05-09 12:20:35 +01:00
Paul Holden 48963bb7a7 MDL-78152 mod_book: set permissions required for page editing. 2023-05-09 12:11:32 +01:00
Shamim Rezaie f36c96411d MDL-77451 gradereport: stress handling for multiple clicks 2023-05-09 19:15:37 +10:00
Andrew Nicols 50d1e69df6 MDL-75552 badges: move apiBase consumption to backpack
The apiBase in .well-known/badgeconnect.json was ignored and it was
causing some failures when connecting or sending badges to an
external backpack.
For OBv2.1, it has been changed to always use the apiBase defined
in the badgeconnect.json backpack provider.
2023-05-09 08:51:30 +02:00
Rajneel Totaram f6c5fa3bfc MDL-77930 accesslib: php81 deprecation warnings on missing plugins 2023-05-09 17:37:00 +12:00
Juan Leyva d838917d28 MDL-78072 airnotifier: Apply payload size optimisation to all push
Backport of MDL-77893.

Co-authored by: Andrew Lyons <andrew@nicols.co.uk>
2023-05-09 13:07:10 +08:00
Juan Leyva 1af4ace4d6 MDL-78072 airnotifier: Allow configuring how to process encrypted notifs
Backport of MDL-77893.

Co-authored by: Andrew Lyons <andrew@nicols.co.uk>
2023-05-09 13:07:09 +08:00
Juan Leyva 5c85fb975d MDL-78072 airnotifier: Leave only message data processed by: Airnotifier
Backport of MDL-77893.

Co-authored by: Andrew Lyons <andrew@nicols.co.uk>
2023-05-09 13:07:09 +08:00
Juan Leyva 2ad6fecca5 MDL-78072 airnotifier: Allow to set publickey when creating new device
Backport of MDL-77893.

Co-authored by: Andrew Lyons <andrew@nicols.co.uk>
2023-05-09 13:07:09 +08:00
Sara Arjona 1201526dbc MDL-78072 core: Fix upgrade failures with MariaDB
Backport of MDL-76722.

Co-authored by: Andrew Lyons <andrew@nicols.co.uk>

The 'enable' field doesn't exist, and this is causing some failures
with MariaDB (and probably with MySQL too).
2023-05-09 12:26:00 +08:00
Alex Morris 6ccb50ea9d MDL-78072 message_airnotifier: Add encrypted notifications
Backport of MDL-76722.

Co-authored by: Andrew Lyons <andrew@nicols.co.uk>
2023-05-09 12:25:47 +08:00
Alex Morris 25498b6b62 MDL-78072 user: Add new update_user_device_public_key webservice
Backport of MDL-76722.

Co-authored by: Andrew Lyons <andrew@nicols.co.uk>
2023-05-09 12:25:29 +08:00
Alex Morris a2179f028c MDL-78072 lib/db: Add new publickey field to user_devices table
Backport of MDL-76722.

Co-authored by: Andrew Lyons <andrew@nicols.co.uk>
2023-05-09 12:25:16 +08:00
Petr Skoda a7920b3860 MDL-76688 filter_text: prevent LaTeX3 syntax 2023-05-08 10:12:46 +02:00
AMOS bot 893fe94c89 Automatically generated installer lang files 2023-05-07 00:07:26 +00:00
Paul Holden ba904934ec MDL-78135 completion: include required criteria library in test. 2023-05-05 16:45:48 +01:00
Jake Dallimore b3feea921d MDL-76821 enrol_lti: prevent legacy launches via upgraded enrolment
If the enrolment instance (the 'published resource') has been upgraded
from LTI 1.1/2.0 to LTI 1.3 (i.e. a new instance was not created),
prevent legacy launches which may occur from old resource links. Only
LTI Advantage launches should be permitted through the method.
2023-05-05 17:11:09 +08:00
Jake Dallimore 22c346831e MDL-76821 enrol_lti: fix bug in repo causing return of legacy lti users
If the enrolment method is updated from an LTI 1.1/2.0 tool to an LTI
1.3 tool, it may have associated enrol_lti_users records not having
ltideploymentid values. These are legacy users and must not be returned
by the repository, which deals only with LTI 1.3 LTI users.
2023-05-05 17:11:06 +08:00
Jun Pataleta 0703a8db55 Merge branch 'MDL-76344-401' of https://github.com/stevandoMoodle/moodle into MOODLE_401_STABLE 2023-05-05 10:34:56 +08:00
Sara Arjona 176a323b4e weekly release 4.1.3+ 2023-05-04 12:42:50 +02:00
Eloy Lafuente (stronk7) 42f3874b80 MDL-78116 core: Run tests against Windows on GHA
Signed-off-by: Andrew Nicols <andrew@nicols.co.uk>
2023-05-04 15:40:18 +08:00
Stevani Andolo 1f07219332 MDL-76344 course: Fixed course image 'non image file' layout 2023-05-04 10:28:15 +08:00
Paul Holden 0c7e7645d5 MDL-78040 block_glossary_random: switch to block test generator.
Random failures after manually adding block instances (specifically
on slow/Oracle systems) can be entirely avoided by using the block
test generator for scenarios that are not specifically concerned with
manually adding blocks.
2023-05-03 17:16:22 +01:00
Laurent David 9117af48ac MDL-77781 core_completion: Completion view must be deleted too
When resetting a course, completion view entries should also be
deleted

Co-authored-by: Georg Moser <georg@moser.link>
2023-05-03 16:05:28 +02:00
Jun Pataleta d1c24b02ab Merge branch 'MDL-77398-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-03 20:36:18 +08:00
Jake Dallimore 79fe493ab4 Merge branch 'MDL-77384-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-03 16:52:45 +08:00
Jake Dallimore d66398c356 Merge branch 'MDL-78023-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-03 16:15:59 +08:00
Sara Arjona 6064ce62f5 Merge branch 'MDL-77312-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-03 09:59:22 +02:00
Ilya Tregubov 88113fc983 Merge branch 'MDL-77320-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-03 15:37:00 +08:00
Ilya Tregubov e19573d6bf Merge branch 'MDL-77810-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-03 13:08:52 +08:00
Ilya Tregubov 3994ab206b Merge branch 'MDL-77987_401' of https://github.com/AnupamaSarjoshi/moodle into MOODLE_401_STABLE 2023-05-03 12:40:34 +08:00
Ilya Tregubov b9d84d03ca Merge branch 'MDL-77546-401' of https://github.com/mihailges/moodle into MOODLE_401_STABLE 2023-05-03 10:57:29 +08:00
Jun Pataleta f8fdd198a9 Merge branch 'MDL-77394-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-03 10:31:00 +08:00
Jun Pataleta 394d592fab Merge branch 'MDL-78038-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-03 10:25:30 +08:00
Jun Pataleta 64c625e590 Merge branch 'MDL-78011-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-03 09:57:56 +08:00
Ilya Tregubov be0eb4369b Merge branch 'MDL-76835-401' of https://github.com/aya-saad1/moodle into MOODLE_401_STABLE 2023-05-03 09:29:59 +08:00
David Woloszyn b3424d620d MDL-77840 enrol_lti: Avoid passing nulls to base64_encode 2023-05-03 09:36:17 +10:00
Sara Arjona 767419571e Merge branch 'MDL-77858-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-05-02 16:49:53 +02:00
Sara Arjona aa63dca768 Merge branch 'MDL-77791_401' of https://github.com/dvdcastro/moodle into MOODLE_401_STABLE 2023-05-02 12:03:59 +02:00
Jun Pataleta 2dc4853ba5 Merge branch 'MDL-76857-401' of https://github.com/sarjona/moodle into MOODLE_401_STABLE 2023-05-02 15:44:55 +08:00
David Woloszyn 5ab74afad4 MDL-77840 lti: Avoid passing nulls to base64_encode 2023-05-02 13:52:12 +10:00
David Woloszyn f25ecd9bfa MDL-77840 backup: Avoid passing nulls to base64_encode 2023-05-02 13:52:12 +10:00
David Woloszyn 88496af850 MDL-77840 mnet: Avoid passing nulls to base64_encode 2023-05-02 13:52:12 +10:00
Ilya Tregubov d28386a29d Merge branch 'MDL-78025_401' of https://github.com/timhunt/moodle into MOODLE_401_STABLE 2023-05-02 11:43:30 +08:00
Jake Dallimore 4fa9ed7956 Merge branch 'MDL-78009-401' of https://github.com/marinaglancy/moodle into MOODLE_401_STABLE 2023-04-28 13:26:02 +08:00
Paul Holden 569a71daea MDL-77810 gradereport_summary: show localised plugin name in report. 2023-04-27 15:19:33 +01:00
Ilya Tregubov 4901ffe1ba weekly release 4.1.3+ 2023-04-27 19:28:38 +08:00
Sara Arjona b10f6f5036 MDL-76857 filter_displayh5p: Improve behat tests
Although the screenshots in the failures for some of the scenarios in
filter/displayh5p/tests/behat/h5p_filter.feature, like "Render a local
H5P file as teacher" were displaying the expected result, there were
some javascript errors (probably due to behat is quicker and the
iframes were not always ready).
I've added one extra step before accessing the iframe to give more
time to the H5P player to load and confirm the page is displayed
properly.
2023-04-27 11:21:36 +02:00
Paul Holden 30e419078f MDL-77320 tool_licensemanager: restrict exposure of user sesskey. 2023-04-27 09:32:37 +01:00
Mihail Geshoski f90a55979a MDL-77546 gradereport_singleview: Fix item order in grade item selector 2023-04-27 12:01:23 +08:00
Jun Pataleta 013735af51 Merge branch 'MDL-77883-401' of https://github.com/danghieu1407/moodle into MOODLE_401_STABLE 2023-04-27 11:08:54 +08:00
Jun Pataleta 6c890a6330 Merge branch 'MDL-77997_401' of https://github.com/timhunt/moodle into MOODLE_401_STABLE 2023-04-27 10:40:43 +08:00
Paul Holden 4f7dd1a54b MDL-78038 mod_bigbluebuttonbn: observe course format section config. 2023-04-26 16:10:52 +01:00
Tim Hunt de8607067f MDL-78025 questions: improve the comment on question_references.version 2023-04-26 15:45:12 +01:00
Tim Hunt 15ff1352c2 MDL-78025 questions: fix PHPdoc on question_require_capability_on 2023-04-26 15:45:00 +01:00
Tim Hunt 384ab12c55 MDL-78025 quiz: fix the quiz_questions_in_use logic 2023-04-26 15:44:54 +01:00
Tim Hunt 4a9c0b1410 MDL-78025 questions: questions_in_use should check question_references
This avoids the needs for plugins to do separate queries, which is
easier for them, and better performing.
2023-04-26 15:40:41 +01:00
Tim Hunt d2d9d762ad MDL-78025 question generator: make the behaviour less surprising
* The object returned by update_question is alwasy a new clone
  and the $question passed in will not be modified.

* The returned object has the fields like questionbankentryid and
  the ones related to versionning, so it is more like the data
  returned by question_bank::load_question_data.
2023-04-26 15:40:36 +01:00
Tim Hunt a9fa52295a MDL-78025 question: move hiding logic into question_delete_question
This logic belongs in the API, so it is applied consistently.

Also this avoids calling the expensive function questions_in_use
twice per question.
2023-04-26 15:38:57 +01:00
Paul Holden eca7a9ce2b MDL-78034 mod_bigbluebuttonbn: format instance group names. 2023-04-26 15:31:44 +01:00
Aya Saad bba6cc2f02 MDL-76835 theme: Fix unordered lists indentation 2023-04-26 13:57:49 +02:00
David Castro e5c89630af MDL-77791 data: Fix file attachment to search doc 2023-04-25 19:49:33 -05:00
Paul Holden 491ee67cd9 MDL-78023 tool_policy: cast timemodified column on table download. 2023-04-25 14:51:13 +01:00
Sara Arjona 9a3430f3fe Merge branch 'MDL-77313-401' of https://github.com/paulholden/moodle into MOODLE_401_STABLE 2023-04-25 15:18:42 +02:00
Paul Holden 79982a82a4 Merge branch 'MDL-78007-401' of https://github.com/HuongNV13/moodle into MOODLE_401_STABLE 2023-04-25 12:36:09 +01:00
Paul Holden 4ee0bdfda6 Merge branch 'MDL-73331_401_toolbrickfieldadvancedtab' of https://github.com/brickfield/moodle into MOODLE_401_STABLE 2023-04-25 11:54:17 +01:00
Paul Holden d10d553890 Merge branch 'MDL-77766-401-2' of https://github.com/junpataleta/moodle into MOODLE_401_STABLE 2023-04-25 09:38:37 +01:00
Paul Holden 22b481f73c MDL-77384 check: clarify statistics check name. 2023-04-25 09:04:41 +01:00
Paul Holden b867e4db40 MDL-78011 qbank_managecategories: require library for pagesize const. 2023-04-24 15:35:54 +01:00
Marina Glancy ccdaef7a68 MDL-78009 auth: require necessary files 2023-04-24 13:38:58 +01:00
Jun Pataleta 952c8770b6 MDL-77766 qtype_truefalse: Respect showstandardinstruction
* When showstandardinstruction is set to no, replace the standard
instruction with the generic "Answer" text for the answer options
fieldset's legend.
2023-04-24 20:11:20 +08:00
Jun Pataleta 132ac7486d MDL-77766 qtype_multichoice: Respect showstandardinstruction
* When showstandardinstruction is set to no, replace the standard
instruction with the generic "Answer" text for the answer options
fieldset's legend.
2023-04-24 20:11:20 +08:00
Tim Hunt a9f629fbf9 MDL-77997 questions: add back Export as XML to the preview screen
The used the exist in Moodle up to 3.11, but then was removed with
insufficient thought in 4.0 (because we had grander long-term plans
which still have not happened). Until those plans happen, this
commit adds the simple link back on the preview screen.
2023-04-24 11:23:47 +01:00
Anupama Sarjoshi c6708db889 MDL-77987 Backup: add progress tracking to some slow steps 2023-04-24 11:17:34 +01:00
Paul Holden 0d7393c7ee MDL-77312 files: exclude drafts when determining license usage. 2023-04-24 11:07:38 +01:00
Paul Holden f3ae5116d0 MDL-77313 restore: re-add field to indicate course/category search.
When the two restore forms for searching courses and categories were
converted to core templates in eb9935c9 they lost the named submit
button, which broke searching.
2023-04-24 11:04:26 +01:00
Paul Holden b01ce337f6 MDL-77858 javascript: correct core/notification exception imports. 2023-04-24 10:44:14 +01:00
Paul Holden bb433919cc MDL-77394 mod_forum: de-couple discussion pinned/starred options. 2023-04-24 10:38:01 +01:00
Paul Holden 1e16260ba7 MDL-77398 mod_assign: disable form submit protection for batch options. 2023-04-24 10:35:15 +01:00
Huong Nguyen 4ec5aac734 MDL-78007 tiny_media: Fix wrong condition for the Tiny Media
Including in this commit:
 - Switched to Tiny editor in manually_mark_question.feature
2023-04-24 15:59:32 +07:00
danghieu1407 b04fab1a2a MDL-77883 forms: fix display of client-side validation for textareas 2023-04-24 13:54:37 +07:00
Max Larkin 60a2798da7 MDL-73331 tool_brickfield: Update advanced tab display 2023-04-19 12:57:37 +01:00
Simey Lameze 5d000c6f35 MDL-77733 behat: enable Axe accessibility tests by default 2023-04-04 09:18:19 +08:00
Stefan Hanauska df0f9fa06d MDL-75623 backup: Encode pluginfile URLs 2023-04-01 09:09:39 +02:00
Mikhail Golenkov af7422edc0 MDL-77592 filter_tex: Use request dir instead of temp shared storage 2023-03-31 14:34:54 +11:00
Brendan Heywood 25edc77d10 MDL-77749 admin: Fix code error with unset config duration 2023-03-26 16:18:13 +11:00
Hiroto Kagotani 5fa1ec0dfa MDL-76936 course: purge course cache on resetting start date 2023-01-19 20:08:58 +09:00
279 changed files with 4846 additions and 1738 deletions
+144
View File
@@ -0,0 +1,144 @@
name: Windows Testing
on:
workflow_dispatch
env:
php: 8.1
jobs:
Grunt:
runs-on: windows-latest
steps:
- name: Set git to use LF
run: |
git config --global core.autocrlf false
git config --global core.eol lf
- name: Checking out code
uses: actions/checkout@v3
- name: Configuring node & npm
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
- name: Installing node stuff
run: npm ci
- name: Running grunt
run: npx grunt
- name: Looking for uncommitted changes
# Add all files to the git index and then run diff --cached to see all changes.
# This ensures that we get the status of all files, including new files.
# We ignore npm-shrinkwrap.json to make the tasks immune to npm changes.
run: |
git add .
git reset -- npm-shrinkwrap.json
git diff --cached --exit-code
PHPUnit:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
php: 8.1
# Ideally we should use mysql/mariadb, but they are 4x slower without tweaks and configuration
# so let's run only postgres (1.5h vs 6h) only, If some day we want to improve the mysql runs,
# this is the place to enable them.
# db: mysqli
db: pgsql
extensions: exif, fileinfo, gd, intl, pgsql, mysql, redis, soap, sodium
- os: windows-latest
php: 8.0
db: pgsql
extensions: exif, fileinfo, gd, intl, pgsql, mysql, redis, soap, sodium
steps:
- name: Setting up DB mysql
if: ${{ matrix.db == 'mysqli' }}
uses: shogo82148/actions-setup-mysql@v1
with:
mysql-version: 8.0
user: test
password: test
- name: Creating DB mysql
if: ${{ matrix.db == 'mysqli' }}
run: mysql --host 127.0.0.1 -utest -ptest -e 'CREATE DATABASE IF NOT EXISTS test COLLATE = utf8mb4_bin;';
- name: Setting up DB pgsql
if: ${{ matrix.db == 'pgsql' }}
run: |
# TODO: Remove these conf. modifications when php74 or php80 are lowest.
# Change to old md5 auth, because php73 does not support it.
# #password_encryption = scram-sha-256
(Get-Content "$env:PGDATA\postgresql.conf"). `
replace('#password_encryption = scram-sha-256', 'password_encryption = md5') | `
Set-Content "$env:PGDATA\postgresql.conf"
(Get-Content "$env:PGDATA\pg_hba.conf"). `
replace('scram-sha-256', 'md5') | `
Set-Content "$env:PGDATA\pg_hba.conf"
$pgService = Get-Service -Name postgresql*
Set-Service -InputObject $pgService -Status running -StartupType automatic
Start-Process -FilePath "$env:PGBIN\pg_isready" -Wait -PassThru
& $env:PGBIN\psql --command="CREATE USER test PASSWORD 'test'" --command="\du"
- name: Creating DB pgsql
if: ${{ matrix.db == 'pgsql' }}
run: |
& $env:PGBIN\createdb --owner=test test
$env:PGPASSWORD = 'test'
& $env:PGBIN\psql --username=test --host=localhost --list test
- name: Configuring git vars
uses: rlespinasse/github-slug-action@v4
- name: Setting up PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: ${{ matrix.extensions }}
ini-values: max_input_vars=5000
coverage: none
- name: Set git to use LF
run: |
git config --global core.autocrlf false
git config --global core.eol lf
- name: Checking out code from ${{ env.GITHUB_REF_SLUG }}
uses: actions/checkout@v3
# Needs to be done after php is available, git configured and Moodle checkout has happened.
- name: Setting up moodle-exttests service
run: |
git clone https://github.com/moodlehq/moodle-exttests.git
nssm install php-built-in C:\tools\php\php.exe -S localhost:8080 -t D:\a\moodle\moodle\moodle-exttests
nssm start php-built-in
- name: Setting up redis service
run: |
choco install redis-64 --version 3.0.503 --no-progress
nssm install redis redis-server
nssm start redis
- name: Setting up PHPUnit
env:
dbtype: ${{ matrix.db }}
shell: bash
run: |
echo "pathtophp=$(which php)" >> $GITHUB_ENV # Inject installed pathtophp to env. The template config needs it.
cp .github/workflows/config-template.php config.php
mkdir ../moodledata
php admin/tool/phpunit/cli/init.php --no-composer-self-update
- name: Running PHPUnit tests
env:
dbtype: ${{ matrix.db }}
phpunit_options: ${{ secrets.phpunit_options }}
run: vendor/bin/phpunit $phpunit_options
+2 -3
View File
@@ -30,10 +30,9 @@
* @param {String} srcPath the matched src path
* @return {String} The rewritten destination path.
*/
const babelRename = function(destPath, srcPath) {
destPath = srcPath.replace('src', 'build');
destPath = destPath.replace('.js', '.min.js');
destPath = srcPath.replace(`amd/src`, `amd/build`);
destPath = destPath.replace(/\.js$/, '.min.js');
return destPath;
};
+1
View File
@@ -3751,6 +3751,7 @@
<VENDOR name="oracle" version="19" />
</DATABASE>
<PHP version="7.4.0" level="required">
<RESTRICT function="restrict_php_version_82" message="unsupportedphpversion82" />
</PHP>
<PCREUNICODE level="optional">
<FEEDBACK>
+10 -2
View File
@@ -6,7 +6,7 @@ require_once(__DIR__ . '/../../config.php');
require_once($CFG->libdir.'/adminlib.php');
include_once($CFG->dirroot.'/mnet/lib.php');
$sort = optional_param('sort', 'username', PARAM_ALPHA);
$sort = optional_param('sort', 'username', PARAM_ALPHAEXT);
$dir = optional_param('dir', 'ASC', PARAM_ALPHA);
$page = optional_param('page', 0, PARAM_INT);
$perpage = optional_param('perpage', 30, PARAM_INT);
@@ -153,7 +153,15 @@ foreach ($columns as $column) {
$headings[$column] = "<a href=\"?sort=$column&amp;dir=$columndir&amp;\">".$string[$column]."</a>$columnicon";
}
$headings['delete'] = '';
$acl = $DB->get_records('mnet_sso_access_control', null, "$sort $dir", '*'); //, $page * $perpage, $perpage);
$sortorder = get_safe_orderby([
'username' => 'username',
'mnet_host_id' => 'mnet_host_id',
'access' => 'accessctrl',
'default' => 'username',
], $sort, $dir, false);
$acl = $DB->get_records('mnet_sso_access_control', null, $sortorder);
$aclcount = $DB->count_records('mnet_sso_access_control');
if (!$acl) {
+12 -10
View File
@@ -49,7 +49,7 @@ list($options, $unrecognized) = cli_get_params(
'torun' => 0,
'optimize-runs' => '',
'add-core-features-to-theme' => false,
'axe' => false,
'axe' => null,
'disable-composer' => false,
'composer-upgrade' => true,
'composer-self-update' => true,
@@ -69,7 +69,7 @@ Behat utilities to initialise behat tests
Usage:
php init.php [--parallel=value [--maxruns=value] [--fromrun=value --torun=value]]
[--axe] [-o | --optimize-runs] [-a | --add-core-features-to-theme]
[--no-axe] [-o | --optimize-runs] [-a | --add-core-features-to-theme]
[--no-composer-self-update] [--no-composer-upgrade]
[--disable-composer]
[--help]
@@ -79,7 +79,7 @@ Options:
-m, --maxruns Max parallel processes to be executed at one time
--fromrun Execute run starting from (Used for parallel runs on different vms)
--torun Execute run till (Used for parallel runs on different vms)
--axe Include axe accessibility tests
--no-axe Disable axe accessibility tests.
-o, --optimize-runs
Split features with specified tags in all parallel runs.
@@ -102,7 +102,7 @@ Options:
Example from Moodle root directory:
\$ php admin/tool/behat/cli/init.php --parallel=2
More info in http://docs.moodle.org/dev/Acceptance_testing#Running_tests
More info in https://moodledev.io/general/development/tools/behat/running
";
if (!empty($options['help'])) {
@@ -110,6 +110,12 @@ if (!empty($options['help'])) {
exit(0);
}
if ($options['axe']) {
echo "Axe accessibility tests are enabled by default, to disable them, use the --no-axe option.\n";
} else if ($options['axe'] === false) {
echo "Axe accessibility tests have been disabled.\n";
}
// Check which util file to call.
$utilfile = 'util_single_run.php';
$commandoptions = "";
@@ -118,9 +124,7 @@ if ($options['parallel'] && $options['parallel'] > 1) {
$utilfile = 'util.php';
// Sanitize all input options, so they can be passed to util.
foreach ($options as $option => $value) {
if ($value) {
$commandoptions .= " --$option=\"$value\"";
}
$commandoptions .= behat_get_command_flags($option, $value);
}
} else {
// Only sanitize options for single run.
@@ -130,9 +134,7 @@ if ($options['parallel'] && $options['parallel'] > 1) {
];
foreach ($cmdoptionsforsinglerun as $option) {
if (!empty($options[$option])) {
$commandoptions .= " --$option='$options[$option]'";
}
$commandoptions .= behat_get_command_flags($option, $options[$option]);
}
}
+1 -1
View File
@@ -87,7 +87,7 @@ Options:
Example from Moodle root directory:
\$ php admin/tool/behat/cli/run.php --tags=\"@javascript\"
More info in http://docs.moodle.org/dev/Acceptance_testing#Running_tests
More info in https://moodledev.io/general/development/tools/behat/running
";
if (!empty($options['help'])) {
+5 -10
View File
@@ -57,7 +57,7 @@ list($options, $unrecognized) = cli_get_params(
'torun' => 0,
'optimize-runs' => '',
'add-core-features-to-theme' => false,
'axe' => false,
'axe' => true,
),
array(
'h' => 'help',
@@ -73,7 +73,7 @@ $help = "
Behat utilities to manage the test environment
Usage:
php util.php [--install|--drop|--enable|--disable|--diag|--updatesteps|--axe|--help] [--parallel=value [--maxruns=value]]
php util.php [--install|--drop|--enable|--disable|--diag|--updatesteps|--no-axe|--help] [--parallel=value [--maxruns=value]]
Options:
--install Installs the test environment for acceptance tests
@@ -82,7 +82,7 @@ Options:
--disable Disables test environment
--diag Get behat test environment status code
--updatesteps Update feature step file.
--axe Include axe accessibility tests
--no-axe Disable axe accessibility tests.
-j, --parallel Number of parallel behat run operation
-m, --maxruns Max parallel processes to be executed at one time.
@@ -94,7 +94,7 @@ Options:
Example from Moodle root directory:
\$ php admin/tool/behat/cli/util.php --enable --parallel=4
More info in http://docs.moodle.org/dev/Acceptance_testing#Running_tests
More info in https://moodledev.io/general/development/tools/behat/running
";
if (!empty($options['help'])) {
@@ -320,12 +320,7 @@ function commands_to_execute($options) {
}
foreach ($extraoptions as $option => $value) {
if ($options[$option]) {
$extra .= " --$option";
if ($value) {
$extra .= "=\"$value\"";
}
}
$extra .= behat_get_command_flags($option, $value);
}
if (empty($options['parallel'])) {
+4 -4
View File
@@ -52,7 +52,7 @@ list($options, $unrecognized) = cli_get_params(
'updatesteps' => false,
'optimize-runs' => '',
'add-core-features-to-theme' => false,
'axe' => false,
'axe' => true,
),
array(
'h' => 'help',
@@ -79,7 +79,7 @@ Options:
--disable Disables test environment
--diag Get behat test environment status code
--updatesteps Update feature step file.
--axe Include axe accessibility tests
--no-axe Disable axe accessibility tests.
-o, --optimize-runs Split features with specified tags in all parallel runs.
-a, --add-core-features-to-theme Add all core features to specified theme's
@@ -89,7 +89,7 @@ Options:
Example from Moodle root directory:
\$ php admin/tool/behat/cli/util_single_run.php --enable
More info in http://docs.moodle.org/dev/Acceptance_testing#Running_tests
More info in https://moodledev.io/general/development/tools/behat/running
";
if (!empty($options['help'])) {
@@ -186,7 +186,7 @@ if ($options['install']) {
behat_config_manager::set_behat_run_config_value('behatsiteenabled', 1);
}
// Define whether to run Behat with axe tests.
// Configure axe according to option.
behat_config_manager::set_behat_run_config_value('axe', $options['axe']);
// Enable test mode.
+1 -1
View File
@@ -30,7 +30,7 @@ $string['errordataroot'] = '$CFG->behat_dataroot is not set or is invalid.';
$string['errorsetconfig'] = '$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot need to be set in config.php.';
$string['erroruniqueconfig'] = '$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot values need to be different than $CFG->dataroot, $CFG->prefix, $CFG->wwwroot, $CFG->phpunit_dataroot and $CFG->phpunit_prefix values.<br/>Or, if $CFG->behat_prefix is the same, $CFG->behat_dbname or $CFG->behat_dbhost need to be different from $CFG->phpunit_dbname and $CFG->phpunit_dbhost and from $CFG->dbname and $CFG->dbhost.';
$string['fieldvalueargument'] = 'Field value arguments';
$string['fieldvalueargument_help'] = 'This argument should be completed by a field value. There are many field types, including simple ones like checkboxes, selects or textareas, or complex ones like date selectors. See the dev documentation <a href="https://docs.moodle.org/dev/Acceptance_testing" target="_blank">Acceptance_testing</a> for details of expected field values.';
$string['fieldvalueargument_help'] = 'This argument should be completed by a field value. There are many field types, including simple ones like checkboxes, selects or textareas, or complex ones like date selectors. See the dev documentation <a href="https://moodledev.io/general/development/tools/behat" target="_blank">Acceptance_testing</a> for details of expected field values.';
$string['giveninfo'] = 'Given. Processes to set up the environment';
$string['infoheading'] = 'Info';
$string['installinfo'] = 'Read {$a} for installation and tests execution info';
+3 -2
View File
@@ -184,9 +184,10 @@ class tool_behat_renderer extends plugin_renderer_base {
// Info.
$installurl = behat_command::DOCS_URL;
$installlink = html_writer::tag('a', $installurl, array('href' => $installurl, 'target' => '_blank'));
$writetestsurl = 'https://docs.moodle.org/dev/Writing acceptance tests';
$writetestsurl = 'https://moodledev.io/general/development/tools/behat/writing';
$writetestslink = html_writer::tag('a', $writetestsurl, array('href' => $writetestsurl, 'target' => '_blank'));
$writestepsurl = 'https://docs.moodle.org/dev/Writing_new_acceptance_test_step_definitions';
$writestepsurl = 'https://moodledev.io/general/development/tools/behat/writing#' .
'writing-new-acceptance-test-step-definitions';
$writestepslink = html_writer::tag('a', $writestepsurl, array('href' => $writestepsurl, 'target' => '_blank'));
$infos = array(
get_string('installinfo', 'tool_behat', $installlink),
+2
View File
@@ -1,5 +1,7 @@
This files describes API changes in the tool_behat code.
=== 4.1.4 ===
* Behat is initialised with Axe accessibility tests enabled by default, if you want to disable them please use the --no-axe option.
=== 3.7 ===
* Behat will now look for behat step definitions in the current
theme and any parents the theme may have.
+1 -1
View File
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2022112801; // The current plugin version (Date: YYYYMMDDXX).
$plugin->version = 2022112802; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2022111800; // Requires this Moodle version.
$plugin->component = 'tool_behat'; // Full name of the plugin (used for diagnostics)
@@ -16,6 +16,8 @@
namespace tool_brickfield\local\tool;
use tool_brickfield\manager;
/**
* Class advanced.
*
@@ -51,12 +53,36 @@ class advanced extends tool {
return 'advanced';
}
/**
* Builds context data used to render a single grid item on the advanced page.
* @param string $icon
* @param string $heading
* @param string $content
* @return array
*/
protected function get_grid_item_context(string $icon, string $heading, string $content): array {
return [
"icon" => "pix/i/$icon.png",
"iconalt" => get_string("icon:$icon", manager::PLUGINNAME),
"heading" => get_string($heading, manager::PLUGINNAME),
"content" => get_string($content, manager::PLUGINNAME)
];
}
/**
* Return the data for renderer / template display.
* @return \stdClass
*/
protected function fetch_data(): \stdClass {
$data = (object)[
'griditems' => [
$this->get_grid_item_context("analytics-custom", "headingone", "contentone"),
$this->get_grid_item_context("tools-custom", "headingtwo", "contenttwo"),
$this->get_grid_item_context("file-edit-custom", "headingthree", "contentthree"),
$this->get_grid_item_context("search-plus-custom", "headingfour", "contentfour"),
$this->get_grid_item_context("wand-magic-custom", "headingfive", "contentfive"),
$this->get_grid_item_context("hands-helping-custom", "headingsix", "contentsix"),
],
'valid' => true,
'error' => '',
];
@@ -270,3 +270,9 @@ $string['headingthree'] = 'Accessible file formats';
$string['headingfour'] = 'Focus effort';
$string['headingfive'] = 'HTML fixes';
$string['headingsix'] = 'Performance support';
$string['icon:analytics-custom'] = 'Analytics icon';
$string['icon:tools-custom'] = 'Tools icon';
$string['icon:file-edit-custom'] = 'Edit file icon';
$string['icon:search-plus-custom'] = 'Search icon';
$string['icon:wand-magic-custom'] = 'Magic wand icon';
$string['icon:hands-helping-custom'] = 'Helping hands icon';
+48 -71
View File
@@ -10,49 +10,22 @@
/* Advanced Page CSS */
@media only screen and (min-width: 768px) {
.tool_brickfield_grid_item {
width: 33.333333333333336%;
}
#tool_brickifeld_advanced_page {
margin: 20px 20px 20px 10px;
}
@media only screen and (max-width: 767px) {
.tool_brickfield_grid_item {
width: 100%;
}
}
.tool_brickfield_grid {
max-width: 1140px;
margin-right: auto;
margin-left: auto;
}
.tool_brickfield_grid_inner {
width: 100%;
.tool_brickfield_banner_heading {
display: flex;
flex-wrap: wrap;
}
.tool_brickfield_grid_item_shape {
height: 96%;
background-color: #fff;
padding: 10% 10% 10% 10%;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.05);
margin: 1% 1% 1% 1%;
border-radius: 15px 15px 15px 70px;
}
.tool_brickfield_grid_display {
display: grid;
grid-template-columns: 0.6fr 1.4fr 1fr;
grid-template-rows: 1fr 1fr;
}
.tool_brickfield_grid_spacing {
justify-content: space-between;
margin-bottom: 20px;
margin-left: auto;
margin-right: 1em;
}
.tool_brickfield_banner_heading > img {
height: 50px;
}
.tool_brickfield_banner > p {
text-align: center;
}
.tool_brickfield_grid_heading {
@@ -60,59 +33,63 @@
font-size: 20px;
font-weight: 400;
color: #040d29;
padding-bottom: 4px;
}
.tool_brickfield_grid_content {
color: #040d29;
font-size: 16px;
line-height: 1.4em;
margin: 0;
padding: 0;
}
.tool_brickfield_logo_height {
height: 3em;
}
.tool_brickfield_logo_position {
padding-bottom: 2em;
}
.tool_brickfield_icon_position {
line-height: 1;
font-size: 50px;
}
.tool_brickfield_icon_height {
.tool_brickfield_grid_heading > img {
height: 30px;
padding-right: 16px;
}
.tool_brickfield_center_button {
text-align: center;
.tool_brickfield_grid {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin: 20px 20px 30px;
}
.tool_brickfield_advanced_button {
.tool_brickfield_grid > div {
margin: 10px;
padding: 20px;
flex-basis: 30%;
border: 1px solid grey;
max-width: 500px;
min-width: 250px;
flex-grow: 4;
border-radius: 15px 15px 15px 50px;
}
.tool_brickfield_grid_heading {
display: flex;
}
.tool_brickfield_footer > a {
font-size: 18px;
letter-spacing: 1px;
color: #fff;
background-color: #4054b2;
border-style: solid;
border-color: #fff;
border-radius: 5px 5px 5px 5px;
padding: 15px 50px 15px 50px;
border-radius: 5px;
padding: 15px 50px;
transition: 0.3s;
margin-bottom: 20px;
}
.tool_brickfield_advanced_button:hover {
.tool_brickfield_footer {
text-align: center;
}
.tool_brickfield_footer > a:hover {
color: #fff;
background-color: #253687;
text-decoration: none;
}
.tool_brickfield_banner_heading {
margin-bottom: 1.5em;
}
.tool_brickfield_banner {
height: 10em;
padding: 2.5em;
margin-bottom: 2em;
.tool_brickfield_footer > p {
margin-bottom: 25px;
}
+31 -173
View File
@@ -21,184 +21,42 @@
Example context (json):
{
"pagetitle": "Advanced"
"griditems": [
{
"icon": "some-icon-name",
"iconalt": "some-icon-alt",
"heading": "some-heading",
"content": "some-content"
}
]
}
}}
<link rel="stylesheet" href="styles.css">
<h3>{{{pagetitle}}}</h3>
<section class="tool_brickfield_banner">
<div class="tool_brickfield_banner_heading">
<img src="pix/i/Brickfield-logo-black.png" alt="Brickfield Logo" class="pull-right tool_brickfield_logo_height">
<h2>{{# str }}bannerheadingone, tool_brickfield{{/ str }}</h2>
<div id="tool_brickifeld_advanced_page">
<div class="tool_brickfield_banner">
<div class="tool_brickfield_banner_heading">
<h2>{{#str}} bannerheadingone, tool_brickfield{{/str}}</h2>
<img src="pix/i/Brickfield-logo-black.png" alt="Brickfield Logo">
</div>
<p>{{#str}} bannercontentone, tool_brickfield, https://www.brickfield.ie/moodle_talk_to_sales/{{/str}}</p>
</div>
<p>
{{# str }}bannercontentone, tool_brickfield, https://www.brickfield.ie/moodle_talk_to_sales/{{/ str }}
</p>
</section>
<div class="tool_brickfield_grid">
<div class="tool_brickfield_grid_inner">
<div class="tool_brickfield_grid">
{{#griditems}}
<div class="tool_brickfield_grid_item">
<div class="tool_brickfield_grid_item_shape">
<div class="tool_brickfield_grid_display" style="grid-template-areas: 'al ar ar' 'al ar ar'; grid-area: a;">
<div style="grid-area: al;">
<div class="tool_brickfield_grid_spacing">
<span class="tool_brickfield_icon_position">
<img src="pix/i/analytics-custom.png" alt="Analytics icon" class="tool_brickfield_icon_height">
</span>
</div>
</div>
<div style="grid-area: ar;">
<h3>
<span class="tool_brickfield_grid_heading">
{{# str }}headingone, tool_brickfield{{/ str }}
</span>
</h3>
<p class="tool_brickfield_grid_content">
{{# str }}contentone, tool_brickfield{{/ str }}
</p>
</div>
</div>
<div class="tool_brickfield_grid_heading">
<img src="{{icon}}" alt="{{iconalt}}">
<h3>{{heading}}</h3>
</div>
<div class="tool_brickfield_grid_content">
<p>{{content}}</p>
</div>
</div>
<div class="tool_brickfield_grid_item">
<div class="tool_brickfield_grid_item_shape">
<div class="tool_brickfield_grid_display" style="grid-template-areas: 'bl br br' 'bl br br'; grid-area: b;">
<div style="grid-area: bl;">
<div class="tool_brickfield_grid_spacing">
<span class="tool_brickfield_icon_position">
<img src="pix/i/tools-custom.png" alt="Tools icon" class="tool_brickfield_icon_height">
</span>
</div>
</div>
<div style="grid-area: br;">
<h3>
<span class="tool_brickfield_grid_heading">
{{# str }}headingtwo, tool_brickfield{{/ str }}
</span>
</h3>
<p class="tool_brickfield_grid_content">
{{# str }}contenttwo, tool_brickfield{{/ str }}
</p>
</div>
</div>
</div>
</div>
<div class="tool_brickfield_grid_item">
<div class="tool_brickfield_grid_item_shape">
<div class="tool_brickfield_grid_display" style="grid-template-areas: 'cl cr cr' 'cl cr cr'; grid-area: c;">
<div style="grid-area: cl;">
<div class="tool_brickfield_grid_spacing">
<span class="tool_brickfield_icon_position">
<img src="pix/i/file-edit-custom.png" alt="Edit file icon" class="tool_brickfield_icon_height">
</span>
</div>
</div>
<div style="grid-area: cr;">
<h3>
<span class="tool_brickfield_grid_heading">
{{# str }}headingthree, tool_brickfield{{/ str }}
</span>
</h3>
<p class="tool_brickfield_grid_content">
{{# str }}contentthree, tool_brickfield{{/ str }}
</p>
</div>
</div>
</div>
</div>
<div class="tool_brickfield_grid_item">
<div class="tool_brickfield_grid_item_shape">
<div class="tool_brickfield_grid_display" style="grid-template-areas: 'dl dr dr' 'dl dr dr'; grid-area: d;">
<div style="grid-area: dl;">
<div class="tool_brickfield_grid_spacing">
<span class="tool_brickfield_icon_position">
<img src="pix/i/search-plus-custom.png" alt="Search icon" class="tool_brickfield_icon_height">
</span>
</div>
</div>
<div style="grid-area: dr;">
<h3>
<span class="tool_brickfield_grid_heading">
{{# str }}headingfour, tool_brickfield{{/ str }}
</span>
</h3>
<p class="tool_brickfield_grid_content">
{{# str }}contentfour, tool_brickfield{{/ str }}
</p>
</div>
</div>
</div>
</div>
<div class="tool_brickfield_grid_item">
<div class="tool_brickfield_grid_item_shape">
<div class="tool_brickfield_grid_display" style="grid-template-areas: 'el er er' 'el er er'; grid-area: e;">
<div style="grid-area: el;">
<div class="tool_brickfield_grid_spacing">
<span class="tool_brickfield_icon_position">
<img src="pix/i/wand-magic-custom.png" alt="Magic wand icon" class="tool_brickfield_icon_height">
</span>
</div>
</div>
<div style="grid-area: er;">
<h3>
<span class="tool_brickfield_grid_heading">
{{# str }}headingfive, tool_brickfield{{/ str }}
</span>
</h3>
<p class="tool_brickfield_grid_content">
{{# str }}contentfive, tool_brickfield{{/ str }}
</p>
</div>
</div>
</div>
</div>
<div class="tool_brickfield_grid_item">
<div class="tool_brickfield_grid_item_shape">
<div class="tool_brickfield_grid_display" style="grid-template-areas: 'fl fr fr' 'fl fr fr'; grid-area: f;">
<div style="grid-area: fl;">
<div class="tool_brickfield_grid_spacing">
<span class="tool_brickfield_icon_position">
<img src="pix/i/hands-helping-custom.png" alt="Helping hands icon" class="tool_brickfield_icon_height">
</span>
</div>
</div>
<div style="grid-area: fr;">
<h3>
<span class="tool_brickfield_grid_heading">
{{# str }}headingsix, tool_brickfield{{/ str }}
</span>
</h3>
<p class="tool_brickfield_grid_content">
{{# str }}contentsix, tool_brickfield{{/ str }}
</p>
</div>
</div>
</div>
</div>
{{/griditems}}
</div>
<div class="tool_brickfield_footer">
<h2 class="accesshide">{{#str}} footerheading, tool_brickfield {{/str}}</h2>
<p>{{#str}}bannercontenttwo, tool_brickfield{{/str}}</p>
<a href="https://www.brickfield.ie/moodle_free_demo/">
{{#str}}buttonone, tool_brickfield{{/str}}
</a>
</div>
</div>
<section class="tool_brickfield_banner">
<h2 class="accesshide">{{# str }}footerheading, tool_brickfield{{/ str }}</h2>
<p>
{{# str }}bannercontenttwo, tool_brickfield{{/ str }}
</p>
<div class="tool_brickfield_center_button">
<form action="https://www.brickfield.ie/moodle_free_demo/">
<button class="tool_brickfield_advanced_button">
{{# str }}buttonone, tool_brickfield{{/ str }}
</button>
</form>
</div>
</section>
+1
View File
@@ -179,6 +179,7 @@ class api {
AND ra.roleid = :roleid
AND ra.userid = :userid
WHERE cm.cohortid ' . $cohortsql . '
AND u.deleted = 0
AND ra.id IS NULL';
$toadd = $DB->get_records_sql($sql, $params);
@@ -0,0 +1,43 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace tool_cohortroles;
use core\event\user_deleted;
/**
* Plugin event observer callbacks
*
* @package tool_cohortroles
* @copyright 2023 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class observers {
/**
* User deleted event, remove cohort role assignments specific to them
*
* @param user_deleted $event
*/
public static function user_deleted(user_deleted $event): void {
$cohortroleassignments = cohort_role_assignment::get_records(['userid' => $event->objectid]);
foreach ($cohortroleassignments as $cohortroleassignment) {
$cohortroleassignment->delete();
}
}
}
+32
View File
@@ -0,0 +1,32 @@
<?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/>.
/**
* Plugin event observers
*
* @package tool_cohortroles
* @copyright 2023 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$observers = [
[
'eventname' => '\core\event\user_deleted',
'callback' => '\tool_cohortroles\observers::user_deleted',
],
];
+8
View File
@@ -50,5 +50,13 @@ function xmldb_tool_cohortroles_upgrade($oldversion) {
// Automatically generated Moodle v4.1.0 release upgrade line.
// Put any upgrade step following this.
if ($oldversion < 2022112801) {
// Delete any tool_cohortroles mappings for users who no longer exist.
$DB->delete_records_select('tool_cohortroles', 'userid NOT IN (SELECT id FROM {user} WHERE deleted = 0)');
// Cohortroles savepoint reached.
upgrade_plugin_savepoint(true, 2022112801, 'tool', 'cohortroles');
}
return true;
}
+1 -2
View File
@@ -24,8 +24,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX).
$plugin->version = 2022112801; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2022111800; // Requires this Moodle version.
$plugin->component = 'tool_cohortroles'; // Full name of the plugin (used for diagnostics).
@@ -24,6 +24,8 @@
namespace tool_langimport;
use moodle_url;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/filelib.php');
require_once($CFG->libdir.'/componentlib.class.php');
@@ -59,6 +61,25 @@ class controller {
$this->availablelangs = $this->installer->get_remote_list_of_languages();
}
/**
* Redirect to the specified url, outputting any required messages.
*
* @param moodle_url $url
*/
public function redirect(moodle_url $url): void {
if ($this->info) {
$info = implode('<br />', $this->info);
\core\notification::success($info);
}
if ($this->errors) {
$info = implode('<br />', $this->errors);
\core\notification::error($info);
}
redirect($url);
}
/**
* Install language packs provided
*
@@ -112,6 +133,18 @@ class controller {
public function uninstall_language($lang) {
global $CFG;
$lang = clean_param($lang, PARAM_LANG);
if ($lang === '') {
// Do not allow uninstallation of invalid languages.
// Note: PARAM_LANG returns an empty string for invalid validation.
return false;
}
if ($lang === 'en') {
// Never allow removal of the default langauge.
return false;
}
$dest1 = $CFG->dataroot.'/lang/'.$lang;
$dest2 = $CFG->dirroot.'/lang/'.$lang;
$rm1 = false;
+10 -2
View File
@@ -43,6 +43,13 @@ $uninstalllang = optional_param_array('uninstalllang', array(), PARAM_LANG)
$confirmtounistall = optional_param('confirmtouninstall', '', PARAM_SAFEPATH); // uninstallation confirmation
$purgecaches = optional_param('purgecaches', false, PARAM_BOOL); // explicit caches reset
// Filter the uninstall language list.
// If the list contains a language which is not installed, it is replaced with an empty string.
// When we try to uninstall an empty string, we uninstall every language.
$uninstalllang = array_filter($uninstalllang, function($lang) {
return !empty($lang);
});
if ($purgecaches) {
require_sesskey();
get_string_manager()->reset_caches();
@@ -69,11 +76,12 @@ if (($mode == INSTALLATION_OF_SELECTED_LANG) and confirm_sesskey() and !empty($p
if (is_array($pack) && count($pack) > 1) {
// Installing multiple languages can take a while - perform it asynchronously in the background.
$controller->schedule_languagepacks_installation($pack);
$controller->redirect($PAGE->url);
} else {
// Single language pack to be installed synchronously. It should be reasonably quick and can be used for debugging, too.
core_php_time_limit::raise();
$controller->install_languagepacks($pack);
$controller->redirect($PAGE->url);
}
}
@@ -102,7 +110,7 @@ if ($mode == DELETION_OF_SELECTED_LANG and (!empty($uninstalllang) or !empty($co
foreach ($uninstalllang as $ulang) {
$controller->uninstall_language($ulang);
}
$controller->redirect($PAGE->url);
}
}
@@ -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/>.
namespace tool_langimport;
/**
* Tests for \tool_langimport\locale class.
*
* @package tool_langimport
* @category test
* @coversDefaultClass \tool_langimport\controller
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class controller_test extends \advanced_testcase {
/**
* Test uninstall of language with invalid values.
*
* @covers ::uninstall_lang
* @dataProvider uninstall_lang_invalid_provider
* @params string $lang
*/
public function test_uninstall_lang_invalid(string $lang): void {
global $CFG;
$controller = new controller();
$this->assertFalse($controller->uninstall_language($lang));
$this->assertFileExists("{$CFG->dataroot}/lang");
$this->assertFileExists("{$CFG->dirroot}/lang/en");
}
/**
* Data provider for uninstall_lang tests with invalid values.
*
* @return array
*/
public function uninstall_lang_invalid_provider(): array {
return [
'Empty string' => [''],
'Meaningless empty string' => [' '],
'Default language' => ['en'],
'Invalid language string' => ['swedish'],
];
}
}
+5 -20
View File
@@ -14,19 +14,11 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* License manager helper class.
*
* @package tool_licensemanager
* @copyright 2019 Tom Dickman <tomdickman@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_licensemanager;
use moodle_url;
defined('MOODLE_INTERNAL') || die();
/**
* License manager helper class.
@@ -48,12 +40,7 @@ class helper {
* @return \moodle_url
*/
public static function get_licensemanager_url() : moodle_url {
global $CFG;
$url = new moodle_url($CFG->wwwroot . self::MANAGER_PATH,
['sesskey' => sesskey()]);
return $url;
return new moodle_url(self::MANAGER_PATH);
}
/**
@@ -90,9 +77,8 @@ class helper {
* @return \moodle_url
*/
public static function get_create_license_url() : moodle_url {
$url = new moodle_url(self::MANAGER_PATH,
['action' => manager::ACTION_CREATE, 'sesskey' => sesskey()]);
$url = self::get_licensemanager_url();
$url->params(['action' => manager::ACTION_CREATE]);
return $url;
}
@@ -104,9 +90,8 @@ class helper {
* @return \moodle_url
*/
public static function get_update_license_url(string $licenseshortname) : moodle_url {
$url = new moodle_url(self::MANAGER_PATH,
['action' => manager::ACTION_UPDATE, 'license' => $licenseshortname, 'sesskey' => sesskey()]);
$url = self::get_licensemanager_url();
$url->params(['action' => manager::ACTION_UPDATE, 'license' => $licenseshortname]);
return $url;
}
+9 -10
View File
@@ -14,22 +14,12 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* License manager.
*
* @package tool_licensemanager
* @copyright 2019 Tom Dickman <tomdickman@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_licensemanager;
use tool_licensemanager\form\edit_license;
use license_manager;
use stdClass;
defined('MOODLE_INTERNAL') || die();
/**
* License manager, main controller for tool_licensemanager.
*
@@ -95,18 +85,25 @@ class manager {
}
$viewmanager = true;
$redirect = helper::get_licensemanager_url();
switch ($action) {
case self::ACTION_DISABLE:
require_sesskey();
license_manager::disable($license);
redirect($redirect);
break;
case self::ACTION_ENABLE:
require_sesskey();
license_manager::enable($license);
redirect($redirect);
break;
case self::ACTION_DELETE:
require_sesskey();
license_manager::delete($license);
redirect($redirect);
break;
case self::ACTION_CREATE:
@@ -116,7 +113,9 @@ class manager {
case self::ACTION_MOVE_UP:
case self::ACTION_MOVE_DOWN:
require_sesskey();
$this->change_license_order($action, $license);
redirect($redirect);
break;
case self::ACTION_VIEW_LICENSE_MANAGER:
-6
View File
@@ -28,15 +28,9 @@ require_once($CFG->libdir . '/licenselib.php');
require_admin();
$returnurl = \tool_licensemanager\helper::get_licensemanager_url();
$action = optional_param('action', '', PARAM_ALPHANUMEXT);
$license = optional_param('license', '', PARAM_SAFEDIR);
if (!confirm_sesskey()) {
redirect($returnurl);
}
// Route via the manager.
$licensemanager = new \tool_licensemanager\manager();
$PAGE->set_context(context_system::instance());
@@ -128,3 +128,18 @@ Feature: Manage competency frameworks
Then I should see "Competency framework updated"
And I should see "Science Year-6 Edited"
And I should see "sc-y-6"
Scenario: Duplicate a competency framework
Given the following lp "frameworks" exist:
| shortname | idnumber |
| CF1 | CF1 |
And the following lp "competencies" exist:
| shortname | framework |
| C1 | CF1 |
And I navigate to "Competencies > Competency frameworks" in site administration
# Duplicate the selected competency framework
When I click on "Duplicate" of edit menu in the "CF1" row
# Confirm that the selected framework was duplicated by clicking on the link
And I click on "CF1 (copy) (CF1_1)" "link"
# Confirm that the corresponding competency also exists in the duplicated framework
Then I should see "C1"
+53 -134
View File
@@ -14,17 +14,11 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Event documentation
*
* @package tool_monitor
* @copyright 2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_monitor;
defined('MOODLE_INTERNAL') || die();
use core_component;
use ReflectionClass;
/**
* Class for returning event information.
@@ -35,126 +29,6 @@ defined('MOODLE_INTERNAL') || die();
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class eventlist {
/**
* Return all of the core event files.
*
* @return array Core events.
*/
protected static function get_core_eventlist() {
global $CFG;
// Disable developer debugging as deprecated events will fire warnings.
// Setup backup variables to restore the following settings back to what they were when we are finished.
$debuglevel = $CFG->debug;
$debugdisplay = $CFG->debugdisplay;
$debugdeveloper = $CFG->debugdeveloper;
$CFG->debug = 0;
$CFG->debugdisplay = false;
$CFG->debugdeveloper = false;
$eventinformation = array();
$directory = $CFG->libdir . '/classes/event';
$files = self::get_file_list($directory);
// Remove exceptional events that will cause problems being displayed.
if (isset($files['unknown_logged'])) {
unset($files['unknown_logged']);
}
foreach ($files as $file => $location) {
$classname = '\\core\\event\\' . $file;
// Check to see if this is actually a valid event.
if (method_exists($classname, 'get_static_info')) {
$ref = new \ReflectionClass($classname);
// Ignore abstracts.
if (!$ref->isAbstract() && $file != 'manager') {
$eventinformation[$classname] = $classname::get_name_with_info();
}
}
}
// Now enable developer debugging as event information has been retrieved.
$CFG->debug = $debuglevel;
$CFG->debugdisplay = $debugdisplay;
$CFG->debugdeveloper = $debugdeveloper;
return $eventinformation;
}
/**
* This function returns an array of all events for the plugins of the system.
*
* @param bool $withoutcomponent Return an eventlist without associated components.
*
* @return array A list of events from all plug-ins.
*/
protected static function get_non_core_eventlist($withoutcomponent = false) {
global $CFG;
// Disable developer debugging as deprecated events will fire warnings.
// Setup backup variables to restore the following settings back to what they were when we are finished.
$debuglevel = $CFG->debug;
$debugdisplay = $CFG->debugdisplay;
$debugdeveloper = $CFG->debugdeveloper;
$CFG->debug = 0;
$CFG->debugdisplay = false;
$CFG->debugdeveloper = false;
$noncorepluginlist = array();
$plugintypes = \core_component::get_plugin_types();
foreach ($plugintypes as $plugintype => $notused) {
$pluginlist = \core_component::get_plugin_list($plugintype);
foreach ($pluginlist as $plugin => $directory) {
$plugindirectory = $directory . '/classes/event';
foreach (self::get_file_list($plugindirectory) as $eventname => $notused) {
$fullpluginname = $plugintype . '_' . $plugin;
$plugineventname = '\\' . $fullpluginname . '\\event\\' . $eventname;
// Check that this is actually an event.
if (method_exists($plugineventname, 'get_static_info') && $fullpluginname !== 'tool_monitor') { // No selfie here.
$ref = new \ReflectionClass($plugineventname);
if (!$ref->isAbstract() && $fullpluginname !== 'logstore_legacy') {
if ($withoutcomponent) {
$noncorepluginlist[$plugineventname] = $plugineventname::get_name_with_info();
} else {
$noncorepluginlist[$fullpluginname][$plugineventname] = $plugineventname::get_name_with_info();
}
}
}
}
}
}
// Now enable developer debugging as event information has been retrieved.
$CFG->debug = $debuglevel;
$CFG->debugdisplay = $debugdisplay;
$CFG->debugdeveloper = $debugdeveloper;
return $noncorepluginlist;
}
/**
* Returns a list of files with a full directory path in a specified directory.
*
* @param string $directory location of files.
* @return array full location of files from the specified directory.
*/
protected static function get_file_list($directory) {
global $CFG;
$directoryroot = $CFG->dirroot;
$finalfiles = array();
if (is_dir($directory)) {
if ($handle = opendir($directory)) {
$files = scandir($directory);
foreach ($files as $file) {
if ($file != '.' && $file != '..') {
// Ignore the file if it is external to the system.
if (strrpos($directory, $directoryroot) !== false) {
$location = substr($directory, strlen($directoryroot));
$name = substr($file, 0, -4);
$finalfiles[$name] = $location . '/' . $file;
}
}
}
}
}
return $finalfiles;
}
/**
* Get a list of events present in the system.
@@ -164,13 +38,58 @@ class eventlist {
* @return array list of events present in the system.
*/
public static function get_all_eventlist($withoutcomponent = false) {
if ($withoutcomponent) {
$return = array_merge(self::get_core_eventlist(), self::get_non_core_eventlist($withoutcomponent));
array_multisort($return, SORT_NATURAL);
} else {
$return = array_merge(array('core' => self::get_core_eventlist()),
self::get_non_core_eventlist($withoutcomponent = false));
global $CFG;
// Disable developer debugging as deprecated events will fire warnings.
// Setup backup variables to restore the following settings back to what they were when we are finished.
$debuglevel = $CFG->debug;
$debugdisplay = $CFG->debugdisplay;
$debugdeveloper = $CFG->debugdeveloper;
$CFG->debug = 0;
$CFG->debugdisplay = false;
$CFG->debugdeveloper = false;
// List of exceptional events that will cause problems if displayed.
$eventsignore = [
\core\event\unknown_logged::class,
\logstore_legacy\event\legacy_logged::class,
];
$return = [];
$events = core_component::get_component_classes_in_namespace(null, 'event');
foreach (array_keys($events) as $event) {
// We need to filter all classes that extend event base, or the base class itself.
if (is_a($event, \core\event\base::class, true) && !in_array($event, $eventsignore)) {
$reflectionclass = new ReflectionClass($event);
if ($reflectionclass->isAbstract()) {
continue;
}
// We can't choose this component's own events.
[$component] = explode('\\', $event);
if ($component === 'tool_monitor') {
continue;
}
if ($withoutcomponent) {
$return["\\{$event}"] = $event::get_name_with_info();
} else {
$return[$component]["\\{$event}"] = $event::get_name_with_info();
}
}
}
// Now enable developer debugging as event information has been retrieved.
$CFG->debug = $debuglevel;
$CFG->debugdisplay = $debugdisplay;
$CFG->debugdeveloper = $debugdeveloper;
if ($withoutcomponent) {
array_multisort($return, SORT_NATURAL);
}
return $return;
}
@@ -151,25 +151,25 @@ Feature: Basic OAuth2 functionality
| Service base URL | https://dc.imsglobal.org/ |
When I press "Save changes"
Then I should see "Changes saved"
And I should see "Open Badges"
And "Allow services" "icon" should exist in the "Open Badges" "table_row"
And "Do not allow login" "icon" should exist in the "Open Badges" "table_row"
And "Service discovery successful" "icon" should exist in the "Open Badges" "table_row"
And the "src" attribute of "table.admintable th img" "css_element" should contain "IMS-Global-Logo.png"
And I click on "Configure endpoints" "link" in the "Open Badges" "table_row"
And I should see "https://dc.imsglobal.org/.well-known/badgeconnect.json" in the "discovery_endpoint" "table_row"
And I should see "authorization_endpoint"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Configure user field mappings" "link" in the "Open Badges" "table_row"
And I should not see "given_name"
And I should not see "middle_name"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Edit" "link" in the "Open Badges" "table_row"
And I should see "IMS Global Reference Implementation"
And I click on "Edit" "link" in the "IMS Global Reference Implementation" "table_row"
And I set the following fields to these values:
| Name | IMS Global |
And I press "Save changes"
And I should see "Changes saved"
And I should see "IMS Global"
And "Allow services" "icon" should exist in the "IMS Global" "table_row"
And "Do not allow login" "icon" should exist in the "IMS Global" "table_row"
And "Service discovery successful" "icon" should exist in the "IMS Global" "table_row"
And the "src" attribute of "table.admintable th img" "css_element" should contain "IMS-Global-Logo.png"
And I click on "Configure endpoints" "link" in the "IMS Global" "table_row"
And I should see "https://dc.imsglobal.org/.well-known/badgeconnect.json" in the "discovery_endpoint" "table_row"
And I should see "authorization_endpoint"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Configure user field mappings" "link" in the "IMS Global" "table_row"
And I should not see "given_name"
And I should not see "middle_name"
And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Delete" "link" in the "IMS Global" "table_row"
And I should see "Are you sure you want to delete the identity issuer \"IMS Global\"?"
And I press "Continue"
@@ -573,7 +573,7 @@ class acceptances_table extends \table_sql {
if ($row->timemodified) {
if ($this->is_downloading()) {
// Use timestamp format readable for both machines and humans.
return date_format_string($row->timemodified, '%Y-%m-%d %H:%M:%S %Z');
return date_format_string((int) $row->timemodified, '%Y-%m-%d %H:%M:%S %Z');
} else {
// Use localised calendar format.
return userdate($row->timemodified, get_string('strftimedatetime'));
+55 -16
View File
@@ -29,6 +29,7 @@ require_once(__DIR__ . '/fixtures/test_target_course_level_shortname.php');
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core_analytics\manager
*/
class manager_test extends \advanced_testcase {
@@ -491,25 +492,63 @@ class manager_test extends \advanced_testcase {
// No potential context restrictions.
$this->assertFalse(\core_analytics\manager::get_potential_context_restrictions([]));
$defaultcategory = \core_course_category::get_default();
$defaultcategorycontext = $defaultcategory->get_context();
// Include the all context levels so the misc. category get included.
$this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions());
$this->assertEquals([
$defaultcategorycontext->id => "Category: {$defaultcategory->name}",
], manager::get_potential_context_restrictions());
$this->getDataGenerator()->create_course();
$this->getDataGenerator()->create_category();
$this->assertCount(3, \core_analytics\manager::get_potential_context_restrictions());
$this->assertCount(3, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE, CONTEXT_COURSECAT]));
$category = $this->getDataGenerator()->create_category(['name' => 'My category']);
$categorycontext = $category->get_context();
$this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE]));
$this->assertCount(2, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT]));
$courseone = $this->getDataGenerator()->create_course(['fullname' => 'Course one', 'shortname' => 'CS1']);
$courseonecontext = \context_course::instance($courseone->id);
$this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'Course category'));
$this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'Course category 1'));
$this->assertCount(
2,
\core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT],
get_string('defaultcategoryname')
));
$this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE], 'Test course 1'));
$this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE], 'Test course'));
$coursetwo = $this->getDataGenerator()->create_course(['fullname' => 'Course two', 'shortname' => 'CS2']);
$coursetwocontext = \context_course::instance($coursetwo->id);
// All context levels.
$this->assertEqualsCanonicalizing([
$defaultcategorycontext->id => "Category: {$defaultcategory->name}",
$categorycontext->id => "Category: {$category->name}",
$courseonecontext->id => "Course: {$courseone->shortname}",
$coursetwocontext->id => "Course: {$coursetwo->shortname}",
], manager::get_potential_context_restrictions());
// All category/course context levels.
$this->assertEqualsCanonicalizing([
$defaultcategorycontext->id => "Category: {$defaultcategory->name}",
$categorycontext->id => "Category: {$category->name}",
$courseonecontext->id => "Course: {$courseone->shortname}",
$coursetwocontext->id => "Course: {$coursetwo->shortname}",
], manager::get_potential_context_restrictions([CONTEXT_COURSECAT, CONTEXT_COURSE]));
// All category context levels.
$this->assertEqualsCanonicalizing([
$defaultcategorycontext->id => "Category: {$defaultcategory->name}",
$categorycontext->id => "Category: {$category->name}",
], manager::get_potential_context_restrictions([CONTEXT_COURSECAT]));
// Filtered category context levels.
$this->assertEquals([
$categorycontext->id => "Category: {$category->name}",
], manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'My cat'));
$this->assertEmpty(manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'nothing'));
// All course context levels.
$this->assertEqualsCanonicalizing([
$courseonecontext->id => "Course: {$courseone->shortname}",
$coursetwocontext->id => "Course: {$coursetwo->shortname}",
], manager::get_potential_context_restrictions([CONTEXT_COURSE]));
// Filtered course context levels.
$this->assertEquals([
$courseonecontext->id => "Course: {$courseone->shortname}",
], manager::get_potential_context_restrictions([CONTEXT_COURSE], 'one'));
$this->assertEmpty(manager::get_potential_context_restrictions([CONTEXT_COURSE], 'nothing'));
}
}
+5 -2
View File
@@ -199,7 +199,6 @@ class auth_plugin_lti extends \auth_plugin_base {
}
}
$user = $this->create_new_account($member, $iss);
$this->update_user_account($user, $member, $iss);
return \core_user::get_user($user->id);
}
}
@@ -381,7 +380,11 @@ class auth_plugin_lti extends \auth_plugin_base {
'lastname' => $userdata['family_name'] ?? $iss,
'email' => $email
];
user_update_user($update);
$userfieldstocompare = array_intersect_key((array) $user, $update);
if (!empty(array_diff($update, $userfieldstocompare))) {
user_update_user($update); // Only update if there's a change.
}
if (!empty($userdata['picture'])) {
try {
+39 -11
View File
@@ -249,12 +249,15 @@ class auth_test extends \advanced_testcase {
$mockjwtdata = $this->get_mock_launchdata_for_user($launchdata['user'], $launchdata['migration_claim'] ?? []);
// Authenticate the platform user.
$sink = $this->redirectEvents();
$countusersbefore = $DB->count_records('user');
$user = $auth->find_or_create_user_from_launch($mockjwtdata, true, $legacysecrets);
if (!empty($expected['migration_debugging'])) {
$this->assertDebuggingCalled();
}
$countusersafter = $DB->count_records('user');
$events = $sink->get_events();
$sink->close();
// Verify user count is correct. i.e. no user is created when migration claim is correctly processed or when
// the user has authenticated with the tool before.
@@ -295,15 +298,18 @@ class auth_test extends \advanced_testcase {
$this->verify_user_profile_image_updated($user->id);
}
// If migrated, verify the user account is reusing the legacy user account.
if (!empty($expected['migrated']) && $expected['migrated']) {
// If migrated, verify the user account is reusing the legacy user account.
$legacyuserids = array_column($legacyusers, 'id');
$this->assertContains($user->id, $legacyuserids);
}
// If the user is authenticating a second time, confirm the same account is being returned.
if (isset($firstauthuser)) {
$this->assertInstanceOf(\core\event\user_updated::class, $events[0]);
} else if (isset($firstauthuser)) {
// If the user is authenticating a second time, confirm the same account is being returned.
$this->assertEquals($firstauthuser->id, $user->id);
$this->assertEmpty($events); // The user authenticated with the same data once before, so we don't expect an update.
} else {
// The user wasn't migrated and hasn't launched before, so we expect a user_created event.
$this->assertInstanceOf(\core\event\user_created::class, $events[0]);
}
}
@@ -819,9 +825,12 @@ class auth_test extends \advanced_testcase {
$mockmemberdata = $this->get_mock_member_data_for_user($memberdata['user'], $memberdata['legacy_user_id'] ?? '');
// Authenticate the platform user.
$sink = $this->redirectEvents();
$countusersbefore = $DB->count_records('user');
$user = $auth->find_or_create_user_from_membership($mockmemberdata, $iss, $legacyconsumerkey ?? '');
$countusersafter = $DB->count_records('user');
$events = $sink->get_events();
$sink->close();
// Verify user count is correct. i.e. no user is created when migration claim is correctly processed or when
// the user has authenticated with the tool before.
@@ -857,15 +866,18 @@ class auth_test extends \advanced_testcase {
break;
}
// If migrated, verify the user account is reusing the legacy user account.
if (!empty($expected['migrated']) && $expected['migrated']) {
// If migrated, verify the user account is reusing the legacy user account.
$legacyuserids = array_column($legacyusers, 'id');
$this->assertContains($user->id, $legacyuserids);
}
// If the user is authenticating a second time, confirm the same account is being returned.
if (isset($firstauthuser)) {
$this->assertInstanceOf(\core\event\user_updated::class, $events[0]);
} else if (isset($firstauthuser)) {
// If the user is authenticating a second time, confirm the same account is being returned.
$this->assertEquals($firstauthuser->id, $user->id);
$this->assertEmpty($events); // The user authenticated with the same data once before, so we don't expect an update.
} else {
// The user wasn't migrated and hasn't launched before, so we expect a user_created event.
$this->assertInstanceOf(\core\event\user_created::class, $events[0]);
}
}
@@ -1090,7 +1102,23 @@ class auth_test extends \advanced_testcase {
'PII' => self::PII_NONE,
'migrated' => false
]
]
],
'Existing (linked) platform learner including PII, no legacy data, no consumer key bound, no legacy id' => [
'legacy_data' => null,
'launch_data' => [
'has_authenticated_before' => true,
'user' => $this->get_mock_users_with_ids(
['1'],
'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'
)[0],
],
'iss' => $this->issuer,
'legacy_consumer_key' => null,
'expected' => [
'PII' => self::PII_ALL,
'migrated' => false
]
],
];
}
@@ -167,6 +167,7 @@ class backup_course_task extends backup_task {
$content = self::encode_links_helper($content, 'GRADEREPORTINDEXBYID', '/grade/report/index.php?id=');
$content = self::encode_links_helper($content, 'BADGESVIEWBYID', '/badges/view.php?type=2&id=');
$content = self::encode_links_helper($content, 'USERINDEXVIEWBYID', '/user/index.php?id=');
$content = self::encode_links_helper($content, 'PLUGINFILEBYCONTEXT', '/pluginfile.php/');
return $content;
}
+2
View File
@@ -93,6 +93,8 @@ class base64_encode_final_element extends backup_final_element {
* @param string $value Original value coming from backup step source, usually db.
*/
public function set_value($value) {
// Avoid null being passed to base64_encode.
$value = $value ?? '';
parent::set_value(base64_encode($value));
}
}
+21 -9
View File
@@ -2383,28 +2383,40 @@ class backup_annotate_all_question_files extends backup_execution_step {
AND bi.itemname = 'question_categoryfinal'", array($this->get_backupid()));
// To know about qtype specific components/fileareas
$components = backup_qtype_plugin::get_components_and_fileareas();
$progress = $this->task->get_progress();
$progress->start_progress($this->get_name());
// Let's loop
foreach($rs as $record) {
// Backup all the file areas the are managed by the core question component.
// That is, by the question_type base class. In particular, we don't want
// to include files belonging to responses here.
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'questiontext', null);
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'generalfeedback', null);
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'answer', null);
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'answerfeedback', null);
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'hint', null);
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'correctfeedback', null);
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'partiallycorrectfeedback', null);
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'incorrectfeedback', null);
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'questiontext', null,
$progress);
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'generalfeedback', null,
$progress);
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'answer', null,
$progress);
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'answerfeedback', null,
$progress);
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'hint', null,
$progress);
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'correctfeedback', null,
$progress);
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question',
'partiallycorrectfeedback', null, $progress);
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'incorrectfeedback', null,
$progress);
// For files belonging to question types, we make the leap of faith that
// all the files belonging to the question type are part of the question definition,
// so we can just backup all the files in bulk, without specifying each
// file area name separately.
foreach ($components as $component => $fileareas) {
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, $component, null, null);
backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, $component, null, null,
$progress);
}
}
$progress->end_progress();
$rs->close();
}
}
@@ -166,6 +166,7 @@ class restore_course_task extends restore_task {
$rules[] = new restore_decode_rule('GRADEREPORTINDEXBYID', '/grade/report/index.php?id=$1', 'course');
$rules[] = new restore_decode_rule('BADGESVIEWBYID', '/badges/view.php?type=2&id=$1', 'course');
$rules[] = new restore_decode_rule('USERINDEXVIEWBYID', '/user/index.php?id=$1', 'course');
$rules[] = new restore_decode_rule('PLUGINFILEBYCONTEXT', '/pluginfile.php/$1', 'context');
return $rules;
}
+18 -43
View File
@@ -119,18 +119,14 @@ class restore_gradebook_structure_step extends restore_structure_step {
return false;
}
// Identify the backup we're dealing with.
$backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
$backupbuild = 0;
preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
if (!empty($matches[1])) {
$backupbuild = (int) $matches[1]; // The date of Moodle build at the time of the backup.
}
$restoretask = $this->get_task();
// On older versions the freeze value has to be converted.
// We do this from here as it is happening right before the file is read.
// This only targets the backup files that can contain the legacy freeze.
if ($backupbuild > 20150618 && (version_compare($backuprelease, '3.0', '<') || $backupbuild < 20160527)) {
if ($restoretask->backup_version_compare(20150618, '>')
&& $restoretask->backup_release_compare('3.0', '<')
|| $restoretask->backup_version_compare(20160527, '<')) {
$this->rewrite_step_backup_file_for_legacy_freeze($fullpath);
}
@@ -505,24 +501,25 @@ class restore_gradebook_structure_step extends restore_structure_step {
protected function gradebook_calculation_freeze() {
global $CFG;
$gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->get_courseid());
preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
$backupbuild = (int)$matches[1];
$backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
$restoretask = $this->get_task();
// Extra credits need adjustments only for backups made between 2.8 release (20141110) and the fix release (20150619).
if (!$gradebookcalculationsfreeze && $backupbuild >= 20141110 && $backupbuild < 20150619) {
if (!$gradebookcalculationsfreeze && $restoretask->backup_version_compare(20141110, '>=')
&& $restoretask->backup_version_compare(20150619, '<')) {
require_once($CFG->libdir . '/db/upgradelib.php');
upgrade_extra_credit_weightoverride($this->get_courseid());
}
// Calculated grade items need recalculating for backups made between 2.8 release (20141110) and the fix release (20150627).
if (!$gradebookcalculationsfreeze && $backupbuild >= 20141110 && $backupbuild < 20150627) {
if (!$gradebookcalculationsfreeze && $restoretask->backup_version_compare(20141110, '>=')
&& $restoretask->backup_version_compare(20150627, '<')) {
require_once($CFG->libdir . '/db/upgradelib.php');
upgrade_calculated_grade_items($this->get_courseid());
}
// Courses from before 3.1 (20160518) may have a letter boundary problem and should be checked for this issue.
// Backups from before and including 2.9 could have a build number that is greater than 20160518 and should
// be checked for this problem.
if (!$gradebookcalculationsfreeze && ($backupbuild < 20160518 || version_compare($backuprelease, '2.9', '<='))) {
if (!$gradebookcalculationsfreeze
&& ($restoretask->backup_version_compare(20160518, '<') || $restoretask->backup_release_compare('2.9', '<='))) {
require_once($CFG->libdir . '/db/upgradelib.php');
upgrade_course_letter_boundary($this->get_courseid());
}
@@ -4810,13 +4807,8 @@ class restore_create_categories_and_questions extends restore_structure_step {
protected function define_structure() {
// Check if the backup is a pre 4.0 one.
$backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
$backupbuild = (int)$matches[1];
$before40 = false;
if (version_compare($backuprelease, '4.0', '<') || $backupbuild < 20220202) {
$before40 = true;
}
$restoretask = $this->get_task();
$before40 = $restoretask->backup_release_compare('4.0', '<') || $restoretask->backup_version_compare(20220202, '<');
// Start creating the path, category should be the first one.
$paths = [];
$paths [] = new restore_path_element('question_category', '/question_categories/question_category');
@@ -4898,13 +4890,8 @@ class restore_create_categories_and_questions extends restore_structure_step {
// Before 3.5, question categories could be created at top level.
// From 3.5 onwards, all question categories should be a child of a special category called the "top" category.
$backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
$backupbuild = (int)$matches[1];
$before35 = false;
if (version_compare($backuprelease, '3.5', '<') || $backupbuild < 20180205) {
$before35 = true;
}
$restoretask = $this->get_task();
$before35 = $restoretask->backup_release_compare('3.5', '<') || $restoretask->backup_version_compare(20180205, '<');
if (empty($mapping->info->parent) && $before35) {
$top = question_get_top_category($data->contextid, true);
$data->parent = $top->id;
@@ -5052,14 +5039,8 @@ class restore_create_categories_and_questions extends restore_structure_step {
$oldid = $data->id;
// Check if the backup is a pre 4.0 one.
$backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
$backupbuild = (int)$matches[1];
$before40 = false;
if (version_compare($backuprelease, '4.0', '<') || $backupbuild < 20220202) {
$before40 = true;
}
if ($before40) {
$restoretask = $this->get_task();
if ($restoretask->backup_release_compare('4.0', '<') || $restoretask->backup_version_compare(20220202, '<')) {
// Check we have one mapping for this question.
if (!$questionmapping = $this->get_mapping('question', $oldid)) {
return; // No mapping = this question doesn't need to be created/mapped.
@@ -5284,13 +5265,7 @@ class restore_move_module_questions_categories extends restore_execution_step {
protected function define_execution() {
global $DB;
$backuprelease = $this->task->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
preg_match('/(\d{8})/', $this->task->get_info()->moodle_release, $matches);
$backupbuild = (int)$matches[1];
$after35 = false;
if (version_compare($backuprelease, '3.5', '>=') && $backupbuild > 20180205) {
$after35 = true;
}
$after35 = $this->task->backup_release_compare('3.5', '>=') && $this->task->backup_version_compare(20180205, '>');
$contexts = restore_dbops::restore_get_question_banks($this->get_restoreid(), CONTEXT_MODULE);
foreach ($contexts as $contextid => $contextlevel) {
+3 -8
View File
@@ -578,16 +578,11 @@ abstract class restore_dbops {
CONTEXT_SYSTEM => CONTEXT_COURSE,
CONTEXT_COURSECAT => CONTEXT_COURSE);
/** @var restore_controller $rc */
$rc = restore_controller_dbops::load_controller($restoreid);
$restoreinfo = $rc->get_info();
$plan = $rc->get_plan();
$after35 = $plan->backup_release_compare('3.5', '>=') && $plan->backup_version_compare(20180205, '>');
$rc->destroy(); // Always need to destroy.
$backuprelease = $restoreinfo->backup_release; // The major version: 2.9, 3.0, 3.10...
preg_match('/(\d{8})/', $restoreinfo->moodle_release, $matches);
$backupbuild = (int)$matches[1];
$after35 = false;
if (version_compare($backuprelease, '3.5', '>=') && $backupbuild > 20180205) {
$after35 = true;
}
// For any contextlevel, follow this process logic:
//
+24
View File
@@ -212,6 +212,30 @@ class restore_plan extends base_plan implements loggable {
}
$progress->end_progress();
}
/**
* Compares the provided moodle version with the one the backup was taken from.
*
* @param int $version Moodle version number (YYYYMMDD or YYYYMMDDXX)
* @param string $operator Operator to compare the provided version to the backup version. {@see version_compare()}
* @return bool True if the comparison passes.
*/
public function backup_version_compare(int $version, string $operator): bool {
preg_match('/(\d{' . strlen($version) . '})/', $this->get_info()->moodle_version, $matches);
$backupbuild = (int)$matches[1];
return version_compare($backupbuild, $version, $operator);
}
/**
* Compares the provided moodle release with the one the backup was taken from.
*
* @param string $release Moodle release (X.Y or X.Y.Z)
* @param string $operator Operator to compare the provided release to the backup release. {@see version_compare()}
* @return bool True if the comparison passes.
*/
public function backup_release_compare(string $release, string $operator): bool {
return version_compare($this->get_info()->backup_release, $release, $operator);
}
}
/*
+22
View File
@@ -124,6 +124,28 @@ abstract class restore_task extends base_task {
$this->after_restore();
}
}
/**
* Compares the provided moodle version with the one the backup was taken from.
*
* @param int $version Moodle version number (YYYYMMDD or YYYYMMDDXX)
* @param string $operator Operator to compare the provided version to the backup version. {@see version_compare()}
* @return bool True if the comparison passes.
*/
public function backup_version_compare(int $version, string $operator) {
return $this->plan->backup_version_compare($version, $operator);
}
/**
* Compares the provided moodle release with the one the backup was taken from.
*
* @param string $release Moodle release (X.Y or X.Y.Z)
* @param string $operator Operator to compare the provided release to the backup release. {@see version_compare()}
* @return bool True if the comparison passes.
*/
public function backup_release_compare(string $release, string $operator) {
return $this->plan->backup_release_compare($release, $operator);
}
}
/*
+8
View File
@@ -786,6 +786,10 @@ class core_backup_renderer extends plugin_renderer_base {
'extraclasses' => 'rcs-search mb-3 w-25',
'inputname' => restore_course_search::$VAR_SEARCH,
'searchstring' => get_string('searchcourses'),
'buttonattributes' => [
(object) ['key' => 'name', 'value' => 'searchcourses'],
(object) ['key' => 'value', 'value' => 1],
],
'query' => $component->get_search(),
];
$output .= $this->output->render_from_template('core/search_input', $data);
@@ -955,6 +959,10 @@ class core_backup_renderer extends plugin_renderer_base {
'extraclasses' => 'rcs-search mb-3 w-25',
'inputname' => restore_category_search::$VAR_SEARCH,
'searchstring' => get_string('searchcoursecategories'),
'buttonattributes' => [
(object) ['key' => 'name', 'value' => 'searchcourses'],
(object) ['key' => 'value', 'value' => 1],
],
'query' => $component->get_search(),
];
$output .= $this->output->render_from_template('core/search_input', $data);
+1 -1
View File
@@ -262,7 +262,7 @@ class xml_writer {
protected function xml_safe_utf8($content) {
$content = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is', '', $content ?? ''); // clean CTRL chars.
$content = preg_replace("/\r\n|\r/", "\n", $content); // Normalize line&return=>line
return $content;
return fix_utf8($content);
}
/**
+49 -5
View File
@@ -36,6 +36,9 @@ use core_badges\backpack_api2p1_mapping;
use core_badges\oauth2\client;
use curl;
use stdClass;
use core\oauth2\issuer;
use core\oauth2\endpoint;
use core\oauth2\discovery\imsbadgeconnect;
/**
* To process badges with backpack and control api request and this class using for Open Badge API v2.1 methods.
@@ -61,8 +64,11 @@ class backpack_api2p1 {
/** @var null version api of the backpack. */
protected $backpackapiversion;
/** @var null api URL of the backpack. */
protected $backpackapiurl = '';
/** @var issuer The OAuth2 Issuer for this backpack */
protected issuer $issuer;
/** @var endpoint The apiBase endpoint */
protected endpoint $apibase;
/**
* backpack_api2p1 constructor.
@@ -75,7 +81,6 @@ class backpack_api2p1 {
if (!empty($externalbackpack)) {
$this->externalbackpack = $externalbackpack;
$this->backpackapiversion = $externalbackpack->apiversion;
$this->backpackapiurl = $externalbackpack->backpackapiurl;
$this->get_clientid = $this->get_clientid($externalbackpack->oauth2_issuerid);
if (!($this->tokendata = $this->get_stored_token($externalbackpack->id))
@@ -87,6 +92,44 @@ class backpack_api2p1 {
$this->define_mappings();
}
/**
* Initialises or returns the OAuth2 issuer associated to this backpack.
*
* @return issuer
*/
protected function get_issuer(): issuer {
if (!isset($this->issuer)) {
$this->issuer = new \core\oauth2\issuer($this->externalbackpack->oauth2_issuerid);
}
return $this->issuer;
}
/**
* Gets the apiBase url associated to this backpack.
*
* @return string
*/
protected function get_api_base_url(): string {
if (!isset($this->apibase)) {
$apibase = endpoint::get_record([
'issuerid' => $this->externalbackpack->oauth2_issuerid,
'name' => 'apiBase',
]);
if (empty($apibase)) {
imsbadgeconnect::create_endpoints($this->get_issuer());
$apibase = endpoint::get_record([
'issuerid' => $this->externalbackpack->oauth2_issuerid,
'name' => 'apiBase',
]);
}
$this->apibase = $apibase;
}
return $this->apibase->get('url');
}
/**
* Define the mappings supported by this usage and api version.
@@ -157,7 +200,7 @@ class backpack_api2p1 {
foreach ($this->mappings as $mapping) {
if ($mapping->is_match($action)) {
return $mapping->request(
$this->backpackapiurl,
$this->get_api_base_url(),
$tokenkey,
$postdata
);
@@ -194,6 +237,7 @@ class backpack_api2p1 {
private function get_clientid($issuerid) {
$issuer = \core\oauth2\api::get_issuer($issuerid);
if (!empty($issuer)) {
$this->issuer = $issuer;
$this->clientid = $issuer->get('clientid');
}
}
@@ -212,7 +256,7 @@ class backpack_api2p1 {
return false;
}
$issuer = new \core\oauth2\issuer($this->externalbackpack->oauth2_issuerid);
$issuer = $this->get_issuer();
$client = new client($issuer, new moodle_url('/badges/mybadges.php'), '', $this->externalbackpack);
if (!$client->is_logged_in()) {
$redirecturl = new moodle_url('/badges/mybadges.php', ['error' => 'backpackexporterror']);
+30 -51
View File
@@ -1,4 +1,4 @@
@core @core_badges @_file_upload
@core @core_badges
Feature: Backpack badges
Test the settings to add/update a backpack for a site and user.
I need to verify display backpack in the my profile
@@ -37,36 +37,21 @@ Feature: Backpack badges
@javascript
Scenario: Verify backback settings
Given I am on homepage
And I log in as "admin"
And I navigate to "Badges > Badges settings" in site administration
And I set the following fields to these values:
| External backpack connection | 1 |
And I press "Save changes"
And I navigate to "Badges > Manage backpacks" in site administration
And I click on "Move up" "link" in the "https://dc.imsglobal.org" "table_row"
And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Test badge verify backpack |
| Version | v1 |
| Language | English |
| Description | Test badge description |
| Image author | http://author.example.com |
| Image caption | Test caption image |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I set the field "type" to "Manual issue by role"
And I set the field "Manager" to "1"
And I press "Save"
And I press "Enable access"
And I press "Continue"
And I select "Recipients (0)" from the "jump" singleselect
And I press "Award badge"
And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
And I press "Award badge"
And I log out
When I am on homepage
And I log in as "student1"
Given the following "core_badges > Badge" exists:
| name | Test badge verify backpack |
| version | 1 |
| language | en |
| description | Test badge description |
| image | badges/tests/behat/badge.png |
| imageauthorurl | http://author.example.com |
| imagecaption | Test caption image |
And the following "core_badges > Criteria" exists:
| badge | Test badge verify backpack |
| role | editingteacher |
And the following "core_badges > Issued badge" exists:
| badge | Test badge verify backpack |
| user | student1 |
When I log in as "student1"
And I follow "Preferences" in the user menu
And I follow "Backpack settings"
Then I should see "https://dc.imsglobal.org"
@@ -82,26 +67,20 @@ Feature: Backpack badges
And I press "Save changes"
And I navigate to "Badges > Manage backpacks" in site administration
And I click on "Move up" "link" in the "https://dc.imsglobal.org" "table_row"
And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Test badge verify backpack |
| Version | v1 |
| Language | English |
| Description | Test badge description |
| Image author | http://author.example.com |
| Image caption | Test caption image |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I set the field "type" to "Manual issue by role"
And I set the field "Manager" to "1"
And I press "Save"
And I press "Enable access"
And I press "Continue"
And I select "Recipients (0)" from the "jump" singleselect
And I press "Award badge"
And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
And I press "Award badge"
And I log out
And the following "core_badges > Badge" exists:
| name | Test badge verify backpack |
| version | 1 |
| language | en |
| description | Test badge description |
| image | badges/tests/behat/badge.png |
| imageauthorurl | http://author.example.com |
| imagecaption | Test caption image |
And the following "core_badges > Criteria" exists:
| badge | Test badge verify backpack |
| role | editingteacher |
And the following "core_badges > Issued badge" exists:
| badge | Test badge verify backpack |
| user | student1 |
And the following "setup backpack connected" exist:
| user | externalbackpack |
| student1 | https://dc.imsglobal.org |
+27 -33
View File
@@ -1,4 +1,4 @@
@core @core_badges @_file_upload @javascript
@core @core_badges @javascript
Feature: Test tertiary navigation as various users
Background:
@@ -15,38 +15,32 @@ Feature: Test tertiary navigation as various users
| teacher | C1 | editingteacher |
| student1 | C1 | student |
| nonediting | C1 | teacher |
# Create system badge and define a criterion.
And I log in as "admin"
And I am on "Course 1" course homepage
And I navigate to "Badges > Add a new badge" in current page administration
And I set the following fields to these values:
| Name | Testing course badge |
| Version | 1.1 |
| Language | Catalan |
| Description | Testing course badge description |
| Image author | http://author.example.com |
| Image caption | Test caption image |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I set the field "type" to "Manual issue by role"
And I expand all fieldsets
And I set the field "Teacher" to "1"
And I press "Save"
And I am on site homepage
And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Testing site badge |
| Version | 1.1 |
| Language | Catalan |
| Description | Testing site badge description |
| Image author | http://author.example.com |
| Image caption | Test caption image |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I set the field "type" to "Manual issue by role"
And I expand all fieldsets
And I set the field "Teacher" to "1"
And I press "Save"
# Create a course badge in our test course.
And the following "core_badges > Badge" exists:
| name | Testing course badge |
| status | inactive |
| type | 2 |
| course | C1 |
| version | 1.1 |
| language | ca |
| description | Testing course badge description |
| image | badges/tests/behat/badge.png |
| imageauthorurl | http://author.example.com |
| imagecaption | Test caption image |
# Create a site badge.
And the following "core_badges > Badge" exists:
| name | Testing site badge |
| status | inactive |
| version | 1.1 |
| language | ca |
| description | Testing site badge description |
| image | badges/tests/behat/badge.png |
| imageauthorurl | http://author.example.com |
| imagecaption | Test caption image |
And the following "core_badges > Criterias" exist:
| badge | role |
| Testing course badge | editingteacher |
| Testing site badge | editingteacher |
Scenario Outline: Check navigation as different users in a course context
Given I log in as "<user>"
+8 -10
View File
@@ -1,4 +1,4 @@
@mod @mod_quiz @core @core_badges @_file_upload @javascript
@mod @mod_quiz @core @core_badges @javascript
Feature: Award badges based on activity completion
In order to ensure a student has learned the material before being marked complete
As a teacher
@@ -38,15 +38,13 @@ Feature: Award badges based on activity completion
And user "student2" has attempted "Test quiz name" with responses:
| slot | response |
| 1 | False |
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I change window size to "large"
And I navigate to "Badges > Add a new badge" in current page administration
And I set the following fields to these values:
| Name | Course Badge |
| Description | Course badge description |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And the following "core_badges > Badge" exists:
| name | Course Badge |
| status | 0 |
| type | 2 |
| course | C1 |
| description | Course badge description |
| image | badges/tests/behat/badge.png |
Scenario: Student earns a badge using activity completion, but does not get passing grade
Given I am on the "Course 1" course page logged in as teacher1
+77 -67
View File
@@ -1,4 +1,4 @@
@core @core_badges @_file_upload
@core @core_badges
Feature: Award badges based on cohort
In order to award badges to users based on their cohort membership
As an admin
@@ -18,13 +18,15 @@ Feature: Award badges based on cohort
| user | cohort |
| user1 | CH1 |
| user2 | CH2 |
And the following "core_badges > Badge" exists:
| name | Site Badge |
| status | 0 |
| description | Site badge description |
| image | badges/tests/behat/badge.png |
And I log in as "admin"
And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge |
| Description | Site badge description |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I navigate to "Badges > Manage badges" in site administration
And I click on "Edit" "link" in the "Site Badge" "table_row"
And I select "Criteria" from the "jump" singleselect
And I set the field "type" to "Cohort membership"
And I set the field "id_cohort_cohorts" to "One Cohort"
And I press "Save"
@@ -53,13 +55,15 @@ Feature: Award badges based on cohort
| user1 | CH2 |
| user2 | CH1 |
| user2 | CH3 |
And the following "core_badges > Badge" exists:
| name | Site Badge |
| status | 0 |
| description | Site badge description |
| image | badges/tests/behat/badge.png |
And I log in as "admin"
And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge |
| Description | Site badge description |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I navigate to "Badges > Manage badges" in site administration
And I click on "Edit" "link" in the "Site Badge" "table_row"
And I select "Criteria" from the "jump" singleselect
And I set the field "type" to "Cohort membership"
And I expand all fieldsets
And I set the field "id_cohort_cohorts" to "One Cohort,Two Cohort"
@@ -93,13 +97,15 @@ Feature: Award badges based on cohort
| user2 | CH3 |
| user3 | CH2 |
| user3 | CH3 |
And the following "core_badges > Badge" exists:
| name | Site Badge |
| status | 0 |
| description | Site badge description |
| image | badges/tests/behat/badge.png |
And I log in as "admin"
And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge |
| Description | Site badge description |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I navigate to "Badges > Manage badges" in site administration
And I click on "Edit" "link" in the "Site Badge" "table_row"
And I select "Criteria" from the "jump" singleselect
And I set the field "type" to "Cohort membership"
And I set the field "id_cohort_cohorts" to "One Cohort"
And I press "Save"
@@ -129,13 +135,15 @@ Feature: Award badges based on cohort
| user | cohort |
| user1 | CH1 |
| user2 | CH2 |
And the following "core_badges > Badge" exists:
| name | Site Badge |
| status | 0 |
| description | Site badge description |
| image | badges/tests/behat/badge.png |
And I log in as "admin"
And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge |
| Description | Site badge description |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I navigate to "Badges > Manage badges" in site administration
And I click on "Edit" "link" in the "Site Badge" "table_row"
And I select "Criteria" from the "jump" singleselect
And I set the field "type" to "Cohort membership"
And I set the field "id_cohort_cohorts" to "One Cohort"
And I press "Save"
@@ -180,13 +188,15 @@ Feature: Award badges based on cohort
| user1 | CH1 |
| user2 | CH2 |
| user3 | CH2 |
And the following "core_badges > Badge" exists:
| name | Site Badge |
| status | 0 |
| description | Site badge description |
| image | badges/tests/behat/badge.png |
And I log in as "admin"
And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge |
| Description | Site badge description |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I navigate to "Badges > Manage badges" in site administration
And I click on "Edit" "link" in the "Site Badge" "table_row"
And I select "Criteria" from the "jump" singleselect
And I set the field "type" to "Cohort membership"
And I set the field "id_cohort_cohorts" to "One Cohort"
And I press "Save"
@@ -237,13 +247,15 @@ Feature: Award badges based on cohort
| user1 | CH2 |
| user2 | CH2 |
| user2 | CH2 |
And the following "core_badges > Badge" exists:
| name | Site Badge |
| status | 0 |
| description | Site badge description |
| image | badges/tests/behat/badge.png |
And I log in as "admin"
And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge |
| Description | Site badge description |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I navigate to "Badges > Manage badges" in site administration
And I click on "Edit" "link" in the "Site Badge" "table_row"
And I select "Criteria" from the "jump" singleselect
And I set the field "type" to "Cohort membership"
And I set the field "id_cohort_cohorts" to "One Cohort"
And I press "Save"
@@ -294,13 +306,15 @@ Feature: Award badges based on cohort
| user1 | CH2 |
| user2 | CH1 |
| user3 | CH2 |
And the following "core_badges > Badge" exists:
| name | Site Badge |
| status | 0 |
| description | Site badge description |
| image | badges/tests/behat/badge.png |
And I log in as "admin"
And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge |
| Description | Site badge description |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I navigate to "Badges > Manage badges" in site administration
And I click on "Edit" "link" in the "Site Badge" "table_row"
And I select "Criteria" from the "jump" singleselect
And I set the field "type" to "Cohort membership"
And I expand all fieldsets
And I set the field "id_cohort_cohorts" to "One Cohort,Two Cohort"
@@ -352,25 +366,23 @@ Feature: Award badges based on cohort
| user1 | CH1 |
| user1 | CH2 |
| user2 | CH2 |
And the following "core_badges > Badges" exist:
| name | status | description | image |
| Site Badge 1 | 0 | Site badge description | badges/tests/behat/badge.png |
| Site Badge 2 | 0 | Site badge description | badges/tests/behat/badge.png |
And I log in as "admin"
And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge 1 |
| Description | Site badge description |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I navigate to "Badges > Manage badges" in site administration
And I click on "Edit" "link" in the "Site Badge 1" "table_row"
And I select "Criteria" from the "jump" singleselect
And I set the field "type" to "Cohort membership"
And I set the field "id_cohort_cohorts" to "One Cohort"
And I press "Save"
And I press "Enable access"
When I press "Continue"
And I should see "Recipients (1)"
And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge 2 |
| Description | Site badge description |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I navigate to "Badges > Manage badges" in site administration
And I click on "Edit" "link" in the "Site Badge 2" "table_row"
And I select "Criteria" from the "jump" singleselect
And I set the field "type" to "Cohort membership"
And I set the field "id_cohort_cohorts" to "Two Cohort"
And I press "Save"
@@ -411,13 +423,14 @@ Feature: Award badges based on cohort
| user1 | CH2 |
| user2 | CH2 |
| user2 | CH3 |
And the following "core_badges > Badges" exist:
| name | status | description | image |
| Site Badge 1 | 0 | Site badge description | badges/tests/behat/badge.png |
| Site Badge 2 | 0 | Site badge description | badges/tests/behat/badge.png |
And I log in as "admin"
And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge 1 |
| Description | Site badge description |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I navigate to "Badges > Manage badges" in site administration
And I click on "Edit" "link" in the "Site Badge 1" "table_row"
And I select "Criteria" from the "jump" singleselect
And I set the field "type" to "Cohort membership"
And I expand all fieldsets
And I set the field "id_cohort_cohorts" to "One Cohort,Two Cohort"
@@ -426,12 +439,9 @@ Feature: Award badges based on cohort
And I press "Enable access"
When I press "Continue"
And I should see "Recipients (1)"
And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge 2 |
| Description | Site badge description |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I navigate to "Badges > Manage badges" in site administration
And I click on "Edit" "link" in the "Site Badge 2" "table_row"
And I select "Criteria" from the "jump" singleselect
And I set the field "type" to "Cohort membership"
And I expand all fieldsets
And I set the field "id_cohort_cohorts" to "Three Cohort,Two Cohort"
+28 -19
View File
@@ -1,4 +1,4 @@
@core @core_badges @_file_upload
@core @core_badges
Feature: Award badges based on competency completion
In order to award badges to users based on competency completion
As an admin
@@ -25,6 +25,13 @@ Feature: Award badges based on competency completion
@javascript
Scenario: Award badge for completing a competency in a course
Given the following "core_badges > Badge" exists:
| name | Course Badge |
| status | 0 |
| type | 2 |
| course | C1 |
| description | Course badge description |
| image | badges/tests/behat/badge.png |
# Add a competency to the course
When I am on "Course 1" course homepage
And I navigate to "Competencies > Add competencies to course" in current page administration
@@ -39,12 +46,10 @@ Feature: Award badges based on competency completion
# Add a badge to the course
And I am on "Course 1" course homepage
And I change window size to "large"
And I navigate to "Badges > Add a new badge" in current page administration
And I set the following fields to these values:
| Name | Course Badge |
| Description | Course badge description |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I navigate to "Badges" in current page administration
And I press "Manage badges"
And I follow "Course Badge"
And I select "Criteria" from the "jump" singleselect
# Set the competency as a criteria for the badge
And I set the field "type" to "Competencies"
When I open the autocomplete suggestions list
@@ -72,6 +77,11 @@ Feature: Award badges based on competency completion
@javascript
Scenario: Award badge for completing a competency in the site
Given the following "core_badges > Badge" exists:
| name | Site Badge |
| status | 0 |
| description | Site badge description |
| image | badges/tests/behat/badge.png |
# Add a competency to the course
When I am on "Course 1" course homepage
And I navigate to "Competencies > Add competencies to course" in current page administration
@@ -83,12 +93,9 @@ Feature: Award badges based on competency completion
And I select "comp2" of the competency tree
And I click on "Add" "button" in the "Competency picker" "dialogue"
# Add a badge to the site
And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge |
| Description | Site badge description |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I navigate to "Badges > Manage badges" in site administration
And I click on "Edit" "link" in the "Site Badge" "table_row"
And I select "Criteria" from the "jump" singleselect
# Set the competency as a criteria for the badge
And I set the field "type" to "Competencies"
And I press "Add competency"
@@ -123,6 +130,11 @@ Feature: Award badges based on competency completion
@javascript
Scenario: Award badge for completing all competencies in the site
Given the following "core_badges > Badge" exists:
| name | Site Badge |
| status | 0 |
| description | Site badge description |
| image | badges/tests/behat/badge.png |
# Add a competency to the course
When I am on "Course 1" course homepage
And I navigate to "Competencies > Add competencies to course" in current page administration
@@ -134,12 +146,9 @@ Feature: Award badges based on competency completion
And I select "comp2" of the competency tree
And I click on "Add" "button" in the "Competency picker" "dialogue"
# Add a badge to the site
And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge |
| Description | Site badge description |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I navigate to "Badges > Manage badges" in site administration
And I click on "Edit" "link" in the "Site Badge" "table_row"
And I select "Criteria" from the "jump" singleselect
# Set the competency as a criteria for the badge
And I set the field "type" to "Competencies"
And I press "Add competency"
+8 -6
View File
@@ -9,13 +9,15 @@ Feature: Award badges based on user profile field
Given the following "users" exist:
| username | firstname | lastname | email |
| user1 | First | User | first@example.com |
And the following "core_badges > Badge" exists:
| name | Site Badge |
| status | 0 |
| description | Site badge description |
| image | badges/tests/behat/badge.png |
And I log in as "admin"
And I navigate to "Badges > Add a new badge" in site administration
And I set the following fields to these values:
| Name | Site Badge |
| Description | Site badge description |
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I navigate to "Badges > Manage badges" in site administration
And I click on "Edit" "link" in the "Site Badge" "table_row"
And I select "Criteria" from the "jump" singleselect
And I set the field "type" to "Profile completion"
And I set the field "id_field_picture" to "1"
And I press "Save"
@@ -0,0 +1,102 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
/**
* Badges test generator for Behat
*
* @package core_badges
* @copyright 2023 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_core_badges_generator extends behat_generator_base {
/**
* Get a list of the entities that can be created for this component
*
* @return array[]
*/
protected function get_creatable_entities(): array {
return [
'Badges' => [
'singular' => 'Badge',
'datagenerator' => 'badge',
'required' => [
'name',
],
'switchids' => [
'course' => 'courseid',
],
],
'Criterias' => [
'singular' => 'Criteria',
'datagenerator' => 'criteria',
'required' => [
'badge',
'role',
],
'switchids' => [
'badge' => 'badgeid',
'role' => 'roleid',
],
],
'Issued badges' => [
'singular' => 'Issued badge',
'datagenerator' => 'issued_badge',
'required' => [
'badge',
'user',
],
'switchids' => [
'badge' => 'badgeid',
'user' => 'userid',
],
],
];
}
/**
* Look up badge ID from given name
*
* @param string $name
* @return int
*/
protected function get_badge_id(string $name): int {
global $DB;
return (int) $DB->get_field('badge', 'id', ['name' => $name], MUST_EXIST);
}
/**
* Pre-process badge entity
*
* @param array $badge
* @return array
*/
protected function preprocess_badge(array $badge): array {
global $CFG;
require_once("{$CFG->libdir}/badgeslib.php");
// Allow text status' that correspond to badge constants.
if (array_key_exists('status', $badge) && !is_numeric($badge['status'])) {
$badge['status'] = constant('BADGE_STATUS_' . strtoupper($badge['status']));
}
return $badge;
}
}
+87 -5
View File
@@ -35,12 +35,17 @@ class core_badges_generator extends component_generator_base {
/**
* Create badge
*
* TODO: MDL-73648 Use from Behat too
*
* @param array|stdClass $record
* @return badge
*/
public function create_badge($record): badge {
global $DB, $USER;
global $CFG, $DB, $USER;
$record = (array) $record;
// Save badge image path for later.
$badgeimage = $record['image'] ?? '';
unset($record['image']);
$record = (object) array_merge([
'name' => 'Test badge',
@@ -67,10 +72,87 @@ class core_badges_generator extends component_generator_base {
'imageauthoremail' => 'author@example.com',
'imageauthorurl' => 'http://image.example.com/',
'imagecaption' => 'Image caption'
], (array) $record);
], $record);
$record->id = $DB->insert_record('badge', $record);
$badge = new badge($record->id);
return new badge($record->id);
// Process badge image (if supplied).
if ($badgeimage !== '') {
$file = get_file_storage()->create_file_from_pathname([
'contextid' => context_user::instance($USER->id)->id,
'userid' => $USER->id,
'component' => 'user',
'filearea' => 'draft',
'itemid' => file_get_unused_draft_itemid(),
'filepath' => '/',
'filename' => basename($badgeimage),
], "{$CFG->dirroot}/$badgeimage");
// Copy image to temp file, as it'll be deleted by the following call.
badges_process_badge_image($badge, $file->copy_content_to_temp());
}
return $badge;
}
/**
* Create badge criteria
*
* Note that only manual criteria issues by role is currently supported
*
* @param array|stdClass $record
* @throws coding_exception
*/
public function create_criteria($record): void {
$record = (array) $record;
if (!array_key_exists('badgeid', $record)) {
throw new coding_exception('Record must contain \'badgeid\' property');
}
if (!array_key_exists('roleid', $record)) {
throw new coding_exception('Record must contain \'roleid\' property');
}
$badge = new badge($record['badgeid']);
// Create the overall criteria.
if (count($badge->criteria) === 0) {
award_criteria::build([
'badgeid' => $badge->id,
'criteriatype' => BADGE_CRITERIA_TYPE_OVERALL,
])->save([
'agg' => BADGE_CRITERIA_AGGREGATION_ALL,
]);
}
// Create the manual criteria.
award_criteria::build([
'badgeid' => $badge->id,
'criteriatype' => BADGE_CRITERIA_TYPE_MANUAL,
])->save([
'role_' . $record['roleid'] => $record['roleid'],
'description' => $record['description'] ?? '',
]);
}
/**
* Create issued badge to a user
*
* @param array|stdClass $record
* @throws coding_exception
*/
public function create_issued_badge($record): void {
$record = (array) $record;
if (!array_key_exists('badgeid', $record)) {
throw new coding_exception('Record must contain \'badgeid\' property');
}
if (!array_key_exists('userid', $record)) {
throw new coding_exception('Record must contain \'userid\' property');
}
$badge = new badge($record['badgeid']);
$badge->issue($record['userid'], true);
}
}
@@ -8,29 +8,29 @@ Feature: Add the glossary random block when main feature is disabled
Given the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And I am on the "C1" "course" page logged in as "admin"
And the following "blocks" exist:
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
| glossary_random | Course | C1 | course-view-* | site-post |
Scenario: The glossary random block is displayed even when glossary module is disabled
Given I turn editing mode on
And I add the "Random glossary entry" block
When I navigate to "Plugins > Activity modules > Manage activities" in site administration
When I log in as "admin"
And I navigate to "Plugins > Activity modules > Manage activities" in site administration
And I click on "Hide" "icon" in the "Glossary" "table_row"
And I am on "Course 1" course homepage with editing mode on
Then I should see "Random glossary entry"
Then "Random glossary entry" "block" should exist
Scenario: The glossary random block can be removed even when glossary module is disabled
Given I turn editing mode on
And I add the "Random glossary entry" block
When I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
And I open the "Random glossary entry" blocks action menu
And I click on "Delete Random glossary entry block" "link" in the "Random glossary entry" "block"
And "Delete block?" "dialogue" should exist
And I click on "Cancel" "button" in the "Delete block?" "dialogue"
And I should see "Random glossary entry"
And "Random glossary entry" "block" should exist
When I navigate to "Plugins > Activity modules > Manage activities" in site administration
And I click on "Hide" "icon" in the "Glossary" "table_row"
And I am on "Course 1" course homepage with editing mode on
And I open the "Random glossary entry" blocks action menu
And I click on "Delete Random glossary entry block" "link" in the "Random glossary entry" "block"
And "Delete block?" "dialogue" should exist
And I click on "Delete" "button" in the "Delete block?" "dialogue"
Then I should not see "Random glossary entry"
Then "Random glossary entry" "block" should not exist
+16 -6
View File
@@ -39,7 +39,7 @@ require_once($CFG->dirroot.'/cache/locallib.php');
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cache_disabled extends cache {
class cache_disabled extends cache implements cache_loader_with_locking {
/**
* Constructs the cache.
@@ -204,20 +204,30 @@ class cache_disabled extends cache {
/**
* Pretend that we got a lock to avoid errors.
*
* @param string $key
* @param int|string $key
* @return bool
*/
public function acquire_lock(string $key) : bool {
public function acquire_lock($key) : bool {
return true;
}
/**
* Pretend that we released a lock to avoid errors.
*
* @param string $key
* @return void
* @param int|string $key
* @return bool
*/
public function release_lock(string $key) : bool {
public function release_lock($key) : bool {
return true;
}
/**
* Pretend that we have a lock to avoid errors.
*
* @param int|string $key
* @return bool
*/
public function check_lock_state($key) : bool {
return true;
}
}
+49 -2
View File
@@ -119,6 +119,15 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
*/
protected $lastiobytes = 0;
/** @var int Maximum number of seconds to wait for a lock before giving up. */
protected $lockwait = 60;
/** @var int Timeout before lock is automatically released (in case of crashes) */
protected $locktimeout = 600;
/** @var ?array Array of current locks, or null if we haven't registered shutdown function */
protected $currentlocks = null;
/**
* Determines if the requirements for this type of store are met.
*
@@ -182,6 +191,12 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
}
$password = !empty($configuration['password']) ? $configuration['password'] : '';
$prefix = !empty($configuration['prefix']) ? $configuration['prefix'] : '';
if (array_key_exists('lockwait', $configuration)) {
$this->lockwait = (int)$configuration['lockwait'];
}
if (array_key_exists('locktimeout', $configuration)) {
$this->locktimeout = (int)$configuration['locktimeout'];
}
$this->redis = $this->new_redis($configuration['server'], $prefix, $password);
}
@@ -527,7 +542,38 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
* @return bool True if the lock was acquired, false if it was not.
*/
public function acquire_lock($key, $ownerid) {
return $this->redis->setnx($key, $ownerid);
$timelimit = time() + $this->lockwait;
do {
// If the key doesn't already exist, grab it and return true.
if ($this->redis->setnx($key, $ownerid)) {
// Ensure Redis deletes the key after a bit in case something goes wrong.
$this->redis->expire($key, $this->locktimeout);
// If we haven't got it already, better register a shutdown function.
if ($this->currentlocks === null) {
core_shutdown_manager::register_function([$this, 'shutdown_release_locks']);
$this->currentlocks = [];
}
$this->currentlocks[$key] = $ownerid;
return true;
}
// Wait 1 second then retry.
sleep(1);
} while (time() < $timelimit);
return false;
}
/**
* Releases any locks when the system shuts down, in case there is a crash or somebody forgets
* to use 'try-finally'.
*
* Do not call this function manually (except from unit test).
*/
public function shutdown_release_locks() {
foreach ($this->currentlocks as $key => $ownerid) {
debugging('Automatically releasing Redis cache lock: ' . $key . ' (' . $ownerid .
') - did somebody forget to call release_lock()?', DEBUG_DEVELOPER);
$this->release_lock($key, $ownerid);
}
}
/**
@@ -541,7 +587,7 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
*/
public function check_lock_state($key, $ownerid) {
$result = $this->redis->get($key);
if ($result === $ownerid) {
if ($result === (string)$ownerid) {
return true;
}
if ($result === false) {
@@ -586,6 +632,7 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
*/
public function release_lock($key, $ownerid) {
if ($this->check_lock_state($key, $ownerid)) {
unset($this->currentlocks[$key]);
return ($this->redis->del($key) !== false);
}
return false;
+52
View File
@@ -141,6 +141,58 @@ class store_test extends \cachestore_tests {
$this->assertTrue($store->release_lock('lock', '123'));
}
/**
* Checks the timeout features of locking.
*/
public function test_lock_timeouts(): void {
$store = $this->create_cachestore_redis(['lockwait' => 2, 'locktimeout' => 4]);
// User 123 acquires lock.
$this->assertTrue($store->acquire_lock('lock', '123'));
$this->assertTrue($store->check_lock_state('lock', '123'));
// User 456 tries to acquire lock - should fail after about 2 seconds.
$before = microtime(true);
$this->assertFalse($store->acquire_lock('lock', '456'));
$after = microtime(true);
$this->assertEqualsWithDelta(2, $after - $before, 0.5);
// Wait another 2 seconds and then it should be able to get the lock because of timeout.
sleep(2);
$this->assertTrue($store->acquire_lock('lock', '456'));
$this->assertTrue($store->check_lock_state('lock', '456'));
// The first user doesn't have the lock any more.
$this->assertFalse($store->check_lock_state('lock', '123'));
// Releasing the lock from the first user does nothing.
$this->assertFalse($store->release_lock('lock', '123'));
$this->assertTrue($store->check_lock_state('lock', '456'));
$this->assertTrue($store->release_lock('lock', '456'));
}
/**
* Tests the shutdown function that is supposed to free any remaining locks.
*/
public function test_lock_shutdown(): void {
$store = $this->create_cachestore_redis();
try {
$this->assertTrue($store->acquire_lock('a', '123'));
$this->assertTrue($store->acquire_lock('b', '123'));
$this->assertTrue($store->acquire_lock('c', '123'));
$this->assertTrue($store->check_lock_state('a', '123'));
$this->assertTrue($store->check_lock_state('b', '123'));
$this->assertTrue($store->check_lock_state('c', '123'));
} finally {
$store->shutdown_release_locks();
$this->assertDebuggingCalledCount(3);
}
$this->assertNull($store->check_lock_state('a', '123'));
$this->assertNull($store->check_lock_state('b', '123'));
$this->assertNull($store->check_lock_state('c', '123'));
}
/**
* Tests the get_last_io_bytes function when not using compression (just returns unknown).
*/
+9
View File
@@ -1426,6 +1426,15 @@ class cache_test extends \advanced_testcase {
$this->assertFalse($cache->set_versioned('v', 1, 'data'));
$this->assertFalse($cache->delete('test'));
$this->assertTrue($cache->purge());
// Checking a lock should always report that we have one.
// Acquiring or releasing a lock should always report success.
$this->assertTrue($cache->check_lock_state('test'));
$this->assertTrue($cache->acquire_lock('test'));
$this->assertTrue($cache->acquire_lock('test'));
$this->assertTrue($cache->check_lock_state('test'));
$this->assertTrue($cache->release_lock('test'));
$this->assertTrue($cache->release_lock('test'));
$this->assertTrue($cache->check_lock_state('test'));
// Test a session cache.
$cache = cache::make_from_params(cache_store::MODE_SESSION, 'phpunit', 'disable');
+1 -1
View File
@@ -6,6 +6,6 @@ define("core_calendar/manage_subscriptions",["exports","core_calendar/selectors"
* @copyright 2021 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 4.0
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,CalendarSelectors=_interopRequireWildcard(CalendarSelectors),CalendarRepository=_interopRequireWildcard(CalendarRepository),Modal=_interopRequireWildcard(Modal),ModalEvents=_interopRequireWildcard(ModalEvents),_prefetch=(obj=_prefetch)&&obj.__esModule?obj:{default:obj};const getSubscriptionName=element=>element.closest("tr").dataset.subname,registerEventListeners=()=>{document.addEventListener("click",(e=>{const deleteAction=e.target.closest(CalendarSelectors.actions.deleteSubscription);if(deleteAction){e.preventDefault();((element,messageCode)=>{const subscriptionName=getSubscriptionName(element);return Modal.create({type:Modal.types.SAVE_CANCEL,title:(0,_str.get_string)("confirmation","admin"),body:(0,_str.get_string)(messageCode,"calendar",subscriptionName),buttons:{save:(0,_str.get_string)("yes")}}).then((modal=>(modal.getRoot().on(ModalEvents.hidden,(()=>{element.focus()})),modal.show(),modal)))})(deleteAction,"confirmsubscriptiondelete").then((modal=>(modal.getRoot().on(ModalEvents.save,(()=>{const subscriptionId=parseInt(deleteAction.closest("tr").dataset.subid);CalendarRepository.deleteSubscription(subscriptionId).then((data=>{const response=(async(element,data)=>{const subscriptionName=getSubscriptionName(element),message=data.status?await(0,_str.get_string)("subscriptionremoved","calendar",subscriptionName):data.warnings[0].message,type=data.status?"info":"error";return(0,_notification.addNotification)({message:message,type:type})})(deleteAction,data);return response.then((()=>{const subscriptionRow=(subscriptionId=>document.querySelector('tr[data-subid="'.concat(subscriptionId,'"]')))(subscriptionId);return subscriptionRow.remove()}))})).catch(_notification.displayException)})),modal))).catch(_notification.displayException)}})),document.addEventListener(_events.eventTypes.elementUpdated,(e=>{"core_calendar"==e.target.getAttribute("data-component")&&(0,_notification.fetchNotifications)()}))};_exports.init=()=>{_prefetch.default.prefetchStrings("moodle",["yes"]),_prefetch.default.prefetchStrings("core_admin",["confirmation"]),_prefetch.default.prefetchStrings("core_calendar",["confirmsubscriptiondelete","subscriptionremoved"]),registerEventListeners()}}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,CalendarSelectors=_interopRequireWildcard(CalendarSelectors),CalendarRepository=_interopRequireWildcard(CalendarRepository),Modal=_interopRequireWildcard(Modal),ModalEvents=_interopRequireWildcard(ModalEvents),_prefetch=(obj=_prefetch)&&obj.__esModule?obj:{default:obj};const getSubscriptionName=element=>element.closest("tr").dataset.subname,registerEventListeners=()=>{document.addEventListener("click",(e=>{const deleteAction=e.target.closest(CalendarSelectors.actions.deleteSubscription);if(deleteAction){e.preventDefault();((element,messageCode)=>{const subscriptionName=getSubscriptionName(element);return Modal.create({type:Modal.types.SAVE_CANCEL,title:(0,_str.get_string)("confirmation","admin"),body:(0,_str.get_string)(messageCode,"calendar",subscriptionName),buttons:{save:(0,_str.get_string)("yes")}}).then((modal=>(modal.getRoot().on(ModalEvents.hidden,(()=>{element.focus()})),modal.show(),modal)))})(deleteAction,"confirmsubscriptiondelete").then((modal=>(modal.getRoot().on(ModalEvents.save,(()=>{const subscriptionId=parseInt(deleteAction.closest("tr").dataset.subid);CalendarRepository.deleteSubscription(subscriptionId).then((data=>{const response=(async(element,data)=>{const subscriptionName=getSubscriptionName(element),message=data.status?await(0,_str.get_string)("subscriptionremoved","calendar",subscriptionName):data.warnings[0].message,type=data.status?"info":"error";return(0,_notification.addNotification)({message:message,type:type})})(deleteAction,data);return response.then((()=>{const subscriptionRow=(subscriptionId=>document.querySelector('tr[data-subid="'.concat(subscriptionId,'"]')))(subscriptionId);return subscriptionRow.remove()}))})).catch(_notification.exception)})),modal))).catch(_notification.exception)}})),document.addEventListener(_events.eventTypes.elementUpdated,(e=>{"core_calendar"==e.target.getAttribute("data-component")&&(0,_notification.fetchNotifications)()}))};_exports.init=()=>{_prefetch.default.prefetchStrings("moodle",["yes"]),_prefetch.default.prefetchStrings("core_admin",["confirmation"]),_prefetch.default.prefetchStrings("core_calendar",["confirmsubscriptiondelete","subscriptionremoved"]),registerEventListeners()}}));
//# sourceMappingURL=manage_subscriptions.min.js.map
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -26,7 +26,7 @@ import * as CalendarSelectors from 'core_calendar/selectors';
import * as CalendarRepository from 'core_calendar/repository';
import * as Modal from 'core/modal_factory';
import * as ModalEvents from 'core/modal_events';
import {displayException, addNotification, fetchNotifications} from 'core/notification';
import {exception as displayException, addNotification, fetchNotifications} from 'core/notification';
import Prefetch from 'core/prefetch';
import {get_string as getString} from 'core/str';
import {eventTypes} from 'core/local/inplace_editable/events';
+3 -1
View File
@@ -126,7 +126,9 @@ class progress_test extends \advanced_testcase {
* @covers \core_completion\progress::get_course_progress_percentage.
*/
public function test_course_progress_percentage_completion_state() {
global $DB;
global $DB, $CFG;
require_once("{$CFG->dirroot}/completion/criteria/completion_criteria_activity.php");
// Add a course that supports completion.
$course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+8 -4
View File
@@ -65,8 +65,10 @@ const registerListenerEvents = (contentBank) => {
});
const heading = fileArea.querySelector(selectors.elements.heading);
heading.removeAttribute('role');
heading.querySelectorAll(selectors.elements.cell).forEach(cell => cell.removeAttribute('role'));
if (heading) {
heading.removeAttribute('role');
heading.querySelectorAll(selectors.elements.cell).forEach(cell => cell.removeAttribute('role'));
}
}
viewGrid.classList.add('active');
viewList.classList.remove('active');
@@ -87,8 +89,10 @@ const registerListenerEvents = (contentBank) => {
});
const heading = fileArea.querySelector(selectors.elements.heading);
heading.setAttribute('role', 'row');
heading.querySelectorAll(selectors.elements.cell).forEach(cell => cell.setAttribute('role', 'columnheader'));
if (heading) {
heading.setAttribute('role', 'row');
heading.querySelectorAll(selectors.elements.cell).forEach(cell => cell.setAttribute('role', 'columnheader'));
}
}
viewList.classList.add('active');
viewGrid.classList.remove('active');
+3 -2
View File
@@ -132,8 +132,9 @@ class bankcontent implements renderable, templatable {
}
$options = [];
foreach ($this->allowedcategories as $allowedcategory) {
$options[$allowedcategory->ctxid] = format_string($allowedcategory->name, true,
context_coursecat::instance($allowedcategory->ctxinstance));
$options[$allowedcategory->ctxid] = format_string($allowedcategory->name, true, [
'context' => context_coursecat::instance($allowedcategory->ctxinstance),
]);
}
if (!empty($options)) {
$allowedcontexts['categories'] = [get_string('coursecategories') => $options];
+1 -1
View File
@@ -900,7 +900,7 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
// Trigger a purge for all caches listening for changes to category enrolment.
cache_helper::purge_by_event('changesincategoryenrolment');
if (!$CFG->coursecontact || !in_array($roleid, explode(',', $CFG->coursecontact))) {
if (empty($CFG->coursecontact) || !in_array($roleid, explode(',', $CFG->coursecontact))) {
// The role is not one of course contact roles.
return;
}
@@ -158,7 +158,7 @@
}
}
}}
<div id="{{uniqid}}-course-format">
<div id="course-format-{{uniqid}}">
<h2 class="accesshide">{{{title}}}</h2>
{{{completionhelp}}}
<ul class="{{format}}" data-for="course_sectionlist">
@@ -202,6 +202,6 @@
</div>
{{#js}}
require(['core_courseformat/local/content'], function(component) {
component.init('{{uniqid}}-course-format', {}, {{sectionreturn}});
component.init('course-format-{{uniqid}}', {}, {{sectionreturn}});
});
{{/js}}
@@ -114,7 +114,7 @@
{{/activityinfo}}
{{#altcontent}}
<div class="activity-altcontent {{#hasname}}course-description-item small{{/hasname}}{{^hasname}}contentwithoutlink{{/hasname}} d-flex">
<div class="flex-fill description-inner">
<div class="flex-fill description-inner text-break">
{{{altcontent}}}
</div>
</div>
@@ -38,7 +38,7 @@
{{^accessvisible}}dimmed{{/accessvisible}}
{{#url}} d-flex {{/url}} {{^url}} d-flex-noedit {{/url}}
{{#indent}} indented {{/indent}}"
id="{{uniqid}}-course-index-cm-{{id}}"
id="course-index-cm-{{id}}"
data-for="cm"
data-id="{{id}}"
role="treeitem"
@@ -68,6 +68,6 @@
</li>
{{#js}}
require(['core_courseformat/local/courseindex/cm'], function(component) {
component.init('{{uniqid}}-course-index-cm-{{id}}');
component.init('course-index-cm-{{id}}');
});
{{/js}}
@@ -99,13 +99,13 @@
}
}}
<div id="{{uniqid}}-course-index" class="courseindex {{#editmode}} editing {{/editmode}}" role="tree">
<div id="course-index" class="courseindex {{#editmode}} editing {{/editmode}}" role="tree">
{{#sections}}
{{> core_courseformat/local/courseindex/section }}
{{/sections}}
</div>
{{#js}}
require(['core_courseformat/local/courseindex/courseindex'], function(component) {
component.init('{{uniqid}}-course-index');
component.init('course-index');
});
{{/js}}
@@ -22,7 +22,7 @@
Example context (json):
{}
}}
<div data-region="loading-placeholder-content" aria-hidden="true" id="{{uniqid}}-course-index-placeholder">
<div data-region="loading-placeholder-content" aria-hidden="true" id="course-index-placeholder">
<ul class="media-list">
<li class="media">
<div class="media-body col-md-6 p-0 d-flex align-items-center">
@@ -52,6 +52,6 @@
</div>
{{#js}}
require(['core_courseformat/local/courseindex/placeholder'], function(component) {
component.init('{{uniqid}}-course-index-placeholder');
component.init('course-index-placeholder');
});
{{/js}}
@@ -57,7 +57,7 @@
}}
<div
class="courseindex-section {{#current}}current{{/current}}"
id="{{uniqid}}-course-index-section-{{id}}"
id="course-index-section-{{id}}"
data-for="section"
data-id="{{id}}"
data-number="{{number}}"
@@ -119,6 +119,6 @@
</div>
{{#js}}
require(['core_courseformat/local/courseindex/section'], function(component) {
component.init('{{uniqid}}-course-index-section-{{id}}');
component.init('course-index-section-{{id}}');
});
{{/js}}
+1 -1
View File
@@ -1218,7 +1218,7 @@ class core_course_renderer extends plugin_renderer_base {
html_writer::tag('span', $file->get_filename(), ['class' => 'fp-filename']);
$contentfiles .= html_writer::tag('span',
html_writer::link($url, $filename),
['class' => 'coursefile fp-filename-icon']);
['class' => 'coursefile fp-filename-icon text-break']);
}
}
return $contentimages . $contentfiles;
@@ -25,15 +25,21 @@
"name": "assign",
"title": "Assignment",
"link": "http://yourmoodle/modedit.php?id=x&itemtype=y",
"icon": "<img class='icon' src='http://urltooptionicon'>",
"icon": "<img class='icon' src='http://urltooptionicon' alt='Icon'>",
"help": "This is a description of the assignment activity",
"archetype": 0,
"componentname": "mod_assign",
"favourite": 1
}
}}
<div class="optionsummary" tabindex="-1" data-region="chooser-option-summary-container" aria-labelledby="optionsummary_label" aria-describedby="optionsumary_desc">
<div class="content text-left mb-5 p-2 px-sm-5 py-sm-4" data-region="chooser-option-summary-content-container">
<div
class="optionsummary d-flex flex-column rounded-bottom"
tabindex="-1"
data-region="chooser-option-summary-container"
aria-labelledby="optionsummary_label-{{uniqid}}"
aria-describedby="optionsumary_desc-{{uniqid}}"
>
<div class="content flex-grow-1 text-left p-2 px-sm-5 py-sm-4" data-region="chooser-option-summary-content-container">
<div class="heading mb-4">
<h5 id="optionsummary_label-{{uniqid}}" data-region="summary-header" tabindex="0">
{{{icon}}}
@@ -45,7 +51,7 @@
</div>
</div>
{{^showFooter}}
<div class="fixed-bottom position-absolute py-3 px-4 border-top modal-footer">
<div class="modal-footer">
{{>core_course/local/activitychooser/footer_partial}}
</div>
{{/showFooter}}
@@ -162,10 +162,18 @@ class user_repository {
"to user '{$ltiuser->userid}' and can't be associated with another user '{$userrecord->id}'.");
}
$userrecord->timemodified = $timenow;
$ltiuserrecord->timemodified = $timenow;
\user_update_user($userrecord);
// Only update the Moodle user record if something has changed.
$rawuser = \core_user::get_user($userrecord->id);
$userfieldstocompare = array_intersect_key(
(array) $rawuser,
(array) $userrecord
);
if (!empty(array_diff((array) $userrecord, $userfieldstocompare))) {
\user_update_user($userrecord);
}
unset($userrecord->id);
$ltiuserrecord->timemodified = $timenow;
$DB->update_record($this->ltiuserstable, $ltiuserrecord);
} else {
// Validate uniqueness of the lti user, in the case of a stale object coming in to be saved.
@@ -173,8 +181,17 @@ class user_repository {
throw new \coding_exception("Cannot create duplicate LTI user '{$user->get_localid()}' for resource " .
"'{$user->get_resourceid()}'.");
}
// Only update the Moodle user record if something has changed.
$userid = $userrecord->id;
\user_update_user($userrecord);
$rawuser = \core_user::get_user($userid);
$userfieldstocompare = array_intersect_key(
(array) $rawuser,
(array) $userrecord
);
if (!empty(array_diff((array) $userrecord, $userfieldstocompare))) {
\user_update_user($userrecord);
}
unset($userrecord->id);
// Create the lti_user record, holding details that have a lifespan equal to that of the enrolment instance.
@@ -218,7 +235,8 @@ class user_repository {
FROM {{$this->ltiuserstable}} lu
JOIN {user} u
ON (u.id = lu.userid)
WHERE lu.id = :id";
WHERE lu.id = :id
AND lu.ltideploymentid IS NOT NULL";
$record = $DB->get_record_sql($sql, ['id' => $id], MUST_EXIST);
return $this->user_from_record($record);
@@ -245,7 +263,8 @@ class user_repository {
JOIN {user} u
ON (u.id = lu.userid)
WHERE lu.userid = :userid
AND lu.toolid = :resourceid";
AND lu.toolid = :resourceid
AND lu.ltideploymentid IS NOT NULL";
$params = ['userid' => $userid, 'resourceid' => $resourceid];
$record = $DB->get_record_sql($sql, $params, MUST_EXIST);
@@ -270,6 +289,7 @@ class user_repository {
JOIN {user} u
ON (u.id = lu.userid)
WHERE lu.toolid = :resourceid
AND lu.ltideploymentid IS NOT NULL
ORDER BY lu.lastaccess DESC";
$records = $DB->get_records_sql($sql, ['resourceid' => $resourceid]);
+5
View File
@@ -148,6 +148,11 @@ class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {
// Sign using the key
$ok = openssl_sign($base_string, $signature, $privatekeyid);
// Avoid passing null values to base64_encode.
if (!$ok) {
throw new OAuthException("OpenSSL unable to sign data");
}
// TODO: Remove this block once PHP 8.0 becomes required.
if (PHP_MAJOR_VERSION < 8) {
// Release the key resource
+1
View File
@@ -6,3 +6,4 @@ In future releases we should look into using a supported library.
2022-01-05 - MDL-73502 - Removed get_magic_quotes_gpc() use, was returning false since ages ago.
2022-01-20 - MDL-73523 - Conditional openssl_free_key() use, deprecated by PHP 8.0
2022-03-05 - MDL-73520 - replace deprecated php_errormsg with error_get_last(), deprecated by PHP 8.0
2023-05-03 - MDL-77840 - Throw exception on openssl_sign to avoid null reaching base64_encode, deprecated by PHP 8.1
+1
View File
@@ -54,6 +54,7 @@ $string['enrolenddate'] = 'End date';
$string['enrolenddate_help'] = 'If enabled, users can access until this date only.';
$string['enrolenddateerror'] = 'Enrolment end date cannot be earlier than start date';
$string['enrolisdisabled'] = 'The \'Publish as LTI tool\' plugin is disabled.';
$string['enrolltiversionincorrect'] = 'The resource is not set up for use over legacy LTI (versions 1.1/2.0). Please contact the administrator of this tool.';
$string['enrolperiod'] = 'Enrolment duration';
$string['enrolperiod_help'] = 'Length of time that the enrolment is valid, starting with the moment the user enrols themselves from the remote system. If disabled, the enrolment duration will be unlimited.';
$string['enrolmentfinished'] = 'Enrolment finished.';
+3 -3
View File
@@ -42,7 +42,7 @@ use enrol_lti\local\ltiadvantage\repository\user_repository;
use enrol_lti\local\ltiadvantage\service\application_registration_service;
require_once(__DIR__."/../../config.php");
global $OUTPUT, $PAGE, $CFG;
global $OUTPUT, $PAGE, $CFG, $SITE;
require_once($CFG->libdir . '/filelib.php');
$PAGE->set_context(context_system::instance());
@@ -113,9 +113,9 @@ $regrequest = (object) [
$CFG->wwwroot . '/enrol/lti/launch_deeplink.php',
],
// TODO: Consider whether to support client_name#ja syntax for multi language support - see MDL-73109.
'client_name' => get_string('moodle', 'enrol_lti'),
'client_name' => format_string($SITE->fullname, true, ['context' => context_system::instance()]),
'jwks_uri' => $CFG->wwwroot . '/enrol/lti/jwks.php',
'logo_uri' => $OUTPUT->image_url('moodlelogo')->out(false),
'logo_uri' => $OUTPUT->get_compact_logo_url() ? $OUTPUT->get_compact_logo_url()->out(false) : '',
'token_endpoint_auth_method' => 'private_key_jwt',
'scope' => implode(" ", $scopes),
'https://purl.imsglobal.org/spec/lti-tool-configuration' => [
@@ -31,9 +31,10 @@ class user_repository_test extends \advanced_testcase {
* Helper to generate a new user instance.
*
* @param int $mockresourceid used to spoof a published resource, to which this user is associated.
* @param array $userfields user information like city, timezone which would normally come from the tool configuration.
* @return user a user instance
*/
protected function generate_user(int $mockresourceid = 1): user {
protected function generate_user(int $mockresourceid = 1, array $userfields = []): user {
global $CFG;
$registration = application_registration::create(
'Test',
@@ -63,16 +64,35 @@ class user_repository_test extends \advanced_testcase {
$savedcontext->get_id());
$savedresourcelink = $resourcelinkrepo->save($resourcelink);
$user = $this->getDataGenerator()->create_user();
// Create a user using the DB defaults to simulate what would have occurred during an auth_lti user auth.
$user = $this->getDataGenerator()->create_user([
'city' => '',
'country' => '',
'institution' => '',
'timezone' => '99',
'maildisplay' => 2,
'lang' => 'en'
]);
$userdefaultvalues = [
'lang' => $CFG->lang,
'city' => '',
'country' => '',
'institution' => '',
'timezone' => '99',
'maildisplay' => 2
];
if (empty($userfields)) {
// If userfields is omitted, assume the tool default configuration values (as if 'User default values' are unchanged).
$userfields = $userdefaultvalues;
} else {
// If they have been provided, merge and override the defaults.
$userfields = array_merge($userdefaultvalues, $userfields);
}
$ltiuser = $savedresourcelink->add_user(
$user->id,
'source-id-123',
$CFG->lang,
'Perth',
'AU',
'An Example Institution',
'99',
2,
...array_values($userfields)
);
$ltiuser->set_lastgrade(67.33333333);
@@ -141,19 +161,45 @@ class user_repository_test extends \advanced_testcase {
}
/**
* Tests adding a user to the store.
* Tests adding a user to the store, assuming that the user has been created using the default 'user default values'.
*
* @covers ::save
*/
public function test_save_new() {
public function test_save_new_unchanged_user_defaults() {
$this->resetAfterTest();
$user = $this->generate_user();
$userrepo = new user_repository();
$sink = $this->redirectEvents();
$saveduser = $userrepo->save($user);
$events = $sink->get_events();
$sink->close();
$this->assertIsInt($saveduser->get_id());
$this->assert_same_user_values($user, $saveduser, true);
$this->assert_user_db_values($saveduser);
// No change to underlying user: city, etc. take on default values matching those of the existing user record.
$this->assertEmpty($events);
}
/**
* Tests adding a user to the store, assuming that the user has been created using modified 'user default values'.
*
* @covers ::save
*/
public function test_save_new_changed_user_defaults() {
$this->resetAfterTest();
$user = $this->generate_user(1, ['city' => 'Perth']);
$userrepo = new user_repository();
$sink = $this->redirectEvents();
$saveduser = $userrepo->save($user);
$events = $sink->get_events();
$sink->close();
$this->assertIsInt($saveduser->get_id());
$this->assert_same_user_values($user, $saveduser, true);
$this->assert_user_db_values($saveduser);
// The underlying user record will change: city ('Perth') differs from that of the existing user ('').
$this->assertInstanceOf(\core\event\user_updated::class, $events[0]);
}
/**
@@ -165,16 +211,25 @@ class user_repository_test extends \advanced_testcase {
$this->resetAfterTest();
$user = $this->generate_user();
$userrepo = new user_repository();
$sink = $this->redirectEvents();
$saveduser = $userrepo->save($user);
$events = $sink->get_events();
$sink->close();
$this->assertEmpty($events); // No event for the first save, since the underlying user record is unchanged.
$saveduser->set_city('New City');
$saveduser->set_country('NZ');
$saveduser->set_lastgrade(99.99999999);
$sink = $this->redirectEvents();
$saveduser2 = $userrepo->save($saveduser);
$events = $sink->get_events();
$sink->close();
$this->assertEquals($saveduser->get_id(), $saveduser2->get_id());
$this->assert_same_user_values($saveduser, $saveduser2, true);
$this->assert_user_db_values($saveduser2);
// The underlying user record will change now, since city and country have changed.
$this->assertInstanceOf(\core\event\user_updated::class, $events[0]);
}
/**
@@ -455,4 +510,43 @@ class user_repository_test extends \advanced_testcase {
$this->assertEquals($saveduser->get_localid(), $saveduser2->get_localid());
$this->assertNotEquals($saveduser->get_id(), $saveduser2->get_id());
}
/**
* Test confirming that any associated legacy lti user records are not returned by the repository.
*
* This test ensures that any enrolment methods (resources) updated in-place from legacy LTI to 1.3 only return LTI 1.3 users.
*
* @covers ::find
* @covers ::find_single_user_by_resource
* @covers ::find_by_resource
*/
public function test_find_filters_legacy_lti_users(): void {
$this->resetAfterTest();
global $DB;
$user = $this->getDataGenerator()->create_user();
$course = $this->getDataGenerator()->create_course();
$resource = $this->getDataGenerator()->create_lti_tool((object)['courseid' => $course->id]);
$ltiuserdata = [
'userid' => $user->id,
'toolid' => $resource->id,
'sourceid' => '1001',
];
$ltiuserid = $DB->insert_record('enrol_lti_users', $ltiuserdata);
$userrepo = new user_repository();
$this->assertNull($userrepo->find($ltiuserid));
$this->assertNull($userrepo->find_single_user_by_resource($user->id, $resource->id));
$this->assertEmpty($userrepo->find_by_resource($resource->id));
// Set deploymentid, indicating the user originated from an LTI 1.3 launch and should now be returned.
$ltiuserdata['id'] = $ltiuserid;
$ltiuserdata['ltideploymentid'] = '234';
$DB->update_record('enrol_lti_users', $ltiuserdata);
$this->assertInstanceOf(user::class, $userrepo->find($ltiuserid));
$this->assertInstanceOf(user::class, $userrepo->find_single_user_by_resource($user->id, $resource->id));
$ltiusers = $userrepo->find_by_resource($resource->id);
$this->assertCount(1, $ltiusers);
$this->assertInstanceOf(user::class, reset($ltiusers));
}
}
+6
View File
@@ -53,6 +53,12 @@ if ($tool->status != ENROL_INSTANCE_ENABLED) {
exit();
}
// Check if the enrolment instance has been upgraded to a newer LTI version.
if ($tool->ltiversion != 'LTI-1p0/LTI-2p0') {
throw new \moodle_exception('enrolltiversionincorrect', 'enrol_lti');
exit();
}
$consumerkey = required_param('oauth_consumer_key', PARAM_TEXT);
$ltiversion = optional_param('lti_version', null, PARAM_TEXT);
$messagetype = required_param('lti_message_type', PARAM_TEXT);
@@ -72,6 +72,7 @@ Feature: Render H5P content using filters
And I click on "Insert H5P" "button" in the "Insert H5P" "dialogue"
And I wait until the page is ready
When I click on "Save and display" "button"
And I should see "PageName1" in the "page-header" "region"
# Switch to iframe created by filter
And I switch to "h5p-iframe" class iframe
# Switch to iframe created by embed.php page
@@ -107,6 +108,7 @@ Feature: Render H5P content using filters
And I click on "Insert H5P" "button" in the "Insert H5P" "dialogue"
And I wait until the page is ready
When I click on "Save and display" "button"
And I should see "PageName1" in the "page-header" "region"
# Switch to iframe created by filter
And I switch to "h5p-iframe" class iframe
Then I should see "Note that the libraries may exist in the file you uploaded, but you're not allowed to upload new libraries."
@@ -132,6 +134,7 @@ Feature: Render H5P content using filters
And I click on "Insert H5P" "button" in the "Insert H5P" "dialogue"
And I wait until the page is ready
And I click on "Save and display" "button"
And I should see "PageName1" in the "page-header" "region"
# Switch to iframe created by filter
And I switch to "h5p-iframe" class iframe
# Libraries don't exist, so an error should be displayed.
@@ -155,6 +158,7 @@ Feature: Render H5P content using filters
And I click on "Insert H5P" "button" in the "Insert H5P" "dialogue"
And I wait until the page is ready
And I click on "Save and display" "button"
And I should see "PageName2" in the "page-header" "region"
# Switch to iframe created by filter
And I switch to "h5p-iframe" class iframe
# Switch to iframe created by embed.php page
@@ -194,6 +198,7 @@ Feature: Render H5P content using filters
And I click on "Select this file" "button"
And I click on "Insert H5P" "button" in the "Insert H5P" "dialogue"
And I click on "Save and display" "button"
And I should see "PageName1" in the "page-header" "region"
And I switch to "h5p-iframe" class iframe
# Library is disabled, so an error should be displayed.
Then I should see "This file can't be displayed because its content type is disabled."
+2 -27
View File
@@ -16,11 +16,8 @@
* Other platforms could/should be added
*/
public function __construct() {
global $CFG;
// construct directory structure
$this->temp_dir = $CFG->tempdir . "/latex";
make_temp_directory('latex');
// Construct directory structure.
$this->temp_dir = make_request_directory();
}
/**
@@ -160,26 +157,4 @@
return $img;
}
/**
* Delete files created in temporary area
* Don't forget to copy the final gif/png before calling this
* @param string $filename file base (no extension)
*/
function clean_up( $filename ) {
global $CFG;
unlink( "{$this->temp_dir}/$filename.tex" );
unlink( "{$this->temp_dir}/$filename.dvi" );
unlink( "{$this->temp_dir}/$filename.ps" );
$convertformat = get_config('filter_tex', 'convertformat');
unlink( "{$this->temp_dir}/$filename.{$convertformat}" );
unlink( "{$this->temp_dir}/$filename.aux" );
unlink( "{$this->temp_dir}/$filename.log" );
return;
}
}
+1
View File
@@ -84,6 +84,7 @@ function filter_tex_sanitize_formula(string $texexp): string {
'\afterassignment', '\expandafter', '\noexpand', '\special',
'\let', '\futurelet', '\else', '\fi', '\chardef', '\makeatletter', '\afterground',
'\noexpand', '\line', '\mathcode', '\item', '\section', '\mbox', '\declarerobustcommand',
'\ExplSyntaxOn',
];
$allowlist = ['inputenc'];
-2
View File
@@ -50,8 +50,6 @@ define('NO_MOODLE_COOKIES', true); // Because it interferes with caching
$lateximage = $latex->render($texexp, $image, 12, $density, $background);
if ($lateximage) {
copy($lateximage, $pathname);
$latex->clean_up($md5);
} else {
// failing that, use mimetex
$texexp = $texcache->rawtext;
+1 -1
View File
@@ -5,6 +5,6 @@ define("core_grades/searchwidget/basewidget",["exports","core/utils","core/templ
* @module core_grades/searchwidget/basewidget
* @copyright 2022 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.showLoader=_exports.registerListenerEvents=_exports.promisesAndResolvers=_exports.init=void 0,Templates=_interopRequireWildcard(Templates),Selectors=_interopRequireWildcard(Selectors),_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};_exports.init=async function(widgetContentContainer,bodyPromise,data,searchFunc){let unsearchableContent=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null;bodyPromise.then((async bodyContent=>{if(widgetContentContainer.innerHTML=bodyContent,unsearchableContent){widgetContentContainer.querySelector(Selectors.regions.unsearchableContent).innerHTML+=unsearchableContent}const searchResultsContainer=widgetContentContainer.querySelector(Selectors.regions.searchResults);await showLoader(searchResultsContainer),await renderSearchResults(searchResultsContainer,data),registerListenerEvents(widgetContentContainer,data,searchFunc)})).catch(_notification.default.exception)};const registerListenerEvents=(widgetContentContainer,data,searchFunc)=>{const searchResultsContainer=widgetContentContainer.querySelector(Selectors.regions.searchResults),searchInput=widgetContentContainer.querySelector(Selectors.actions.search);searchInput.focus();const clearSearchButton=widgetContentContainer.querySelector(Selectors.actions.clearSearch);searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{searchInput.value.length>0?clearSearchButton.classList.remove("d-none"):clearSearchButton.classList.add("d-none"),await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}),300)),clearSearchButton.addEventListener("click",(async e=>{e.stopPropagation(),searchInput.value="",searchInput.focus(),clearSearchButton.classList.add("d-none"),await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}))};_exports.registerListenerEvents=registerListenerEvents;const showLoader=async container=>{const{html:html,js:js}=await Templates.renderForPromise("core_grades/searchwidget/loading",{});Templates.replaceNodeContents(container,html,js)};_exports.showLoader=showLoader;const debounceCallee=(searchValue,data,searchFunction)=>searchValue.length>0?searchFunction(data,searchValue):data,renderSearchResults=async(searchResultsContainer,searchResultsData)=>{const templateData={searchresults:searchResultsData},{html:html,js:js}=await Templates.renderForPromise("core_grades/searchwidget/searchresults",templateData);await Templates.replaceNodeContents(searchResultsContainer,html,js)};_exports.promisesAndResolvers=()=>{let bodyPromiseResolver;const bodyPromise=new Promise((resolve=>{bodyPromiseResolver=resolve}));return{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}}}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.showLoader=_exports.registerListenerEvents=_exports.promisesAndResolvers=_exports.init=void 0,Templates=_interopRequireWildcard(Templates),Selectors=_interopRequireWildcard(Selectors),_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};_exports.init=async function(widgetContentContainer,bodyPromise,data,searchFunc){let unsearchableContent=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null;bodyPromise.then((async bodyContent=>{if(widgetContentContainer.innerHTML=bodyContent,unsearchableContent){widgetContentContainer.querySelector(Selectors.regions.unsearchableContent).innerHTML+=unsearchableContent}const searchResultsContainer=widgetContentContainer.querySelector(Selectors.regions.searchResults);await showLoader(searchResultsContainer),await renderSearchResults(searchResultsContainer,data),registerListenerEvents(widgetContentContainer,data,searchFunc)})).catch(_notification.default.exception)};const registerListenerEvents=(widgetContentContainer,data,searchFunc)=>{const searchResultsContainer=widgetContentContainer.querySelector(Selectors.regions.searchResults),searchInput=widgetContentContainer.querySelector(Selectors.actions.search);if(!searchInput)return;searchInput.focus();const clearSearchButton=widgetContentContainer.querySelector(Selectors.actions.clearSearch);searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{searchInput.value.length>0?clearSearchButton.classList.remove("d-none"):clearSearchButton.classList.add("d-none"),await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}),300)),clearSearchButton.addEventListener("click",(async e=>{e.stopPropagation(),searchInput.value="",searchInput.focus(),clearSearchButton.classList.add("d-none"),await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}))};_exports.registerListenerEvents=registerListenerEvents;const showLoader=async container=>{const{html:html,js:js}=await Templates.renderForPromise("core_grades/searchwidget/loading",{});Templates.replaceNodeContents(container,html,js)};_exports.showLoader=showLoader;const debounceCallee=(searchValue,data,searchFunction)=>searchValue.length>0?searchFunction(data,searchValue):data,renderSearchResults=async(searchResultsContainer,searchResultsData)=>{const templateData={searchresults:searchResultsData},{html:html,js:js}=await Templates.renderForPromise("core_grades/searchwidget/searchresults",templateData);await Templates.replaceNodeContents(searchResultsContainer,html,js)};_exports.promisesAndResolvers=()=>{let bodyPromiseResolver;const bodyPromise=new Promise((resolve=>{bodyPromiseResolver=resolve}));return{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}}}));
//# sourceMappingURL=basewidget.min.js.map
File diff suppressed because one or more lines are too long
+6
View File
@@ -68,6 +68,12 @@ export const init = async(widgetContentContainer, bodyPromise, data, searchFunc,
export const registerListenerEvents = (widgetContentContainer, data, searchFunc) => {
const searchResultsContainer = widgetContentContainer.querySelector(Selectors.regions.searchResults);
const searchInput = widgetContentContainer.querySelector(Selectors.actions.search);
if (!searchInput) {
// Too late. The widget is already closed and its content is empty.
return;
}
// We want to focus on the first known user interable element within the dropdown.
searchInput.focus();
const clearSearchButton = widgetContentContainer.querySelector(Selectors.actions.clearSearch);
+1 -2
View File
@@ -80,7 +80,7 @@ class singleview extends core_course_external {
$coursecontext = context_course::instance($params['courseid']);
parent::validate_context($coursecontext);
$gtree = new grade_tree($params['courseid'], false, false, null, !$CFG->enableoutcomes);
$gtree = new grade_tree($params['courseid'], false, true, null, !$CFG->enableoutcomes);
$gradeableitems = $gtree->get_items();
$gradeitems = array_map(function ($gradeitem) use ($PAGE, $USER, $params) {
@@ -98,7 +98,6 @@ class singleview extends core_course_external {
return $item;
}, $gradeableitems);
sort($gradeitems);
return [
'gradeitems' => $gradeitems,
@@ -157,7 +157,8 @@ class grade_items extends base {
$html = \html_writer::start_div('page-context-header' . $dimmed);
// Image data.
$html .= \html_writer::div($imagedata, 'page-header-image mr-2');
$prefix = \html_writer::div($row->itemmodule, 'text-muted text-uppercase small line-height-3');
$prefix = \html_writer::div(get_string('pluginname', "mod_{$row->itemmodule}"),
'text-muted text-uppercase small line-height-3');
$name = $prefix . \html_writer::link($url, format_string($cm->name, true));
$html .= \html_writer::tag('div', $name, ['class' => 'page-header-headings']);
} else {
+11 -7
View File
@@ -187,7 +187,8 @@ class core_group_external extends external_api {
}
require_capability('moodle/course:managegroups', $context);
list($group->description, $group->descriptionformat) =
$group->name = external_format_string($group->name, $context);
[$group->description, $group->descriptionformat] =
external_format_text($group->description, $group->descriptionformat,
$context->id, 'group', 'description', $group->id);
@@ -209,7 +210,7 @@ class core_group_external extends external_api {
array(
'id' => new external_value(PARAM_INT, 'group record id'),
'courseid' => new external_value(PARAM_INT, 'id of course'),
'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
'name' => new external_value(PARAM_TEXT, 'group name'),
'description' => new external_value(PARAM_RAW, 'group description text'),
'descriptionformat' => new external_format_value('description'),
'enrolmentkey' => new external_value(PARAM_RAW, 'group enrol secret phrase'),
@@ -260,7 +261,8 @@ class core_group_external extends external_api {
$groups = array();
foreach ($gs as $group) {
list($group->description, $group->descriptionformat) =
$group->name = external_format_string($group->name, $context);
[$group->description, $group->descriptionformat] =
external_format_text($group->description, $group->descriptionformat,
$context->id, 'group', 'description', $group->id);
$groups[] = (array)$group;
@@ -281,7 +283,7 @@ class core_group_external extends external_api {
array(
'id' => new external_value(PARAM_INT, 'group record id'),
'courseid' => new external_value(PARAM_INT, 'id of course'),
'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
'name' => new external_value(PARAM_TEXT, 'group name'),
'description' => new external_value(PARAM_RAW, 'group description text'),
'descriptionformat' => new external_format_value('description'),
'enrolmentkey' => new external_value(PARAM_RAW, 'group enrol secret phrase'),
@@ -1264,7 +1266,8 @@ class core_group_external extends external_api {
'g.id, g.name, g.description, g.descriptionformat, g.idnumber');
foreach ($groups as $group) {
list($group->description, $group->descriptionformat) =
$group->name = external_format_string($group->name, $course->context);
[$group->description, $group->descriptionformat] =
external_format_text($group->description, $group->descriptionformat,
$course->context->id, 'group', 'description', $group->id);
$group->courseid = $course->id;
@@ -1303,7 +1306,7 @@ class core_group_external extends external_api {
return new external_single_structure(
array(
'id' => new external_value(PARAM_INT, 'group record id'),
'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
'name' => new external_value(PARAM_TEXT, 'group name'),
'description' => new external_value(PARAM_RAW, 'group description text'),
'descriptionformat' => new external_format_value('description'),
'idnumber' => new external_value(PARAM_RAW, 'id number'),
@@ -1388,7 +1391,8 @@ class core_group_external extends external_api {
$groups = groups_get_activity_allowed_groups($cm, $user->id);
foreach ($groups as $group) {
list($group->description, $group->descriptionformat) =
$group->name = external_format_string($group->name, $coursecontext);
[$group->description, $group->descriptionformat] =
external_format_text($group->description, $group->descriptionformat,
$coursecontext->id, 'group', 'description', $group->id);
$group->courseid = $cm->course;
+4 -3
View File
@@ -95,7 +95,7 @@ switch ($action) {
foreach($groupmemberroles as $roleid=>$roledata) {
$shortroledata = new stdClass();
$shortroledata->name = $roledata->name;
$shortroledata->name = html_entity_decode($roledata->name, ENT_QUOTES, 'UTF-8');
$shortroledata->users = array();
foreach($roledata->users as $member) {
$shortmember = new stdClass();
@@ -183,7 +183,7 @@ if ($groups) {
foreach ($groups as $group) {
$selected = false;
$usercount = $DB->count_records('groups_members', array('groupid' => $group->id));
$groupname = format_string($group->name) . ' (' . $usercount . ')';
$groupname = format_string($group->name, true, ['context' => $context, 'escape' => false]) . ' (' . $usercount . ')';
if (in_array($group->id, $groupids)) {
$selected = true;
if ($singlegroup) {
@@ -234,8 +234,9 @@ if ($singlegroup) {
$users[] = $shortmember;
}
$members[] = (object)[
'role' => s($roledata->name),
'role' => html_entity_decode($roledata->name, ENT_QUOTES, 'UTF-8'),
'rolemembers' => $users
];
}
+1 -1
View File
@@ -120,7 +120,7 @@
{{#members}}
<optgroup label="{{role}}">
{{#rolemembers}}
<option value="{{value}}" title="{{{text}}}">{{{text}}}</option>
<option value="{{value}}" title="{{text}}">{{{text}}}</option>
{{/rolemembers}}
</optgroup>
{{/members}}
+7
View File
@@ -1,6 +1,13 @@
This files describes API changes in /group/*,
information provided here is intended especially for developers.
=== 4.1.4 ===
* The following external methods now return group names correctly formatted:
- `core_group_get_groups`
- `core_group_get_course_groups`
- `core_group_get_course_user_groups`
- `core_group_get_activity_allowed_groups`
=== 3.11 ===
* The groups do not support 'hidepicture' any more, and so the column 'hidepicture'
+1
View File
@@ -32,4 +32,5 @@ defined('MOODLE_INTERNAL') || die();
$string['language'] = 'भाषा';
$string['moodlelogo'] = 'Moodle लोगो';
$string['next'] = 'अगला';
$string['previous'] = 'पिछला';
$string['reload'] = 'सीमा से अधिक लादना';
+1
View File
@@ -29,5 +29,6 @@
defined('MOODLE_INTERNAL') || die();
$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Монгол';
+2 -2
View File
@@ -33,9 +33,9 @@ $string['admindirname'] = 'Director Admin';
$string['availablelangs'] = 'Pachete de limbă disponibile';
$string['chooselanguagehead'] = 'Selectare limbă';
$string['chooselanguagesub'] = 'Vă rugăm selectaţi limba pentru interfaţa de instalare, limba selectată va fi folosită EXCLUSIV în cadrul procedurii de instalare. Ulterior veţi putea selecta limba în care doriţi să fie afişată interfaţa.';
$string['clialreadyconfigured'] = 'Fișierul de configurare';
$string['clialreadyconfigured'] = 'Fișierul de configurare config.php există deja. Te rugăm să utilizezi admin/cli/install_database.php pentru a instala Moodle pe acest site.';
$string['clialreadyinstalled'] = 'Fișierul de configurare config.php există deja. Vă rugăm să folosiți dmin/cli/install_database.php to pentru a upgrada Moodle pentru acest site.';
$string['cliinstallheader'] = 'Program command line installation Moodle {$a}';
$string['cliinstallheader'] = 'Program command line installation {$a} Moodle';
$string['clitablesexist'] = 'Tabele de baze de date deja prezente; Instalarea CLI nu poate continua.';
$string['databasehost'] = 'Gazdă baza de date';
$string['databasename'] = 'Nume baza de date';
+5 -5
View File
@@ -383,7 +383,7 @@ $string['configstripalltitletags'] = 'Uncheck this setting to allow HTML tags in
$string['configsupportavailability'] = 'Determines who has access to contact site support from the footer.';
$string['configsupportemail'] = 'If SMTP is configured on this site and a support page is not set, this email address will receive messages submitted through the support form. If sending fails, the email address will be displayed to logged-in users.';
$string['configsupportname'] = 'The name of the person or other entity providing support via the support form or support page.';
$string['configsupportpage'] = 'Enter the URL of a support page or leave empty to link to a contact form.';
$string['configsupportpage'] = 'Enter the URL of a support page or leave empty to link to a contact form. The link is displayed in the site footer.';
$string['configtempdatafoldercleanup'] = 'Remove temporary data files from the data folder that are older than the selected time.';
$string['configthemedesignermode'] = 'Normally all theme images and style sheets are cached in browsers and on the server for a very long time, for performance. If you are designing themes or developing code then you probably want to turn this mode on so that you are not served cached versions. Warning: this will make your site slower for all users! Alternatively, you can also reset the theme caches manually from the Theme selection page.';
$string['configthemelist'] = 'Leave this blank to allow any valid theme to be used. If you want to shorten the theme menu, you can specify a comma-separated list of names here (Don\'t use spaces!).
@@ -606,7 +606,7 @@ $string['environmentrequireversion'] = 'version {$a->needed} is required and you
$string['environmentsettingok'] = 'recommended setting detected';
$string['environmentshouldfixsetting'] = 'PHP setting should be changed.';
$string['environmentxmlerror'] = 'Error reading environment data ({$a->error_code})';
$string['environmentmariadbwrongdbtype'] = 'Wrong <code>$CFG->dbtype</code>: you need to change it in your <code>config.php</code> file, from \'<code>mysql</code>\' to \'<code>mariadb</code>\'.';
$string['environmentmariadbwrongdbtype'] = 'Wrong $CFG->dbtype. You need to change it in your config.php file from \'mysqli\' to \'mariadb\'.';
$string['errordeletingconfig'] = 'An error occurred while deleting the configuration records for plugin \'{$a}\'.';
$string['errorsetting'] = 'Could not save setting:';
$string['errorwithsettings'] = 'Some settings were not changed due to an error.';
@@ -724,9 +724,8 @@ $string['ipblocker'] = 'IP blocker';
$string['ipblockersyntax'] = 'Put every entry on one line. Valid entries are either full IP address (such as <b>192.168.10.1</b>) which matches a single host; or partial address (such as <b>192.168</b>) which matches any address starting with those numbers; or CIDR notation (such as <b>231.54.211.0/20</b>); or a range of IP addresses (such as <b>231.3.56.10-20</b>) where the range applies to the last part of the address. Text domain names (like \'example.com\') are not supported. Blank lines, and text following a "#" character are ignored.';
$string['iplookup'] = 'IP address lookup';
$string['iplookupgeoplugin'] = 'The <a href="https://www.geoplugin.com">geoPlugin</a> service is currently being used to look up geographical information. For more accurate results we recommend installing a local copy of the MaxMind GeoLite database.';
$string['iplookupinfo'] = 'By default Moodle uses the free online NetGeo (The Internet Geographic Database) server to lookup location of IP addresses, unfortunately this database is not maintained anymore and may return <em>wildly incorrect</em> data.
It is recommended to install local copy of free GeoLite2 City database from MaxMind.<br />
IP address location is displayed on simple map or using Google Maps. Please note that you need to have a Google account and apply for free Google Maps API key to enable interactive maps.';
$string['iplookupinfo'] = 'The free online NetGeo server is used to look up IP address locations. The database is not maintained any more though, and may return incorrect data. Installing a local copy of the free GeoLite2 City database from MaxMind is recommended.<br />
If a Google Maps API V3 key is provided, IP address location is displayed using Google Maps. You need a Google account and to apply for a Google Maps API key. If no Google Maps API V3 key is provided, IP address location is displayed using OpenStreetMap.';
$string['iplookupmaxmindnote'] = 'This product includes GeoLite2 data created by MaxMind, available from <a href="https://www.maxmind.com">https://www.maxmind.com</a>.';
$string['iscustomadminwarnings'] = 'It has been detected that your site is using a custom admin directory. This feature is no longer supported and will be removed after Moodle 4.2.';
$string['ishttpswarning'] = 'It has been detected that your site is not secured using HTTPS. It is strongly recommended to migrate your site to HTTPS for increased security and improved integration with other systems.';
@@ -1449,6 +1448,7 @@ $string['unsupportedphpversion73'] = 'PHP version 7.3 and higher are not support
$string['unsupportedphpversion74'] = 'PHP version 7.4 and higher are not supported.';
$string['unsupportedphpversion80'] = 'PHP version 8.0 and higher are not supported.';
$string['unsupportedphpversion81'] = 'PHP version 8.1 and higher are not supported.';
$string['unsupportedphpversion82'] = 'PHP version 8.2 and higher are not supported.';
$string['unsuspenduser'] = 'Activate user account';
$string['updateaccounts'] = 'Update existing accounts';
$string['updatecomponent'] = 'Update component';
+2
View File
@@ -488,6 +488,8 @@ $string['onlyadmins'] = 'Only administrators can do that';
$string['onlyeditingteachers'] = 'Only editing teachers can do that';
$string['onlyeditown'] = 'You can only edit your own information';
$string['orderidnotfound'] = 'Order ID {$a} not found';
$string['opensslsignerror'] = 'OpenSSL unable to sign data';
$string['opensslsealerror'] = 'OpenSSL unable to seal data';
$string['pagenotexisttitle'] = '404 Error: File not found';
$string['pagenotexist'] = '<p>An unusual error occurred trying to view a page that does not exist:</p>{$a}';
$string['pathdoesnotstartslash'] = 'No valid arguments supplied, path does not start with slash!';

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