Compare commits
536 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1321ff0d5 | |||
| e25fbec380 | |||
| 8464ca1b72 | |||
| 03429740d2 | |||
| 32897778cd | |||
| 1e2dcb145f | |||
| df9109fa9e | |||
| ecc53ae6a1 | |||
| e37eadb84b | |||
| d893d639d0 | |||
| 8d365edffe | |||
| 7a5a3f261e | |||
| a8ecc443fe | |||
| 0b32ae0b46 | |||
| 519a09da8b | |||
| beb129d286 | |||
| 1849cca694 | |||
| 628bb639ae | |||
| 8e409341bd | |||
| f856c7ba24 | |||
| 7a281ba10c | |||
| d0a9dba00d | |||
| 37d10d8350 | |||
| 9c3ea97d9b | |||
| 62ae269490 | |||
| 9f97900524 | |||
| e778f6fbbd | |||
| ee439d8fa8 | |||
| de0150412b | |||
| d1a9ba5386 | |||
| d1a3d40f65 | |||
| 7b0eac6677 | |||
| 1044306ce9 | |||
| 8648dc6046 | |||
| f7f1a4562d | |||
| 30b02b20ef | |||
| 94d260a4f8 | |||
| 63c2d91bc5 | |||
| 4fdc0b6768 | |||
| 9883338e8c | |||
| 08ccef4aec | |||
| cc69e589a4 | |||
| 29857c9cde | |||
| 2f6ec1d60a | |||
| 34ce92a046 | |||
| e96ec9501a | |||
| 76997fbf33 | |||
| 8135ded9e3 | |||
| c8f0320b24 | |||
| ab4c436f10 | |||
| 9cf08382ae | |||
| 85d0a666a3 | |||
| 0ea18ecce6 | |||
| 38a820017a | |||
| 4f6526ddb1 | |||
| bf9b563340 | |||
| 02c4d8c217 | |||
| 321a86a858 | |||
| 2d2fd03b2c | |||
| 9ce91a9588 | |||
| b2293b37f3 | |||
| afab4c9d25 | |||
| f1e8b839f8 | |||
| b7a3d429e2 | |||
| 10b28ebb08 | |||
| dfaa45838e | |||
| af0713a6b7 | |||
| 5ee8e97b61 | |||
| 91c8edfbef | |||
| ebdd862f3c | |||
| eb664d6d41 | |||
| c4f1af803c | |||
| 0418c85bb4 | |||
| d5bad6b64d | |||
| c922b363c4 | |||
| 38be896581 | |||
| 6efd33e7b3 | |||
| 0a87d4507b | |||
| 907a3442e9 | |||
| 5e20e1d8b8 | |||
| 7c6d7a9e1f | |||
| c131932352 | |||
| c7d4f457d7 | |||
| 4e09d70272 | |||
| 05d0c9b002 | |||
| e309724b41 | |||
| de7c1fdbe1 | |||
| 8c5c5eaee4 | |||
| e19a406db8 | |||
| 39d9131ad7 | |||
| d9a7733e16 | |||
| a11ccf13b6 | |||
| dc3598bcb4 | |||
| 095ce67f60 | |||
| 81a0fbee7f | |||
| ce4b7b94e0 | |||
| 5d8db4b548 | |||
| 4b9ad6b9fd | |||
| b10dacbc75 | |||
| 9d9d52a404 | |||
| ee4dd60780 | |||
| a923f3724b | |||
| 641528e9d3 | |||
| 7d24bb77b3 | |||
| d5521c1871 | |||
| 6bbceaa405 | |||
| f6c66d2be9 | |||
| bb18341e72 | |||
| c93be20aa5 | |||
| 0214ea3e19 | |||
| 3b847d4860 | |||
| 44dbf84bcb | |||
| a3b82724be | |||
| 4ded9db847 | |||
| ebeb8b6af2 | |||
| bbda6fabe0 | |||
| 850b45a996 | |||
| edbd895f60 | |||
| 01834f4bc1 | |||
| c9ee1c582d | |||
| 00970d6e59 | |||
| 170a6056de | |||
| ffc6111f4e | |||
| a8e32ae7f3 | |||
| ea7b951d3c | |||
| c0cd20aa61 | |||
| a0ffa26132 | |||
| f27faa1305 | |||
| ebc1084cdc | |||
| 0357a43862 | |||
| 85a77b7a6c | |||
| e42e6b6408 | |||
| 8e52bbe341 | |||
| 3204204ad9 | |||
| cc335884de | |||
| 30c73fa641 | |||
| 44f9fb5150 | |||
| b9af7918c8 | |||
| b9a6b71e3c | |||
| 9f330b6b40 | |||
| dcc7e81ac2 | |||
| 54e319351a | |||
| bfeda89d8c | |||
| 84306938ac | |||
| a46f6637cc | |||
| e756ec3228 | |||
| 1c8e7f1c7b | |||
| e6c202c420 | |||
| f666a0d059 | |||
| dfa3176e03 | |||
| 8d5cec0164 | |||
| 7636570179 | |||
| ae21155775 | |||
| b637072b76 | |||
| f10d3c78e0 | |||
| aab8839ab6 | |||
| 7b6463fbba | |||
| 42a7c6df26 | |||
| a3968e0b73 | |||
| aec7468f67 | |||
| ee36d500c9 | |||
| 6cf9ed0f40 | |||
| d0be2a57ce | |||
| 2653626ca0 | |||
| ace600fe84 | |||
| 28fe42c4ec | |||
| c27fde3701 | |||
| a9cf380584 | |||
| 21cb028390 | |||
| d8cbfc285a | |||
| bc3f269ac8 | |||
| 7a6e74a9fe | |||
| c489b7ac0a | |||
| 69c42304f0 | |||
| dea5950384 | |||
| 6aec3fe946 | |||
| 72fe24b91a | |||
| a7c0d3605a | |||
| d53466d1c6 | |||
| cc10e8e245 | |||
| 65bddce33c | |||
| 4bb0555b62 | |||
| 63448b6f98 | |||
| bc472d3ea1 | |||
| ed9673fe1f | |||
| 004cc09738 | |||
| a41dc037c9 | |||
| 4158ea095b | |||
| 337346e3d1 | |||
| b04e0bd518 | |||
| 028728b561 | |||
| b33729ab1d | |||
| f69c22ecda | |||
| ca2c397895 | |||
| 7dfc4481d4 | |||
| dc74787166 | |||
| a6146d1c58 | |||
| ca050231fb | |||
| e4e7d72571 | |||
| 938dbde96c | |||
| 15f30a7314 | |||
| 3bb18b29e0 | |||
| c69a91921e | |||
| bb56dbdc74 | |||
| dc3c3ca98a | |||
| 15bc48978d | |||
| a93254f43c | |||
| 77beaab116 | |||
| ce3a869c07 | |||
| e76ec78b37 | |||
| fe0f5baa2d | |||
| caf61abd94 | |||
| 179848ea1f | |||
| b4c7d832b7 | |||
| a061fd95bd | |||
| c9661b9a52 | |||
| af96e46e56 | |||
| c1bdb76d18 | |||
| a97e536341 | |||
| 0e1de8d591 | |||
| 51587e9909 | |||
| 43767635b7 | |||
| 68776525be | |||
| 41728c3d87 | |||
| fcdcd9d7e6 | |||
| 7e3e2077f0 | |||
| 2395392c99 | |||
| aef8d9e6f1 | |||
| e74c4acec4 | |||
| 74c9f95c4c | |||
| 5ce61f4be4 | |||
| c9a677dc7c | |||
| e9b11f36d5 | |||
| e6bae7d01f | |||
| 9359d08bd6 | |||
| 7b1af357ff | |||
| ff4c249e39 | |||
| 106fae466d | |||
| 2be96308d3 | |||
| 2bf704c153 | |||
| e07cf88f4d | |||
| 31ce71608d | |||
| 84858b01cf | |||
| 969b1a7f98 | |||
| 33035402a4 | |||
| 943801d2d9 | |||
| 59aef0a010 | |||
| 20e708cf73 | |||
| 47fb08fc6d | |||
| a576b9db5d | |||
| 9cd0fe59a3 | |||
| 66db7639e9 | |||
| 168d27f0b3 | |||
| cc7a7d2b2c | |||
| fb02f4fa9f | |||
| 85a6851a8e | |||
| 2db7c80f80 | |||
| ec3ab5528a | |||
| 256080fb2c | |||
| 8c2110c80c | |||
| 9a45e5c064 | |||
| db896863cb | |||
| 5f0d1b806c | |||
| 789f112c86 | |||
| f35a5c1047 | |||
| 43aa89c255 | |||
| 99dc7feb31 | |||
| eb0dd85d88 | |||
| 13d7e5dc6b | |||
| 1646ce747e | |||
| 0082009561 | |||
| ec3f9ed5e4 | |||
| ca7501babb | |||
| 6035d07677 | |||
| f71bd594d1 | |||
| 19e31c4169 | |||
| 17c8757314 | |||
| 0427e1bf7b | |||
| 8c84d14195 | |||
| e375efca79 | |||
| d870292683 | |||
| 2e9c76095c | |||
| 0a3e64de5d | |||
| 5008146b77 | |||
| 03430a4060 | |||
| 7616aa1eef | |||
| 6978147a03 | |||
| a0f12b787b | |||
| 890d43e1c3 | |||
| 4068e8ee2b | |||
| 4d12c8fac3 | |||
| 98659fad08 | |||
| f56e272ff7 | |||
| 98f8fdee6a | |||
| a08193f094 | |||
| 1159f0763b | |||
| 33b7ccdd88 | |||
| eb97c10d89 | |||
| 49e3a29b00 | |||
| b784f20565 | |||
| 375e56bb9b | |||
| 76f0a88d89 | |||
| c4b864606f | |||
| 86d14f5932 | |||
| 9f56af660a | |||
| d1575b2739 | |||
| 9a419d4378 | |||
| dc869f0c05 | |||
| 9e7c8cd9c0 | |||
| 43b5b809e6 | |||
| b56614fce6 | |||
| 8143ad5cb5 | |||
| 580a46bf12 | |||
| b479c7a056 | |||
| 353f4ed7cc | |||
| 599b74c1a2 | |||
| 6498193e6d | |||
| eea759e64c | |||
| b9bea60fa8 | |||
| afd0e41b69 | |||
| 5bf57f83c9 | |||
| b00188cd82 | |||
| 5ea0f0f35e | |||
| 3e43057840 | |||
| 8730b522e6 | |||
| 3bcbaf2ec8 | |||
| d205f1bcb4 | |||
| 116dc12ed1 | |||
| de57e4a1cf | |||
| 86479d5871 | |||
| 63a21837db | |||
| 2181327bf2 | |||
| f12170bb56 | |||
| 4985662a5b | |||
| 763bff0289 | |||
| af455ccfe2 | |||
| f83d35c631 | |||
| bc100f6efa | |||
| fde36bb49c | |||
| 38fe5cc384 | |||
| 67249bea47 | |||
| 20dfe6475b | |||
| b08eaaa028 | |||
| b3e2e81b0d | |||
| eb0634d489 | |||
| 59808c6870 | |||
| 4a02a9975d | |||
| 51662cb8ef | |||
| c3e13a4ca6 | |||
| 35eec5b9aa | |||
| 3b68cecb15 | |||
| 4c3be75ef2 | |||
| dc91c2144c | |||
| 319113f872 | |||
| e894f95f6c | |||
| 977eacdfc0 | |||
| d43a89d86f | |||
| 39c57e2f02 | |||
| c68104c12f | |||
| 63cc9efdbc | |||
| b177a0cee6 | |||
| 3d41378956 | |||
| 09f9052355 | |||
| 1ef0196b0d | |||
| a5e3d3f018 | |||
| 7c4210b6ea | |||
| 5aec7f1992 | |||
| c97987e9ed | |||
| 266d5f8857 | |||
| 6a1d35dfa0 | |||
| d7d824e872 | |||
| 396f77e552 | |||
| a736250f33 | |||
| b0c912d6f0 | |||
| 72640fbc5a | |||
| 29fab26069 | |||
| 114917dc65 | |||
| c9c1d71c32 | |||
| a61e0d92ed | |||
| 4e822cccfd | |||
| 07af6ccc73 | |||
| cada977938 | |||
| cd409009e4 | |||
| ef0ba8a988 | |||
| 4de8318ca9 | |||
| 9bd33ef7e6 | |||
| 4baac8c6ce | |||
| b4092640b8 | |||
| 75ac9fc3ae | |||
| b7d3d4f191 | |||
| ccb3a73f98 | |||
| d78eccdb13 | |||
| 12e3626a6d | |||
| 86fe6f4ae7 | |||
| 1708db93e6 | |||
| 9f953080d1 | |||
| 0a01a0d119 | |||
| ce34e8ff08 | |||
| 34a0d939dd | |||
| 14d442c52c | |||
| c4c11a559f | |||
| 3e8f439d17 | |||
| ab271c4c24 | |||
| ed44561319 | |||
| b85edb5f5a | |||
| 0a7c65c43e | |||
| ba71a95d34 | |||
| 09bbcdaf32 | |||
| 0de18a98cf | |||
| 0f5f7c8dbe | |||
| 4c406b2d81 | |||
| 19edba7020 | |||
| aee5428591 | |||
| 2d4b15f317 | |||
| 6462abc6df | |||
| 422a6f94a9 | |||
| 6f961cacc5 | |||
| eed6310a08 | |||
| ddcaa5f8a6 | |||
| 6b9cbd5a19 | |||
| 0b86e7f574 | |||
| 547cd0de94 | |||
| 323d7ba996 | |||
| 22c2f2455e | |||
| 4e9c6b3f37 | |||
| a442938279 | |||
| 067138aab4 | |||
| e41473d9ff | |||
| 97aa3dd1f9 | |||
| 2fce831627 | |||
| 6fe2fb1f0f | |||
| 396a02396e | |||
| cb9bebdc8e | |||
| 9a28c68655 | |||
| 98329e1d71 | |||
| 046a9c4905 | |||
| b37969c4f9 | |||
| cc2d6b276d | |||
| 6a4b730dfd | |||
| 9fa2846df4 | |||
| e721309453 | |||
| 3101c07a97 | |||
| 8c9d16e605 | |||
| d3d2d67c8e | |||
| 7c0600ae16 | |||
| b6f3972fad | |||
| 76275dadf5 | |||
| 202fe57da2 | |||
| c36274a15a | |||
| c080b4bf58 | |||
| be23488e54 | |||
| c8116a0dbe | |||
| 2ed1dd557c | |||
| fc34abe738 | |||
| c477dcd640 | |||
| c4e40860e4 | |||
| 91530e92ee | |||
| f7b694d15f | |||
| 471a73e98c | |||
| 651af33600 | |||
| 002398450f | |||
| 7f708151eb | |||
| 1263027e0f | |||
| 90ce0974bd | |||
| 23dc4c7ca4 | |||
| 97ed6b7083 | |||
| 499f6f3688 | |||
| 5a22bfc175 | |||
| 66bf9156b3 | |||
| a14b77ec1c | |||
| 8d6e21135c | |||
| 9d661d516e | |||
| 8369f19a64 | |||
| 1338ddaa3f | |||
| 67827bf0fa | |||
| 7479f88d22 | |||
| 0e70433873 | |||
| d54a04197a | |||
| f6497cbe1f | |||
| 693d89cbdb | |||
| ccc6b2ee89 | |||
| 5d7c70c21e | |||
| d2e1d0e76e | |||
| fc21f64863 | |||
| b5fd491b73 | |||
| e8ac6e2fec | |||
| e3411249d6 | |||
| 10aece9d65 | |||
| b419fd08fc | |||
| 5c3e474160 | |||
| 1bf62c2e46 | |||
| 036f61da49 | |||
| e6894fcf11 | |||
| 4fceaed909 | |||
| 95518023c6 | |||
| fe0d7c9b0a | |||
| 56801cdf3b | |||
| 9691e4d4c6 | |||
| d2711e792f | |||
| 102621f53d | |||
| 44fc7bd927 | |||
| 18b20273fd | |||
| 49729e14a4 | |||
| 97ccd815f9 | |||
| abdf765a28 | |||
| bd54d58d8a | |||
| 172277eae4 | |||
| 110e4f7aed | |||
| 8061a3cdc8 | |||
| 60c73f7ff7 | |||
| d83b08f92c | |||
| 5dd77e0022 | |||
| cc7efbf977 | |||
| cd11b9533f | |||
| 436c8070e2 | |||
| 4e1feb2c4e | |||
| 8c96b52f08 | |||
| dd639a9516 | |||
| 8cce7a8a82 | |||
| 4390e42ea2 | |||
| a9b8b52a16 | |||
| 3f16708b55 | |||
| 586ead512c | |||
| 1e5ad34897 | |||
| 57e7f3ae94 | |||
| 9a23fc37fb | |||
| bf3c210d82 | |||
| 14c33bf2a8 | |||
| cbda104d14 | |||
| 0b719c3951 | |||
| 5d640c4b5c | |||
| ac30d9e451 | |||
| e3ccdbee8a | |||
| b9144ae56a | |||
| 14d365dcf0 | |||
| 1c4b98c2d2 |
@@ -19,7 +19,7 @@ env:
|
||||
|
||||
jobs:
|
||||
Grunt:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Checking out code
|
||||
@@ -61,12 +61,12 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
# MySQL builds always run with the lowest PHP supported version.
|
||||
- os: ubuntu-22.04
|
||||
- os: ubuntu-24.04
|
||||
php: 8.1
|
||||
extensions:
|
||||
db: mysqli
|
||||
# PostgreSQL builds always run with the highest PHP supported version.
|
||||
- os: ubuntu-22.04
|
||||
- os: ubuntu-24.04
|
||||
php: 8.3
|
||||
db: pgsql
|
||||
|
||||
|
||||
@@ -6,6 +6,85 @@ More detailed information on key changes can be found in the [Developer update n
|
||||
|
||||
The format of this change log follows the advice given at [Keep a CHANGELOG](https://keepachangelog.com).
|
||||
|
||||
## 4.5.7
|
||||
|
||||
### core
|
||||
|
||||
#### Added
|
||||
|
||||
- The Behat `::execute()` method now accepts an array-style callable in addition to the string `classname::method` format.
|
||||
|
||||
The following formats are now accepted:
|
||||
|
||||
```php
|
||||
// String format:
|
||||
$this->execute('behat_general::i_click_on', [...]);
|
||||
|
||||
// Array format:
|
||||
$this->execute([behat_general::class,' i_click_on'], [...]);
|
||||
```
|
||||
|
||||
For more information see [MDL-86231](https://tracker.moodle.org/browse/MDL-86231)
|
||||
- The `\externallib_advanced_testcase` has been replaced by `\core_external\tests\externallib_testcase` and is now autoloadable.
|
||||
|
||||
For more information see [MDL-86283](https://tracker.moodle.org/browse/MDL-86283)
|
||||
|
||||
### core_badges
|
||||
|
||||
#### Added
|
||||
|
||||
- A number of new static methods have been added to `core_badges\backpack_api` to support the new Canvas Credentials backpack provider. These methods allow you to retrieve lists of providers and regions, check if Canvas Credentials fields should be displayed, and get a region URL or API URL based on a given region ID. The new methods include `get_providers`, `get_regions`, `display_canvas_credentials_fields`, `get_region_url`, `get_region_api_url`, `get_regionid_from_url`, and `is_canvas_credentials_region`.
|
||||
|
||||
For more information see [MDL-86174](https://tracker.moodle.org/browse/MDL-86174)
|
||||
|
||||
### core_grades
|
||||
|
||||
#### Added
|
||||
|
||||
- New 'is_gradable()' function has been created to return whether the item has any gradeitem that is GRADE_TYPE_VALUE or GRADE_TYPE_SCALE.
|
||||
|
||||
For more information see [MDL-85837](https://tracker.moodle.org/browse/MDL-85837)
|
||||
|
||||
### core_message
|
||||
|
||||
#### Added
|
||||
|
||||
- The `contexturl` property to `\core\message\message` instances can now contain `\core\url` values in addition to plain strings
|
||||
|
||||
For more information see [MDL-83080](https://tracker.moodle.org/browse/MDL-83080)
|
||||
|
||||
## 4.5.6
|
||||
|
||||
### core
|
||||
|
||||
#### Added
|
||||
|
||||
- Add a new method has_valid_group in \core\report_helper that will return true or false depending if the user has a valid group. This is mainly false in case the user is not in any group in SEPARATEGROUPS. Used in report_log and report_loglive
|
||||
|
||||
For more information see [MDL-84464](https://tracker.moodle.org/browse/MDL-84464)
|
||||
|
||||
#### Changed
|
||||
|
||||
- The `\core\attribute\deprecated` attribute constructor `$replacement` parameter now defaults to null, and can be omitted
|
||||
|
||||
For more information see [MDL-84531](https://tracker.moodle.org/browse/MDL-84531)
|
||||
- Added a new `\core\deprecation::emit_deprecation()` method which should be used in places where a deprecation is known to occur. This method will throw debugging if no deprecation notice was found, for example:
|
||||
```php
|
||||
public function deprecated_method(): void {
|
||||
\core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
|
||||
}
|
||||
```
|
||||
|
||||
For more information see [MDL-85897](https://tracker.moodle.org/browse/MDL-85897)
|
||||
|
||||
### core_message
|
||||
|
||||
#### Added
|
||||
|
||||
- The web service `core_message_get_member_info` additionally returns `cancreatecontact` which is a boolean value for a user's permission to add a contact.
|
||||
|
||||
For more information see [MDL-72123](https://tracker.moodle.org/browse/MDL-72123)
|
||||
|
||||
## 4.5.5
|
||||
|
||||
### core
|
||||
|
||||
@@ -69,7 +69,7 @@ if ($options['enablelater']) {
|
||||
}
|
||||
|
||||
$time = time() + ($options['enablelater']*60);
|
||||
set_config('maintenance_later', $time);
|
||||
set_config('maintenance_later', $time, null, true);
|
||||
|
||||
echo get_string('clistatusenabledlater', 'admin', userdate($time))."\n";
|
||||
return 0;
|
||||
@@ -77,23 +77,32 @@ if ($options['enablelater']) {
|
||||
} else if ($options['enable']) {
|
||||
if (file_exists("$CFG->dataroot/climaintenance.html")) {
|
||||
// The maintenance is already enabled, nothing to do.
|
||||
} else {
|
||||
enable_cli_maintenance_mode();
|
||||
exit(0);
|
||||
}
|
||||
enable_cli_maintenance_mode();
|
||||
set_config('maintenance_enabled', 'cli mode', null, true);
|
||||
|
||||
if (isset($CFG->maintenance_later)) {
|
||||
unset_config('maintenance_later', null, true);
|
||||
}
|
||||
set_config('maintenance_enabled', 0);
|
||||
unset_config('maintenance_later');
|
||||
echo get_string('sitemaintenanceoncli', 'admin')."\n";
|
||||
exit(0);
|
||||
|
||||
} else if ($options['enableold']) {
|
||||
set_config('maintenance_enabled', 1);
|
||||
unset_config('maintenance_later');
|
||||
set_config('maintenance_enabled', 1, null, true);
|
||||
if (isset($CFG->maintenance_later)) {
|
||||
unset_config('maintenance_later', null, true);
|
||||
}
|
||||
echo get_string('sitemaintenanceon', 'admin')."\n";
|
||||
exit(0);
|
||||
|
||||
} else if ($options['disable']) {
|
||||
set_config('maintenance_enabled', 0);
|
||||
unset_config('maintenance_later');
|
||||
if ($CFG->maintenance_enabled !== '0') {
|
||||
set_config('maintenance_enabled', 0, null, true);
|
||||
}
|
||||
if (isset($CFG->maintenance_later)) {
|
||||
unset_config('maintenance_later', null, true);
|
||||
}
|
||||
if (file_exists("$CFG->dataroot/climaintenance.html")) {
|
||||
unlink("$CFG->dataroot/climaintenance.html");
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
*/
|
||||
|
||||
define('CLI_SCRIPT', true);
|
||||
define('IGNORE_COMPONENT_CACHE', true);
|
||||
|
||||
require(__DIR__.'/../../config.php');
|
||||
require_once($CFG->libdir.'/clilib.php');
|
||||
|
||||
@@ -158,13 +158,13 @@ if ($options['plugins']) {
|
||||
if (is_null($plugin)) {
|
||||
cli_writeln('Unknown plugin: ' . $component);
|
||||
} else {
|
||||
$pluginstring = $plugin->component . "\t" . $plugin->displayname;
|
||||
$pluginstring = $component . "\t" . $plugin->displayname;
|
||||
|
||||
if ($pluginman->can_uninstall_plugin($plugin->component)) {
|
||||
if ($pluginman->can_uninstall_plugin($component)) {
|
||||
if ($options['run']) {
|
||||
cli_writeln('Uninstalling: ' . $pluginstring);
|
||||
$progress = new progress_trace_buffer(new text_progress_trace(), true);
|
||||
$pluginman->uninstall_plugin($plugin->component, $progress);
|
||||
$pluginman->uninstall_plugin($component, $progress);
|
||||
$progress->finished();
|
||||
cli_write($progress->get_buffer());
|
||||
} else {
|
||||
|
||||
@@ -4716,6 +4716,7 @@
|
||||
<VENDOR name="mysql" version="8.4" />
|
||||
<VENDOR name="postgres" version="14" />
|
||||
<VENDOR name="mssql" version="14.0" />
|
||||
<VENDOR name="auroramysql" version="8.0" />
|
||||
</DATABASE>
|
||||
<PHP version="8.2.0" level="required">
|
||||
</PHP>
|
||||
@@ -4895,6 +4896,8 @@
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_async_backup" level="recommended">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_aurora_version" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="5.1" requires="4.2.3">
|
||||
@@ -4906,8 +4909,9 @@
|
||||
<DATABASE level="required">
|
||||
<VENDOR name="mariadb" version="10.11.0" />
|
||||
<VENDOR name="mysql" version="8.4" />
|
||||
<VENDOR name="postgres" version="14" />
|
||||
<VENDOR name="postgres" version="15" />
|
||||
<VENDOR name="mssql" version="14.0" />
|
||||
<VENDOR name="auroramysql" version="8.0" />
|
||||
</DATABASE>
|
||||
<PHP version="8.2.0" level="required">
|
||||
</PHP>
|
||||
@@ -5092,6 +5096,8 @@
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_async_backup" level="recommended">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_aurora_version" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
</COMPATIBILITY_MATRIX>
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
define('IGNORE_COMPONENT_CACHE', true);
|
||||
|
||||
require_once('../config.php');
|
||||
require_once($CFG->libdir.'/adminlib.php');
|
||||
|
||||
|
||||
+1
-1
@@ -2218,7 +2218,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
public function moodleorg_registration_message() {
|
||||
$a = new stdClass();
|
||||
$a->moreinformation = '#id_sitestats'; // More information anchor.
|
||||
$a->moodleapp = HUB_MOODLEORGHUBURL . '/solutions/moodle-app/';
|
||||
$a->moodleapp = MOODLE_PRODUCTURL . '/solutions/moodle-app/';
|
||||
$out = format_text(get_string('registerwithmoodleorginfo', 'core_hub', $a), FORMAT_MARKDOWN);
|
||||
|
||||
$out .= html_writer::link(
|
||||
|
||||
@@ -65,6 +65,21 @@ if (($hassiteconfig || has_any_capability(array(
|
||||
new lang_string('allowexternalbackpack', 'badges'),
|
||||
new lang_string('allowexternalbackpack_desc', 'badges'), 1));
|
||||
|
||||
$defaultcanvasregions = [
|
||||
'Australia|https://au.badgr.io|https://api.au.badgr.io/v2',
|
||||
'Canada|https://ca.badgr.io|https://api.ca.badgr.io/v2',
|
||||
'Europe|https://eu.badgr.io|https://api.eu.badgr.io/v2',
|
||||
'Singapore|https://sg.badgr.io|https://api.sg.badgr.io/v2',
|
||||
'United States|https://badgr.io|https://api.badgr.io/v2',
|
||||
];
|
||||
$globalsettings->add(new admin_setting_configtextarea(
|
||||
'badges_canvasregions',
|
||||
new lang_string('canvasregions', 'badges'),
|
||||
new lang_string('canvasregions_desc', 'badges'),
|
||||
implode("\n", $defaultcanvasregions),
|
||||
PARAM_RAW,
|
||||
));
|
||||
|
||||
$ADMIN->add('badges', $globalsettings);
|
||||
|
||||
$ADMIN->add('badges',
|
||||
|
||||
@@ -70,6 +70,14 @@ if ($hassiteconfig) {
|
||||
|
||||
$temp->add(new admin_setting_configtext('googlemapkey3', new lang_string('googlemapkey3', 'core_admin'),
|
||||
new lang_string('googlemapkey3_help', 'core_admin'), '', PARAM_RAW, 60));
|
||||
|
||||
$temp->add(new admin_setting_configtext(
|
||||
'geopluginapikey',
|
||||
new lang_string('geopluginapikey', 'core_admin'),
|
||||
new lang_string('geopluginapikey_desc', 'core_admin'),
|
||||
'',
|
||||
PARAM_TEXT,
|
||||
));
|
||||
}
|
||||
|
||||
$ADMIN->add('location', $temp);
|
||||
|
||||
@@ -79,7 +79,7 @@ Feature: An administrator can browse user accounts
|
||||
Scenario: Delete a user account
|
||||
Given I navigate to "Users > Accounts > Browse list of users" in site administration
|
||||
And I press "Delete" action in the "User One" report row
|
||||
And I should see "Are you absolutely sure you want to completely delete the user User One" in the "Delete user" "dialogue"
|
||||
And I should see "Are you sure you want to delete user User One" in the "Delete user" "dialogue"
|
||||
And I click on "Delete" "button" in the "Delete user" "dialogue"
|
||||
Then I should see "Deleted user User One"
|
||||
And I should not see "User One" in the "reportbuilder-table" "table"
|
||||
|
||||
@@ -20,9 +20,9 @@ Feature: I can apply presets
|
||||
And I navigate to "Plugins > Course formats > Manage course formats" in site administration
|
||||
And "Disable" "icon" should exist in the "Social" "table_row"
|
||||
And I navigate to "Plugins > Question behaviours > Manage question behaviours" in site administration
|
||||
And "Enabled" "icon" should exist in the "Immediate feedback with CBM" "table_row"
|
||||
And "Enabled" "link" should exist in the "Immediate feedback with CBM" "table_row"
|
||||
And I navigate to "Plugins > Question types > Manage question types" in site administration
|
||||
And "Enabled" "icon" should exist in the "Calculated multichoice" "table_row"
|
||||
And "Enabled" "link" should exist in the "Calculated multichoice" "table_row"
|
||||
When I navigate to "Site admin presets" in site administration
|
||||
And I press "Review settings and apply" action in the "Starter" report row
|
||||
And I should see "Setting changes"
|
||||
@@ -113,9 +113,9 @@ Feature: I can apply presets
|
||||
And I navigate to "Plugins > Course formats > Manage course formats" in site administration
|
||||
And "Disable" "icon" should not exist in the "Social" "table_row"
|
||||
And I navigate to "Plugins > Question behaviours > Manage question behaviours" in site administration
|
||||
And "Enabled" "icon" should not exist in the "Immediate feedback with CBM" "table_row"
|
||||
And "Enabled" "link" should not exist in the "Immediate feedback with CBM" "table_row"
|
||||
And I navigate to "Plugins > Question types > Manage question types" in site administration
|
||||
And "Enabled" "icon" should not exist in the "Calculated multichoice" "table_row"
|
||||
And "Enabled" "link" should not exist in the "Calculated multichoice" "table_row"
|
||||
|
||||
Scenario: Re-applying Starter Moodle preset does not display setting changes
|
||||
# Apply Starter preset.
|
||||
|
||||
@@ -27,9 +27,9 @@ Feature: I can revert changes after a load
|
||||
And I navigate to "Plugins > Course formats > Manage course formats" in site administration
|
||||
And "Disable" "icon" should not exist in the "Social" "table_row"
|
||||
And I navigate to "Plugins > Question behaviours > Manage question behaviours" in site administration
|
||||
And "Enabled" "icon" should not exist in the "Immediate feedback with CBM" "table_row"
|
||||
And "Enabled" "link" should not exist in the "Immediate feedback with CBM" "table_row"
|
||||
And I navigate to "Plugins > Question types > Manage question types" in site administration
|
||||
And "Enabled" "icon" should not exist in the "Calculated multichoice" "table_row"
|
||||
And "Enabled" "link" should not exist in the "Calculated multichoice" "table_row"
|
||||
And I navigate to "Site admin presets" in site administration
|
||||
And I press "Show version history" action in the "Starter" report row
|
||||
When I click on "Restore this version" "link"
|
||||
@@ -45,6 +45,6 @@ Feature: I can revert changes after a load
|
||||
And I navigate to "Plugins > Course formats > Manage course formats" in site administration
|
||||
And "Disable" "icon" should exist in the "Social" "table_row"
|
||||
And I navigate to "Plugins > Question behaviours > Manage question behaviours" in site administration
|
||||
And "Enabled" "icon" should exist in the "Immediate feedback with CBM" "table_row"
|
||||
And "Enabled" "link" should exist in the "Immediate feedback with CBM" "table_row"
|
||||
And I navigate to "Plugins > Question types > Manage question types" in site administration
|
||||
And "Enabled" "icon" should exist in the "Calculated multichoice" "table_row"
|
||||
And "Enabled" "link" should exist in the "Calculated multichoice" "table_row"
|
||||
|
||||
@@ -24,5 +24,14 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
// Remove the 'enableaccessibilitytools' configuration setting in case of reinstall.
|
||||
unset_config('enableaccessibilitytools');
|
||||
/**
|
||||
* Plugin uninstall code.
|
||||
*
|
||||
* @return true
|
||||
*/
|
||||
function xmldb_tool_brickfield_uninstall(): bool {
|
||||
// Remove the 'enableaccessibilitytools' configuration setting in case of reinstall.
|
||||
unset_config('enableaccessibilitytools');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace tool_capability;
|
||||
|
||||
// phpcs:disable moodle.PHPUnit.TestCaseProvider.dataProviderSyntaxMethodNotFound
|
||||
|
||||
/**
|
||||
* Detect common problems in capability definitions of plugins.
|
||||
*
|
||||
* @group plugin_checks
|
||||
* @package tool_capability
|
||||
* @copyright 2025 Petr Skoda
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
final class plugin_checks_test extends \core\tests\plugin_checks_testcase {
|
||||
/**
|
||||
* Verify contents of plugin db/access.php file.
|
||||
*
|
||||
* @dataProvider all_plugins_provider
|
||||
* @coversNothing
|
||||
*
|
||||
* @param string $component
|
||||
* @param string $plugintype
|
||||
* @param string $pluginname
|
||||
* @param string $dir
|
||||
*/
|
||||
public function test_db_access_file(string $component, string $plugintype, string $pluginname, string $dir): void {
|
||||
global $CFG;
|
||||
|
||||
$stringmanager = get_string_manager();
|
||||
$corerolefile = "$CFG->dirroot/lang/en/role.php";
|
||||
$langfile = "$dir/lang/en/$component.php";
|
||||
|
||||
$file = "$dir/db/access.php";
|
||||
$capabilities = $this->fetch_array_from_file($file, 'capabilities');
|
||||
if (!$capabilities) {
|
||||
$this->expectNotToPerformAssertions();
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($capabilities as $capname => $capability) {
|
||||
if ($plugintype === 'qbank' && str_starts_with($capname, 'moodle/question:')) {
|
||||
// Question bank capabilities are irregular.
|
||||
$strname = explode('/', $capname, 2)[1];
|
||||
$this->assertTrue($stringmanager->string_exists($strname, 'core_role'),
|
||||
"Missing capability name string '$strname' in $corerolefile");
|
||||
continue;
|
||||
}
|
||||
$this->assertMatchesRegularExpression("|^$plugintype/$pluginname:[a-z0-9_]+$|", $capname);
|
||||
$strname = substr($capname, strlen($plugintype) + 1);
|
||||
$this->assertTrue($stringmanager->string_exists($strname, $component),
|
||||
"Missing capability name string '$strname' in $langfile");
|
||||
$this->assertSame($capname, clean_param($capname, PARAM_CAPABILITY));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ The component output classes can render an action menu entirely in PHP. The step
|
||||
|
||||
The following code is a basic example of an action menu:
|
||||
|
||||
{{< php >}}
|
||||
```php
|
||||
/** @var core_renderer $output*/
|
||||
$output = $PAGE->get_renderer('core');
|
||||
|
||||
@@ -54,11 +54,11 @@ $menu->add(new action_menu_link(
|
||||
));
|
||||
|
||||
echo $output->render($menu);
|
||||
{{< / php >}}
|
||||
```
|
||||
|
||||
And this is the same example but passing the items in the creation:
|
||||
|
||||
{{< php >}}
|
||||
```php
|
||||
/** @var core_renderer $output*/
|
||||
$output = $PAGE->get_renderer('core');
|
||||
|
||||
@@ -72,32 +72,64 @@ $menu = new action_menu([
|
||||
]);
|
||||
|
||||
echo $output->render($menu);
|
||||
{{< / php >}}
|
||||
```
|
||||
|
||||
### Setup the menu trigger
|
||||
|
||||
By default, the action menu trigger is a cog icon. However, the class has methods to convert it to a kebab menu or even display any arbitrary content.
|
||||
By default, the action menu trigger is a button that uses the `t/edit_menu` icon and is displayed with a caret. However, the class has methods to convert it to a kebab menu or even display any arbitrary content.
|
||||
|
||||
Example of a kebab menu:
|
||||
#### Example of a kebab menu
|
||||
|
||||
{{< php >}}
|
||||
```php
|
||||
/** @var core_renderer $output*/
|
||||
$output = $PAGE->get_renderer('core');
|
||||
|
||||
$menu = new action_menu();
|
||||
$menu->set_kebab_trigger(get_string('edit'), $output);
|
||||
$menu->set_additional_classes('fields-actions');
|
||||
{{< / php >}}
|
||||
```
|
||||
|
||||
Example of a custom trigger:
|
||||
#### Customising the menu trigger
|
||||
|
||||
{{< php >}}
|
||||
/** @var core_renderer $output*/
|
||||
$output = $PAGE->get_renderer('core');
|
||||
##### Trigger with a text label
|
||||
|
||||
```php
|
||||
// This example displays an "Edit" label for the trigger.
|
||||
$menu = new action_menu();
|
||||
$menu->set_menu_trigger(get_string('edit'));
|
||||
{{< / php >}}
|
||||
```
|
||||
|
||||
##### Trigger with an icon
|
||||
|
||||
This example displays an icon for the trigger. When rendering the menu trigger button as an icon button, ensure that the icon is rendered as a decorative image. If you are using a `pix_icon`, pass an empty `$alt` parameter to make the icon decorative.
|
||||
|
||||
The accessible name of the icon button should be set within the button element itself. You can set an accessible name for the icon trigger button either by:
|
||||
* Using the `::set_action_label()` method.
|
||||
```php
|
||||
$menu = new action_menu();
|
||||
// Make sure the pix icon is rendered as a decorative image by passing an empty alt parameter.
|
||||
$icon = $output->pix_icon('t/edit', '');
|
||||
$menu->set_menu_trigger($icon);
|
||||
$menu->set_action_label(get_string('edit'));
|
||||
```
|
||||
|
||||
* Or alternatively, by adding a visually hidden text alongside the icon.
|
||||
```php
|
||||
$menu = new action_menu();
|
||||
// Make sure the pix icon is rendered as a decorative image by passing an empty alt parameter.
|
||||
$icon = $output->pix_icon('t/edit', '');
|
||||
// Add a visually hidden text label for the trigger button.
|
||||
$icon .= html_writer::span(get_string('edit'), 'sr-only');
|
||||
$menu->set_menu_trigger($icon);
|
||||
```
|
||||
|
||||
##### Removing the caret symbol
|
||||
|
||||
You may also remove the caret symbol by adding a `no-caret` class to the `triggerextraclasses` property.
|
||||
|
||||
```php
|
||||
$menu->triggerextraclasses = 'no-caret';
|
||||
```
|
||||
|
||||
### Add items
|
||||
|
||||
@@ -108,7 +140,7 @@ Secondary items: are displayed inside the action menu dropdown.
|
||||
|
||||
The item location must be configured before adding the element. The following example shows different ways to add primary and secondary menu items.
|
||||
|
||||
{{< php >}}
|
||||
```php
|
||||
// Primary items examples.
|
||||
$menu->add(new action_menu_link(
|
||||
new moodle_url($PAGE->url),
|
||||
@@ -134,7 +166,7 @@ $menu->add(new action_menu_link_secondary(
|
||||
new pix_icon('t/user', ''),
|
||||
'Action link example',
|
||||
));
|
||||
{{< / php >}}
|
||||
```
|
||||
|
||||
## Types of items
|
||||
|
||||
@@ -178,7 +210,7 @@ Construct params:
|
||||
|
||||
The following example creates a subpanel using a renderable choicelist instance:
|
||||
|
||||
{{< php >}}
|
||||
```php
|
||||
/** @var core_renderer $output*/
|
||||
$output = $PAGE->get_renderer('core');
|
||||
|
||||
@@ -205,7 +237,7 @@ $menu->add(new core\output\local\action_menu\subpanel(
|
||||
));
|
||||
|
||||
echo $output->render($menu);
|
||||
{{< / php >}}
|
||||
```
|
||||
|
||||
### HTML string
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ echo '<p><strong>Important note:</strong> actions menus are not prepared
|
||||
to be displayed inside iframes. You may need to scroll to see the
|
||||
action menu options.</p>';
|
||||
|
||||
echo $output->heading("Action menu default example", 4);
|
||||
echo $output->heading("Action menu default example", 3);
|
||||
|
||||
$menu = new action_menu();
|
||||
|
||||
@@ -87,11 +87,11 @@ $menu->add($subpanel);
|
||||
$menu->add($basicactionlink);
|
||||
|
||||
echo '<div class="border m-3 p-3 d-flex flex-row">';
|
||||
echo '<div class="flex-fill">Example of default an action menu</div><div>';
|
||||
echo '<div class="flex-fill">An action menu rendered without customisation</div><div>';
|
||||
echo $OUTPUT->render($menu);
|
||||
echo '</div></div>';
|
||||
|
||||
echo $output->heading("Kebab menu example", 4);
|
||||
echo $output->heading("Kebab menu example", 3);
|
||||
|
||||
$menu = new action_menu();
|
||||
$menu->set_kebab_trigger(get_string('edit'), $output);
|
||||
@@ -106,11 +106,11 @@ $menu->add(new core\output\local\action_menu\subpanel(
|
||||
$menu->add($basicactionlink);
|
||||
|
||||
echo '<div class="border m-3 p-3 d-flex flex-row">';
|
||||
echo '<div class="flex-fill">Example of kebab menu</div><div>';
|
||||
echo '<div class="flex-fill">An action menu with a kebab menu trigger button</div><div>';
|
||||
echo $OUTPUT->render($menu);
|
||||
echo '</div></div>';
|
||||
|
||||
echo $output->heading("Custom trigger menu example", 4);
|
||||
echo $output->heading("Custom trigger menu examples", 3);
|
||||
|
||||
$menu = new action_menu();
|
||||
$menu->set_menu_trigger(get_string('edit'));
|
||||
@@ -124,11 +124,53 @@ $menu->add(new core\output\local\action_menu\subpanel(
|
||||
$menu->add($basicactionlink);
|
||||
|
||||
echo '<div class="border m-3 p-3 d-flex flex-row">';
|
||||
echo '<div class="flex-fill">Example of kebab menu</div><div>';
|
||||
echo '<div class="flex-fill">An action menu with a menu trigger button with a custom text label</div><div>';
|
||||
echo $OUTPUT->render($menu);
|
||||
echo '</div></div>';
|
||||
|
||||
echo $output->heading("Primary actions menu example", 4);
|
||||
$links = [
|
||||
$basicactionlink,
|
||||
$basicactionlink,
|
||||
];
|
||||
$moreicon = $OUTPUT->pix_icon('i/moremenu', '');
|
||||
$editicon = $OUTPUT->pix_icon('t/edit', '');
|
||||
|
||||
$menu = new action_menu($links);
|
||||
|
||||
$menu->set_menu_trigger($moreicon);
|
||||
$menu->set_action_label(get_string('moremenu'));
|
||||
$menu->triggerattributes = [
|
||||
'title' => get_string('moremenu'),
|
||||
];
|
||||
|
||||
echo '<div class="border m-3 p-3 d-flex flex-row">';
|
||||
echo '<div class="flex-fill">An action menu with only an icon for its custom menu trigger button</div>';
|
||||
echo html_writer::div($OUTPUT->render($menu));
|
||||
echo '</div>';
|
||||
|
||||
echo '<div class="border m-3 p-3 d-flex flex-row">';
|
||||
echo '<div class="flex-fill">An action menu with only an icon for its custom menu trigger button with the caret removed</div>';
|
||||
$menu->triggerextraclasses = 'no-caret';
|
||||
echo html_writer::div($OUTPUT->render($menu));
|
||||
echo '</div>';
|
||||
|
||||
$menu = new action_menu($links);
|
||||
$menu->set_menu_trigger($editicon . ' ' . get_string('edit'));
|
||||
|
||||
echo '<div class="border m-3 p-3 d-flex flex-row">';
|
||||
echo '<div class="flex-fill">An action menu with an icon and visible text for its custom menu trigger button</div>';
|
||||
echo html_writer::div($OUTPUT->render($menu));
|
||||
echo '</div>';
|
||||
|
||||
$menu = new action_menu($links);
|
||||
$menu->set_menu_trigger($editicon . ' ' . html_writer::span(get_string('edit'), 'sr-only'));
|
||||
|
||||
echo '<div class="border m-3 p-3 d-flex flex-row">';
|
||||
echo '<div class="flex-fill">An action menu with an icon and visually hidden text for its custom menu trigger button</div>';
|
||||
echo html_writer::div($OUTPUT->render($menu));
|
||||
echo '</div>';
|
||||
|
||||
echo $output->heading("Primary actions menu example", 3);
|
||||
|
||||
$menu = new action_menu();
|
||||
$menu->set_menu_trigger(get_string('edit'));
|
||||
|
||||
@@ -28,6 +28,7 @@ use renderable;
|
||||
use templatable;
|
||||
use renderer_base;
|
||||
use single_button;
|
||||
use action_link;
|
||||
use stdClass;
|
||||
use moodle_url;
|
||||
use context;
|
||||
@@ -65,6 +66,8 @@ class manage_competency_frameworks_page implements renderable, templatable {
|
||||
* @param context $pagecontext The page context
|
||||
*/
|
||||
public function __construct(context $pagecontext) {
|
||||
global $OUTPUT;
|
||||
|
||||
$this->pagecontext = $pagecontext;
|
||||
|
||||
if (competency_framework::can_manage_context($this->pagecontext)) {
|
||||
@@ -74,10 +77,13 @@ class manage_competency_frameworks_page implements renderable, templatable {
|
||||
'get'
|
||||
);
|
||||
$this->navigation[] = $addpage;
|
||||
$competenciesrepository = new single_button(
|
||||
|
||||
$icon = $OUTPUT->pix_icon('i/externallink', get_string('opensinnewwindow'), 'moodle', ['class' => 'ms-1']);
|
||||
$competenciesrepository = new action_link(
|
||||
new moodle_url('https://moodle.net/search', ['q' => 'competency frameworks']),
|
||||
get_string('competencyframeworksrepository', 'tool_lp'),
|
||||
'get'
|
||||
get_string('competencyframeworksrepository', 'tool_lp') . $icon,
|
||||
null,
|
||||
['target' => '_blank', 'class' => 'btn btn-secondary ms-sm-2'],
|
||||
);
|
||||
$this->navigation[] = $competenciesrepository;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ class manager {
|
||||
],
|
||||
'tool_mfa' => [
|
||||
'guidance',
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -441,6 +441,21 @@ class manager {
|
||||
$url = new \moodle_url($url);
|
||||
}
|
||||
|
||||
// Admin not setup.
|
||||
if (!empty($CFG->adminsetuppending)) {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Honor prevent_redirect.
|
||||
if ($preventredirect) {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Login as.
|
||||
if (\core\session\manager::is_loggedinas()) {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Check for pluginfile.php urls.
|
||||
$pluginfileurl = new \moodle_url('/pluginfile.php');
|
||||
if ($url->compare($pluginfileurl)) {
|
||||
@@ -485,22 +500,6 @@ class manager {
|
||||
}
|
||||
}
|
||||
|
||||
// Admin not setup.
|
||||
if (!empty($CFG->adminsetuppending)) {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Initial installation.
|
||||
// We get this for free from get_plugins_with_function.
|
||||
|
||||
// Upgrade check.
|
||||
// We get this for free from get_plugins_with_function.
|
||||
|
||||
// Honor prevent_redirect.
|
||||
if ($preventredirect) {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// User not properly setup.
|
||||
if (user_not_fully_set_up($USER)) {
|
||||
return self::NO_REDIRECT;
|
||||
@@ -516,11 +515,6 @@ class manager {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Login as.
|
||||
if (\core\session\manager::is_loggedinas()) {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Site policy.
|
||||
if (isset($USER->policyagreed) && !$USER->policyagreed) {
|
||||
$manager = new \core_privacy\local\sitepolicy\manager();
|
||||
@@ -538,11 +532,8 @@ class manager {
|
||||
}
|
||||
|
||||
// WS/AJAX check.
|
||||
// Prevents any potential bypassing of multi-factor authentication.
|
||||
if (WS_SERVER || AJAX_SCRIPT) {
|
||||
if (isset($SESSION->mfa_pending) && !empty($SESSION->mfa_pending)) {
|
||||
// Allow AJAX and WS, but never from auth.php.
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
return self::REDIRECT_EXCEPTION;
|
||||
}
|
||||
|
||||
|
||||
@@ -379,7 +379,7 @@ class renderer extends \plugin_renderer_base {
|
||||
WHERE deleted = 0
|
||||
AND suspended = 0
|
||||
GROUP BY auth";
|
||||
$allusersinfo = $DB->get_records_sql($alluserssql, []);
|
||||
$allusersinfo = $DB->get_records_sql_menu($alluserssql);
|
||||
|
||||
$noncompletesql = "SELECT u.auth, COUNT(u.id)
|
||||
FROM {user} u
|
||||
@@ -388,7 +388,7 @@ class renderer extends \plugin_renderer_base {
|
||||
AND (mfaa.lastverified < ?
|
||||
OR mfaa.lastverified IS NULL)
|
||||
GROUP BY u.auth";
|
||||
$noncompleteinfo = $DB->get_records_sql($noncompletesql, [$lookback, $lookback]);
|
||||
$noncompleteinfo = $DB->get_records_sql_menu($noncompletesql, [$lookback, $lookback]);
|
||||
|
||||
$nologinsql = "SELECT auth, COUNT(id)
|
||||
FROM {user}
|
||||
@@ -396,7 +396,7 @@ class renderer extends \plugin_renderer_base {
|
||||
AND suspended = 0
|
||||
AND lastlogin < ?
|
||||
GROUP BY auth";
|
||||
$nologininfo = $DB->get_records_sql($nologinsql, [$lookback]);
|
||||
$nologininfo = $DB->get_records_sql_menu($nologinsql, [$lookback]);
|
||||
|
||||
$mfauserssql = "SELECT auth,
|
||||
COUNT(DISTINCT tm.userid)
|
||||
@@ -406,7 +406,7 @@ class renderer extends \plugin_renderer_base {
|
||||
AND u.deleted = 0
|
||||
AND u.suspended = 0
|
||||
GROUP BY u.auth";
|
||||
$mfausersinfo = $DB->get_records_sql($mfauserssql, [$lookback]);
|
||||
$mfausersinfo = $DB->get_records_sql_menu($mfauserssql, [$lookback]);
|
||||
|
||||
$factorsusedsql = "SELECT CONCAT(u.auth, '_', tm.factor) as id,
|
||||
COUNT(*)
|
||||
@@ -417,7 +417,7 @@ class renderer extends \plugin_renderer_base {
|
||||
AND u.suspended = 0
|
||||
AND (tm.revoked = 0 OR (tm.revoked = 1 AND tm.timemodified > ?))
|
||||
GROUP BY CONCAT(u.auth, '_', tm.factor)";
|
||||
$factorsusedinfo = $DB->get_records_sql($factorsusedsql, [$lookback, $lookback]);
|
||||
$factorsusedinfo = $DB->get_records_sql_menu($factorsusedsql, [$lookback, $lookback]);
|
||||
|
||||
// Auth rows.
|
||||
$authtypes = get_enabled_auth_plugins(true);
|
||||
@@ -426,10 +426,10 @@ class renderer extends \plugin_renderer_base {
|
||||
$row[] = \html_writer::tag('b', $authtype);
|
||||
|
||||
// Setup the overall totals columns.
|
||||
$row[] = $allusersinfo[$authtype]->count ?? '-';
|
||||
$row[] = $mfausersinfo[$authtype]->count ?? '-';
|
||||
$row[] = $noncompleteinfo[$authtype]->count ?? '-';
|
||||
$row[] = $nologininfo[$authtype]->count ?? '-';
|
||||
$row[] = $allusersinfo[$authtype] ?? '-';
|
||||
$row[] = $mfausersinfo[$authtype] ?? '-';
|
||||
$row[] = $noncompleteinfo[$authtype] ?? '-';
|
||||
$row[] = $nologininfo[$authtype] ?? '-';
|
||||
|
||||
// Create a running counter for the total.
|
||||
$authtotal = 0;
|
||||
@@ -439,7 +439,7 @@ class renderer extends \plugin_renderer_base {
|
||||
if (!empty($column)) {
|
||||
// Get the information from the data key.
|
||||
$key = $authtype . '_' . $column;
|
||||
$count = $factorsusedinfo[$key]->count ?? 0;
|
||||
$count = $factorsusedinfo[$key] ?? 0;
|
||||
$authtotal += $count;
|
||||
|
||||
$row[] = $count ? format_float($count, 0) : '-';
|
||||
|
||||
@@ -71,7 +71,7 @@ $string['setupfactor:instructionsverification'] = '3. Enter the verification cod
|
||||
$string['setupfactor:intro'] = 'To set up this method, you need to have a device with an authenticator app. If you don\'t have an app, you can download one. For example, <a href="https://2fas.com/" target="_blank">2FAS Auth</a>, <a href="https://freeotp.github.io/" target="_blank">FreeOTP</a>, Google Authenticator, Microsoft Authenticator or Twilio Authy.';
|
||||
$string['setupfactor:key'] = 'Secret key: ';
|
||||
$string['setupfactor:link'] = 'Or enter details manually.';
|
||||
$string['setupfactor:link_help'] = 'If you are on a mobile device and already have an authenticator app installed this link may work. Note that using TOTP on the same device as you login on can weaken the benefits of MFA.';
|
||||
$string['setupfactor:link_help'] = 'If you are on a mobile device and already have an authenticator app installed this link may work. Note that using TOTP on the same device as you log in on can weaken the benefits of MFA.';
|
||||
$string['setupfactor:linklabel'] = 'Open app already installed on this device';
|
||||
$string['setupfactor:mode'] = 'Mode:';
|
||||
$string['setupfactor:mode:timebased'] = 'Time-based';
|
||||
|
||||
@@ -2,4 +2,3 @@ created,tool_mfa
|
||||
createdfromip,tool_mfa
|
||||
lastverified,tool_mfa
|
||||
revoke,tool_mfa
|
||||
setup,tool_mfa
|
||||
|
||||
@@ -150,6 +150,7 @@ $string['settings:redir_exclusions'] = 'URLS which should not redirect the MFA c
|
||||
$string['settings:redir_exclusions_help'] = 'Each new line is a relative URL from the siteroot for which the MFA check will not redirect from';
|
||||
$string['settings:weight'] = 'Factor weight';
|
||||
$string['settings:weight_help'] = 'The weight of this factor if passed. A user needs at least 100 points to log in.';
|
||||
$string['setup'] = 'Setup';
|
||||
$string['setupfactor'] = 'Set up factor';
|
||||
$string['setupfactorbuttonadditional'] = 'Add additional factor';
|
||||
$string['setuprequired'] = 'User setup';
|
||||
@@ -180,4 +181,3 @@ $string['created'] = 'Created';
|
||||
$string['createdfromip'] = 'Created from IP';
|
||||
$string['lastverified'] = 'Last verified';
|
||||
$string['revoke'] = 'Revoke';
|
||||
$string['setup'] = 'Setup';
|
||||
|
||||
@@ -590,6 +590,7 @@ class api {
|
||||
'subdirs' => false,
|
||||
'maxfiles' => -1,
|
||||
'context' => context_system::instance(),
|
||||
'noclean' => true,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -606,6 +607,7 @@ class api {
|
||||
'subdirs' => false,
|
||||
'maxfiles' => -1,
|
||||
'context' => context_system::instance(),
|
||||
'noclean' => true,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -52,9 +52,9 @@ if ($listurl) {
|
||||
}
|
||||
|
||||
// Add a new nav item to make $listurl clickable for the Boost theme.
|
||||
if (isset($script)) {
|
||||
$lastrunnav = get_string('lastrun', 'tool_profiling');
|
||||
$PAGE->navbar->add($lastrunnav);
|
||||
if (isset($script) || isset($runid)) {
|
||||
$detailnav = get_string('detailview', 'tool_profiling');
|
||||
$PAGE->navbar->add($detailnav);
|
||||
}
|
||||
|
||||
// Header
|
||||
|
||||
@@ -31,6 +31,7 @@ $string['cannotfindanyrunforurl'] = 'Sorry, cannot find any profiling run for th
|
||||
$string['cannotfindanyrunforrunid'] = 'Sorry, cannot find the \'{$a}\' profiling run';
|
||||
$string['comment'] = 'Comment';
|
||||
$string['cputime'] = 'CPU time';
|
||||
$string['detailview'] = 'Summary of run';
|
||||
$string['differencesbetween2runsof'] = 'Differences between 2 runs of {$a}';
|
||||
$string['executiontime'] = 'Execution time';
|
||||
$string['export'] = 'Export';
|
||||
|
||||
@@ -254,7 +254,7 @@ class course_bin extends base_bin {
|
||||
// This hack will be removed once recycle bin switches to use its own backup mode, with
|
||||
// own preferences and 100% separate from MOODLE_AUTOMATED.
|
||||
// TODO: Remove this as part of MDL-65228.
|
||||
$forcedrestoresettings = $CFG->forced_plugin_settings['restore'] ?? null;
|
||||
$forcedrestoresettings = $CFG->forced_plugin_settings['restore'] ?? [];
|
||||
$CFG->forced_plugin_settings['restore']['restore_general_users'] = 1;
|
||||
$CFG->forced_plugin_settings['restore']['restore_general_groups'] = 1;
|
||||
|
||||
|
||||
@@ -274,7 +274,7 @@ class tool_task_renderer extends plugin_renderer_base {
|
||||
$nextruntime = $task->get_next_run_time();
|
||||
$due = $nextruntime < $now;
|
||||
if ($task->get_attempts_available() > 0) {
|
||||
$nextrun = $due ? userdate($nextruntime) : get_string('asap', 'tool_task');
|
||||
$nextrun = $due ? get_string('asap', 'tool_task') : userdate($nextruntime);
|
||||
} else {
|
||||
$nextrun = get_string('never', 'admin');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
@tool @tool_task @javascript
|
||||
Feature: Manage adhoc task
|
||||
In order to manage adhoc tasks
|
||||
As an admin
|
||||
I need to be able to view adhoc tasks
|
||||
|
||||
Scenario Outline: View adhoc tasks next run time
|
||||
Given the following "tool_task > adhoc tasks" exist:
|
||||
| classname | seconds | hostname | pid | nextruntime |
|
||||
| \core\task\asynchronous_backup_task | 0 | c69335460f7f | 1915 | <nextruntime> |
|
||||
When I log in as "admin"
|
||||
And I navigate to "Server > Tasks > Ad hoc tasks" in site administration
|
||||
Then the following should exist in the "Ad hoc tasks" table:
|
||||
| Component / Class name | Next run |
|
||||
| asynchronous_backup_task | <nextruntimestr> |
|
||||
And I click on "asynchronous_backup_task" "link" in the "Ad hoc tasks" "table"
|
||||
And the following should exist in the "\core\task\asynchronous_backup_task Ad hoc tasks" table:
|
||||
| Next run |
|
||||
| <nextruntimestr> |
|
||||
Examples:
|
||||
| nextruntime | nextruntimestr |
|
||||
| ##yesterday## | ASAP |
|
||||
| ##tomorrow noon## | ##tomorrow noon##%A, %d %B %Y, %I:%M## |
|
||||
@@ -43,7 +43,7 @@ class tool_task_generator extends testing_module_generator {
|
||||
global $DB;
|
||||
$conditions = ['classname' => $data['classname']];
|
||||
$record = $DB->get_record('task_scheduled', $conditions, '*', MUST_EXIST);
|
||||
$record->timestarted = time() - $data['seconds'];
|
||||
$record->timestarted = $data['seconds'] > 0 ? time() - $data['seconds'] : 0;
|
||||
$record->hostname = $data['hostname'];
|
||||
$record->pid = $data['pid'];
|
||||
$DB->update_record('task_scheduled', $record);
|
||||
@@ -59,8 +59,8 @@ class tool_task_generator extends testing_module_generator {
|
||||
global $DB;
|
||||
$adhoctask = (object)[
|
||||
'classname' => $data['classname'],
|
||||
'nextruntime' => 0,
|
||||
'timestarted' => time() - $data['seconds'],
|
||||
'nextruntime' => $data['nextruntime'] ?? 0,
|
||||
'timestarted' => $data['seconds'] > 0 ? time() - $data['seconds'] : 0,
|
||||
'hostname' => $data['hostname'],
|
||||
'pid' => $data['pid'],
|
||||
];
|
||||
|
||||
@@ -1146,7 +1146,7 @@ class tool_uploadcourse_course {
|
||||
|
||||
// Now update values.
|
||||
// Sort out plugin specific fields.
|
||||
$modifiedinstance = $plugin->update_enrol_plugin_data($course->id, $method, $instance);
|
||||
$modifiedinstance = $plugin->update_enrol_plugin_data($course->id, $method, clone $instance);
|
||||
$plugin->update_instance($instance, $modifiedinstance);
|
||||
} else {
|
||||
foreach ($errors as $key => $message) {
|
||||
|
||||
@@ -126,3 +126,19 @@ Feature: An admin can create courses with cohort enrolments using a CSV file
|
||||
And I press "Cancel"
|
||||
And I click on "Edit" "link" in the "Cohort 4" "table_row"
|
||||
And the field "Assign role" matches value "Non-editing teacher"
|
||||
|
||||
@javascript
|
||||
Scenario: Uploading a cohort enrolment method with a non-default role applies only the specified role
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| t1 | Teacher | 1 | s1@example.com |
|
||||
And the following "cohort members" exist:
|
||||
| user | cohort |
|
||||
| t1 | CV4 |
|
||||
And I upload "admin/tool/uploadcourse/tests/fixtures/enrolment_cohort_multiple.csv" file to "File" filemanager
|
||||
And I click on "Preview" "button"
|
||||
And I click on "Upload courses" "button"
|
||||
When I am on the "Course 1" "course" page
|
||||
And I navigate to course participants
|
||||
Then I should see "Non-editing teacher" in the "Teacher 1" "table_row"
|
||||
And I should not see "Student" in the "Teacher 1" "table_row"
|
||||
|
||||
@@ -25,7 +25,7 @@ Feature: An admin can update courses enrolments using a CSV file
|
||||
When I click on "Upload courses" "button"
|
||||
Then I should see "Course updated"
|
||||
And I am on the "Course 1" "enrolment methods" page
|
||||
And "Disable" "icon" should exist in the "Guest access" "table_row"
|
||||
And "Disable" "link" should exist in the "Guest access" "table_row"
|
||||
|
||||
@javascript
|
||||
Scenario: Creating enrolment method by disabling it
|
||||
@@ -42,7 +42,7 @@ Feature: An admin can update courses enrolments using a CSV file
|
||||
When I click on "Upload courses" "button"
|
||||
Then I should see "Course updated"
|
||||
And I am on the "Course 1" "enrolment methods" page
|
||||
And "Enable" "icon" should exist in the "Guest access" "table_row"
|
||||
And "Enable" "link" should exist in the "Guest access" "table_row"
|
||||
|
||||
@javascript
|
||||
Scenario: Enabling enrolment method
|
||||
@@ -55,13 +55,13 @@ Feature: An admin can update courses enrolments using a CSV file
|
||||
When I click on "Upload courses" "button"
|
||||
Then I should see "Course updated"
|
||||
And I am on the "Course 1" "enrolment methods" page
|
||||
And "Disable" "icon" should exist in the "Guest access" "table_row"
|
||||
And "Disable" "link" should exist in the "Guest access" "table_row"
|
||||
|
||||
@javascript
|
||||
Scenario: Disable an enrolment method
|
||||
Given I am on the "Course 1" "enrolment methods" page
|
||||
And I click on "Enable" "link" in the "Guest access" "table_row"
|
||||
And "Disable" "icon" should exist in the "Guest access" "table_row"
|
||||
And "Disable" "link" should exist in the "Guest access" "table_row"
|
||||
And I navigate to "Courses > Upload courses" in site administration
|
||||
And I upload "admin/tool/uploadcourse/tests/fixtures/enrolment_disable.csv" file to "File" filemanager
|
||||
And I set the field "Upload mode" to "Only update existing courses"
|
||||
@@ -71,7 +71,7 @@ Feature: An admin can update courses enrolments using a CSV file
|
||||
When I click on "Upload courses" "button"
|
||||
Then I should see "Course updated"
|
||||
And I am on the "Course 1" "enrolment methods" page
|
||||
And "Enable" "icon" should exist in the "Guest access" "table_row"
|
||||
And "Enable" "link" should exist in the "Guest access" "table_row"
|
||||
|
||||
@javascript
|
||||
Scenario: Delete an enrolment method
|
||||
|
||||
@@ -910,8 +910,8 @@ class process {
|
||||
}
|
||||
|
||||
if ($this->get_bulk() == UU_BULK_UPDATED or $this->get_bulk() == UU_BULK_ALL) {
|
||||
if (!in_array($user->id, $SESSION->bulk_users)) {
|
||||
$SESSION->bulk_users[] = $user->id;
|
||||
if (!array_key_exists($user->id, $SESSION->bulk_users)) {
|
||||
$SESSION->bulk_users[$user->id] = $user->id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -924,8 +924,8 @@ class process {
|
||||
$this->usersuptodate++;
|
||||
|
||||
if ($this->get_bulk() == UU_BULK_ALL) {
|
||||
if (!in_array($user->id, $SESSION->bulk_users)) {
|
||||
$SESSION->bulk_users[] = $user->id;
|
||||
if (!array_key_exists($user->id, $SESSION->bulk_users)) {
|
||||
$SESSION->bulk_users[$user->id] = $user->id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1059,8 +1059,8 @@ class process {
|
||||
\context_user::instance($user->id);
|
||||
|
||||
if ($this->get_bulk() == UU_BULK_NEW or $this->get_bulk() == UU_BULK_ALL) {
|
||||
if (!in_array($user->id, $SESSION->bulk_users)) {
|
||||
$SESSION->bulk_users[] = $user->id;
|
||||
if (!array_key_exists($user->id, $SESSION->bulk_users)) {
|
||||
$SESSION->bulk_users[$user->id] = $user->id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
@tool @tool_xmldb
|
||||
Feature: The Reconcile XMLDB files report works and all the files are ok
|
||||
In order to ensure that all the XMLDB Editor xml files are generated properly
|
||||
As a developer
|
||||
I need to be able to run the Reconcile XMLDB files report and verify that everything is ok
|
||||
|
||||
Scenario: The Reconcile XMLDB files reports that all files are ok
|
||||
Given I log in as "admin"
|
||||
And I navigate to "Development > XMLDB editor" in site administration
|
||||
When I follow "[Reconcile XMLDB files]"
|
||||
Then I should see "Look for XMLDB files needing reconciling"
|
||||
And I should see "All files are OK. No reconciling is needed."
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -31,6 +31,8 @@ import AIHelper from 'core_ai/helper';
|
||||
import DrawerEvents from 'core/drawer_events';
|
||||
import {subscribe} from 'core/pubsub';
|
||||
import * as MessageDrawerHelper from 'core_message/message_drawer_helper';
|
||||
import * as FocusLock from 'core/local/aria/focuslock';
|
||||
import {isSmall} from "core/pagehelpers";
|
||||
|
||||
const AICourseAssist = class {
|
||||
|
||||
@@ -60,6 +62,7 @@ const AICourseAssist = class {
|
||||
this.jumpToElement = document.querySelector(Selectors.ELEMENTS.JUMPTO);
|
||||
this.summaryActionElement = document.querySelector(Selectors.ACTIONS.SUMMARY);
|
||||
this.aiDrawerCloseElement = this.aiDrawerElement.querySelector(Selectors.ELEMENTS.AIDRAWER_CLOSE);
|
||||
this.isDrawerFocusLocked = false;
|
||||
|
||||
this.registerEventListeners();
|
||||
}
|
||||
@@ -85,6 +88,12 @@ const AICourseAssist = class {
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (this.isAIDrawerOpen() && e.key === 'Escape') {
|
||||
this.closeAIDrawer();
|
||||
}
|
||||
});
|
||||
|
||||
// Close AI drawer if message drawer is shown.
|
||||
subscribe(DrawerEvents.DRAWER_SHOWN, () => {
|
||||
if (this.isAIDrawerOpen()) {
|
||||
@@ -198,12 +207,27 @@ const AICourseAssist = class {
|
||||
}
|
||||
this.jumpToElement.setAttribute('tabindex', 0);
|
||||
this.jumpToElement.focus();
|
||||
|
||||
// If the AI drawer is opened on a small screen, we need to trap the focus tab within the AI drawer.
|
||||
if (isSmall()) {
|
||||
FocusLock.trapFocus(this.aiDrawerElement);
|
||||
this.aiDrawerElement.setAttribute('aria-modal', 'true');
|
||||
this.aiDrawerElement.setAttribute('role', 'dialog');
|
||||
this.isDrawerFocusLocked = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the AI drawer.
|
||||
*/
|
||||
closeAIDrawer() {
|
||||
// Untrap focus if it was locked.
|
||||
if (this.isDrawerFocusLocked) {
|
||||
FocusLock.untrapFocus();
|
||||
this.aiDrawerElement.removeAttribute('aria-modal');
|
||||
this.aiDrawerElement.setAttribute('role', 'region');
|
||||
}
|
||||
|
||||
this.aiDrawerElement.classList.remove('show');
|
||||
this.aiDrawerElement.setAttribute('tabindex', '-1');
|
||||
this.aiDrawerBodyElement.removeAttribute('aria-live');
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string['aidrawerlabel'] = 'AI-generated content drawer';
|
||||
$string['aisummary'] = 'AI summary';
|
||||
$string['courseassist:summarise_text'] = 'Summarise text';
|
||||
$string['copy'] = 'Copy';
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"content": "<p>Content to display</p>"
|
||||
}
|
||||
}}
|
||||
<div class="ai-drawer" id="ai-drawer" tabindex="-1">
|
||||
<div class="ai-drawer" id="ai-drawer" aria-label={{#quote}}{{#str}} aidrawerlabel, aiplacement_courseassist {{/str}}{{/quote}} tabindex="-1" role="region">
|
||||
<div class="ai-drawer-header">
|
||||
<button id="ai-drawer-close" class="btn ai-drawer-button" type="button" data-action="course-summarise">
|
||||
{{#pix}} e/cancel, core {{/pix}}
|
||||
|
||||
@@ -938,6 +938,7 @@ class auth_plugin_ldap extends auth_plugin_base {
|
||||
} catch (Exception $e) {
|
||||
print_string('invaliduserexception', 'auth_ldap', print_r($user, true) . $e->getMessage());
|
||||
$errors++;
|
||||
$transaction->allow_commit();
|
||||
continue;
|
||||
}
|
||||
echo "\t"; print_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)); echo "\n";
|
||||
|
||||
@@ -87,7 +87,7 @@ Please log in as before, using the link on the login page.
|
||||
$string['emailpasswordchangeinfosubject'] = '{$a}: Change password information';
|
||||
$string['info'] = 'External account';
|
||||
$string['issuer'] = 'OAuth 2 service';
|
||||
$string['issuernologin'] = 'This issuer can not be used to login';
|
||||
$string['issuernologin'] = 'This issuer can not be used to log in.';
|
||||
$string['key'] = 'Key';
|
||||
$string['linkedlogins'] = 'Linked logins';
|
||||
$string['linkedloginshelp'] = 'Help with linked logins';
|
||||
|
||||
@@ -31,7 +31,7 @@ $string['auth_shib_contact_administrator'] = 'In case you are not associated wit
|
||||
$string['auth_shibbolethdescription'] = 'Using this method users are created and authenticated using Shibboleth. For set-up details, see the <a href="{$a}">Shibboleth README</a>.';
|
||||
$string['auth_shibboleth_errormsg'] = 'Please select the organization you are member of!';
|
||||
$string['auth_shibboleth_login'] = 'Shibboleth login';
|
||||
$string['auth_shibboleth_login_long'] = 'Login to Moodle via Shibboleth';
|
||||
$string['auth_shibboleth_login_long'] = 'Log in via Shibboleth';
|
||||
$string['auth_shibboleth_manual_login'] = 'Manual login';
|
||||
$string['auth_shibboleth_select_member'] = 'I\'m a member of ...';
|
||||
$string['auth_shibboleth_select_organization'] = 'For authentication via Shibboleth, please select your organisation from the drop-down menu:';
|
||||
|
||||
@@ -73,7 +73,15 @@ class auth_plugin_webservice extends auth_plugin_base {
|
||||
function user_login_webservice($username, $password) {
|
||||
global $CFG, $DB;
|
||||
// special web service login
|
||||
if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) {
|
||||
$user = $DB->get_record('user', [
|
||||
'username' => $username,
|
||||
'auth' => 'webservice',
|
||||
'deleted' => '0',
|
||||
'suspended' => '0',
|
||||
'mnethostid' => $CFG->mnet_localhost_id,
|
||||
]);
|
||||
|
||||
if ($user) {
|
||||
return validate_internal_user_password($user, $password);
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -31,6 +31,6 @@ class core_availability_renderer extends plugin_renderer_base {
|
||||
*/
|
||||
#[\core\attribute\deprecated(availability_info::class, since: '4.0', mdl: 'MDL-71691', final: true)]
|
||||
public function render_core_availability_multiple_messages(): void {
|
||||
\core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]);
|
||||
\core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+2
-2
@@ -764,7 +764,7 @@ M.core_availability.List.prototype.clickAdd = function() {
|
||||
dialogRef.dialog = new M.core.dialogue(config);
|
||||
dialogRef.dialog.show();
|
||||
cancel.on('click', function() {
|
||||
dialogRef.dialog.destroy();
|
||||
dialogRef.dialog.hide();
|
||||
// Focus the button they clicked originally.
|
||||
this.inner.one('> .availability-button').one('button').focus();
|
||||
}, this);
|
||||
@@ -801,7 +801,7 @@ M.core_availability.List.prototype.getAddHandler = function(type, dialogRef) {
|
||||
M.core_availability.form.rootList.renumber();
|
||||
this.updateHtml();
|
||||
// Hide dialog.
|
||||
dialogRef.dialog.destroy();
|
||||
dialogRef.dialog.hide();
|
||||
newItem.focusAfterAdd();
|
||||
};
|
||||
};
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
@@ -764,7 +764,7 @@ M.core_availability.List.prototype.clickAdd = function() {
|
||||
dialogRef.dialog = new M.core.dialogue(config);
|
||||
dialogRef.dialog.show();
|
||||
cancel.on('click', function() {
|
||||
dialogRef.dialog.destroy();
|
||||
dialogRef.dialog.hide();
|
||||
// Focus the button they clicked originally.
|
||||
this.inner.one('> .availability-button').one('button').focus();
|
||||
}, this);
|
||||
@@ -801,7 +801,7 @@ M.core_availability.List.prototype.getAddHandler = function(type, dialogRef) {
|
||||
M.core_availability.form.rootList.renumber();
|
||||
this.updateHtml();
|
||||
// Hide dialog.
|
||||
dialogRef.dialog.destroy();
|
||||
dialogRef.dialog.hide();
|
||||
newItem.focusAfterAdd();
|
||||
};
|
||||
};
|
||||
|
||||
Vendored
+2
-2
@@ -762,7 +762,7 @@ M.core_availability.List.prototype.clickAdd = function() {
|
||||
dialogRef.dialog = new M.core.dialogue(config);
|
||||
dialogRef.dialog.show();
|
||||
cancel.on('click', function() {
|
||||
dialogRef.dialog.destroy();
|
||||
dialogRef.dialog.hide();
|
||||
// Focus the button they clicked originally.
|
||||
this.inner.one('> .availability-button').one('button').focus();
|
||||
}, this);
|
||||
@@ -799,7 +799,7 @@ M.core_availability.List.prototype.getAddHandler = function(type, dialogRef) {
|
||||
M.core_availability.form.rootList.renumber();
|
||||
this.updateHtml();
|
||||
// Hide dialog.
|
||||
dialogRef.dialog.destroy();
|
||||
dialogRef.dialog.hide();
|
||||
newItem.focusAfterAdd();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -111,7 +111,7 @@ abstract class base_controller extends backup implements loggable {
|
||||
*/
|
||||
#[\core\attribute\deprecated(since: '4.1', mdl: 'MDL-74548', final: true)]
|
||||
public function set_copy(): void {
|
||||
\core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]);
|
||||
\core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,6 +119,6 @@ abstract class base_controller extends backup implements loggable {
|
||||
*/
|
||||
#[\core\attribute\deprecated('restore_controller::get_copy()', since: '4.1', mdl: 'MDL-74548', final: true)]
|
||||
public function get_copy() {
|
||||
\core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]);
|
||||
\core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# core_badges (subsystem) Upgrade notes
|
||||
|
||||
## 4.5.7
|
||||
|
||||
### Added
|
||||
|
||||
- A number of new static methods have been added to `core_badges\backpack_api` to support the new Canvas Credentials backpack provider. These methods allow you to retrieve lists of providers and regions, check if Canvas Credentials fields should be displayed, and get a region URL or API URL based on a given region ID. The new methods include `get_providers`, `get_regions`, `display_canvas_credentials_fields`, `get_region_url`, `get_region_api_url`, `get_regionid_from_url`, and `is_canvas_credentials_region`.
|
||||
|
||||
For more information see [MDL-86174](https://tracker.moodle.org/browse/MDL-86174)
|
||||
|
||||
## 4.5
|
||||
|
||||
### Added
|
||||
|
||||
+113
-59
@@ -60,6 +60,7 @@ if (!empty($issuedbadge->recipient->id)) {
|
||||
$assertionid = $assertion->get_assertion_hash();
|
||||
$assertionentityid = $assertiondata['id'];
|
||||
$badgeadded = false;
|
||||
$issuerexists = false;
|
||||
if (badges_open_badges_backpack_api() == OPEN_BADGES_V2) {
|
||||
$sitebackpack = badges_get_site_primary_backpack();
|
||||
$api = new \core_badges\backpack_api($sitebackpack);
|
||||
@@ -71,62 +72,75 @@ if (!empty($issuedbadge->recipient->id)) {
|
||||
$issuer = $assertion->get_issuer();
|
||||
if (!($issuerentityid = badges_external_get_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_ISSUER, $issuer['email']))) {
|
||||
$response = $api->put_issuer($issuer);
|
||||
if (!$response) {
|
||||
throw new moodle_exception('invalidrequest', 'error');
|
||||
if ($response) {
|
||||
$issuerexists = true;
|
||||
$issuerentityid = $response->id;
|
||||
badges_external_create_mapping(
|
||||
$sitebackpack->id,
|
||||
OPEN_BADGES_V2_TYPE_ISSUER,
|
||||
$issuer['email'],
|
||||
$issuerentityid,
|
||||
);
|
||||
}
|
||||
$issuerentityid = $response->id;
|
||||
badges_external_create_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_ISSUER, $issuer['email'],
|
||||
$issuerentityid);
|
||||
}
|
||||
// Create badge.
|
||||
$badge = $assertion->get_badge_class(false);
|
||||
$badgeid = $assertion->get_badge_id();
|
||||
if (!($badgeentityid = badges_external_get_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_BADGE, $badgeid))) {
|
||||
$response = $api->put_badgeclass($issuerentityid, $badge);
|
||||
if (!$response) {
|
||||
throw new moodle_exception('invalidrequest', 'error');
|
||||
if ($issuerexists) {
|
||||
// Create badge.
|
||||
$badge = $assertion->get_badge_class(false);
|
||||
$badgeid = $assertion->get_badge_id();
|
||||
if (!($badgeentityid = badges_external_get_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_BADGE, $badgeid))) {
|
||||
$response = $api->put_badgeclass($issuerentityid, $badge);
|
||||
if ($response) {
|
||||
$badgeentityid = $response->id;
|
||||
badges_external_create_mapping(
|
||||
$sitebackpack->id,
|
||||
OPEN_BADGES_V2_TYPE_BADGE,
|
||||
$badgeid,
|
||||
$badgeentityid,
|
||||
);
|
||||
}
|
||||
}
|
||||
$badgeentityid = $response->id;
|
||||
badges_external_create_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_BADGE, $badgeid,
|
||||
$badgeentityid);
|
||||
}
|
||||
|
||||
// Create assertion (Award the badge!).
|
||||
$assertionentityid = badges_external_get_mapping(
|
||||
$sitebackpack->id,
|
||||
OPEN_BADGES_V2_TYPE_ASSERTION,
|
||||
$assertionid
|
||||
);
|
||||
|
||||
if ($assertionentityid && strpos($sitebackpack->backpackapiurl, 'badgr')) {
|
||||
$assertionentityid = badges_generate_badgr_open_url(
|
||||
$sitebackpack,
|
||||
OPEN_BADGES_V2_TYPE_ASSERTION,
|
||||
$assertionentityid
|
||||
);
|
||||
}
|
||||
|
||||
// Create an assertion for the recipient in the issuer's account.
|
||||
if (!$assertionentityid) {
|
||||
$response = $api->put_badgeclass_assertion($badgeentityid, $assertiondata);
|
||||
if (!$response) {
|
||||
throw new moodle_exception('invalidrequest', 'error');
|
||||
}
|
||||
$assertionentityid = badges_generate_badgr_open_url($sitebackpack, OPEN_BADGES_V2_TYPE_ASSERTION, $response->id);
|
||||
$badgeadded = true;
|
||||
badges_external_create_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_ASSERTION, $assertionid,
|
||||
$response->id);
|
||||
} else {
|
||||
// An assertion already exists. Make sure it's up to date.
|
||||
$internalid = badges_external_get_mapping(
|
||||
// Create assertion (Award the badge!).
|
||||
$assertionentityid = badges_external_get_mapping(
|
||||
$sitebackpack->id,
|
||||
OPEN_BADGES_V2_TYPE_ASSERTION,
|
||||
$assertionid,
|
||||
'externalid'
|
||||
$assertionid
|
||||
);
|
||||
$response = $api->update_assertion($internalid, $assertiondata);
|
||||
if (!$response) {
|
||||
throw new moodle_exception('invalidrequest', 'error');
|
||||
|
||||
if ($assertionentityid && strpos($sitebackpack->backpackapiurl, 'badgr')) {
|
||||
$assertionentityid = badges_generate_badgr_open_url(
|
||||
$sitebackpack,
|
||||
OPEN_BADGES_V2_TYPE_ASSERTION,
|
||||
$assertionentityid
|
||||
);
|
||||
}
|
||||
|
||||
// Create an assertion for the recipient in the issuer's account.
|
||||
if (!$assertionentityid) {
|
||||
$response = $api->put_badgeclass_assertion($badgeentityid, $assertiondata);
|
||||
if ($response) {
|
||||
$assertionentityid = badges_generate_badgr_open_url(
|
||||
$sitebackpack,
|
||||
OPEN_BADGES_V2_TYPE_ASSERTION,
|
||||
$response->id,
|
||||
);
|
||||
$badgeadded = true;
|
||||
badges_external_create_mapping(
|
||||
$sitebackpack->id,
|
||||
OPEN_BADGES_V2_TYPE_ASSERTION,
|
||||
$assertionid,
|
||||
$response->id,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// An assertion already exists. Make sure it's up to date.
|
||||
$internalid = badges_external_get_mapping(
|
||||
$sitebackpack->id,
|
||||
OPEN_BADGES_V2_TYPE_ASSERTION,
|
||||
$assertionid,
|
||||
'externalid'
|
||||
);
|
||||
$response = $api->update_assertion($internalid, $assertiondata);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,21 +150,61 @@ if (!empty($issuedbadge->recipient->id)) {
|
||||
// - If a user and site backpack have the same provider we can skip this as Badgr automatically maps recipients
|
||||
// based on email address.
|
||||
// - This is only needed when the backpacks are from different regions.
|
||||
if ($assertionentityid && !badges_external_get_mapping($userbackpack->id, OPEN_BADGES_V2_TYPE_ASSERTION, $assertionid)) {
|
||||
if (
|
||||
$assertionentityid
|
||||
&& (!$issuerexists || !badges_external_get_mapping($userbackpack->id, OPEN_BADGES_V2_TYPE_ASSERTION, $assertionid))
|
||||
) {
|
||||
$userapi = new \core_badges\backpack_api($userbackpack, $backpack);
|
||||
$userapi->authenticate();
|
||||
$response = $userapi->import_badge_assertion($assertionentityid);
|
||||
if (!$response) {
|
||||
throw new moodle_exception('invalidrequest', 'error');
|
||||
if ($response) {
|
||||
$assertionentityid = $response->id;
|
||||
$badgeadded = true;
|
||||
badges_external_create_mapping(
|
||||
$userbackpack->id,
|
||||
OPEN_BADGES_V2_TYPE_ASSERTION,
|
||||
$assertionid,
|
||||
$assertionentityid,
|
||||
);
|
||||
}
|
||||
$assertionentityid = $response->id;
|
||||
$badgeadded = true;
|
||||
badges_external_create_mapping($userbackpack->id, OPEN_BADGES_V2_TYPE_ASSERTION, $assertionid,
|
||||
$assertionentityid);
|
||||
}
|
||||
|
||||
$response = $badgeadded ? ['success' => 'addedtobackpack'] : ['warning' => 'existsinbackpack'];
|
||||
redirect(new moodle_url('/badges/mybadges.php', $response));
|
||||
if ($badgeadded) {
|
||||
$message = get_string('addedtobackpack', 'badges');
|
||||
$messagetype = \core\output\notification::NOTIFY_SUCCESS;
|
||||
} else {
|
||||
if (isset($userapi) && !empty($userapi->get_errors())) {
|
||||
// If the api used to import the badge to the backpack has errors, show them to inform the user.
|
||||
if (array_filter($userapi->get_errors(), fn($element) => str_contains($element, "DUPLICATE_BADGE"))) {
|
||||
// Duplicated badges are displayed as a warning.
|
||||
$message = get_string('existsinbackpack', 'badges');
|
||||
$messagetype = \core\output\notification::NOTIFY_WARNING;
|
||||
} else {
|
||||
// If the userapi has any other errors, we will use those to inform the user.
|
||||
$message = get_string(
|
||||
'error:cannotsendtobackpack',
|
||||
'badges',
|
||||
implode($userapi->get_errors()),
|
||||
);
|
||||
$messagetype = \core\output\notification::NOTIFY_ERROR;
|
||||
}
|
||||
} else if (isset($api) && !empty($api->get_errors())) {
|
||||
// If the api used to create/update the issuer has errors, show them to inform the user.
|
||||
$errors = $api->get_errors() ?? [get_string('invalidrequest', 'error')];
|
||||
$message = get_string(
|
||||
'error:cannotsendtobackpack',
|
||||
'badges',
|
||||
implode($errors),
|
||||
);
|
||||
$messagetype = \core\output\notification::NOTIFY_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
redirect(
|
||||
url: new \core\url('/badges/mybadges.php'),
|
||||
message: $message,
|
||||
messagetype: $messagetype,
|
||||
);
|
||||
} else {
|
||||
redirect(new moodle_url('/badges/mybadges.php'));
|
||||
redirect(new \core\url('/badges/mybadges.php'));
|
||||
}
|
||||
|
||||
@@ -42,6 +42,17 @@ define('BADGE_EXPIRES_TOKEN', 'expires');
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class backpack_api {
|
||||
/** @var int Canvas Credentials backpack provider */
|
||||
public const PROVIDER_CANVAS_CREDENTIALS = 0;
|
||||
|
||||
/** @var int Other backpack provider */
|
||||
public const PROVIDER_OTHER = 1;
|
||||
|
||||
/** @var int Empty provider */
|
||||
public const PROVIDER_EMPTY = -1;
|
||||
|
||||
/** @var int Empty region */
|
||||
public const REGION_EMPTY = -1;
|
||||
|
||||
/** @var string The email address of the issuer or the backpack owner. */
|
||||
private $email;
|
||||
@@ -512,6 +523,20 @@ class backpack_api {
|
||||
return backpack_api_mapping::get_authentication_error();
|
||||
}
|
||||
|
||||
/**
|
||||
* List all errors occurred during the requests to the backpack.
|
||||
*
|
||||
* @return array The list of errors.
|
||||
*/
|
||||
public function get_errors(): array {
|
||||
$errors = [];
|
||||
foreach ($this->mappings as $mapping) {
|
||||
$errors = array_merge($errors, $mapping->get_errors());
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of badges in a collection.
|
||||
*
|
||||
@@ -565,4 +590,123 @@ class backpack_api {
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of backpack providers for OBv2.0.
|
||||
*
|
||||
* @return string[] Array with the OBv2.0 backpack providers.
|
||||
*/
|
||||
public static function get_providers(): array {
|
||||
$allproviders = [
|
||||
self::PROVIDER_CANVAS_CREDENTIALS => 'canvascredentialsprovider',
|
||||
self::PROVIDER_OTHER => 'otherprovider',
|
||||
];
|
||||
|
||||
foreach ($allproviders as $key => $value) {
|
||||
if (get_string_manager()->string_exists($value, 'badges')) {
|
||||
$providers[$key] = get_string($value, 'badges');
|
||||
} else {
|
||||
// If the string does not exist, use the key as a fallback.
|
||||
$providers[$key] = $value;
|
||||
}
|
||||
}
|
||||
return $providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of regions for backpack providers.
|
||||
*
|
||||
* @return array Regions with the following information: name, url and apiurl.
|
||||
*/
|
||||
public static function get_regions() {
|
||||
global $CFG;
|
||||
|
||||
$regions = [];
|
||||
if (empty(trim($CFG->badges_canvasregions))) {
|
||||
return $regions;
|
||||
}
|
||||
|
||||
$entries = explode("\n", $CFG->badges_canvasregions);
|
||||
foreach ($entries as $entry) {
|
||||
if (empty(trim($entry)) || substr_count($entry, '|') != 2) {
|
||||
continue;
|
||||
}
|
||||
$entry = trim($entry);
|
||||
$parts = explode('|', $entry);
|
||||
$regions[] = [
|
||||
'name' => $parts[0],
|
||||
'url' => rtrim($parts[1], '/'),
|
||||
'apiurl' => rtrim($parts[2], '/'),
|
||||
];
|
||||
}
|
||||
|
||||
return $regions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the Canvas Credentials fields should be displayed or not in the backpack form.
|
||||
*
|
||||
* @return bool True if the fields should be displayed; false otherwise.
|
||||
*/
|
||||
public static function display_canvas_credentials_fields(): bool {
|
||||
return !empty(self::get_providers()) && !empty(self::get_regions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backpack URL for a given regionid.
|
||||
*
|
||||
* @param int $regionid The region identifier.
|
||||
* @return string|null The backpack URL.
|
||||
*/
|
||||
public static function get_region_url(int $regionid): ?string {
|
||||
$regions = self::get_regions();
|
||||
if (!array_key_exists($regionid, $regions)) {
|
||||
return null;
|
||||
}
|
||||
return $regions[$regionid]['url'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backpack API URL for a given regionid.
|
||||
*
|
||||
* @param int $regionid The region identifier.
|
||||
* @return string|null The backpack API URL.
|
||||
*/
|
||||
public static function get_region_api_url(int $regionid): ?string {
|
||||
$regions = self::get_regions();
|
||||
if (!array_key_exists($regionid, $regions)) {
|
||||
return null;
|
||||
}
|
||||
return $regions[$regionid]['apiurl'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get region identifier from a given backpack URL.
|
||||
* When the URL is not found, the last region index is returned.
|
||||
*
|
||||
* @param string $url The backpack URL.
|
||||
* @return int The region identifier associated to the given backpack URL or the last region index if not found.
|
||||
*/
|
||||
public static function get_regionid_from_url(string $url): int {
|
||||
$regions = self::get_regions();
|
||||
if (empty($regions)) {
|
||||
return self::REGION_EMPTY;
|
||||
}
|
||||
|
||||
// Normalize the URL by removing the trailing slash.
|
||||
$normalizedurl = rtrim($url, '/');
|
||||
$regionurl = array_search($normalizedurl, array_column($regions, 'url'));
|
||||
return $regionurl !== false ? (int)$regionurl : count($regions) - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given URL is a Canvas Credentials one.
|
||||
*
|
||||
* @param string $url The backpack URL.
|
||||
* @return bool True is the given URL is a Canvas Credentials region; false otherwise.
|
||||
*/
|
||||
public static function is_canvas_credentials_region(string $url): bool {
|
||||
$regions = self::get_regions();
|
||||
return in_array($url, array_column($regions, 'url'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,9 @@ class backpack_api_mapping {
|
||||
/** @var int OpenBadges version. */
|
||||
protected $backpackapiversion;
|
||||
|
||||
/** @var array Errors encountered during the request. */
|
||||
protected $errors = [];
|
||||
|
||||
/**
|
||||
* Create a mapping.
|
||||
*
|
||||
@@ -146,6 +149,26 @@ class backpack_api_mapping {
|
||||
return self::$authenticationerror;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the errors encountered during the request.
|
||||
*
|
||||
* @return array The list of errors.
|
||||
*/
|
||||
public function get_errors() {
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an error to the list of errors.
|
||||
*
|
||||
* @param string $error The error message.
|
||||
* @return self This instance for method chaining.
|
||||
*/
|
||||
public function add_error(string $error): self {
|
||||
$this->errors[] = $error;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the action match this mapping?
|
||||
*
|
||||
@@ -342,6 +365,23 @@ class backpack_api_mapping {
|
||||
$response = $curl->put($url, $post, $options);
|
||||
}
|
||||
$response = json_decode($response);
|
||||
if ($response === null) {
|
||||
$this->add_error(get_string('invalidrequest', 'error'));
|
||||
return null;
|
||||
}
|
||||
if (isset($response->status) && isset($response->status->success) && $response->status->success != true) {
|
||||
// If the response wasn't successful, store the errors and return null.
|
||||
if (isset($response->validationErrors)) {
|
||||
$error = implode(', ', $response->validationErrors);
|
||||
} else if (isset($response->status->description)) {
|
||||
$error = $response->status->description;
|
||||
} else {
|
||||
$error = get_string('invalidrequest', 'error');
|
||||
}
|
||||
$this->add_error($error);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($response->result)) {
|
||||
$response = $response->result;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
namespace core_badges\form;
|
||||
|
||||
use core_badges\backpack_api;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->libdir.'/formslib.php');
|
||||
@@ -37,11 +39,7 @@ class external_backpack extends \moodleform {
|
||||
global $CFG;
|
||||
|
||||
$mform = $this->_form;
|
||||
$backpack = false;
|
||||
|
||||
if (isset($this->_customdata['externalbackpack'])) {
|
||||
$backpack = $this->_customdata['externalbackpack'];
|
||||
}
|
||||
$backpack = $this->_customdata['externalbackpack'] ?? null;
|
||||
|
||||
$mform->addElement('hidden', 'action', 'edit');
|
||||
$mform->setType('action', PARAM_ALPHA);
|
||||
@@ -52,14 +50,22 @@ class external_backpack extends \moodleform {
|
||||
$mform->setDefault('apiversion', OPEN_BADGES_V2P1);
|
||||
$mform->addRule('apiversion', null, 'required', null, 'client');
|
||||
|
||||
$this->add_provider_fields();
|
||||
|
||||
$mform->addElement('text', 'backpackweburl', get_string('backpackweburl', 'core_badges'));
|
||||
$mform->setType('backpackweburl', PARAM_URL);
|
||||
$mform->addRule('backpackweburl', null, 'required', null, 'client');
|
||||
$mform->addRule('backpackweburl', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
|
||||
$mform->hideIf('backpackweburl', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
|
||||
$mform->addElement('text', 'backpackapiurl', get_string('backpackapiurl', 'core_badges'));
|
||||
$mform->setType('backpackapiurl', PARAM_URL);
|
||||
$mform->addRule('backpackapiurl', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
|
||||
$mform->hideIf('backpackapiurl', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
|
||||
$mform->addElement('text', 'backpackweburlv2p1', get_string('backpackweburl', 'core_badges'));
|
||||
$mform->setType('backpackweburlv2p1', PARAM_URL);
|
||||
$mform->addRule('backpackweburlv2p1', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
|
||||
$mform->hideIf('backpackweburlv2p1', 'apiversion', 'ne', (string) OPEN_BADGES_V2P1);
|
||||
|
||||
$mform->addElement('hidden', 'id', ($backpack->id ?? null));
|
||||
$mform->setType('id', PARAM_INT);
|
||||
@@ -70,31 +76,116 @@ class external_backpack extends \moodleform {
|
||||
$mform->addElement('hidden', 'backpackuid', 0);
|
||||
$mform->setType('backpackuid', PARAM_INT);
|
||||
|
||||
$mform->addElement('advcheckbox', 'includeauthdetails', null, get_string('includeauthdetails', 'core_badges'));
|
||||
if (!empty($backpack->backpackemail) || !empty($backpack->password)) {
|
||||
$mform->setDefault('includeauthdetails', 1);
|
||||
// Add rules for backpack URL fields.
|
||||
if (backpack_api::display_canvas_credentials_fields()) {
|
||||
$mform->hideIf('backpackweburl', 'provider', 'ne', backpack_api::PROVIDER_OTHER);
|
||||
$mform->hideIf('backpackapiurl', 'provider', 'ne', backpack_api::PROVIDER_OTHER);
|
||||
}
|
||||
|
||||
$issuercontact = $CFG->badges_defaultissuercontact;
|
||||
$this->add_auth_fields($issuercontact);
|
||||
$issueremail = $CFG->badges_defaultissuercontact;
|
||||
// Connect to a Canvas Credentials provider.
|
||||
$this->add_connect_issuer_canvas_fields($issueremail);
|
||||
|
||||
// Connect to another provider.
|
||||
$this->add_connect_issuer_fields($backpack, $issueremail);
|
||||
|
||||
if ($backpack) {
|
||||
$this->set_data($backpack);
|
||||
}
|
||||
|
||||
$mform->hideIf('includeauthdetails', 'apiversion', 'in', [OPEN_BADGES_V2P1]);
|
||||
$mform->hideIf('backpackemail', 'includeauthdetails');
|
||||
$mform->hideIf('backpackemail', 'apiversion', 'in', [OPEN_BADGES_V2P1]);
|
||||
$mform->hideIf('password', 'includeauthdetails');
|
||||
$mform->hideIf('password', 'apiversion', 'in', [OPEN_BADGES_V2P1]);
|
||||
$mform->hideIf('backpackapiurl', 'apiversion', 'in', [OPEN_BADGES_V2P1]);
|
||||
|
||||
// Disable short forms.
|
||||
$mform->setDisableShortforms();
|
||||
|
||||
$this->add_action_buttons();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function definition_after_data(): void {
|
||||
parent::definition_after_data();
|
||||
$mform = $this->_form;
|
||||
|
||||
if ($this->is_submitted()) {
|
||||
if (!$mform->elementExists('apiversion')) {
|
||||
return;
|
||||
}
|
||||
$apiversion = $mform->getElement('apiversion')->getValue();
|
||||
$apiversion = $apiversion ? array_pop($apiversion) : null;
|
||||
$provider = $mform->elementExists('provider') ? $mform->getElement('provider')->getValue() : null;
|
||||
$provider = $provider ? array_pop($provider) : null;
|
||||
$region = $mform->elementExists('region') ? $mform->getElement('region')->getValue() : null;
|
||||
$region = $region ? array_pop($region) : null;
|
||||
if ($apiversion == OPEN_BADGES_V2) {
|
||||
if (
|
||||
$provider == backpack_api::PROVIDER_CANVAS_CREDENTIALS
|
||||
&& isset($region) && $region != backpack_api::REGION_EMPTY
|
||||
) {
|
||||
$mform->getElement('backpackweburl')->setValue(
|
||||
backpack_api::get_region_url($region),
|
||||
);
|
||||
$mform->getElement('backpackapiurl')->setValue(
|
||||
backpack_api::get_region_api_url($region),
|
||||
);
|
||||
|
||||
if ($mform->getElement('includeauthdetailscanvas')->getValue()) {
|
||||
$mform->getElement('backpackemail')->setValue(
|
||||
$mform->getElement('backpackemailcanvas')->getValue(),
|
||||
);
|
||||
$mform->getElement('password')->setValue(
|
||||
$mform->getElement('backpackpasswordcanvas')->getValue(),
|
||||
);
|
||||
}
|
||||
} else if (is_null($provider) || $provider == backpack_api::PROVIDER_OTHER) {
|
||||
if ($mform->getElement('includeauthdetails')->getValue() == 0) {
|
||||
// Clear backpack issuer fields when authentication details checkbox is not checked.
|
||||
$mform->getElement('backpackemail')->setValue('');
|
||||
$mform->getElement('password')->setValue('');
|
||||
}
|
||||
}
|
||||
} else if ($apiversion == OPEN_BADGES_V2P1) {
|
||||
if (!empty($mform->getElement('backpackweburlv2p1')->getValue())) {
|
||||
$mform->getElement('backpackweburl')->setValue(
|
||||
$mform->getElement('backpackweburlv2p1')->getValue(),
|
||||
);
|
||||
}
|
||||
// Clear backpack issuer fields when OBv2.1 is selected.
|
||||
$mform->getElement('includeauthdetails')->setValue(0);
|
||||
$mform->getElement('backpackemail')->setValue('');
|
||||
$mform->getElement('password')->setValue('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function set_data($backpack) {
|
||||
if ($backpack->apiversion == OPEN_BADGES_V2) {
|
||||
if (backpack_api::is_canvas_credentials_region($backpack->backpackweburl)) {
|
||||
// Calculate provider and region fields based on backpack URLs.
|
||||
$backpack->provider = backpack_api::PROVIDER_CANVAS_CREDENTIALS;
|
||||
$backpack->region = backpack_api::get_regionid_from_url($backpack->backpackweburl);
|
||||
$backpack->backpackweburl = '';
|
||||
$backpack->backpackapiurl = '';
|
||||
if (isset($backpack->backpackemail) && !empty($backpack->backpackemail)) {
|
||||
// Update Canvas Credentials fields.
|
||||
$backpack->includeauthdetailscanvas = 1;
|
||||
$backpack->backpackemailcanvas = $backpack->backpackemail;
|
||||
$backpack->backpackpasswordcanvas = $backpack->password;
|
||||
// Clear email and password fields for another providers.
|
||||
$backpack->includeauthdetails = 0;
|
||||
$backpack->backpackemail = '';
|
||||
$backpack->password = '';
|
||||
}
|
||||
} else {
|
||||
$backpack->provider = backpack_api::PROVIDER_OTHER;
|
||||
}
|
||||
} else if ($backpack->apiversion == OPEN_BADGES_V2P1) {
|
||||
$backpack->backpackweburlv2p1 = $backpack->backpackweburl;
|
||||
$backpack->backpackweburl = '';
|
||||
$backpack->backpackapiurl = '';
|
||||
}
|
||||
|
||||
parent::set_data($backpack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the data from the form.
|
||||
*
|
||||
@@ -106,42 +197,212 @@ class external_backpack extends \moodleform {
|
||||
$errors = parent::validation($data, $files);
|
||||
|
||||
// Ensure backpackapiurl and backpackweburl are valid URLs.
|
||||
$isobv21 = isset($data['apiversion']) && $data['apiversion'] == OPEN_BADGES_V2P1;
|
||||
if (!$isobv21) {
|
||||
if (empty($data['backpackapiurl'])) {
|
||||
$errors['backpackapiurl'] = get_string('err_required', 'form');
|
||||
} else if (!preg_match('@^https?://.+@', $data['backpackapiurl'])) {
|
||||
$errors['backpackapiurl'] = get_string('invalidurl', 'badges');
|
||||
}
|
||||
$isobv20 = isset($data['apiversion']) && $data['apiversion'] == OPEN_BADGES_V2;
|
||||
$isobv2p1 = isset($data['apiversion']) && $data['apiversion'] == OPEN_BADGES_V2P1;
|
||||
if ($isobv20) {
|
||||
$errors = array_merge($errors, $this->validate_obv20($data));
|
||||
} else if ($isobv2p1) {
|
||||
$errors = array_merge($errors, $this->validate_obv2p1($data));
|
||||
}
|
||||
if (!empty($data['backpackweburl']) && !preg_match('@^https?://.+@', $data['backpackweburl'])) {
|
||||
$errors['backpackweburl'] = get_string('invalidurl', 'badges');
|
||||
|
||||
// Check email and password are not empty when including auth details.
|
||||
if (!empty($data['includeauthdetails']) && empty($data['backpackemail'])) {
|
||||
$errors['backpackemail'] = get_string('err_required', 'form');
|
||||
}
|
||||
if (!empty($data['includeauthdetails']) && empty($data['password'])) {
|
||||
$errors['password'] = get_string('err_required', 'form');
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return submitted data if properly submitted or returns NULL if validation fails or
|
||||
* if there is no submitted data.
|
||||
* Validate the data for Open Badges v2.0.
|
||||
*
|
||||
* @return object|void
|
||||
* @param array $data Form data.
|
||||
* @return string[] An array of error messages.
|
||||
*/
|
||||
public function get_data() {
|
||||
$data = parent::get_data();
|
||||
if ($data ) {
|
||||
if ((isset($data->includeauthdetails) && !$data->includeauthdetails)
|
||||
|| (isset($data->apiversion) && $data->apiversion == 2.1)) {
|
||||
$data->backpackemail = "";
|
||||
$data->password = "";
|
||||
}
|
||||
private function validate_obv20(array $data): array {
|
||||
$errors = [];
|
||||
|
||||
if ((isset($data->apiversion) && $data->apiversion == 1)) {
|
||||
$data->password = "";
|
||||
$displaycanvasfields = backpack_api::display_canvas_credentials_fields();
|
||||
if (
|
||||
$displaycanvasfields
|
||||
&& (!array_key_exists('provider', $data) || $data['provider'] == backpack_api::PROVIDER_EMPTY)
|
||||
) {
|
||||
// Check provider is set.
|
||||
$errors['provider'] = get_string('err_required', 'form');
|
||||
} else if (
|
||||
$displaycanvasfields
|
||||
&& ($data['provider'] == backpack_api::PROVIDER_CANVAS_CREDENTIALS)
|
||||
) {
|
||||
// Check region is set.
|
||||
if (!array_key_exists('region', $data) || $data['region'] == backpack_api::REGION_EMPTY) {
|
||||
$errors['region'] = get_string('err_required', 'form');
|
||||
}
|
||||
} else {
|
||||
if (empty($data['backpackweburl'])) {
|
||||
$errors['backpackweburl'] = get_string('err_required', 'form');
|
||||
} else if (!preg_match('@^https?://.+@', $data['backpackweburl'])) {
|
||||
$errors['backpackweburl'] = get_string('invalidurl', 'badges');
|
||||
}
|
||||
if (empty($data['backpackapiurl'])) {
|
||||
$errors['backpackapiurl'] = get_string('err_required', 'form');
|
||||
} else if (!preg_match('@^https?://.+@', $data['backpackapiurl'])) {
|
||||
$errors['backpackapiurl'] = get_string('invalidurl', 'badges');
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
if ($displaycanvasfields) {
|
||||
if (!empty($data['includeauthdetailscanvas']) && empty($data['backpackemailcanvas'])) {
|
||||
$errors['backpackemailcanvas'] = get_string('err_required', 'form');
|
||||
}
|
||||
if (!empty($data['includeauthdetailscanvas']) && empty($data['backpackpasswordcanvas'])) {
|
||||
$errors['backpackpasswordcanvas'] = get_string('err_required', 'form');
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the data for Open Badges v2.1.
|
||||
*
|
||||
* @param array $data Form data.
|
||||
* @return string[] An array of error messages.
|
||||
*/
|
||||
private function validate_obv2p1(array $data): array {
|
||||
$errors = [];
|
||||
|
||||
if (empty($data['backpackweburlv2p1'])) {
|
||||
$errors['backpackweburlv2p1'] = get_string('err_required', 'form');
|
||||
} else if (!preg_match('@^https?://.+@', $data['backpackweburlv2p1'])) {
|
||||
$errors['backpackweburlv2p1'] = get_string('invalidurl', 'badges');
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add provider fields to the form.
|
||||
*/
|
||||
protected function add_provider_fields(): void {
|
||||
$mform = $this->_form;
|
||||
|
||||
if (!backpack_api::display_canvas_credentials_fields()) {
|
||||
// If canvas credentials fields are not to be displayed, return early.
|
||||
return;
|
||||
}
|
||||
|
||||
// Add an empty option at the start.
|
||||
$providers = backpack_api::get_providers();
|
||||
$providers = [backpack_api::PROVIDER_EMPTY => ''] + $providers;
|
||||
$mform->addElement('select', 'provider', get_string('provider', 'core_badges'), $providers);
|
||||
$mform->setType('provider', PARAM_RAW);
|
||||
$mform->hideIf('provider', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
|
||||
// Add an empty option at the start.
|
||||
$regions = backpack_api::get_regions();
|
||||
$regions = [backpack_api::REGION_EMPTY => ''] + array_column($regions, 'name');
|
||||
$mform->addElement('select', 'region', get_string('region', 'core_badges'), $regions);
|
||||
$mform->setType('region', PARAM_RAW);
|
||||
$mform->hideIf('region', 'provider', 'ne', backpack_api::PROVIDER_CANVAS_CREDENTIALS);
|
||||
$mform->hideIf('region', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Canvas backpack specific issuer auth details.
|
||||
*
|
||||
* @param string|null $email The email addressed provided or null if it's new.
|
||||
*/
|
||||
protected function add_connect_issuer_canvas_fields(?string $email): void {
|
||||
$mform = $this->_form;
|
||||
|
||||
if (!backpack_api::display_canvas_credentials_fields()) {
|
||||
// If canvas credentials fields are not to be displayed, return early.
|
||||
return;
|
||||
}
|
||||
|
||||
$providers = backpack_api::get_providers();
|
||||
$regions = backpack_api::get_regions();
|
||||
if (empty($providers) || empty($regions)) {
|
||||
// If no providers or regions are available, return early.
|
||||
return;
|
||||
}
|
||||
|
||||
// Checkbox and information to enable/disable issuer account.
|
||||
$mform->addElement('static', '', null, '');
|
||||
$mform->addElement(
|
||||
'advcheckbox',
|
||||
'includeauthdetailscanvas',
|
||||
null,
|
||||
'<strong>' . get_string('includeauthdetailscanvas', 'core_badges') . '</strong> '
|
||||
. get_string('includeauthdetailscanvas_subtitle', 'core_badges'),
|
||||
);
|
||||
if (!empty($backpack->backpackemail) || !empty($backpack->password)) {
|
||||
$mform->setDefault('includeauthdetailscanvas', 1);
|
||||
}
|
||||
$mform->addHelpButton('includeauthdetailscanvas', 'includeauthdetailscanvas', 'core_badges');
|
||||
$mform->hideIf('includeauthdetailscanvas', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
$mform->hideIf('includeauthdetailscanvas', 'region', 'eq', backpack_api::REGION_EMPTY);
|
||||
$mform->hideIf('includeauthdetailscanvas', 'provider', 'ne', backpack_api::PROVIDER_CANVAS_CREDENTIALS);
|
||||
|
||||
$mform->addElement(
|
||||
'static',
|
||||
'includeauthdetailscanvasdesc',
|
||||
null,
|
||||
get_string('includeauthdetailscanvas_desc', 'core_badges'),
|
||||
);
|
||||
$mform->hideIf('includeauthdetailscanvasdesc', 'includeauthdetailscanvas');
|
||||
$mform->hideIf('includeauthdetailscanvasdesc', 'region', 'eq', backpack_api::REGION_EMPTY);
|
||||
$mform->hideIf('includeauthdetailscanvasdesc', 'provider', 'ne', backpack_api::PROVIDER_CANVAS_CREDENTIALS);
|
||||
$mform->hideIf('includeauthdetailscanvasdesc', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
|
||||
// Email.
|
||||
$mform->addElement('text', 'backpackemailcanvas', get_string('issueremail', 'core_badges'));
|
||||
$mform->setType('backpackemailcanvas', PARAM_EMAIL);
|
||||
$mform->setDefault('backpackemailcanvas', $email);
|
||||
$mform->hideIf('backpackemailcanvas', 'includeauthdetailscanvas');
|
||||
$mform->hideIf('backpackemailcanvas', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
$mform->hideIf('backpackemailcanvas', 'provider', 'ne', backpack_api::PROVIDER_CANVAS_CREDENTIALS);
|
||||
|
||||
// Password.
|
||||
$mform->addElement('passwordunmask', 'backpackpasswordcanvas', get_string('password'));
|
||||
$mform->setType('backpackpasswordcanvas', PARAM_RAW);
|
||||
$mform->hideIf('backpackpasswordcanvas', 'includeauthdetailscanvas');
|
||||
$mform->hideIf('backpackpasswordcanvas', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
$mform->hideIf('backpackpasswordcanvas', 'provider', 'ne', backpack_api::PROVIDER_CANVAS_CREDENTIALS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add generic backpack issuer auth details.
|
||||
*
|
||||
* @param \stdClass|null $backpack The backpack instance.
|
||||
* @param string|null $email The issuer email or null if it's new.
|
||||
*/
|
||||
protected function add_connect_issuer_fields(?\stdClass $backpack, ?string $email): void {
|
||||
$mform = $this->_form;
|
||||
|
||||
// Checkbox and information to enable/disable issuer account.
|
||||
$mform->addElement(
|
||||
'advcheckbox',
|
||||
'includeauthdetails',
|
||||
null,
|
||||
'<strong>' . get_string('includeauthdetails', 'core_badges') . '</strong>',
|
||||
);
|
||||
if ($backpack && (!empty($backpack->backpackemail) || !empty($backpack->password))) {
|
||||
$mform->setDefault('includeauthdetails', 1);
|
||||
}
|
||||
$mform->addHelpButton('includeauthdetails', 'includeauthdetails', 'core_badges');
|
||||
$mform->hideIf('includeauthdetails', 'provider', 'eq', backpack_api::PROVIDER_CANVAS_CREDENTIALS);
|
||||
$mform->hideIf('includeauthdetails', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
|
||||
$mform->addElement('static', 'includeauthdetailsdesc', null, get_string('includeauthdetails_desc', 'core_badges'));
|
||||
$mform->hideIf('includeauthdetailsdesc', 'includeauthdetails');
|
||||
$mform->hideIf('includeauthdetailsdesc', 'provider', 'eq', backpack_api::PROVIDER_CANVAS_CREDENTIALS);
|
||||
|
||||
// Email and password fields.
|
||||
$this->add_auth_fields($email);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,25 +414,22 @@ class external_backpack extends \moodleform {
|
||||
*/
|
||||
protected function add_auth_fields(?string $email, bool $includepassword = true) {
|
||||
$mform = $this->_form;
|
||||
$emailstring = get_string('email');
|
||||
$passwordstring = get_string('password');
|
||||
$showpasswordhelp = false;
|
||||
if (!isset($this->_customdata['userbackpack'])) {
|
||||
$emailstring = get_string('defaultissuercontact', 'core_badges');
|
||||
$passwordstring = get_string('defaultissuerpassword', 'core_badges');
|
||||
$showpasswordhelp = true;
|
||||
}
|
||||
|
||||
$mform->addElement('text', 'backpackemail', $emailstring);
|
||||
// Email.
|
||||
$mform->addElement('text', 'backpackemail', get_string('issueremail', 'core_badges'));
|
||||
$mform->setType('backpackemail', PARAM_EMAIL);
|
||||
$mform->setDefault('backpackemail', $email);
|
||||
$mform->hideIf('backpackemail', 'includeauthdetails');
|
||||
$mform->hideIf('backpackemail', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
$mform->hideIf('backpackemail', 'provider', 'eq', backpack_api::PROVIDER_CANVAS_CREDENTIALS);
|
||||
|
||||
// Password.
|
||||
if ($includepassword) {
|
||||
$mform->addElement('passwordunmask', 'password', $passwordstring);
|
||||
$mform->addElement('passwordunmask', 'password', get_string('password'));
|
||||
$mform->setType('password', PARAM_RAW);
|
||||
if ($showpasswordhelp) {
|
||||
$mform->addHelpButton('password', 'defaultissuerpassword', 'badges');
|
||||
}
|
||||
}
|
||||
$mform->hideIf('password', 'includeauthdetails');
|
||||
$mform->hideIf('password', 'apiversion', 'ne', OPEN_BADGES_V2);
|
||||
$mform->hideIf('password', 'provider', 'eq', backpack_api::PROVIDER_CANVAS_CREDENTIALS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,9 @@ class manage_badge_action_bar extends base_action_bar {
|
||||
$elements['button'] = new single_button(new moodle_url('/badges/index.php', $params), get_string('back'), 'get');
|
||||
$badgenav = $this->generate_badge_navigation();
|
||||
if ($badgenav) {
|
||||
$elements['urlselect'] = new url_select($badgenav, $this->page->url->out(false), null);
|
||||
$badgenavselect = new url_select($badgenav, $this->page->url->out(false), null);
|
||||
$badgenavselect->set_label(get_string('badgesnavigation', 'badges'), ['class' => 'sr-only']);
|
||||
$elements['urlselect'] = $badgenavselect;
|
||||
}
|
||||
foreach ($elements as $key => $element) {
|
||||
$elements[$key] = $element->export_for_template($output);
|
||||
|
||||
@@ -130,6 +130,38 @@ class badge extends base {
|
||||
return html_writer::link($url, $row->name);
|
||||
});
|
||||
|
||||
// Name with image and link.
|
||||
$columns[] = (new column(
|
||||
'namewithimagelink',
|
||||
new lang_string('namewithimagelink', 'core_badges'),
|
||||
$this->get_entity_name()
|
||||
))
|
||||
->add_joins($this->get_joins())
|
||||
->add_join("LEFT JOIN {context} {$contextalias}
|
||||
ON {$contextalias}.contextlevel = " . CONTEXT_COURSE . "
|
||||
AND {$contextalias}.instanceid = {$badgealias}.courseid")
|
||||
->add_fields(
|
||||
"{$badgealias}.name, {$badgealias}.id, {$badgealias}.type, {$badgealias}.courseid"
|
||||
)
|
||||
->add_field($DB->sql_cast_to_char("{$badgealias}.imagecaption"), 'imagecaption')
|
||||
->add_fields(context_helper::get_preload_record_columns_sql($contextalias))
|
||||
->set_is_sortable(true)
|
||||
->add_callback(static function ($value, stdClass $badge): string {
|
||||
if ($badge->id === null) {
|
||||
return '';
|
||||
}
|
||||
if ($badge->type == BADGE_TYPE_SITE) {
|
||||
$context = context_system::instance();
|
||||
} else {
|
||||
context_helper::preload_from_record($badge);
|
||||
$context = context_course::instance($badge->courseid);
|
||||
}
|
||||
|
||||
$badgeimage = moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/', 'f2');
|
||||
$url = new moodle_url('/badges/overview.php', ['id' => $badge->id]);
|
||||
return html_writer::img($badgeimage, $badge->imagecaption) . ' ' . html_writer::link($url, $badge->name);
|
||||
});
|
||||
|
||||
// Description (note, this column contains plaintext so requires no post-processing).
|
||||
$descriptionfieldsql = "{$badgealias}.description";
|
||||
if ($DB->get_dbfamily() === 'oracle') {
|
||||
|
||||
@@ -94,8 +94,6 @@ class badges extends system_report {
|
||||
$this->add_filters();
|
||||
$this->add_actions();
|
||||
|
||||
// Set initial sorting by name.
|
||||
$this->set_initial_sort_column('badge:namewithlink', SORT_ASC);
|
||||
$this->set_default_no_results_notice(new lang_string('nomatchingbadges', 'core_badges'));
|
||||
|
||||
// Set if report can be downloaded.
|
||||
@@ -129,8 +127,7 @@ class badges extends system_report {
|
||||
*/
|
||||
public function add_columns(string $badgeissuedalias): void {
|
||||
$columns = [
|
||||
'badge:image',
|
||||
'badge:namewithlink',
|
||||
'badge:namewithimagelink',
|
||||
'badge:status',
|
||||
'badge:criteria',
|
||||
];
|
||||
@@ -138,15 +135,12 @@ class badges extends system_report {
|
||||
$canviewdraftbadges = $this->can_view_draft_badges();
|
||||
if (!$canviewdraftbadges) {
|
||||
// Remove status and recipients column.
|
||||
unset($columns[2]);
|
||||
unset($columns[1]);
|
||||
}
|
||||
$this->add_columns_from_entities($columns);
|
||||
|
||||
// Remove title from image column.
|
||||
$this->get_column('badge:image')->set_title(null);
|
||||
|
||||
// Change title from namewithlink column.
|
||||
$this->get_column('badge:namewithlink')->set_title(new lang_string('name'));
|
||||
// Change title of the `namewithimagelink` column to 'Name'.
|
||||
$this->get_column('badge:namewithimagelink')->set_title(new lang_string('name'));
|
||||
|
||||
// Recipients column.
|
||||
if ($canviewdraftbadges) {
|
||||
@@ -193,7 +187,7 @@ class badges extends system_report {
|
||||
return $OUTPUT->action_icon($badgeurl, $icon, null, null, true);
|
||||
});
|
||||
|
||||
$this->set_initial_sort_column('badge:namewithlink', SORT_ASC);
|
||||
$this->set_initial_sort_column('badge:namewithimagelink', SORT_ASC);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -51,7 +51,7 @@ class course_badges extends system_report {
|
||||
*/
|
||||
#[\core\attribute\deprecated('course_badges::initialise', since: '4.5', mdl: 'MDL-82503')]
|
||||
protected function initialise(): void {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
\core\deprecation::emit_deprecation([$this, __FUNCTION__]);
|
||||
global $USER;
|
||||
// Our main entity, it contains all of the column definitions that we need.
|
||||
$badgeentity = new badge();
|
||||
@@ -103,7 +103,7 @@ class course_badges extends system_report {
|
||||
*/
|
||||
#[\core\attribute\deprecated('course_badges::can_view', since: '4.5', mdl: 'MDL-82503')]
|
||||
protected function can_view(): bool {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
\core\deprecation::emit_deprecation([$this, __FUNCTION__]);
|
||||
return has_capability('moodle/badges:viewbadges', $this->get_context());
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ class course_badges extends system_report {
|
||||
*/
|
||||
#[\core\attribute\deprecated('course_badges::add_columns', since: '4.5', mdl: 'MDL-82503')]
|
||||
protected function add_columns(): void {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
\core\deprecation::emit_deprecation([$this, __FUNCTION__]);
|
||||
$badgeissuedalias = $this->get_entity('badge_issued')->get_table_alias('badge_issued');
|
||||
|
||||
$this->add_columns_from_entities([
|
||||
@@ -156,7 +156,7 @@ class course_badges extends system_report {
|
||||
*/
|
||||
#[\core\attribute\deprecated('course_badges::add_filters', since: '4.5', mdl: 'MDL-82503')]
|
||||
protected function add_filters(): void {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
\core\deprecation::emit_deprecation([$this, __FUNCTION__]);
|
||||
$this->add_filters_from_entities([
|
||||
'badge:name',
|
||||
'badge_issued:issued',
|
||||
|
||||
+40
-13
@@ -151,12 +151,17 @@ class core_badges_renderer extends plugin_renderer_base {
|
||||
'value' => $this->output->larrow() . ' ' . get_string('award', 'badges'),
|
||||
'class' => 'actionbutton btn btn-secondary')
|
||||
);
|
||||
$actioncell->text .= html_writer::empty_tag('input', array(
|
||||
if (has_capability('moodle/badges:revokebadge', $this->page->context)) {
|
||||
$actioncell->text .= html_writer::empty_tag(
|
||||
'input',
|
||||
[
|
||||
'type' => 'submit',
|
||||
'name' => 'revoke',
|
||||
'value' => get_string('revoke', 'badges') . ' ' . $this->output->rarrow(),
|
||||
'class' => 'actionbutton btn btn-secondary')
|
||||
);
|
||||
'class' => 'actionbutton btn btn-secondary',
|
||||
]
|
||||
);
|
||||
}
|
||||
$actioncell->text .= html_writer::end_tag('div', array());
|
||||
$actioncell->attributes['class'] = 'actions';
|
||||
$potentialcell = new html_table_cell();
|
||||
@@ -187,10 +192,22 @@ class core_badges_renderer extends plugin_renderer_base {
|
||||
$dl[get_string('createdon', 'search')] = userdate($badge->timecreated);
|
||||
$dl[get_string('badgeimage', 'badges')] = print_badge_image($badge, $context, 'large');
|
||||
$dl[get_string('imageauthorname', 'badges')] = $badge->imageauthorname;
|
||||
$dl[get_string('imageauthoremail', 'badges')] =
|
||||
html_writer::tag('a', $badge->imageauthoremail, array('href' => 'mailto:' . $badge->imageauthoremail));
|
||||
$dl[get_string('imageauthorurl', 'badges')] =
|
||||
html_writer::link($badge->imageauthorurl, $badge->imageauthorurl, array('target' => '_blank'));
|
||||
$dl[get_string('imageauthoremail', 'badges')] = '';
|
||||
if (trim($badge->imageauthoremail)) {
|
||||
$dl[get_string('imageauthoremail', 'badges')] = html_writer::tag(
|
||||
'a',
|
||||
$badge->imageauthoremail,
|
||||
['href' => 'mailto:' . $badge->imageauthoremail],
|
||||
);
|
||||
}
|
||||
$dl[get_string('imageauthorurl', 'badges')] = '';
|
||||
if (trim($badge->imageauthorurl)) {
|
||||
$dl[get_string('imageauthorurl', 'badges')] = html_writer::link(
|
||||
$badge->imageauthorurl,
|
||||
$badge->imageauthorurl,
|
||||
['target' => '_blank'],
|
||||
);
|
||||
}
|
||||
$dl[get_string('imagecaption', 'badges')] = $badge->imagecaption;
|
||||
$tags = \core_tag_tag::get_item_tags('core_badges', 'badge', $badge->id);
|
||||
$dl[get_string('tags', 'badges')] = $this->output->tag_list($tags, '');
|
||||
@@ -200,12 +217,22 @@ class core_badges_renderer extends plugin_renderer_base {
|
||||
$display .= $this->heading(get_string('issuerdetails', 'badges'), 3);
|
||||
$dl = array();
|
||||
$dl[get_string('issuername', 'badges')] = $badge->issuername;
|
||||
$dl[get_string('contact', 'badges')] = html_writer::tag('a', $badge->issuercontact, array('href' => 'mailto:' . $badge->issuercontact));
|
||||
$dl[get_string('issuerurl', 'badges')] = html_writer::tag(
|
||||
'a',
|
||||
$badge->issuerurl,
|
||||
['href' => $badge->issuerurl, 'target' => '_blank'],
|
||||
);
|
||||
$dl[get_string('contact', 'badges')] = '';
|
||||
if (trim($badge->issuercontact)) {
|
||||
$dl[get_string('contact', 'badges')] = html_writer::tag(
|
||||
'a',
|
||||
$badge->issuercontact,
|
||||
['href' => 'mailto:' . $badge->issuercontact],
|
||||
);
|
||||
}
|
||||
$dl[get_string('issuerurl', 'badges')] = '';
|
||||
if (trim($badge->issuerurl)) {
|
||||
$dl[get_string('issuerurl', 'badges')] = html_writer::tag(
|
||||
'a',
|
||||
$badge->issuerurl,
|
||||
['href' => $badge->issuerurl, 'target' => '_blank'],
|
||||
);
|
||||
}
|
||||
$display .= $this->definition_list($dl);
|
||||
|
||||
// Issuance details if any.
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
<?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 core_badges;
|
||||
|
||||
/**
|
||||
* Unit tests for backpack_api class.
|
||||
*
|
||||
* @package core_badges
|
||||
* @covers \core_badges\backpack_api
|
||||
* @copyright 2025 Sara Arjona <sara@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
final class backpack_api_test extends \advanced_testcase {
|
||||
/**
|
||||
* Test get_providers function.
|
||||
*/
|
||||
public function test_get_providers(): void {
|
||||
global $CFG;
|
||||
|
||||
$providers = backpack_api::get_providers();
|
||||
$this->assertCount(2, $providers);
|
||||
$this->assertArrayHasKey(backpack_api::PROVIDER_CANVAS_CREDENTIALS, $providers);
|
||||
$this->assertArrayHasKey(backpack_api::PROVIDER_OTHER, $providers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_regions function.
|
||||
*/
|
||||
public function test_get_regions(): void {
|
||||
global $CFG;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Default: 5 regions (Canvas Credentials).
|
||||
$regions = backpack_api::get_regions();
|
||||
$this->assertCount(5, $regions);
|
||||
|
||||
// No regions.
|
||||
$CFG->badges_canvasregions = '';
|
||||
$regions = backpack_api::get_regions();
|
||||
$this->assertEmpty($regions);
|
||||
|
||||
// One region.
|
||||
$CFG->badges_canvasregions = 'Australia|https://au.badgr.io|https://api.au.badgr.io/v2';
|
||||
$regions = backpack_api::get_regions();
|
||||
$this->assertCount(1, $regions);
|
||||
$this->assertEquals('Australia', $regions[0]['name']);
|
||||
$this->assertEquals('https://au.badgr.io', $regions[0]['url']);
|
||||
$this->assertEquals('https://api.au.badgr.io/v2', $regions[0]['apiurl']);
|
||||
|
||||
// Two regions + empty lines + invalid line.
|
||||
$CFG->badges_canvasregions = "\nUnited States|https://badgr.io|https://api.badgr.io/v2\ninvalidline\n" .
|
||||
'Europe|https://eu.badgr.io|https://api.eu.badgr.io/v2' . "\n";
|
||||
$regions = backpack_api::get_regions();
|
||||
$this->assertCount(2, $regions);
|
||||
$expected = [
|
||||
[
|
||||
'name' => 'United States',
|
||||
'url' => 'https://badgr.io',
|
||||
'apiurl' => 'https://api.badgr.io/v2',
|
||||
],
|
||||
[
|
||||
'name' => 'Europe',
|
||||
'url' => 'https://eu.badgr.io',
|
||||
'apiurl' => 'https://api.eu.badgr.io/v2',
|
||||
],
|
||||
];
|
||||
$this->assertEquals($expected, $regions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test display_canvas_credentials_fields function.
|
||||
*/
|
||||
public function test_display_canvas_credentials_fields(): void {
|
||||
global $CFG;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// By default, the fields should be displayed (5 regions).
|
||||
$this->assertTrue(backpack_api::display_canvas_credentials_fields());
|
||||
|
||||
// No regions configured, fields should not be displayed.
|
||||
$CFG->badges_canvasregions = '';
|
||||
$this->assertFalse(backpack_api::display_canvas_credentials_fields());
|
||||
|
||||
// One region configured, fields should be displayed.
|
||||
$CFG->badges_canvasregions = 'Australia|https://au.badgr.io|https://api.au.badgr.io/v2';
|
||||
$this->assertTrue(backpack_api::display_canvas_credentials_fields());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_region_url and get_region_api_url functions.
|
||||
*/
|
||||
public function test_get_region_urls(): void {
|
||||
global $CFG;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Default: 5 regions (Canvas Credentials).
|
||||
$regions = backpack_api::get_regions();
|
||||
$this->assertCount(5, $regions);
|
||||
$this->assertEquals('https://au.badgr.io', backpack_api::get_region_url(0));
|
||||
$this->assertEquals('https://ca.badgr.io', backpack_api::get_region_url(1));
|
||||
$this->assertEquals('https://eu.badgr.io', backpack_api::get_region_url(2));
|
||||
$this->assertEquals('https://sg.badgr.io', backpack_api::get_region_url(3));
|
||||
$this->assertEquals('https://badgr.io', backpack_api::get_region_url(4));
|
||||
$this->assertEquals('https://api.au.badgr.io/v2', backpack_api::get_region_api_url(0));
|
||||
$this->assertEquals('https://api.ca.badgr.io/v2', backpack_api::get_region_api_url(1));
|
||||
$this->assertEquals('https://api.eu.badgr.io/v2', backpack_api::get_region_api_url(2));
|
||||
$this->assertEquals('https://api.sg.badgr.io/v2', backpack_api::get_region_api_url(3));
|
||||
$this->assertEquals('https://api.badgr.io/v2', backpack_api::get_region_api_url(4));
|
||||
|
||||
// Wrong index.
|
||||
$this->assertNull(backpack_api::get_region_url(10));
|
||||
$this->assertNull(backpack_api::get_region_api_url(10));
|
||||
|
||||
// No regions.
|
||||
$CFG->badges_canvasregions = '';
|
||||
$this->assertNull(backpack_api::get_region_url(0));
|
||||
$this->assertNull(backpack_api::get_region_api_url(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_regionid_from_url function.
|
||||
*/
|
||||
public function test_get_regionid_from_url(): void {
|
||||
global $CFG;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Default: 5 regions (Canvas Credentials).
|
||||
$regions = backpack_api::get_regions();
|
||||
$this->assertCount(5, $regions);
|
||||
$this->assertEquals(0, backpack_api::get_regionid_from_url('https://au.badgr.io'));
|
||||
$this->assertEquals(1, backpack_api::get_regionid_from_url('https://ca.badgr.io'));
|
||||
$this->assertEquals(2, backpack_api::get_regionid_from_url('https://eu.badgr.io'));
|
||||
$this->assertEquals(3, backpack_api::get_regionid_from_url('https://sg.badgr.io'));
|
||||
$this->assertEquals(4, backpack_api::get_regionid_from_url('https://badgr.io'));
|
||||
// Test with trailing slash.
|
||||
$this->assertEquals(0, backpack_api::get_regionid_from_url('https://au.badgr.io/'));
|
||||
|
||||
// Wrong URL.
|
||||
$this->assertEquals(4, backpack_api::get_regionid_from_url('https://unknown.badgr.io'));
|
||||
|
||||
// One region.
|
||||
$CFG->badges_canvasregions = 'Australia|https://au.badgr.io|https://api.au.badgr.io/v2';
|
||||
$regions = backpack_api::get_regions();
|
||||
$this->assertEquals(0, backpack_api::get_regionid_from_url('https://au.badgr.io'));
|
||||
|
||||
// No regions.
|
||||
$CFG->badges_canvasregions = '';
|
||||
$this->assertEquals(backpack_api::REGION_EMPTY, backpack_api::get_regionid_from_url('https://au.badgr.io'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test is_canvas_credentials_region function.
|
||||
*/
|
||||
public function test_is_canvas_credentials_region(): void {
|
||||
global $CFG;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Default: 5 regions (Canvas Credentials).
|
||||
$regions = backpack_api::get_regions();
|
||||
$this->assertCount(5, $regions);
|
||||
$this->assertTrue(backpack_api::is_canvas_credentials_region('https://au.badgr.io'));
|
||||
$this->assertTrue(backpack_api::is_canvas_credentials_region('https://ca.badgr.io'));
|
||||
$this->assertTrue(backpack_api::is_canvas_credentials_region('https://eu.badgr.io'));
|
||||
$this->assertTrue(backpack_api::is_canvas_credentials_region('https://sg.badgr.io'));
|
||||
$this->assertTrue(backpack_api::is_canvas_credentials_region('https://badgr.io'));
|
||||
|
||||
// Non Canvas URL.
|
||||
$this->assertFalse(backpack_api::is_canvas_credentials_region('https://unknown.badgr.io'));
|
||||
|
||||
// No regions.
|
||||
$CFG->badges_canvasregions = '';
|
||||
$this->assertFalse(backpack_api::is_canvas_credentials_region('https://au.badgr.io'));
|
||||
}
|
||||
}
|
||||
@@ -93,12 +93,111 @@ Feature: Backpack badges
|
||||
And "Add to backpack" "link" should exist
|
||||
|
||||
@javascript
|
||||
Scenario: Add a new site backpack
|
||||
Scenario: Add a new site OBv2.1 backpack
|
||||
Given I am on homepage
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Manage backpacks" in site administration
|
||||
When I press "Add a new backpack"
|
||||
And I set the field "apiversion" to "2.1"
|
||||
And I should see "Backpack URL"
|
||||
And I set the field "backpackweburlv2p1" to "http://backpackweburl.cat"
|
||||
And I should not see "Backpack API URL"
|
||||
Then "Connect to backpack provider account" "checkbox" should not be visible
|
||||
And "Connect to a Canvas Credentials issuer account" "checkbox" should not be visible
|
||||
And I should not see "Email"
|
||||
And I should not see "Password"
|
||||
|
||||
@javascript
|
||||
Scenario: Add a new site OBv2.0 backpack with Canvas provider
|
||||
Given I am on homepage
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Manage backpacks" in site administration
|
||||
When I press "Add a new backpack"
|
||||
And I set the field "apiversion" to "2"
|
||||
And I press "Save changes"
|
||||
And I should see "You must supply a value here"
|
||||
And I set the field "provider" to "Canvas Credentials"
|
||||
And I press "Save changes"
|
||||
And I should see "You must supply a value here"
|
||||
And I set the field "region" to "Singapore"
|
||||
And I should not see "Backpack web URL"
|
||||
And I should not see "Backpack API URL"
|
||||
And I press "Save changes"
|
||||
Then I should see "https://sg.badgr.io"
|
||||
And "Delete" "icon" should exist in the "https://sg.badgr.io" "table_row"
|
||||
And "Edit settings" "icon" should exist in the "https://sg.badgr.io" "table_row"
|
||||
And "Test settings" "icon" should exist in the "https://sg.badgr.io" "table_row"
|
||||
# Check that editing the backpack shows the correct values.
|
||||
And I click on "Edit settings" "link" in the "https://sg.badgr.io" "table_row"
|
||||
And I should see "API version supported"
|
||||
And the field "apiversion" matches value "2"
|
||||
And I should see "Provider"
|
||||
And the field "provider" matches value "Canvas Credentials"
|
||||
And I should see "Region"
|
||||
And the field "region" matches value "Singapore"
|
||||
And I should see "Connect to a Canvas Credentials issuer account"
|
||||
And the field "Connect to a Canvas Credentials issuer account" matches value "0"
|
||||
And I should not see "Connect to backpack provider account"
|
||||
|
||||
@javascript
|
||||
Scenario: Add a new site OBv2.0 backpack with Canvas provider and issuer authentication details
|
||||
Given I am on homepage
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Manage backpacks" in site administration
|
||||
When I press "Add a new backpack"
|
||||
And I set the field "apiversion" to "2"
|
||||
And I set the field "provider" to "Canvas Credentials"
|
||||
And I set the field "region" to "Canada"
|
||||
And I should see "Connect to a Canvas Credentials issuer account"
|
||||
And I should not see "Connect to backpack provider account"
|
||||
And the field "Connect to a Canvas Credentials issuer account" matches value "0"
|
||||
And I click on "includeauthdetailscanvas" "checkbox"
|
||||
And I should see "Email"
|
||||
And I should see "Password"
|
||||
And I press "Save changes"
|
||||
And I should see "You must supply a value here"
|
||||
And I set the field "backpackemailcanvas" to "test@test.com"
|
||||
And I should see "You must supply a value here"
|
||||
And I press "Save changes"
|
||||
And I set the field "backpackpasswordcanvas" to "123456"
|
||||
And I press "Save changes"
|
||||
Then I should see "https://ca.badgr.io"
|
||||
# Check that editing the backpack shows the correct values.
|
||||
And I click on "Edit settings" "link" in the "https://ca.badgr.io" "table_row"
|
||||
And I should see "API version supported"
|
||||
And the field "apiversion" matches value "2"
|
||||
And I should see "Provider"
|
||||
And the field "provider" matches value "Canvas Credentials"
|
||||
And I should see "Region"
|
||||
And the field "region" matches value "Canada"
|
||||
And I should see "Connect to a Canvas Credentials issuer account"
|
||||
And the field "Connect to a Canvas Credentials issuer account" matches value "1"
|
||||
And the field "backpackemailcanvas" matches value "test@test.com"
|
||||
And the field "backpackpasswordcanvas" matches value "123456"
|
||||
# Disable authentication details and check that email and password are cleared.
|
||||
But I click on "includeauthdetailscanvas" "checkbox"
|
||||
And I press "Save changes"
|
||||
And I click on "Edit settings" "link" in the "https://ca.badgr.io" "table_row"
|
||||
And I should see "API version supported"
|
||||
And the field "apiversion" matches value "2"
|
||||
And I should see "Provider"
|
||||
And the field "provider" matches value "Canvas Credentials"
|
||||
And I should see "Region"
|
||||
And the field "region" matches value "Canada"
|
||||
And I should see "Connect to a Canvas Credentials issuer account"
|
||||
And the field "Connect to a Canvas Credentials issuer account" matches value "0"
|
||||
And the field "backpackemailcanvas" matches value ""
|
||||
And the field "backpackpasswordcanvas" matches value ""
|
||||
|
||||
@javascript
|
||||
Scenario: Add a new site OBv2.0 backpack with Other provider
|
||||
Given I am on homepage
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Manage backpacks" in site administration
|
||||
When I press "Add a new backpack"
|
||||
And I set the field "apiversion" to "2"
|
||||
And I set the field "provider" to "Other"
|
||||
And I should not see "Region"
|
||||
And I set the field "backpackweburl" to "aaa"
|
||||
And I press "Save changes"
|
||||
And I should see "Invalid URL"
|
||||
@@ -110,6 +209,115 @@ Feature: Backpack badges
|
||||
Then I should see "http://backpackweburl.cat"
|
||||
And "Delete" "icon" should exist in the "http://backpackweburl.cat" "table_row"
|
||||
And "Edit settings" "icon" should exist in the "http://backpackweburl.cat" "table_row"
|
||||
And "Test settings" "icon" should exist in the "http://backpackweburl.cat" "table_row"
|
||||
# Check that editing the backpack shows the correct values.
|
||||
And I click on "Edit settings" "link" in the "http://backpackweburl.cat" "table_row"
|
||||
And I should see "API version supported"
|
||||
And the field "apiversion" matches value "2"
|
||||
And I should see "Provider"
|
||||
And the field "provider" matches value "Other"
|
||||
And I should not see "Region"
|
||||
And the field "backpackweburl" matches value "http://backpackweburl.cat"
|
||||
And the field "backpackapiurl" matches value "http://backpackapiurl.cat"
|
||||
And the field "Connect to backpack provider account" matches value "0"
|
||||
And I should not see "Connect to a Canvas Credentials issuer account"
|
||||
|
||||
@javascript
|
||||
Scenario: Add a new site OBv2.0 backpack with Other provider and issuer authentication details
|
||||
Given I am on homepage
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Manage backpacks" in site administration
|
||||
When I press "Add a new backpack"
|
||||
And I set the field "apiversion" to "2"
|
||||
And I set the field "provider" to "Other"
|
||||
And I set the field "backpackweburl" to "http://backpackweburl.cat"
|
||||
And I set the field "backpackapiurl" to "http://backpackapiurl.cat"
|
||||
And I should see "Connect to backpack provider account"
|
||||
And I should not see "Connect to a Canvas Credentials issuer account"
|
||||
And the field "Connect to backpack provider account" matches value "0"
|
||||
And I click on "includeauthdetails" "checkbox"
|
||||
And I should see "Email"
|
||||
And I should see "Password"
|
||||
And I press "Save changes"
|
||||
And I should see "You must supply a value here"
|
||||
And I set the field "backpackemail" to "test@test.com"
|
||||
And I press "Save changes"
|
||||
And I should see "You must supply a value here"
|
||||
And I set the field "password" to "123456"
|
||||
And I press "Save changes"
|
||||
Then I should see "http://backpackweburl.cat"
|
||||
# Check that editing the backpack shows the correct values.
|
||||
And I click on "Edit settings" "link" in the "http://backpackweburl.cat" "table_row"
|
||||
And I should see "API version supported"
|
||||
And the field "apiversion" matches value "2"
|
||||
And the field "provider" matches value "Other"
|
||||
And the field "backpackweburl" matches value "http://backpackweburl.cat"
|
||||
And the field "backpackapiurl" matches value "http://backpackapiurl.cat"
|
||||
And the field "Connect to backpack provider account" matches value "1"
|
||||
And the field "backpackemail" matches value "test@test.com"
|
||||
And the field "password" matches value "123456"
|
||||
# Disable authentication details and check that email and password are cleared.
|
||||
But I click on "includeauthdetails" "checkbox"
|
||||
And I press "Save changes"
|
||||
And I click on "Edit settings" "link" in the "http://backpackweburl.cat" "table_row"
|
||||
And I should see "API version supported"
|
||||
And the field "apiversion" matches value "2"
|
||||
And the field "provider" matches value "Other"
|
||||
And the field "backpackweburl" matches value "http://backpackweburl.cat"
|
||||
And the field "backpackapiurl" matches value "http://backpackapiurl.cat"
|
||||
And the field "Connect to backpack provider account" matches value "0"
|
||||
And the field "backpackemail" matches value ""
|
||||
And the field "password" matches value ""
|
||||
|
||||
@javascript
|
||||
Scenario: Add a new site OBv2.0 backpack without providers
|
||||
Given the following config values are set as admin:
|
||||
| badges_canvasregions | |
|
||||
And I am on homepage
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Manage backpacks" in site administration
|
||||
When I press "Add a new backpack"
|
||||
And I set the field "apiversion" to "2"
|
||||
And I should not see "Provider"
|
||||
And I should see "Backpack URL"
|
||||
And I should see "Backpack API URL"
|
||||
And I press "Save changes"
|
||||
And I should see "You must supply a value here"
|
||||
And I set the field "backpackweburl" to "https://eu.badgr.io"
|
||||
And I set the field "backpackapiurl" to "https://api.eu.badgr.io/v2"
|
||||
And I should see "Connect to backpack provider account"
|
||||
And I should not see "Connect to a Canvas Credentials issuer account"
|
||||
And I press "Save changes"
|
||||
Then I should see "https://eu.badgr.io"
|
||||
And "Delete" "icon" should exist in the "https://eu.badgr.io" "table_row"
|
||||
And "Edit settings" "icon" should exist in the "https://eu.badgr.io" "table_row"
|
||||
And "Test settings" "icon" should exist in the "https://eu.badgr.io" "table_row"
|
||||
# Check that editing the backpack shows the correct values.
|
||||
And I click on "Edit settings" "link" in the "https://eu.badgr.io" "table_row"
|
||||
And I should see "API version supported"
|
||||
And the field "apiversion" matches value "2"
|
||||
And I should not see "Provider"
|
||||
And the field "backpackweburl" matches value "https://eu.badgr.io"
|
||||
And the field "backpackapiurl" matches value "https://api.eu.badgr.io/v2"
|
||||
And I should see "Connect to backpack provider account"
|
||||
And the field "Connect to backpack provider account" matches value "0"
|
||||
And I should not see "Connect to a Canvas Credentials issuer account"
|
||||
And I press "Cancel"
|
||||
# Add Europe to the providers list and check that editing the backpack shows the correct values.
|
||||
But the following config values are set as admin:
|
||||
| badges_canvasregions | Europe\|https://eu.badgr.io\|https://api.eu.badgr.io/v2 |
|
||||
And I click on "Edit settings" "link" in the "https://eu.badgr.io" "table_row"
|
||||
And I should see "API version supported"
|
||||
And the field "apiversion" matches value "2"
|
||||
And I should see "Provider"
|
||||
And the field "provider" matches value "Canvas Credentials"
|
||||
And I should see "Region"
|
||||
And the field "region" matches value "Europe"
|
||||
And I should see "Connect to a Canvas Credentials issuer account"
|
||||
And the field "Connect to a Canvas Credentials issuer account" matches value "0"
|
||||
And I should not see "Connect to backpack provider account"
|
||||
And I should not see "Backpack URL"
|
||||
And I should not see "Backpack API URL"
|
||||
|
||||
@javascript
|
||||
Scenario: Remove a site backpack
|
||||
@@ -138,36 +346,6 @@ Feature: Backpack badges
|
||||
And "Move up" "icon" should exist in the "https://dc.imsglobal.org" "table_row"
|
||||
And "Move down" "icon" should not exist in the "https://dc.imsglobal.org" "table_row"
|
||||
|
||||
@javascript
|
||||
Scenario: Add a new site backpack with authentication details checkbox
|
||||
Given I am on homepage
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Manage backpacks" in site administration
|
||||
When I press "Add a new backpack"
|
||||
And I set the field "apiversion" to "2.1"
|
||||
And I set the field "backpackweburl" to "http://backpackweburl.cat"
|
||||
And I should not see "Backpack API URL"
|
||||
Then "Include authentication details with the backpack" "checkbox" should not be visible
|
||||
And I should not see "Badge issuer email address"
|
||||
And I should not see "Badge issuer password"
|
||||
And I set the field "apiversion" to "2"
|
||||
And "Include authentication details with the backpack" "checkbox" should be visible
|
||||
And I click on "includeauthdetails" "checkbox"
|
||||
And I should see "Badge issuer email address"
|
||||
And I should see "Badge issuer password"
|
||||
And I set the field "backpackemail" to "test@test.com"
|
||||
And I set the field "password" to "123456"
|
||||
And I set the field "backpackapiurl" to "http://backpackapiurl.cat"
|
||||
And I press "Save changes"
|
||||
And I click on "Edit" "link" in the "http://backpackweburl.cat" "table_row"
|
||||
And the field "Include authentication details with the backpack" matches value "1"
|
||||
And I click on "includeauthdetails" "checkbox"
|
||||
And I press "Save changes"
|
||||
And I click on "Edit" "link" in the "http://backpackweburl.cat" "table_row"
|
||||
And the field "Include authentication details with the backpack" matches value "0"
|
||||
And I click on "includeauthdetails" "checkbox"
|
||||
And I should not see "test@test.com"
|
||||
|
||||
@javascript
|
||||
Scenario: View backpack form as a student
|
||||
Given I log in as "student1"
|
||||
@@ -175,11 +353,11 @@ Feature: Backpack badges
|
||||
And I follow "Backpack settings"
|
||||
When I set the field "externalbackpackid" to "https://dc.imsglobal.org"
|
||||
Then I should not see "Log in to your backpack"
|
||||
And I should not see "Email address"
|
||||
And I should not see "Email"
|
||||
And I should not see "Password"
|
||||
But I set the field "externalbackpackid" to "https://test.com/"
|
||||
And I should see "Log in to your backpack"
|
||||
And I should see "Email address"
|
||||
And I should see "Email"
|
||||
And I should see "Password"
|
||||
|
||||
@javascript
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
@core @core_badges
|
||||
Feature: Badge overview
|
||||
In order to view a badge's information
|
||||
As an user with the capability to view badges
|
||||
I need to access the badge overview page
|
||||
|
||||
Background:
|
||||
Given the following config values are set as admin:
|
||||
| badges_defaultissuername | Test Badge Site |
|
||||
|
||||
@javascript @accessibility @_file_upload
|
||||
Scenario: Test accessibility of badge overview page
|
||||
Given 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 | Cool badge |
|
||||
| Description | Badge for cool people |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
When I select "Overview" from the "Badges navigation" singleselect
|
||||
Then the "region-main" "region" should meet accessibility standards with "best-practice" extra tests
|
||||
@@ -118,6 +118,7 @@ Feature: Manage badges
|
||||
| Admin User |
|
||||
| User One |
|
||||
|
||||
@accessibility
|
||||
Scenario: View list of badges with recipients
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname |
|
||||
@@ -139,6 +140,7 @@ Feature: Manage badges
|
||||
| Badge #1 | Not available | 2 |
|
||||
| Badge #2 | Available | 1 |
|
||||
| Badge #3 | Available | 0 |
|
||||
And the "Badges" "table" should meet accessibility standards with "best-practice" extra tests
|
||||
|
||||
@_file_upload
|
||||
Scenario: Badge names are not unique anymore
|
||||
|
||||
@@ -24,6 +24,7 @@ Feature: Display badges
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
|
||||
@accessibility
|
||||
Scenario: Display badge without expired date
|
||||
# Enable the badge.
|
||||
Given I press "Enable access" action in the "Testing system badge" report row
|
||||
@@ -44,6 +45,7 @@ Feature: Display badges
|
||||
And I follow "More details"
|
||||
And I should see "Catalan"
|
||||
And I should see "1.1"
|
||||
And the page should meet accessibility standards with "best-practice" extra tests
|
||||
|
||||
Scenario: Display badge with ALL criteria
|
||||
# Add another criterion and enable the badge.
|
||||
|
||||
@@ -125,6 +125,7 @@ final class badges_test extends core_reportbuilder_testcase {
|
||||
$generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname', 'sortenabled' => 1]);
|
||||
|
||||
$generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'badge:namewithlink']);
|
||||
$generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'badge:namewithimagelink']);
|
||||
$generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'badge:criteria']);
|
||||
$generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'badge:image']);
|
||||
$generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'badge:language']);
|
||||
@@ -146,11 +147,12 @@ final class badges_test extends core_reportbuilder_testcase {
|
||||
['id' => $badgeone->id]), ($badgeone->name));
|
||||
|
||||
// First badge, issued to user one.
|
||||
[$badgename, $fullname, $namewithlink, $criteria, $image, $language, $version, $status, $expiry, $tag, $expires,
|
||||
$visible, $coursename] = array_values($content[0]);
|
||||
[$badgename, $fullname, $namewithlink, $namewithimagelink, $criteria, $image, $language, $version, $status, $expiry, $tag,
|
||||
$expires, $visible, $coursename] = array_values($content[0]);
|
||||
$this->assertEquals($badgeone->name, $badgename);
|
||||
$this->assertEquals(fullname($user1), $fullname);
|
||||
$this->assertEquals($expectedbadgeonelink, $namewithlink);
|
||||
$this->assertEquals($image . ' ' . $expectedbadgeonelink, $namewithimagelink);
|
||||
$this->assertStringContainsString('Awarded by: Manager', $criteria);
|
||||
$this->assertStringContainsString('Image caption', $image);
|
||||
$this->assertEquals('German', $language);
|
||||
@@ -163,11 +165,12 @@ final class badges_test extends core_reportbuilder_testcase {
|
||||
$this->assertEquals('PHPUnit test site', $coursename);
|
||||
|
||||
// First badge, issued to user two.
|
||||
[$badgename, $fullname, $namewithlink, $criteria, $image, $language, $version, $status, $expiry, $tag, $expires,
|
||||
$visible, $coursename] = array_values($content[1]);
|
||||
[$badgename, $fullname, $namewithlink, $namewithimagelink, $criteria, $image, $language, $version, $status, $expiry, $tag,
|
||||
$expires, $visible, $coursename] = array_values($content[1]);
|
||||
$this->assertEquals($badgeone->name, $badgename);
|
||||
$this->assertEquals(fullname($user2), $fullname);
|
||||
$this->assertEquals($expectedbadgeonelink, $namewithlink);
|
||||
$this->assertEquals($image . ' ' . $expectedbadgeonelink, $namewithimagelink);
|
||||
$this->assertStringContainsString('Awarded by: Manager', $criteria);
|
||||
$this->assertStringContainsString('Image caption', $image);
|
||||
$this->assertEquals('German', $language);
|
||||
@@ -183,11 +186,12 @@ final class badges_test extends core_reportbuilder_testcase {
|
||||
['id' => $badgetwo->id]), ($badgetwo->name));
|
||||
|
||||
// Course badge, not issues to any users.
|
||||
[$badgename, $fullname, $namewithlink, $criteria, $image, $language, $version, $status, $expiry, $tag, $expires,
|
||||
$visible, $coursename] = array_values($content[2]);
|
||||
[$badgename, $fullname, $namewithlink, $namewithimagelink, $criteria, $image, $language, $version, $status, $expiry, $tag,
|
||||
$expires, $visible, $coursename] = array_values($content[2]);
|
||||
$this->assertEquals($badgetwo->name, $badgename);
|
||||
$this->assertEmpty($fullname);
|
||||
$this->assertEquals($expectedbadgetwolink, $namewithlink);
|
||||
$this->assertEquals($image . ' ' . $expectedbadgetwolink, $namewithimagelink);
|
||||
$this->assertEquals('Criteria for this badge have not been set up yet.', $criteria);
|
||||
$this->assertStringContainsString('Image caption', $image);
|
||||
$this->assertEquals('English', $language);
|
||||
|
||||
@@ -22,9 +22,9 @@ Feature: Block accessreview
|
||||
When I add the "Accessibility Review" block
|
||||
Then I should see "Accessibility Review"
|
||||
And I should see "Your accessibility toolkit needs to be registered."
|
||||
And I click on "Actions menu" "icon" in the "Accessibility Review" "block"
|
||||
And I click on "Actions menu" "menuitem" in the "Accessibility Review" "block"
|
||||
And I follow "Hide Accessibility Review block"
|
||||
And I should not see "Your accessibility toolkit needs to be registered."
|
||||
And I click on "Actions menu" "icon" in the "Accessibility Review" "block"
|
||||
And I click on "Actions menu" "menuitem" in the "Accessibility Review" "block"
|
||||
And I follow "Show Accessibility Review block"
|
||||
And I should see "Your accessibility toolkit needs to be registered."
|
||||
|
||||
@@ -24,9 +24,9 @@ Feature: Block accessreview
|
||||
When I add the "Accessibility review" block
|
||||
Then I should see "Accessibility review"
|
||||
And I should see "Your accessibility toolkit needs to be registered."
|
||||
And I click on "Actions menu" "icon" in the "Accessibility review" "block"
|
||||
And I click on "Actions menu" "menuitem" in the "Accessibility review" "block"
|
||||
And I follow "Hide Accessibility review block"
|
||||
And I should not see "Your accessibility toolkit needs to be registered."
|
||||
And I click on "Actions menu" "icon" in the "Accessibility review" "block"
|
||||
And I click on "Actions menu" "menuitem" in the "Accessibility review" "block"
|
||||
And I follow "Show Accessibility review block"
|
||||
And I should see "Your accessibility toolkit needs to be registered."
|
||||
|
||||
+9
-4
@@ -32,7 +32,9 @@
|
||||
* @todo MDL-36050 improve capability check on stick blocks, so we can check user capability before sending images.
|
||||
*/
|
||||
function block_html_pluginfile($course, $birecord_or_cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
|
||||
global $DB, $CFG, $USER;
|
||||
global $CFG;
|
||||
|
||||
require_once("{$CFG->dirroot}/user/lib.php");
|
||||
|
||||
if ($context->contextlevel != CONTEXT_BLOCK) {
|
||||
send_file_not_found();
|
||||
@@ -51,9 +53,12 @@ function block_html_pluginfile($course, $birecord_or_cm, $context, $filearea, $a
|
||||
if (!core_course_category::get($parentcontext->instanceid, IGNORE_MISSING)) {
|
||||
send_file_not_found();
|
||||
}
|
||||
} else if ($parentcontext->contextlevel === CONTEXT_USER && $parentcontext->instanceid != $USER->id) {
|
||||
// The block is in the context of a user, it is only visible to the user who it belongs to.
|
||||
send_file_not_found();
|
||||
} else if ($parentcontext->contextlevel === CONTEXT_USER) {
|
||||
$user = core_user::get_user($parentcontext->instanceid, '*', MUST_EXIST);
|
||||
$extracaps = block_method_result('html', 'get_extra_capabilities');
|
||||
if (!user_can_view_profile($user, null, $parentcontext) || !has_any_capability($extracaps, $context)) {
|
||||
send_file_not_found();
|
||||
}
|
||||
}
|
||||
// At this point there is no way to check SYSTEM context, so ignoring it.
|
||||
}
|
||||
|
||||
@@ -297,6 +297,7 @@ final class provider_test extends \core_privacy\tests\provider_testcase {
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$this->setUser($user);
|
||||
$this->getDataGenerator()->enrol_user($user->id, $course->id, 'editingteacher');
|
||||
|
||||
$block = $this->create_course_block($course, $title, $content, $format);
|
||||
$context = \context_block::instance($block->instance->id);
|
||||
@@ -324,6 +325,7 @@ final class provider_test extends \core_privacy\tests\provider_testcase {
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$this->setUser($user);
|
||||
$this->getDataGenerator()->enrol_user($user->id, $course->id, 'editingteacher');
|
||||
|
||||
$block = $this->create_course_block($course, $title, $content, $format);
|
||||
$context = \context_block::instance($block->instance->id);
|
||||
@@ -369,6 +371,7 @@ final class provider_test extends \core_privacy\tests\provider_testcase {
|
||||
$this->setUser($user2);
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$this->getDataGenerator()->enrol_user($user2->id, $course->id, 'editingteacher');
|
||||
$courseblock = $this->create_course_block($course, $title, $content, $blockformat);
|
||||
$coursecontext = \context_block::instance($courseblock->instance->id);
|
||||
|
||||
|
||||
@@ -24,8 +24,6 @@
|
||||
}}
|
||||
|
||||
<div id="block-myoverview-{{uniqid}}" class="block-myoverview block-cards" data-region="myoverview" role="navigation">
|
||||
<hr class="mt-0"/>
|
||||
|
||||
<div role="search" data-region="filter" class="d-flex align-items-center my-2" aria-label="{{#str}} aria:controls, block_myoverview {{/str}}">
|
||||
<div class="row g-0">
|
||||
{{> block_myoverview/nav-grouping-selector }}
|
||||
|
||||
@@ -9,8 +9,9 @@ Feature: Block removal via modal
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| search_forums | Course | C1 | course-view-* | side-pre |
|
||||
| blockname | contextlevel | reference | pagetypepattern | showinsubcontexts | defaultregion |
|
||||
| search_forums | Course | C1 | course-view-* | 0 | side-pre |
|
||||
| myprofile | System | | * | 1 | side-pre |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
|
||||
@@ -18,16 +19,24 @@ Feature: Block removal via modal
|
||||
Scenario: Removing a block via modal should remove the block on the page
|
||||
Given I open the "Search forums" blocks action menu
|
||||
When I click on "Delete Search forums block" "link" in the "Search forums" "block"
|
||||
Then "Delete block?" "dialogue" should exist
|
||||
And I should see "This will delete the block Search forums." in the "Delete block?" "dialogue"
|
||||
And I click on "Delete" "button" in the "Delete block?" "dialogue"
|
||||
And I wait to be redirected
|
||||
And "Search forums" "block" should not exist
|
||||
Then "Search forums" "block" should not exist
|
||||
And I open the "Logged in user" blocks action menu
|
||||
And I click on "Delete Logged in user block" "link" in the "Logged in user" "block"
|
||||
And I should see "You are about to delete a block that appears elsewhere" in the "Delete block?" "dialogue"
|
||||
And I should see "Original block location: System" in the "Delete block?" "dialogue"
|
||||
And I should see "Display on page types: Any page" in the "Delete block?" "dialogue"
|
||||
And I should see "Are you sure you want to continue?" in the "Delete block?" "dialogue"
|
||||
And I click on "Delete" "button" in the "Delete block?" "dialogue"
|
||||
And I wait to be redirected
|
||||
And "Logged in user" "block" should not exist
|
||||
|
||||
@javascript
|
||||
Scenario: Cancel removing a block via modal should retain the block on the page
|
||||
Given I open the "Search forums" blocks action menu
|
||||
When I click on "Delete Search forums block" "link" in the "Search forums" "block"
|
||||
Then "Delete block?" "dialogue" should exist
|
||||
And I click on "Cancel" "button" in the "Delete block?" "dialogue"
|
||||
And I should not see "Delete block?"
|
||||
And "Search forums" "block" should exist
|
||||
Then "Search forums" "block" should exist
|
||||
|
||||
@@ -44,6 +44,7 @@ final class externallib_test extends externallib_advanced_testcase {
|
||||
global $DB, $FULLME;
|
||||
|
||||
$this->resetAfterTest(true);
|
||||
$this->setAdminUser();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
@@ -77,6 +78,7 @@ final class externallib_test extends externallib_advanced_testcase {
|
||||
global $DB, $FULLME;
|
||||
|
||||
$this->resetAfterTest(true);
|
||||
$this->setAdminUser();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
@@ -141,6 +143,7 @@ final class externallib_test extends externallib_advanced_testcase {
|
||||
global $DB, $FULLME;
|
||||
|
||||
$this->resetAfterTest(true);
|
||||
$this->setAdminUser();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
@@ -237,6 +240,7 @@ final class externallib_test extends externallib_advanced_testcase {
|
||||
global $DB, $CFG;
|
||||
|
||||
$this->resetAfterTest(true);
|
||||
$this->setAdminUser();
|
||||
|
||||
// Enable MathJax filter in content and headings.
|
||||
$this->configure_filters([
|
||||
@@ -362,6 +366,7 @@ final class externallib_test extends externallib_advanced_testcase {
|
||||
public function test_get_dashboard_blocks_default_dashboard_including_sticky_block(): void {
|
||||
global $PAGE, $DB;
|
||||
$this->resetAfterTest(true);
|
||||
$this->setAdminUser();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$PAGE->set_url('/my/index.php'); // Need this because some internal API calls require the $PAGE url to be set.
|
||||
@@ -410,6 +415,7 @@ final class externallib_test extends externallib_advanced_testcase {
|
||||
public function test_get_dashboard_blocks_custom_user_dashboard(): void {
|
||||
global $PAGE, $DB;
|
||||
$this->resetAfterTest(true);
|
||||
$this->setAdminUser();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$PAGE->set_url('/my/index.php'); // Need this because some internal API calls require the $PAGE url to be set.
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ class footer_options_exporter extends exporter {
|
||||
*/
|
||||
protected function get_manage_subscriptions_link(): ?string {
|
||||
if (calendar_user_can_add_event($this->calendar->course)) {
|
||||
$managesubscriptionurl = new moodle_url('/calendar/managesubscriptions.php');
|
||||
$managesubscriptionurl = new moodle_url('/calendar/managesubscriptions.php', ['course' => $this->calendar->courseid]);
|
||||
return $managesubscriptionurl->out(true);
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -213,7 +213,8 @@ class create extends \moodleform {
|
||||
$group[] = $mform->createElement('radio', 'duration', null, get_string('durationuntil', 'calendar'), 1);
|
||||
$group[] = $mform->createElement('date_time_selector', 'timedurationuntil', '');
|
||||
$group[] = $mform->createElement('radio', 'duration', null, get_string('durationminutes', 'calendar'), 2);
|
||||
$group[] = $mform->createElement('text', 'timedurationminutes', get_string('durationminutes', 'calendar'));
|
||||
$group[] = $mform->createElement('text', 'timedurationminutes',
|
||||
get_string('durationminutes', 'calendar'), 'maxlength="7" size="7"');
|
||||
|
||||
$mform->addGroup($group, 'durationgroup', get_string('eventduration', 'calendar'), '<br />', false);
|
||||
$mform->setAdvanced('durationgroup');
|
||||
@@ -227,6 +228,9 @@ class create extends \moodleform {
|
||||
|
||||
$mform->setType('timedurationminutes', PARAM_INT);
|
||||
$mform->disabledIf('timedurationminutes', 'duration', 'noteq', 2);
|
||||
$mform->addGroupRule('durationgroup', [
|
||||
'timedurationminutes' => [[get_string('maximumchars', '', 7), 'maxlength', 7, 'server']],
|
||||
]);
|
||||
|
||||
$mform->setDefault('duration', 0);
|
||||
}
|
||||
@@ -238,11 +242,12 @@ class create extends \moodleform {
|
||||
*/
|
||||
protected function add_event_repeat_elements($mform) {
|
||||
$mform->addElement('checkbox', 'repeat', get_string('repeatevent', 'calendar'), null);
|
||||
$mform->addElement('text', 'repeats', get_string('repeatweeksl', 'calendar'), 'maxlength="10" size="10"');
|
||||
$mform->addElement('text', 'repeats', get_string('repeatweeksl', 'calendar'), 'maxlength="3" size="3"');
|
||||
$mform->setType('repeats', PARAM_INT);
|
||||
$mform->setDefault('repeats', 1);
|
||||
$mform->disabledIf('repeats', 'repeat', 'notchecked');
|
||||
$mform->setAdvanced('repeat');
|
||||
$mform->setAdvanced('repeats');
|
||||
$mform->addRule('repeats', get_string('maximumchars', '', 3), 'maxlength', 3, 'server');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,8 @@ class event_form extends moodleform {
|
||||
$group[] =& $mform->createElement('radio', 'duration', null, get_string('durationuntil', 'calendar'), 1);
|
||||
$group[] =& $mform->createElement('date_time_selector', 'timedurationuntil', '');
|
||||
$group[] =& $mform->createElement('radio', 'duration', null, get_string('durationminutes', 'calendar'), 2);
|
||||
$group[] =& $mform->createElement('text', 'timedurationminutes', get_string('durationminutes', 'calendar'));
|
||||
$group[] =& $mform->createElement('text', 'timedurationminutes',
|
||||
get_string('durationminutes', 'calendar'), 'maxlength="7" size="7"');
|
||||
|
||||
$mform->addGroup($group, 'durationgroup', '', '<br />', false);
|
||||
|
||||
@@ -134,6 +135,9 @@ class event_form extends moodleform {
|
||||
|
||||
$mform->setType('timedurationminutes', PARAM_INT);
|
||||
$mform->disabledIf('timedurationminutes','duration','noteq', 2);
|
||||
$mform->addGroupRule('durationgroup', [
|
||||
'timedurationminutes' => [[get_string('maximumchars', '', 7), 'maxlength', 7, 'server']],
|
||||
]);
|
||||
|
||||
$mform->setDefault('duration', ($hasduration)?1:0);
|
||||
|
||||
@@ -141,10 +145,11 @@ class event_form extends moodleform {
|
||||
|
||||
$mform->addElement('header', 'repeatevents', get_string('repeatedevents', 'calendar'));
|
||||
$mform->addElement('checkbox', 'repeat', get_string('repeatevent', 'calendar'), null);
|
||||
$mform->addElement('text', 'repeats', get_string('repeatweeksl', 'calendar'), 'maxlength="10" size="10"');
|
||||
$mform->addElement('text', 'repeats', get_string('repeatweeksl', 'calendar'), 'maxlength="3" size="3"');
|
||||
$mform->setType('repeats', PARAM_INT);
|
||||
$mform->setDefault('repeats', 1);
|
||||
$mform->disabledIf('repeats','repeat','notchecked');
|
||||
$mform->addRule('repeats', get_string('maximumchars', '', 3), 'maxlength', 3, 'server');
|
||||
|
||||
} else if ($repeatedevents) {
|
||||
|
||||
|
||||
@@ -915,10 +915,10 @@ class core_calendar_external extends external_api {
|
||||
$formoptions['eventtypes'] = $allowedeeventtypes;
|
||||
if ($courseid) {
|
||||
require_once($CFG->libdir . '/grouplib.php');
|
||||
$groupcoursedata = groups_get_course_data($courseid);
|
||||
if (!empty($groupcoursedata->groups)) {
|
||||
$groupcoursedata = groups_get_all_groups($courseid);
|
||||
if (!empty($groupcoursedata)) {
|
||||
$formoptions['groups'] = [];
|
||||
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
|
||||
foreach ($groupcoursedata as $groupid => $groupdata) {
|
||||
$formoptions['groups'][$groupid] = $groupdata->name;
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -3639,10 +3639,10 @@ function calendar_output_fragment_event_form($args) {
|
||||
|
||||
if (is_null($eventid)) {
|
||||
if (!empty($courseid)) {
|
||||
$groupcoursedata = groups_get_course_data($courseid);
|
||||
$groupcoursedata = groups_get_all_groups($courseid);
|
||||
$formoptions['groups'] = [];
|
||||
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
|
||||
$formoptions['groups'][$groupid] = $groupdata->name;
|
||||
foreach ($groupcoursedata as $groupid => $groupdata) {
|
||||
$formoptions['groups'][$groupid] = format_string($groupdata->name, false, ['context' => $context]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3683,10 +3683,10 @@ function calendar_output_fragment_event_form($args) {
|
||||
$formoptions['event'] = $event;
|
||||
|
||||
if (!empty($event->courseid)) {
|
||||
$groupcoursedata = groups_get_course_data($event->courseid);
|
||||
$groupcoursedata = groups_get_all_groups($event->courseid);
|
||||
$formoptions['groups'] = [];
|
||||
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
|
||||
$formoptions['groups'][$groupid] = $groupdata->name;
|
||||
foreach ($groupcoursedata as $groupid => $groupdata) {
|
||||
$formoptions['groups'][$groupid] = format_string($groupdata->name, false, ['context' => $context]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,11 +48,12 @@
|
||||
<a{{!
|
||||
}} href="#"{{!
|
||||
}} class="arrow_link previous"{{!
|
||||
}} title="{{#str}}monthprev, calendar{{/str}}"{{!
|
||||
}} title="{{#str}}monthprevwithname, calendar, {{previousperiodname}}{{/str}}"{{!
|
||||
}} data-year="{{previousperiod.year}}"{{!
|
||||
}} data-month="{{previousperiod.mon}}"{{!
|
||||
}} aria-label="{{#str}}monthprevwithname, calendar, {{previousperiodname}}{{/str}}" {{!
|
||||
}}>
|
||||
<span class="arrow">{{{larrow}}}</span>
|
||||
<span class="arrow" aria-hidden="true">{{{larrow}}}</span>
|
||||
</a>
|
||||
<span class="hide"> | </span>
|
||||
<span class="current">
|
||||
@@ -62,11 +63,12 @@
|
||||
<a{{!
|
||||
}} href="#"{{!
|
||||
}} class="arrow_link next"{{!
|
||||
}} title="{{#str}}monthnext, calendar{{/str}}"{{!
|
||||
}} title="{{#str}}monthnextwithname, calendar, {{nextperiodname}}{{/str}}"{{!
|
||||
}} data-year="{{nextperiod.year}}"{{!
|
||||
}} data-month="{{nextperiod.mon}}"{{!
|
||||
}} aria-label="{{#str}}monthnextwithname, calendar, {{nextperiodname}}{{/str}}" {{!
|
||||
}}>
|
||||
<span class="arrow">{{{rarrow}}}</span>
|
||||
<span class="arrow" aria-hidden="true">{{{rarrow}}}</span>
|
||||
</a>
|
||||
{{/includenavigation}}
|
||||
{{^includenavigation}}
|
||||
|
||||
@@ -36,10 +36,11 @@
|
||||
<a{{!
|
||||
}} href="{{previousperiodlink}}"{{!
|
||||
}} class="arrow_link previous"{{!
|
||||
}} title="{{#str}}monthprev, calendar{{/str}}"{{!
|
||||
}} {{#viewinginblock}}title="{{#str}}monthprevwithname, calendar, {{previousperiodname}}{{/str}}" {{/viewinginblock}} {{!
|
||||
}} data-year="{{previousperiod.year}}"{{!
|
||||
}} data-month="{{previousperiod.mon}}"{{!
|
||||
}} data-drop-zone="nav-link" {{!
|
||||
}} aria-label="{{#str}}monthprevwithname, calendar, {{previousperiodname}}{{/str}}" {{!
|
||||
}}>
|
||||
<span class="arrow" aria-hidden="true">{{{larrow}}}</span>
|
||||
|
||||
@@ -56,10 +57,11 @@
|
||||
<a{{!
|
||||
}} href="{{nextperiodlink}}"{{!
|
||||
}} class="arrow_link next"{{!
|
||||
}} title="{{#str}}monthnext, calendar{{/str}}"{{!
|
||||
}} {{#viewinginblock}}title="{{#str}}monthnextwithname, calendar, {{nextperiodname}}{{/str}}" {{/viewinginblock}} {{!
|
||||
}} data-year="{{nextperiod.year}}"{{!
|
||||
}} data-month="{{nextperiod.mon}}"{{!
|
||||
}} data-drop-zone="nav-link" {{!
|
||||
}} aria-label="{{#str}}monthnextwithname, calendar, {{nextperiodname}}{{/str}}" {{!
|
||||
}}>
|
||||
<span class="arrow_text">{{nextperiodname}}</span>
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ Feature: Perform basic calendar functionality
|
||||
| teacher1 | C1 | teacher |
|
||||
| admin | C1 | editingteacher |
|
||||
And the following "groups" exist:
|
||||
| name | course | idnumber |
|
||||
| Group 1 | C1 | G1 |
|
||||
| name | course | idnumber |
|
||||
| <span lang="en" class="multilang">Group</span><span lang="es" class="multilang">Grupo</span> 1 | C1 | G1 |
|
||||
And the following "group members" exist:
|
||||
| user | group |
|
||||
| student1 | G1 |
|
||||
@@ -75,7 +75,9 @@ Feature: Perform basic calendar functionality
|
||||
|
||||
@javascript
|
||||
Scenario: Create a group event
|
||||
Given I log in as "teacher1"
|
||||
Given the "multilang" filter is "on"
|
||||
And the "multilang" filter applies to "content and headings"
|
||||
And I log in as "teacher1"
|
||||
And I follow "Full calendar"
|
||||
And I set the field "course" to "Course 1"
|
||||
And I create a calendar event:
|
||||
@@ -322,18 +324,23 @@ Feature: Perform basic calendar functionality
|
||||
|
||||
@javascript
|
||||
Scenario: Changing the event type should clear previous data
|
||||
Given I am on the "Course 1" course page logged in as admin
|
||||
Given the "multilang" filter is "on"
|
||||
And the "multilang" filter applies to "content and headings"
|
||||
And I am on the "Course 1" course page logged in as admin
|
||||
And I follow "Course calendar"
|
||||
And I press "New event"
|
||||
And I set the following fields to these values:
|
||||
| Event title | Group 1 event |
|
||||
| Type of event | Group |
|
||||
And I should see "Group 1"
|
||||
And I should not see "GroupGrupo 1"
|
||||
And I press "Save"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Course calendar"
|
||||
And I click on "Group 1 event" "link"
|
||||
And I should see "Group event"
|
||||
And I should see "Group 1"
|
||||
And I should not see "GroupGrupo 1"
|
||||
When I click on "Edit" "button" in the "Group 1 event" "dialogue"
|
||||
And I set the following fields to these values:
|
||||
| Event title | My own user event |
|
||||
@@ -369,7 +376,9 @@ Feature: Perform basic calendar functionality
|
||||
|
||||
@javascript
|
||||
Scenario: Changing the event type from user to anything else should work
|
||||
Given I log in as "teacher1"
|
||||
Given the "multilang" filter is "on"
|
||||
And the "multilang" filter applies to "content and headings"
|
||||
And I log in as "teacher1"
|
||||
# We need this so we can see the groups.
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
|
||||
+4
-2
@@ -144,8 +144,10 @@ $headingstr = get_string('calendar', 'core_calendar');
|
||||
// If the user is on the course page,
|
||||
// then make the course name linkable to ease the user's navigation to the course page.
|
||||
if ($iscoursecalendar) {
|
||||
$url = new \moodle_url('/course/view.php', ['id' => $courseid]);
|
||||
$linkcourse = html_writer::link($url, $course->shortname);
|
||||
$linkcourse = html_writer::link(
|
||||
course_get_url($course),
|
||||
format_string($course->shortname, options: ['context' => $PAGE->context]),
|
||||
);
|
||||
$headingstr = "{$headingstr}: {$linkcourse}";
|
||||
}
|
||||
$PAGE->set_heading($headingstr, false);
|
||||
|
||||
@@ -405,6 +405,12 @@ class core_cohort_external extends external_api {
|
||||
$results = $results + cohort_get_available_cohorts($context, COHORT_ALL, $limitfrom, $limitnum, $query);
|
||||
}
|
||||
} else if ($includes == 'all') {
|
||||
$contextsystem = context_system::instance();
|
||||
if (!$context instanceof context_system &&
|
||||
!has_any_capability(['moodle/cohort:view', 'moodle/cohort:manage'], $contextsystem)) {
|
||||
|
||||
throw new required_capability_exception($contextsystem, 'moodle/cohort:view', 'nopermissions', '');
|
||||
}
|
||||
$results = cohort_get_all_cohorts($limitfrom, $limitnum, $query);
|
||||
$results = $results['cohorts'];
|
||||
} else {
|
||||
|
||||
@@ -672,6 +672,7 @@ final class externallib_test extends externallib_advanced_testcase {
|
||||
role_assign($userrole, $catuser->id, $catcontext->id);
|
||||
|
||||
// Enrol user in the course.
|
||||
$this->getDataGenerator()->enrol_user($creator->id, $course->id);
|
||||
$this->getDataGenerator()->enrol_user($courseuser->id, $course->id, 'courserole');
|
||||
|
||||
$syscontext = array('contextid' => \context_system::instance()->id);
|
||||
@@ -758,18 +759,27 @@ final class externallib_test extends externallib_advanced_testcase {
|
||||
$this->assertCount(4, $result['cohorts']);
|
||||
|
||||
// A user in the course context with the system cohort:view capability. Check that all the system cohorts are returned.
|
||||
$this->setUser($courseuser);
|
||||
$result = core_cohort_external::search_cohorts("Cohortsearch", $coursecontext, 'all');
|
||||
$this->assertCount(2, $result['cohorts']);
|
||||
$this->assertEquals('Cohortsearch 1', $result['cohorts'][$cohort1->id]->name);
|
||||
$this->assertCount(4, $result['cohorts']);
|
||||
|
||||
// A user in the course context without the ability to view system cohorts.
|
||||
$this->setUser($courseuser);
|
||||
try {
|
||||
$result = core_cohort_external::search_cohorts("Cohortsearch", $coursecontext, 'all');
|
||||
$this->fail('Exception expected');
|
||||
} catch (\Throwable $e) {
|
||||
$this->assertInstanceOf(\required_capability_exception::class, $e);
|
||||
$this->assertStringContainsString('(View site-wide cohorts)', $e->getMessage());
|
||||
}
|
||||
|
||||
// Detect invalid parameter $includes.
|
||||
$this->setUser($creator);
|
||||
try {
|
||||
$result = core_cohort_external::search_cohorts("Cohortsearch", $syscontext, 'invalid');
|
||||
$this->fail('Invalid parameter includes');
|
||||
} catch (\coding_exception $e) {
|
||||
// All good.
|
||||
$this->fail('Exception expected');
|
||||
} catch (\Throwable $e) {
|
||||
$this->assertInstanceOf(\coding_exception::class, $e);
|
||||
$this->assertStringContainsString('Invalid parameter value for \'includes\'', $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +239,7 @@ class provider implements
|
||||
$params = (isset($user)) ? ['userid' => $user->id, 'coursemoduleid' => $cmid] : ['coursemoduleid' => $cmid];
|
||||
// Only delete the record for course modules completion.
|
||||
$DB->delete_records('course_modules_completion', $params);
|
||||
$DB->delete_records('course_modules_viewed', $params);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,25 +14,19 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Unit Tests for the request helper.
|
||||
*
|
||||
* @package core_completion
|
||||
* @category test
|
||||
* @copyright 2018 Adrian Greeve <adriangreeve.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace core_completion\privacy;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/completion/tests/fixtures/completion_creation.php');
|
||||
|
||||
/**
|
||||
* Tests for the \core_completion API's provider functionality.
|
||||
*
|
||||
* @package core_completion
|
||||
* @category test
|
||||
* @covers \core_completion\privacy\provider
|
||||
* @copyright 2018 Adrian Greeve <adriangreeve.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
@@ -122,14 +116,35 @@ final class provider_test extends \core_privacy\tests\provider_testcase {
|
||||
* Test deleting activity completion information for a user.
|
||||
*/
|
||||
public function test_delete_completion_activity_user(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$this->create_course_completion();
|
||||
$this->complete_course($user);
|
||||
\core_completion\privacy\provider::delete_completion($user, null, $this->cm->id);
|
||||
$activitycompletion = \core_completion\privacy\provider::get_activity_completion_info($user, $this->course,
|
||||
$this->cm);
|
||||
|
||||
$course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
|
||||
$user = $this->getDataGenerator()->create_and_enrol($course);
|
||||
|
||||
$page = $this->getDataGenerator()->create_module('page', [
|
||||
'course' => $course->id,
|
||||
'completion' => 1,
|
||||
'completionview' => 1,
|
||||
]);
|
||||
$cm = get_coursemodule_from_id('page', $page->cmid);
|
||||
|
||||
$completion = new \completion_info($course);
|
||||
$completion->set_module_viewed($cm, $user->id);
|
||||
|
||||
// Sanity test.
|
||||
$this->assertTrue($DB->record_exists('course_modules_completion', ['userid' => $user->id, 'coursemoduleid' => $cm->id]));
|
||||
$this->assertTrue($DB->record_exists('course_modules_viewed', ['userid' => $user->id, 'coursemoduleid' => $cm->id]));
|
||||
|
||||
provider::delete_completion($user, null, $cm->id);
|
||||
|
||||
$activitycompletion = provider::get_activity_completion_info($user, $course, $cm);
|
||||
$this->assertEquals(0, $activitycompletion->completionstate);
|
||||
|
||||
// Assert we cleared that data.
|
||||
$this->assertFalse($DB->record_exists('course_modules_completion', ['userid' => $user->id, 'coursemoduleid' => $cm->id]));
|
||||
$this->assertFalse($DB->record_exists('course_modules_viewed', ['userid' => $user->id, 'coursemoduleid' => $cm->id]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+100
-89
@@ -38,94 +38,106 @@ $CFG = new stdClass();
|
||||
// will be stored. This database must already have been created //
|
||||
// and a username/password created to access it. //
|
||||
|
||||
$CFG->dbtype = 'pgsql'; // 'pgsql', 'mariadb', 'mysqli', 'auroramysql', 'sqlsrv' or 'oci'
|
||||
$CFG->dblibrary = 'native'; // 'native' only at the moment
|
||||
$CFG->dbhost = 'localhost'; // eg 'localhost' or 'db.isp.com' or IP
|
||||
$CFG->dbname = 'moodle'; // database name, eg moodle
|
||||
$CFG->dbuser = 'username'; // your database username
|
||||
$CFG->dbpass = 'password'; // your database password
|
||||
$CFG->prefix = 'mdl_'; // prefix to use for all table names
|
||||
$CFG->dboptions = array(
|
||||
'dbpersist' => false, // should persistent database connections be
|
||||
// used? set to 'false' for the most stable
|
||||
// setting, 'true' can improve performance
|
||||
// sometimes
|
||||
'dbsocket' => false, // should connection via UNIX socket be used?
|
||||
// if you set it to 'true' or custom path
|
||||
// here set dbhost to 'localhost',
|
||||
// (please note mysql is always using socket
|
||||
// if dbhost is 'localhost' - if you need
|
||||
// local port connection use '127.0.0.1')
|
||||
'dbport' => '', // the TCP port number to use when connecting
|
||||
// to the server. keep empty string for the
|
||||
// default port
|
||||
'dbhandlesoptions' => false,// On PostgreSQL poolers like pgbouncer don't
|
||||
// support advanced options on connection.
|
||||
// If you set those in the database then
|
||||
// the advanced settings will not be sent.
|
||||
'dbcollation' => 'utf8mb4_unicode_ci', // MySQL has partial and full UTF-8
|
||||
// support. If you wish to use partial UTF-8
|
||||
// (three bytes) then set this option to
|
||||
// 'utf8_unicode_ci'. If using the recommended
|
||||
// settings with full UTF-8 support this should
|
||||
// be set to 'utf8mb4_unicode_ci'. This option
|
||||
// should be removed for all other databases.
|
||||
// 'versionfromdb' => false, // On MySQL and MariaDB, this can force
|
||||
// the DB version to be evaluated using
|
||||
// the VERSION function instead of the version
|
||||
// provided by the PHP client which could be
|
||||
// wrong based on the DB server infrastructure,
|
||||
// e.g. PaaS on Azure. Default is false/unset.
|
||||
// Uncomment and set to true to force MySQL and
|
||||
// MariaDB to use 'SELECT VERSION();'.
|
||||
// 'extrainfo' => [], // Extra information for the DB driver, e.g. SQL Server,
|
||||
// has additional configuration according to its environment,
|
||||
// which the administrator can specify to alter and
|
||||
// override any connection options.
|
||||
// 'ssl' => '', // A connection mode string from the list below.
|
||||
// Not supported by all drivers.
|
||||
// prefer Use SSL if available - postgres default Postgres only
|
||||
// disable Force non secure connection Postgres only
|
||||
// require Force SSL Postgres and MySQL
|
||||
// verify-full Force SSL and verify root CA Postgres and MySQL
|
||||
// All mode names are adopted from Postgres
|
||||
// and other databases align where possible:
|
||||
// Postgres: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLMODE
|
||||
// MySql: https://www.php.net/manual/en/mysqli.real-connect.php
|
||||
// It is worth noting that for MySQL require and verify-full are the same - in both cases
|
||||
// verification will take place if you specify hostname as a name,
|
||||
// and it will be omitted if you put an IP address.
|
||||
// 'fetchbuffersize' => 100000, // On PostgreSQL, this option sets a limit
|
||||
// on the number of rows that are fetched into
|
||||
// memory when doing a large recordset query
|
||||
// (e.g. search indexing). Default is 100000.
|
||||
// Uncomment and set to a value to change it,
|
||||
// or zero to turn off the limit. You need to
|
||||
// set to zero if you are using pg_bouncer in
|
||||
// 'transaction' mode (it is fine in 'session'
|
||||
// mode).
|
||||
// 'clientcompress' => true // Use compression protocol to communicate with the database server.
|
||||
// Decreases traffic from the database server.
|
||||
// Not needed if the databse is on the same host.
|
||||
// Currently supported only with mysqli, mariadb, and aurora drivers.
|
||||
$CFG->dbtype = 'pgsql'; // 'pgsql', 'mariadb', 'mysqli', 'auroramysql', 'sqlsrv' or 'oci'
|
||||
$CFG->dblibrary = 'native'; // 'native' only at the moment
|
||||
$CFG->dbhost = 'localhost'; // eg 'localhost' or 'db.isp.com' or IP
|
||||
$CFG->dbname = 'moodle'; // database name, eg moodle
|
||||
$CFG->dbuser = 'username'; // your database username
|
||||
$CFG->dbpass = 'password'; // your database password
|
||||
$CFG->prefix = 'mdl_'; // prefix to use for all table names
|
||||
$CFG->dboptions = [
|
||||
'dbpersist' => false, // Should persistent database connections be
|
||||
// used? Set to 'false' for the most stable
|
||||
// setting, 'true' can improve performance
|
||||
// sometimes
|
||||
'dbsocket' => false, // Should connection via UNIX socket be used?
|
||||
// if you set it to 'true' or custom path
|
||||
// here set dbhost to 'localhost',
|
||||
// (please note mysql is always using socket
|
||||
// if dbhost is 'localhost' - if you need
|
||||
// local port connection use '127.0.0.1')
|
||||
'dbport' => '', // The TCP port number to use when connecting
|
||||
// to the server. Keep empty string for the
|
||||
// default port
|
||||
'dbhandlesoptions' => false, // On PostgreSQL poolers like pgbouncer don't
|
||||
// support advanced options on connection.
|
||||
// If you set those in the database then
|
||||
// the advanced settings will not be sent.
|
||||
'dbcollation' => 'utf8mb4_unicode_ci', // MySQL has partial and full UTF-8
|
||||
// support. If you wish to use partial UTF-8
|
||||
// (three bytes) then set this option to
|
||||
// 'utf8_unicode_ci'. If using the recommended
|
||||
// settings with full UTF-8 support this should
|
||||
// be set to 'utf8mb4_unicode_ci'. This option
|
||||
// should be removed for all other databases.
|
||||
// 'dbschema' => '', // On PostgreSQL this sets the database schema to use
|
||||
// for the connection.
|
||||
// 'dbtransactions' => null // Set this to true to explicitly enable database transactions
|
||||
// for MySQL. By default only the following storage engines are
|
||||
// configured to use transactions: InnoDB, INNOBASE, BDB, XtraDB,
|
||||
// Aria, Falcon.
|
||||
// 'versionfromdb' => false, // On MySQL and MariaDB, this can force
|
||||
// the DB version to be evaluated using
|
||||
// the VERSION function instead of the version
|
||||
// provided by the PHP client which could be
|
||||
// wrong based on the DB server infrastructure,
|
||||
// e.g. PaaS on Azure. Default is false/unset.
|
||||
// Uncomment and set to true to force MySQL and
|
||||
// MariaDB to use 'SELECT VERSION();'.
|
||||
// 'extrainfo' => [], // Extra information for the DB driver, e.g. SQL Server,
|
||||
// has additional configuration according to its environment,
|
||||
// which the administrator can specify to alter and
|
||||
// override any connection options.
|
||||
// 'ssl' => '', // A connection mode string from the list below.
|
||||
// Not supported by all drivers.
|
||||
// prefer Use SSL if available - postgres default Postgres only
|
||||
// disable Force non secure connection Postgres only
|
||||
// require Force SSL Postgres and MySQL
|
||||
// verify-full Force SSL and verify root CA Postgres and MySQL
|
||||
// All mode names are adopted from Postgres
|
||||
// and other databases align where possible:
|
||||
// Postgres: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLMODE
|
||||
// MySQL: https://www.php.net/manual/en/mysqli.real-connect.php
|
||||
// It is worth noting that for MySQL require and verify-full are the same - in both cases
|
||||
// verification will take place if you specify hostname as a name,
|
||||
// and it will be omitted if you put an IP address.
|
||||
// 'fetchbuffersize' => 100000, // On PostgreSQL, this option sets a limit
|
||||
// on the number of rows that are fetched into
|
||||
// memory when doing a large recordset query
|
||||
// (e.g. search indexing). Default is 100000.
|
||||
// Uncomment and set to a value to change it,
|
||||
// or zero to turn off the limit. You need to
|
||||
// set to zero if you are using pg_bouncer in
|
||||
// 'transaction' mode (it is fine in 'session'
|
||||
// mode).
|
||||
// 'clientcompress' => true // Use compression protocol to communicate with the database server.
|
||||
// Decreases traffic from the database server.
|
||||
// Not needed if the databse is on the same host.
|
||||
// Currently supported only with mysqli, mariadb, and aurora drivers.
|
||||
// 'connecttimeout' => null, // Set connect timeout in seconds. Not all drivers support it.
|
||||
// 'logall' => false, // Log all queries to the database. They are stored in the table log_queries.
|
||||
// 'logslow' => 0, // Log slow queries to the database. They are stored in the table log_queries.
|
||||
// This parameter controls the threshold in milliseconds and requires an integer,
|
||||
// not a string!
|
||||
// 'logerror' => false, // Log erroneous queries to the database. They are stored in the table log_queries.
|
||||
// 'bulkinsertsize' => null, // Set the bulk insert size for database operations. MySQL has a relatively small
|
||||
// query length limit by default, make sure 'max_allowed_packet' in my.cnf is
|
||||
// high enough if you set this setting.
|
||||
// 'readonly' => [ // Set to read-only slave details, to get safe reads
|
||||
// from there instead of the master node. Optional.
|
||||
// Currently supported by pgsql and mysqli variety classes.
|
||||
// If not supported silently ignored.
|
||||
// 'instance' => [ // Readonly slave connection parameters
|
||||
// [
|
||||
// 'dbhost' => 'slave.dbhost',
|
||||
// 'dbport' => '', // Defaults to master port
|
||||
// 'dbuser' => '', // Defaults to master user
|
||||
// 'dbpass' => '', // Defaults to master password
|
||||
// ],
|
||||
// [...],
|
||||
// ],
|
||||
/*
|
||||
'connecttimeout' => null, // Set connect timeout in seconds. Not all drivers support it.
|
||||
'readonly' => [ // Set to read-only slave details, to get safe reads
|
||||
// from there instead of the master node. Optional.
|
||||
// Currently supported by pgsql and mysqli variety classes.
|
||||
// If not supported silently ignored.
|
||||
'instance' => [ // Readonly slave connection parameters
|
||||
[
|
||||
'dbhost' => 'slave.dbhost',
|
||||
'dbport' => '', // Defaults to master port
|
||||
'dbuser' => '', // Defaults to master user
|
||||
'dbpass' => '', // Defaults to master password
|
||||
],
|
||||
[...],
|
||||
],
|
||||
|
||||
Instance(s) can alternatively be specified as:
|
||||
|
||||
'instance' => 'slave.dbhost',
|
||||
'instance' => ['slave.dbhost1', 'slave.dbhost2'],
|
||||
'instance' => ['dbhost' => 'slave.dbhost', 'dbport' => '', 'dbuser' => '', 'dbpass' => ''],
|
||||
@@ -146,9 +158,8 @@ $CFG->dboptions = array(
|
||||
|
||||
More info available in lib/dml/moodle_read_slave_trait.php where the feature is implemented.
|
||||
]
|
||||
*/
|
||||
// For all database config settings see https://docs.moodle.org/en/Database_settings
|
||||
);
|
||||
*/
|
||||
];
|
||||
|
||||
|
||||
//=========================================================================
|
||||
|
||||
@@ -133,7 +133,8 @@ class course_content_item_exporter extends exporter {
|
||||
'title' => $this->contentitem->get_title()->get_value(),
|
||||
'link' => $this->contentitem->get_link()->out(false),
|
||||
'icon' => $this->contentitem->get_icon(),
|
||||
'help' => format_text($this->contentitem->get_help(), FORMAT_MARKDOWN),
|
||||
// Help text should not be parsed using course filters.
|
||||
'help' => format_text($this->contentitem->get_help(), FORMAT_MARKDOWN, ['filter' => false]),
|
||||
'archetype' => $this->contentitem->get_archetype(),
|
||||
'componentname' => $this->contentitem->get_component_name(),
|
||||
'favourite' => $favourite,
|
||||
|
||||
@@ -51,7 +51,9 @@ class content_item_readonly_repository implements content_item_readonly_reposito
|
||||
if ($sm->string_exists('modulename_help', $modname)) {
|
||||
$help = get_string('modulename_help', $modname);
|
||||
if ($sm->string_exists('modulename_link', $modname)) { // Link to further info in Moodle docs.
|
||||
$link = get_string('modulename_link', $modname);
|
||||
// The link is stored in a language file but should not be translated, use value for English.
|
||||
$link = $sm->get_string('modulename_link', $modname, null, 'en');
|
||||
// The text 'More help' and other strings should be in the current language.
|
||||
$linktext = get_string('morehelp');
|
||||
$arialabel = get_string('morehelpaboutmodule', '', get_string('modulename', $modname));
|
||||
$doclink = $OUTPUT->doc_link($link, $linktext, true, ['aria-label' => $arialabel]);
|
||||
|
||||
@@ -71,7 +71,7 @@ class core_course_management_renderer extends plugin_renderer_base {
|
||||
final: true,
|
||||
)]
|
||||
public function management_heading() {
|
||||
\core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]);
|
||||
\core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -187,7 +187,7 @@ class core_course_management_renderer extends plugin_renderer_base {
|
||||
$a->parentcategory = $parent->get_formatted_name();
|
||||
$textlabel = get_string('categorysubcategoryof', 'moodle', $a);
|
||||
}
|
||||
$courseicon = $this->output->pix_icon('i/course', get_string('courses'));
|
||||
$courseicon = $this->output->pix_icon('i/course', get_string('courses'), 'core', ['class' => 'ps-1']);
|
||||
$bcatinput = array(
|
||||
'id' => 'categorylistitem' . $category->id,
|
||||
'type' => 'checkbox',
|
||||
@@ -343,6 +343,10 @@ class core_course_management_renderer extends plugin_renderer_base {
|
||||
$actions = \core_course\management\helper::get_category_listitem_actions($category);
|
||||
}
|
||||
$menu = new action_menu();
|
||||
$label = get_string('actionsmenu');
|
||||
$actionicon = $this->output->pix_icon('t/edit_menu', '') . html_writer::span($label, 'sr-only');
|
||||
$menu->set_menu_trigger($actionicon, 'iconsmall actionmenu');
|
||||
$menu->triggerattributes['title'] = $label;
|
||||
$menu->attributes['class'] .= ' category-item-actions item-actions';
|
||||
$hasitems = false;
|
||||
foreach ($actions as $key => $action) {
|
||||
@@ -1286,7 +1290,7 @@ class core_course_management_renderer extends plugin_renderer_base {
|
||||
final: true,
|
||||
)]
|
||||
public function course_search_form() {
|
||||
\core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]);
|
||||
\core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -71,7 +71,7 @@ class completion {
|
||||
*/
|
||||
#[\core\attribute\deprecated(null, mdl: 'MDL-82467', since: '4.5')]
|
||||
public static function get_days(?int $value, stdClass $row): ?int {
|
||||
\core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]);
|
||||
\core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
|
||||
|
||||
// Do not show anything if there is no userid.
|
||||
if (!$row->userid) {
|
||||
|
||||
@@ -896,8 +896,12 @@ class core_course_external extends external_api {
|
||||
'showreports' => new external_value(PARAM_INT,
|
||||
'are activity report shown (yes = 1, no =0)', VALUE_DEFAULT,
|
||||
$courseconfig->showreports),
|
||||
'visible' => new external_value(PARAM_INT,
|
||||
'1: available to student, 0:not available', VALUE_OPTIONAL),
|
||||
'visible' => new external_value(
|
||||
PARAM_INT,
|
||||
'1: available to student, 0: not available',
|
||||
VALUE_DEFAULT,
|
||||
$courseconfig->visible,
|
||||
),
|
||||
'hiddensections' => new external_value(PARAM_INT,
|
||||
'(deprecated, use courseformatoptions) How the hidden sections in the course are displayed to students',
|
||||
VALUE_OPTIONAL),
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user