Compare commits
601 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db959720ed | |||
| 7836fde50f | |||
| 2bf0d3b149 | |||
| a8f220b2b7 | |||
| 65463981dc | |||
| 5bb490e979 | |||
| c7b2b1c439 | |||
| d869697f97 | |||
| 9ad5f88019 | |||
| 90f82cbe86 | |||
| e22816a954 | |||
| 23f51d88cf | |||
| 6aa12043d1 | |||
| 772c763e6b | |||
| 9f773c160e | |||
| d3b930d67c | |||
| 886f5c8465 | |||
| 0138b520a4 | |||
| f2f64d40c5 | |||
| d56e90c1b7 | |||
| ab291aa8ed | |||
| a60f0883cc | |||
| 34b45fea59 | |||
| 5a24beab3c | |||
| 7ba0f509c7 | |||
| fa5eec299a | |||
| deb427c38f | |||
| caa14d5283 | |||
| f5af008e0f | |||
| 22b6d706bd | |||
| 1f4bca1a2e | |||
| 4b36a967d0 | |||
| 071cc916fd | |||
| d2595b4a4e | |||
| 9c811f8a4c | |||
| 8e40b64f8d | |||
| e600fb2782 | |||
| fd4f256768 | |||
| 57f527587b | |||
| 5e95aa4b3a | |||
| c36c4e8caf | |||
| c217ab2947 | |||
| a64e2bc235 | |||
| d9f2f7006e | |||
| 5481ef00a1 | |||
| 099f65274c | |||
| 15eb08acc2 | |||
| b5b838e16b | |||
| 31537778b9 | |||
| 78912de2ab | |||
| 415195088b | |||
| c90226fc23 | |||
| 372318257c | |||
| 8c6c3f0d06 | |||
| 7a91817a1c | |||
| a4290bec3f | |||
| 1dcf079286 | |||
| fcbe432066 | |||
| b7852c7225 | |||
| 4e7e308a77 | |||
| a686489653 | |||
| 1179e1f534 | |||
| ee66d8f2bf | |||
| 818863cccd | |||
| 1a6d6694a2 | |||
| da802580da | |||
| 534f220f21 | |||
| d2e842f7b3 | |||
| 4eb7c5a590 | |||
| 3b6f8e334f | |||
| 5ba84639dd | |||
| 7cc4823684 | |||
| 476ae75b70 | |||
| 2528a52808 | |||
| 197aac357e | |||
| 26babd2ac0 | |||
| e494cab5b9 | |||
| 298a87baf3 | |||
| fd18519ea6 | |||
| 6f3c6fc5eb | |||
| cf1e58654c | |||
| f2773ec1cc | |||
| 04cf9b4e06 | |||
| 895acd4aa1 | |||
| 1f45291f24 | |||
| db7296f2f6 | |||
| 94a643c0d8 | |||
| f7dd7e096a | |||
| 674fbe100b | |||
| 93dd090c38 | |||
| 7d229de154 | |||
| 0457ed850c | |||
| 6c325a2b3c | |||
| 31734b11c8 | |||
| 52746d4c9a | |||
| 9a0a83a456 | |||
| 3d2052dd89 | |||
| 84d831318f | |||
| 49d74934f1 | |||
| f01bc02e78 | |||
| 2ab39f557b | |||
| e0933f4f41 | |||
| 7eb8fce830 | |||
| 3c3c7b3268 | |||
| 340e5f9a2c | |||
| 28adbc1374 | |||
| 4a5d428cd3 | |||
| abeec88715 | |||
| f16681b267 | |||
| 15b4403980 | |||
| eedb5e5f81 | |||
| 6b3c54d981 | |||
| c77d234c19 | |||
| aa07578b4c | |||
| 9b9965110e | |||
| cd8a797f31 | |||
| 342f13d5df | |||
| 8341d84ce5 | |||
| 6dff42d1af | |||
| 7b7e907e66 | |||
| 0ae1c37e6d | |||
| bdba5a0594 | |||
| 782d262ef6 | |||
| 91932bf748 | |||
| e9f261c0f9 | |||
| 19e92ca42c | |||
| 7f6d05ad04 | |||
| f0630a0576 | |||
| 0fadf1ed19 | |||
| c9e55faacf | |||
| d76e843d8c | |||
| a776b0bf55 | |||
| 24d28bf68e | |||
| 80e2791f25 | |||
| 1e53070414 | |||
| c3d725a3cd | |||
| ee96ff39cb | |||
| 98086bf044 | |||
| dd94fb1f8c | |||
| 755dea3652 | |||
| cfe13f3b73 | |||
| c0ef7bb0e4 | |||
| 16457d9346 | |||
| b62d3965f6 | |||
| d14f5eaa6c | |||
| 68ac6e6ef8 | |||
| 4e9a865984 | |||
| e6a32d87a5 | |||
| 26f41c348d | |||
| 01e3740d89 | |||
| 2a9305dbf5 | |||
| c0afa63f6e | |||
| b7c05d2cc4 | |||
| 7676cd334f | |||
| e28403c470 | |||
| 6554262006 | |||
| 3f94e61e49 | |||
| e109b9dd21 | |||
| 896fa8b5a0 | |||
| 47deadb259 | |||
| ff344e2495 | |||
| 3ea261a1c1 | |||
| f4e7f201bc | |||
| 0bad70f9c4 | |||
| c5f8a854b7 | |||
| 7945536fde | |||
| 330ecf9203 | |||
| 75af70591b | |||
| 46f2f2cc29 | |||
| 7d2988dfe5 | |||
| def0c592a0 | |||
| 7669660f9b | |||
| ed24ab1b04 | |||
| 3891da010b | |||
| 9e40eb38c0 | |||
| 8c875ed525 | |||
| 330d8baf27 | |||
| b1c64a3b25 | |||
| 55f37d0dc7 | |||
| 1619484716 | |||
| cd08a0ecee | |||
| 062791efe1 | |||
| 6bc54e875d | |||
| f308827fa8 | |||
| 37bca6ce6b | |||
| 916bd72fc1 | |||
| 342b3707b3 | |||
| 19f639210a | |||
| c66b5072e1 | |||
| 377fcdf1bd | |||
| 2ad2ba2d2c | |||
| 56b3532c63 | |||
| 71a0ce575f | |||
| 1d101c4a52 | |||
| f980061174 | |||
| 2af12db84c | |||
| 869921831d | |||
| 594795daa6 | |||
| 536c0f3e53 | |||
| bff37a3eab | |||
| c8f98fe85d | |||
| 9d7695c5fa | |||
| 027ee0fef8 | |||
| 2e1aa0c321 | |||
| 1ffabb9359 | |||
| 48e0029f0f | |||
| f48077b2ed | |||
| b3bdb05db1 | |||
| 07c3ab9006 | |||
| b6887b94ce | |||
| fcf82fab25 | |||
| 0ac01eea47 | |||
| 765b5e48bd | |||
| 0cda695df0 | |||
| 6f950a3ccf | |||
| 9c63160dcc | |||
| 758503c85d | |||
| effe286ca8 | |||
| c3ed25c71c | |||
| b0a74d3487 | |||
| 8e1891c8cb | |||
| 3460632bf1 | |||
| da1163b2b9 | |||
| 734a5f1e80 | |||
| 6c4be65b3e | |||
| c49e9f7fc9 | |||
| e1daeec17d | |||
| 092edd5696 | |||
| 4aa8e531a6 | |||
| d0302639fa | |||
| 5312f57718 | |||
| e88a87e22e | |||
| fcf23db6be | |||
| 78d0d69cfa | |||
| 6e8dcf515a | |||
| e1d320d4bb | |||
| ce59fcf650 | |||
| 019bcd6f18 | |||
| a17934ee11 | |||
| 07776fe1d5 | |||
| 8ecbb1bea3 | |||
| 0194edd6c4 | |||
| 27c875e461 | |||
| ef4d45b20b | |||
| 88f41107c6 | |||
| 4940dc9470 | |||
| 2f9e8dddd2 | |||
| f9639f1404 | |||
| c2158534c0 | |||
| 78813b3766 | |||
| cfef0be6c5 | |||
| fa8af811d3 | |||
| 9c02db4d4d | |||
| a30b6e8547 | |||
| f83bba28b5 | |||
| f682ff4097 | |||
| d2a1df8890 | |||
| 51e55e9fba | |||
| 82243bf8c8 | |||
| 2b36e64a26 | |||
| 2503e35684 | |||
| f4aa615c74 | |||
| 6f8db95ccf | |||
| 0b4100c13e | |||
| 01375bdaa0 | |||
| e360a5dc2e | |||
| 57de85a0a0 | |||
| ee7b35cd2f | |||
| 2c5f827dcc | |||
| 14feac6458 | |||
| 4e7ee4e0b9 | |||
| 5760639c16 | |||
| 299bb8180c | |||
| 43ccc81b1b | |||
| a4d92e88cf | |||
| 0c26411ad5 | |||
| 7323860c03 | |||
| dd9a9980ad | |||
| 025cb77c93 | |||
| 2e975de3ed | |||
| 018f792769 | |||
| 41301da68e | |||
| d98864859a | |||
| 6f77e20d2b | |||
| 69048fc549 | |||
| d9a462727b | |||
| 264c286a63 | |||
| eadd93065c | |||
| 8ec33d16c7 | |||
| bc39fac69f | |||
| 431f27e7bf | |||
| c3097a8084 | |||
| 7a325a2824 | |||
| c065e8b259 | |||
| df998bfd47 | |||
| 0cd3880ef3 | |||
| 794464cdca | |||
| 32c6297d21 | |||
| ab84cbafd1 | |||
| ec8f02fe93 | |||
| 1098d89c96 | |||
| 9316aeadc8 | |||
| 9437d3f3e5 | |||
| 18b0d6e27b | |||
| 3f163bb902 | |||
| bfa3494d6c | |||
| 5302515c40 | |||
| c48c1d29ab | |||
| d78df6e8e4 | |||
| 105686c55b | |||
| 9364de6276 | |||
| 041ed9043b | |||
| 17af3c7054 | |||
| 7878dda84b | |||
| eeb9847c61 | |||
| 2e8b0d29ee | |||
| 680fc50218 | |||
| 5264d41125 | |||
| e79703fa30 | |||
| 9247c786e9 | |||
| b3363ff7b7 | |||
| 4870d18dad | |||
| 16931c9db2 | |||
| ad0188abae | |||
| 8f86be0184 | |||
| c05ead813e | |||
| e119a01a4f | |||
| 3548666713 | |||
| 4e9d0e5632 | |||
| 9624bc226c | |||
| 35e3be3c33 | |||
| 5e4e5bf13e | |||
| 9cb172a65d | |||
| 0e14c24d9a | |||
| cb247b8c0f | |||
| 2548f1ddf5 | |||
| 1aa1ffba4e | |||
| 9439acba52 | |||
| 6ee8b49e95 | |||
| ad9485bf80 | |||
| 977656caaf | |||
| 537b00e8b0 | |||
| 9ccfc52146 | |||
| 184977d3dc | |||
| c51ea2b1fb | |||
| c07afde99f | |||
| fab4e5c529 | |||
| b906e01ec6 | |||
| 66ebd9db13 | |||
| 117165c346 | |||
| e94157a6dd | |||
| 09ea3a579b | |||
| 140094cad8 | |||
| 3f9b5fa648 | |||
| 587685d0c5 | |||
| b2c3857364 | |||
| 659af23164 | |||
| c5451fb629 | |||
| 2e4f29999f | |||
| ab0045f9a4 | |||
| 36bc92acf0 | |||
| 8efeae492e | |||
| 93f67ab063 | |||
| a557f745bc | |||
| 17f0e4e776 | |||
| d99a6bbfe4 | |||
| 45ec12f24f | |||
| 5f104acde5 | |||
| 661a9ce962 | |||
| 11f0f20e34 | |||
| afaf5187e1 | |||
| ddc90af2da | |||
| 8af45284aa | |||
| 77104c61cb | |||
| 116e5abbc5 | |||
| 64796d367e | |||
| 23b8b0fdc6 | |||
| ec40091d0a | |||
| a6a970124a | |||
| 42fb4adf3e | |||
| 25e9ddf536 | |||
| f08c8b7db7 | |||
| 66f526f878 | |||
| 783477d51b | |||
| 5b665180f1 | |||
| e3d9453e61 | |||
| c8cb3660ac | |||
| 3d43a712bc | |||
| beb6ab9e4e | |||
| fd6e1984dd | |||
| 034588c559 | |||
| 66712ce0d8 | |||
| 02d483b628 | |||
| 6c5ba9b786 | |||
| 927cfa7e7d | |||
| c5bca30726 | |||
| 1f9f1afb90 | |||
| 5ec0023392 | |||
| e478edc496 | |||
| fc5a993443 | |||
| 5a890734c3 | |||
| d4f3368d28 | |||
| a7f98a4509 | |||
| 15d78691a8 | |||
| dd72df20f2 | |||
| b8a075bed6 | |||
| 71e0b0f26b | |||
| 4cee58fa33 | |||
| 024a1f04d5 | |||
| 3abaca02cc | |||
| 90fa52089e | |||
| 7b03f08342 | |||
| e8ecb6156e | |||
| 6c0ce5f8a2 | |||
| 5fe898496b | |||
| 4a27a1707b | |||
| 4bfd122d5c | |||
| 49abebf564 | |||
| 7c323f7b61 | |||
| 554ea6e8bd | |||
| f771d66f5e | |||
| 18fc1b5f5e | |||
| 0f0ed01461 | |||
| b89e5414b5 | |||
| aaa03767c8 | |||
| 9bc5163ef8 | |||
| c8a0ac0b9d | |||
| d4438e6011 | |||
| 9e1bb0d779 | |||
| 49b603a177 | |||
| 0926c469aa | |||
| c1bcf5867a | |||
| 978f3d670a | |||
| 1f88a03ab9 | |||
| bf5569ace8 | |||
| 90734c4b83 | |||
| c71635f492 | |||
| 71cc856e51 | |||
| 603c6e3717 | |||
| 08a8a41f10 | |||
| 4fa51ddcff | |||
| b14cfb6d1b | |||
| 894ab9cf82 | |||
| e82d9489b6 | |||
| 6bdf1b78b2 | |||
| eaf9f48ce8 | |||
| 0548230e7e | |||
| 3b73f08330 | |||
| fd32ceac7c | |||
| 4e98ef312b | |||
| 7dec802efd | |||
| e2a7ef37db | |||
| f363d21a13 | |||
| c9a86c636d | |||
| 923ebdfdef | |||
| 4e318cb269 | |||
| ca607d2ce0 | |||
| 8d000f1883 | |||
| 22749997d8 | |||
| e19f7c0357 | |||
| 16bad766e7 | |||
| 5bb5629bb3 | |||
| e7dc124935 | |||
| c5a33da692 | |||
| 9c41c1e66f | |||
| 36cd78e14b | |||
| bf18f9f12e | |||
| c16225e5fb | |||
| 92564aca0a | |||
| 1899587ff8 | |||
| 6a1277cc52 | |||
| 3615bec113 | |||
| c8c9171455 | |||
| 0e30f51806 | |||
| 1dad92a893 | |||
| be28d88b29 | |||
| 67719360a9 | |||
| 8c5cc0d32f | |||
| 3e6584c714 | |||
| 93643512c3 | |||
| dc6dda1726 | |||
| 293ad7857a | |||
| c741492c38 | |||
| b47c6c290d | |||
| 05711947e6 | |||
| f4a5b5e82d | |||
| e3743ea066 | |||
| 5f01161095 | |||
| 1cdcee4020 | |||
| 6ed88f797b | |||
| 399e4ead35 | |||
| be48e8d19b | |||
| 05081cc0b6 | |||
| acfb658796 | |||
| e099ffcf76 | |||
| 0f405fd48e | |||
| 71046dbd93 | |||
| f860bf0a87 | |||
| 41874ea307 | |||
| fcd49e4377 | |||
| f70abdc6f3 | |||
| df5a0e6447 | |||
| c61ed0b47c | |||
| 524779dc8c | |||
| 10fbeaa3ed | |||
| bbcc3531e1 | |||
| 8854212b09 | |||
| 4cf33f06ba | |||
| 93ca582a25 | |||
| 9eaecc9336 | |||
| ebac092c6e | |||
| 91811a4bbc | |||
| aea1811fdb | |||
| 7962b734aa | |||
| d4376c28f1 | |||
| e752a63a54 | |||
| 5a2d39ccb3 | |||
| eded187633 | |||
| 1407ac2079 | |||
| e0836aa36d | |||
| 495620cd22 | |||
| 73ba9560ea | |||
| cce376b29b | |||
| 5db3e2bae6 | |||
| 490dfa0b21 | |||
| b905dc4e0a | |||
| b25713d6fe | |||
| 15de651960 | |||
| b167c9cc11 | |||
| b263eac8ca | |||
| f7836e1c04 | |||
| b3716d7a25 | |||
| 2056d3d629 | |||
| 2bae7d4dc2 | |||
| 166de7ade5 | |||
| d55c386e8e | |||
| 59022b4e06 | |||
| 3aa5d3040b | |||
| da4e0701ee | |||
| dd27138387 | |||
| a843986e1d | |||
| 21c542e3a8 | |||
| 20e3829749 | |||
| c11eab4121 | |||
| 181f8e7813 | |||
| 1deaaf1964 | |||
| 2e869d5e8b | |||
| e812185a8e | |||
| eb090d8f10 | |||
| 35f015b33d | |||
| a585bda18f | |||
| 0d0a3381c3 | |||
| 3b97ae1814 | |||
| 9c6571471f | |||
| cd7f4b6126 | |||
| 1da88d59c8 | |||
| 18b872dea6 | |||
| cf54f312c7 | |||
| 57d3bddedf | |||
| 55b56767d6 | |||
| 5f9d59a527 | |||
| fbd10311b3 | |||
| 1780506eb3 | |||
| f8f8dee642 | |||
| 332a467776 | |||
| ecb70f2924 | |||
| a70030127e | |||
| 937c1200a3 | |||
| 1ce27d703f | |||
| b21b6214e1 | |||
| 7d94489bcb | |||
| eb851bbb53 | |||
| 945ae4127d | |||
| 8ff873452f | |||
| 83fefe7f72 | |||
| 9a47d8f32c | |||
| 5a52d16d53 | |||
| 1ecaf21c7c | |||
| ebfcdc6563 | |||
| 442a0b4edd | |||
| 49bab9140b | |||
| cb6096faca | |||
| e7381b48f2 | |||
| ea52af0c77 | |||
| 612ac0766a | |||
| 2a72cf032d | |||
| daebafdb2d | |||
| 4ab6a8552f | |||
| c6f9ae9b4f | |||
| 27dbdbe702 | |||
| 7c7ebb074e | |||
| 5446cc3dcd | |||
| f537f9c067 | |||
| 8c7892ceff | |||
| 966f3548ae | |||
| 9dbfe16744 | |||
| d5be854a11 | |||
| 627fe4cfe8 | |||
| 88ea564e66 | |||
| 09d7b78fee | |||
| 59e774be9a |
@@ -3,7 +3,7 @@ name: Core
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- master
|
||||
- main
|
||||
- MOODLE_[0-9]+_STABLE
|
||||
tags-ignore:
|
||||
- v[0-9]+.[0-9]+.[0-9]+*
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Web Installer Testing
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
InstallationTest:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
php: [8.1, 8.3]
|
||||
steps:
|
||||
- name: Construct repository URL
|
||||
run: echo "REPOSITORY_URL=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- uses: moodlehq/moodle-webinstaller-test@main
|
||||
with:
|
||||
repository: ${{ env.REPOSITORY_URL }}
|
||||
branch: ${{ github.ref_name }}
|
||||
php: ${{ matrix.php }}
|
||||
@@ -500,4 +500,15 @@ abstract class plugin_management_table extends flexible_table implements dynamic
|
||||
protected function supports_ordering(): bool {
|
||||
return $this->plugininfoclass::plugintype_supports_ordering();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has the capability to access this table.
|
||||
*
|
||||
* Default implementation for plugin management tables is to require 'moodle/site:config' capability
|
||||
*
|
||||
* @return bool Return true if capability check passed.
|
||||
*/
|
||||
public function has_capability(): bool {
|
||||
return has_capability('moodle/site:config', $this->get_context());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +177,7 @@ $CFG->admin = array_pop($parts);
|
||||
ini_set('include_path', $CFG->libdir.'/pear' . PATH_SEPARATOR . ini_get('include_path'));
|
||||
|
||||
require_once($CFG->libdir.'/classes/component.php');
|
||||
|
||||
require_once($CFG->libdir.'/classes/text.php');
|
||||
require_once($CFG->libdir.'/classes/string_manager.php');
|
||||
require_once($CFG->libdir.'/classes/string_manager_install.php');
|
||||
@@ -192,13 +193,8 @@ require_once($CFG->libdir.'/adminlib.php');
|
||||
require_once($CFG->libdir.'/componentlib.class.php');
|
||||
require_once($CFG->dirroot.'/cache/lib.php');
|
||||
|
||||
// Register our classloader, in theory somebody might want to replace it to load other hacked core classes.
|
||||
// Required because the database checks below lead to session interaction which is going to lead us to requiring autoloaded classes.
|
||||
if (defined('COMPONENT_CLASSLOADER')) {
|
||||
spl_autoload_register(COMPONENT_CLASSLOADER);
|
||||
} else {
|
||||
spl_autoload_register('core_component::classloader');
|
||||
}
|
||||
// Register our classloader.
|
||||
\core_component::register_autoloader();
|
||||
|
||||
require($CFG->dirroot.'/version.php');
|
||||
$CFG->target_release = $release;
|
||||
|
||||
@@ -4504,6 +4504,203 @@
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_oracle_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_async_backup" level="recommended">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="4.5" requires="4.1.2">
|
||||
<UNICODE level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unicoderequired" />
|
||||
</FEEDBACK>
|
||||
</UNICODE>
|
||||
<DATABASE level="required">
|
||||
<VENDOR name="mariadb" version="10.6.7" />
|
||||
<VENDOR name="mysql" version="8.0" />
|
||||
<VENDOR name="postgres" version="13" />
|
||||
<VENDOR name="mssql" version="14.0" />
|
||||
<VENDOR name="oracle" version="19" />
|
||||
</DATABASE>
|
||||
<PHP version="8.1.0" level="required">
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="pcreunicodewarning" />
|
||||
</FEEDBACK>
|
||||
</PCREUNICODE>
|
||||
<PHP_EXTENSIONS>
|
||||
<PHP_EXTENSION name="iconv" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="iconvrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="mbstring" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="mbstringrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="curl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="curlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="openssl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="opensslrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="tokenizer" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="tokenizerrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="soap" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="soaprecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="ctype" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ctyperequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zip" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ziprequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zlib" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="gd" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="gdrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="simplexml" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="simplexmlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="spl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="splrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="pcre" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="dom" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xml" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xmlreader" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="intl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="intlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="json" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="hash" level="required"/>
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="required"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="settingmemorylimit" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="file_uploads" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="settingfileuploads" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="opcache.enable" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="opcacherecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
</PHP_SETTINGS>
|
||||
<CUSTOM_CHECKS>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbstorageengine" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="quizattemptsupgradedmessage" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="slashargumentswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unsupporteddbtablerowformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unoconvwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="libcurlwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfileformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfilepertable" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddblargeprefix" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="ishttpswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="incompleteunicodesupport" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="sixtyfourbitsrequired" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_max_input_vars" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_admin_dir_usage" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="iscustomadminwarnings" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_xmlrpc_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mod_assignment" level="required">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_db_prefix_length" level="required">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_oracle_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_async_backup" level="recommended">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
</COMPATIBILITY_MATRIX>
|
||||
|
||||
+7
-4
@@ -874,8 +874,8 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
protected function mobile_configuration_warning($mobileconfigured) {
|
||||
$output = '';
|
||||
if (!$mobileconfigured) {
|
||||
$settingslink = new moodle_url('/admin/settings.php', ['section' => 'mobilesettings']);
|
||||
$configurebutton = $this->single_button($settingslink, get_string('enablemobilewebservice', 'admin'));
|
||||
$settingslink = new moodle_url('/admin/search.php', ['query' => 'enablemobilewebservice']);
|
||||
$configurebutton = $this->single_button($settingslink, get_string('enablemobilewebservice', 'admin'), 'get');
|
||||
$output .= $this->warning(get_string('mobilenotconfiguredwarning', 'admin') . ' ' . $configurebutton);
|
||||
}
|
||||
|
||||
@@ -2051,8 +2051,10 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
// We are checking installed & enabled things
|
||||
if ($environment_result->getLevel() == 'required') {
|
||||
$stringtouse = 'environmentrequirecustomcheck';
|
||||
} else {
|
||||
} else if ($environment_result->getLevel() == 'optional') {
|
||||
$stringtouse = 'environmentrecommendcustomcheck';
|
||||
} else {
|
||||
$stringtouse = 'environmentshouldfixcustomcheck';
|
||||
}
|
||||
|
||||
} else if ($environment_result->getPart() == 'php_setting') {
|
||||
@@ -2083,7 +2085,8 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
if ($status) { //Handle ok result (ok)
|
||||
$status = get_string('statusok');
|
||||
} else {
|
||||
if ($environment_result->getLevel() == 'optional') {//Handle check result (warning)
|
||||
// Handle check result (warning).
|
||||
if (in_array($environment_result->getLevel(), ['optional', 'recommended'])) {
|
||||
$status = get_string('check');
|
||||
$warningline = true;
|
||||
} else { //Handle error result (error)
|
||||
|
||||
@@ -152,7 +152,9 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
|
||||
$sensiblesettingsdefault .= 'server_password@@search_solr, ssl_keypassword@@search_solr, ';
|
||||
$sensiblesettingsdefault .= 'alternateserver_password@@search_solr, alternatessl_keypassword@@search_solr, ';
|
||||
$sensiblesettingsdefault .= 'test_password@@cachestore_redis, password@@mlbackend_python, ';
|
||||
$sensiblesettingsdefault .= 'badges_badgesalt@@none, calendar_exportsalt@@none';
|
||||
$sensiblesettingsdefault .= 'badges_badgesalt@@none, calendar_exportsalt@@none, ';
|
||||
$sensiblesettingsdefault .= 'bigbluebuttonbn_shared_secret@@none, apikey@@tiny_premium, ';
|
||||
$sensiblesettingsdefault .= 'matrixaccesstoken@@communication_matrix';
|
||||
$temp->add(new admin_setting_configtextarea('adminpresets/sensiblesettings',
|
||||
get_string('sensiblesettings', 'core_adminpresets'),
|
||||
get_string('sensiblesettingstext', 'core_adminpresets'),
|
||||
|
||||
@@ -38,6 +38,6 @@
|
||||
}
|
||||
}}
|
||||
<div class="form-checkbox defaultsnext">
|
||||
<input type="hidden" name="{{name}}" value="{{no}}">
|
||||
{{^readonly}}<input type="hidden" name="{{name}}" value="{{no}}">{{/readonly}}
|
||||
<input {{#readonly}}disabled{{/readonly}} type="checkbox" name="{{name}}" value="{{value}}" id="{{id}}" {{#checked}}checked{{/checked}}>
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
}}
|
||||
<div class="form-multicheckbox">
|
||||
<input type="hidden" name="{{name}}[xxxxx]" value="1">
|
||||
{{^readonly}}<input type="hidden" name="{{name}}[xxxxx]" value="1">{{/readonly}}
|
||||
{{#hasoptions}}
|
||||
<ul>
|
||||
{{#options}}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
Setting configmultiselect.
|
||||
}}
|
||||
<div class="form-select">
|
||||
<input type="hidden" name="{{name}}[xxxxx]" value="1">
|
||||
{{^readonly}}<input type="hidden" name="{{name}}[xxxxx]" value="1">{{/readonly}}
|
||||
<select {{#readonly}}disabled{{/readonly}} id="{{id}}" name="{{name}}[]" size="{{size}}" class="form-control" multiple>
|
||||
{{#options}}
|
||||
<option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
Setting configmultiselect with optgroup support.
|
||||
}}
|
||||
<div class="form-select">
|
||||
<input type="hidden" name="{{name}}[xxxxx]" value="1">
|
||||
{{^readonly}}<input type="hidden" name="{{name}}[xxxxx]" value="1">{{/readonly}}
|
||||
<select {{#readonly}}disabled{{/readonly}} id="{{id}}" name="{{name}}[]" size="{{size}}" class="form-control" multiple>
|
||||
{{#options}}
|
||||
<option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
|
||||
|
||||
@@ -18,7 +18,7 @@ Feature: An administrator can browse user accounts
|
||||
When I navigate to "Users > Accounts > Browse list of users" in site administration
|
||||
# Name field always present, email field is default for showidentity.
|
||||
Then the following should exist in the "users" table:
|
||||
| First name / Last name | Email address |
|
||||
| First name | Email address |
|
||||
| User One | one@example.com |
|
||||
| User Two | two@example.com |
|
||||
# Should not see other identity fields or non-default name fields.
|
||||
@@ -34,7 +34,7 @@ Feature: An administrator can browse user accounts
|
||||
| alternativefullnameformat | firstnamephonetic lastname |
|
||||
When I navigate to "Users > Accounts > Browse list of users" in site administration
|
||||
Then the following should exist in the "users" table:
|
||||
| First name - phonetic / Last name | Email address |
|
||||
| First name - phonetic | Email address |
|
||||
| Yewzer One | one@example.com |
|
||||
| Yoozare Two | two@example.com |
|
||||
|
||||
@@ -43,7 +43,7 @@ Feature: An administrator can browse user accounts
|
||||
| showuseridentity | department,profile_field_frog |
|
||||
When I navigate to "Users > Accounts > Browse list of users" in site administration
|
||||
Then the following should exist in the "users" table:
|
||||
| First name / Last name | Favourite frog | Department |
|
||||
| First name | Favourite frog | Department |
|
||||
| User One | Kermit | Attack |
|
||||
| User Two | Tree | Defence |
|
||||
And I should not see "Email address" in the "table" "css_element"
|
||||
|
||||
@@ -48,25 +48,3 @@ Feature: View task logs report and use its filters
|
||||
| operator | shouldornotsee |
|
||||
| Less than | should not see |
|
||||
| Greater than | should see |
|
||||
|
||||
@javascript
|
||||
Scenario: Reset task log filters
|
||||
Given I log in as "admin"
|
||||
And I change window size to "large"
|
||||
And I navigate to "Server > Tasks > Task logs" in site administration
|
||||
When I click on "Filters" "button"
|
||||
And I set the following fields in the "Result" "core_reportbuilder > Filter" to these values:
|
||||
| Result operator | Is equal to |
|
||||
| Result value | Fail |
|
||||
And I click on "Apply" "button" in the "[data-region='report-filters']" "css_element"
|
||||
Then I should see "Filters applied"
|
||||
And I should see "Filters (1)" in the "#dropdownFiltersButton" "css_element"
|
||||
And I should see "Nothing to display"
|
||||
And I click on "Reset all" "button" in the "[data-region='report-filters']" "css_element"
|
||||
And I should see "Filters reset"
|
||||
And I should not see "Filters (1)" in the "#dropdownFiltersButton" "css_element"
|
||||
And I should see "Filters" in the "#dropdownFiltersButton" "css_element"
|
||||
And "[data-region='report-filters']" "css_element" should be visible
|
||||
And the following fields in the "Result" "core_reportbuilder > Filter" match these values:
|
||||
| Result operator | Is any value |
|
||||
And I should not see "Nothing to display"
|
||||
|
||||
@@ -5,16 +5,18 @@ Feature: I can download a preset
|
||||
| name |
|
||||
| Custom preset |
|
||||
|
||||
@javascript
|
||||
Scenario: Custom preset settings can be downloaded
|
||||
Given I log in as "admin"
|
||||
And I navigate to "Site admin presets" in site administration
|
||||
When I open the action menu in "Custom preset" "table_row"
|
||||
Then following "Download" "link" in the "Custom preset" "table_row" should download between "0" and "5000" bytes
|
||||
Then following "Download" in the "Custom preset" "table_row" should download a file that:
|
||||
| Has mimetype | text/xml |
|
||||
| Contains text in xml element | Custom preset |
|
||||
|
||||
@javascript
|
||||
Scenario: Core preset settings can be downloaded
|
||||
Given I log in as "admin"
|
||||
And I navigate to "Site admin presets" in site administration
|
||||
When I open the action menu in "Starter" "table_row"
|
||||
Then following "Download" "link" in the "Starter" "table_row" should download between "0" and "5000" bytes
|
||||
Then following "Download" in the "Starter" "table_row" should download a file that:
|
||||
| Has mimetype | text/xml |
|
||||
| Contains text in xml element | Starter |
|
||||
|
||||
@@ -25,7 +25,7 @@ Feature: Confirm that we can open multiple browser tabs
|
||||
And I open a tab named "CourseViewer4" on the "My courses" page
|
||||
|
||||
# Switch between all the tabs and confirm their different contents.
|
||||
Then I should see "You're not enrolled in any course"
|
||||
Then I should see "You're not enrolled in any courses."
|
||||
And I switch to "CourseViewer2" tab
|
||||
And "Course 3" "heading" should exist
|
||||
And I switch to "CourseViewer1" tab
|
||||
|
||||
+29
-11
@@ -245,7 +245,6 @@ class brickfield_accessibility_color_test extends brickfield_accessibility_test
|
||||
return $luminosity;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the decimal equivalents for a HEX color. Returns null if it cannot be determined.
|
||||
* @param string $color The hex color value
|
||||
@@ -268,13 +267,21 @@ class brickfield_accessibility_color_test extends brickfield_accessibility_test
|
||||
*/
|
||||
public function convert_color(string $color): string {
|
||||
$color = trim($color);
|
||||
if (strpos($color, ' ') !== false) {
|
||||
$colors = explode(' ', $color);
|
||||
foreach ($colors as $backgroundpart) {
|
||||
if (substr(trim($backgroundpart), 0, 1) == '#' ||
|
||||
in_array(trim($backgroundpart), array_keys($this->colornames)) ||
|
||||
strtolower(substr(trim($backgroundpart), 0, 3)) == 'rgb') {
|
||||
$color = $backgroundpart;
|
||||
// Search for color in rgb format first, as this can potentially contain a space.
|
||||
if (strpos($color, 'rgb') !== false) {
|
||||
$colors = explode('rgb', $color, 2); // Getting 2 only in array.
|
||||
// Getting end point of rgb value, i.e. the end bracket.
|
||||
$endpos = strpos($colors[1], ')');
|
||||
$color = 'rgb' . substr($colors[1], 0, ($endpos + 1)); // Recompiling rgb value.
|
||||
} else {
|
||||
// Splitting multi-value css background value.
|
||||
if (strpos($color, ' ') !== false) {
|
||||
$colors = explode(' ', $color);
|
||||
foreach ($colors as $backgroundpart) {
|
||||
if (substr(trim($backgroundpart), 0, 1) == '#' ||
|
||||
in_array(trim($backgroundpart), array_keys($this->colornames))) {
|
||||
$color = $backgroundpart;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,9 +303,20 @@ class brickfield_accessibility_color_test extends brickfield_accessibility_test
|
||||
}
|
||||
// RGB values.
|
||||
if (strtolower(substr($color, 0, 3)) == 'rgb') {
|
||||
$colors = explode(',', trim(str_replace('rgb(', '', $color), '()'));
|
||||
if (count($colors) != 3) {
|
||||
return false;
|
||||
if (strpos($color, 'rgba') !== false) {
|
||||
$tmpbg = $this->get_rgb($this->defaultbackground);
|
||||
$colors = explode(',', trim(str_replace('rgba(', '', $color), '()'));
|
||||
if (count($colors) != 4) {
|
||||
return false;
|
||||
}
|
||||
$colors[0] = round(((1 - $colors[3]) * $tmpbg['r']) + ($colors[3] * $colors[0]));
|
||||
$colors[1] = round((1 - $colors[3]) * $tmpbg['g']) + ($colors[3] * $colors[1]);
|
||||
$colors[2] = round((1 - $colors[3]) * $tmpbg['b']) + ($colors[3] * $colors[2]);
|
||||
} else {
|
||||
$colors = explode(',', trim(str_replace('rgb(', '', $color), '()'));
|
||||
if (count($colors) != 3) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$r = intval($colors[0]);
|
||||
$g = intval($colors[1]);
|
||||
|
||||
+12
-7
@@ -62,9 +62,18 @@ class css_text_has_contrast extends brickfield_accessibility_color_test {
|
||||
foreach ($entries as $element) {
|
||||
$style = $this->css->get_style($element);
|
||||
|
||||
if (isset($style['background-color']) || isset($style['color'])) {
|
||||
if (isset($style['background-color']) || isset($style['color']) || isset($style['background'])) {
|
||||
if (!isset($style['background-color'])) {
|
||||
$style['background-color'] = $this->defaultbackground;
|
||||
if (isset($style['background'])) {
|
||||
// Parsing background-color from CSS background shortcut string.
|
||||
$style['background-color'] = '#' . $this->convert_color($style['background']);
|
||||
// If value is empty after hash, then use defaultbackground.
|
||||
if ($style['background-color'] == '#') {
|
||||
$style['background-color'] = $this->defaultbackground;
|
||||
}
|
||||
} else {
|
||||
$style['background-color'] = $this->defaultbackground;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($style['color'])) {
|
||||
@@ -82,11 +91,7 @@ class css_text_has_contrast extends brickfield_accessibility_color_test {
|
||||
$style['color'] = '#' . $this->convert_color($style['color']);
|
||||
$style['background-color'] = '#' . $this->convert_color($background);
|
||||
|
||||
if (substr($background, 0, 3) == "rgb") {
|
||||
$background = '#' . $this->convert_color($background);
|
||||
}
|
||||
|
||||
$luminosity = $this->get_luminosity($style['color'], $background);
|
||||
$luminosity = $this->get_luminosity($style['color'], $style['background-color']);
|
||||
$fontsize = 0;
|
||||
$bold = false;
|
||||
$italic = false;
|
||||
|
||||
+124
-13
@@ -30,6 +30,7 @@ require_once('all_checks.php');
|
||||
|
||||
/**
|
||||
* Class test_css_text_has_contrast_test
|
||||
* @covers \tool_brickfield\local\htmlchecker\brickfield_accessibility
|
||||
*/
|
||||
class css_text_has_contrast_test extends all_checks {
|
||||
/** @var string The check type. */
|
||||
@@ -229,7 +230,7 @@ EOD;
|
||||
/**
|
||||
* Test for the area assign intro
|
||||
*/
|
||||
public function test_check() {
|
||||
public function test_check(): void {
|
||||
$results = $this->get_checker_results($this->htmlfail1);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
|
||||
@@ -255,7 +256,7 @@ EOD;
|
||||
/**
|
||||
* Test with valid colour names.
|
||||
*/
|
||||
public function test_check_for_namedcolours() {
|
||||
public function test_check_for_namedcolours(): void {
|
||||
$results = $this->get_checker_results($this->namecolours);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
}
|
||||
@@ -263,7 +264,7 @@ EOD;
|
||||
/**
|
||||
* Test with invalid colour names.
|
||||
*/
|
||||
public function test_check_for_invalidcolours() {
|
||||
public function test_check_for_invalidcolours(): void {
|
||||
$results = $this->get_checker_results($this->invalidcolours);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
}
|
||||
@@ -271,7 +272,7 @@ EOD;
|
||||
/**
|
||||
* Test with invalid colour numeric values.
|
||||
*/
|
||||
public function test_check_for_invalidvalues() {
|
||||
public function test_check_for_invalidvalues(): void {
|
||||
$results = $this->get_checker_results($this->invalidvalue);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
}
|
||||
@@ -279,7 +280,7 @@ EOD;
|
||||
/**
|
||||
* Test with empty colour values.
|
||||
*/
|
||||
public function test_check_for_emptyvalues() {
|
||||
public function test_check_for_emptyvalues(): void {
|
||||
$results = $this->get_checker_results($this->emptyvalue);
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
@@ -287,7 +288,7 @@ EOD;
|
||||
/**
|
||||
* Test for text px18 with insufficient contrast of 4.49.
|
||||
*/
|
||||
public function test_check_for_px18_fail() {
|
||||
public function test_check_for_px18_fail(): void {
|
||||
$results = $this->get_checker_results($this->px18);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
}
|
||||
@@ -295,7 +296,7 @@ EOD;
|
||||
/**
|
||||
* Test for text px19 bold with sufficient contrast of 4.49.
|
||||
*/
|
||||
public function test_check_for_px19bold_pass() {
|
||||
public function test_check_for_px19bold_pass(): void {
|
||||
$results = $this->get_checker_results($this->px19bold);
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
@@ -303,7 +304,7 @@ EOD;
|
||||
/**
|
||||
* Test for text px18 with sufficient contrast of 4.81.
|
||||
*/
|
||||
public function test_check_for_px18_pass() {
|
||||
public function test_check_for_px18_pass(): void {
|
||||
$results = $this->get_checker_results($this->px18pass);
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
@@ -311,7 +312,7 @@ EOD;
|
||||
/**
|
||||
* Test for medium (12pt) text with insufficient contrast of 4.49.
|
||||
*/
|
||||
public function test_check_for_medium_fail() {
|
||||
public function test_check_for_medium_fail(): void {
|
||||
$results = $this->get_checker_results($this->mediumfail);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
}
|
||||
@@ -319,7 +320,7 @@ EOD;
|
||||
/**
|
||||
* Test for medium (12pt) text with sufficient contrast of 4.81.
|
||||
*/
|
||||
public function test_check_for_medium_pass() {
|
||||
public function test_check_for_medium_pass(): void {
|
||||
$results = $this->get_checker_results($this->mediumpass);
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
@@ -327,7 +328,7 @@ EOD;
|
||||
/**
|
||||
* Test for larger (14pt) text with insufficient contrast of 2.94.
|
||||
*/
|
||||
public function test_check_for_larger_fail() {
|
||||
public function test_check_for_larger_fail(): void {
|
||||
$results = $this->get_checker_results($this->largerfail);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
}
|
||||
@@ -335,7 +336,7 @@ EOD;
|
||||
/**
|
||||
* Test for larger (14pt) text with insufficient contrast of 3.02.
|
||||
*/
|
||||
public function test_check_for_larger_pass() {
|
||||
public function test_check_for_larger_pass(): void {
|
||||
$results = $this->get_checker_results($this->largerpass);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
}
|
||||
@@ -343,8 +344,118 @@ EOD;
|
||||
/**
|
||||
* Test for larger (14pt) bold text with sufficient contrast of 3.02.
|
||||
*/
|
||||
public function test_check_for_largerbold_pass() {
|
||||
public function test_check_for_largerbold_pass(): void {
|
||||
$results = $this->get_checker_results($this->largerboldpass);
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for rgb colors with insufficient contrast.
|
||||
*/
|
||||
public function test_bad_rgbcolor(): void {
|
||||
$html = '<body><p style="color:rgb(255, 255, 255); background-color:rgb(204, 204, 204);">
|
||||
This is not contrasty enough.</p></body>';
|
||||
$results = $this->get_checker_results($html);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for rgb colors with sufficient contrast.
|
||||
*/
|
||||
public function test_good_rgbcolor(): void {
|
||||
$html = '<body><p style="color:rgb(255, 255, 255); background-color:rgb(0, 0, 0);">
|
||||
This is contrasty enough.</p></body>';
|
||||
$results = $this->get_checker_results($html);
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for named colors with insufficient contrast.
|
||||
*/
|
||||
public function test_bad_namedcolor2(): void {
|
||||
$html = '<body><p style="color:lightcyan; background-color:lavender;">
|
||||
This is not contrasty enough.</p></body>';
|
||||
$results = $this->get_checker_results($html);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for named colors with sufficient contrast.
|
||||
*/
|
||||
public function test_good_namedcolor2(): void {
|
||||
$html = '<body><p style="color:linen; background-color:darkslategray;">
|
||||
This is contrasty enough.</p></body>';
|
||||
$results = $this->get_checker_results($html);
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for background value with insufficient contrast.
|
||||
*/
|
||||
public function test_bad_backgroundcss(): void {
|
||||
$html = '<body><p style="color:lightcyan; background:fixed lavender center;">
|
||||
This is not contrasty enough.</p></body>';
|
||||
$results = $this->get_checker_results($html);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for background value with sufficient contrast.
|
||||
*/
|
||||
public function test_good_backgroundcss(): void {
|
||||
$html = '<body><p style="color:linen; background:fixed darkslategray center;">
|
||||
This is contrasty enough.</p></body>';
|
||||
$results = $this->get_checker_results($html);
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for background value with rgb with insufficient contrast.
|
||||
*/
|
||||
public function test_bad_backgroundcssrgb(): void {
|
||||
$html = '<body><p style="color:rgb(255, 255, 255); background:fixed rgb(204, 204, 204) center;">
|
||||
This is not contrasty enough.</p></body>';
|
||||
$results = $this->get_checker_results($html);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for background value with rgb with sufficient contrast.
|
||||
*/
|
||||
public function test_good_backgroundcssrgb(): void {
|
||||
$html = '<body><p style="color:rgb(255, 255, 255); background:fixed rgb(0, 0, 0) center;">
|
||||
This is contrasty enough.</p></body>';
|
||||
$results = $this->get_checker_results($html);
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for text with insufficient contrast of 4.3.
|
||||
*/
|
||||
public function test_bad_contrastrounding(): void {
|
||||
$html = '<body><p style="color:#F50000; background-color:white; font-size: 12px">
|
||||
This is not contrasty enough.</p></body>';
|
||||
$results = $this->get_checker_results($html);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for background value with rgba with insufficient contrast.
|
||||
*/
|
||||
public function test_bad_backgroundcssrgba(): void {
|
||||
$html = '<body><p style="color:rgba(255, 255, 255, 0.5); background:fixed rgba(0, 204, 204, 0.5) center;">
|
||||
This is not contrasty enough.</p></body>';
|
||||
$results = $this->get_checker_results($html);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for background value with rgba with sufficient contrast.
|
||||
*/
|
||||
public function test_good_backgroundcssrgba(): void {
|
||||
$html = '<body><p style="color:rgba(255, 255, 255, 0.75); background:fixed rgba(0, 0, 0, 0.75) center;">
|
||||
This is contrasty enough.</p></body>';
|
||||
$results = $this->get_checker_results($html);
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,9 +156,9 @@ class data_request extends persistent {
|
||||
case api::DATAREQUEST_STATUS_EXPIRED:
|
||||
$result = true;
|
||||
break;
|
||||
// Complete requests are expired if the expiry time has elapsed.
|
||||
// Complete requests are expired if the expiry time is a positive value, and has elapsed.
|
||||
case api::DATAREQUEST_STATUS_DOWNLOAD_READY:
|
||||
$expiryseconds = get_config('tool_dataprivacy', 'privacyrequestexpiry');
|
||||
$expiryseconds = (int) get_config('tool_dataprivacy', 'privacyrequestexpiry');
|
||||
if ($expiryseconds > 0 && time() >= ($request->get('timemodified') + $expiryseconds)) {
|
||||
$result = true;
|
||||
}
|
||||
@@ -179,7 +179,12 @@ class data_request extends persistent {
|
||||
public static function get_expired_requests($userid = 0) {
|
||||
global $DB;
|
||||
|
||||
$expiryseconds = get_config('tool_dataprivacy', 'privacyrequestexpiry');
|
||||
// Complete requests are expired if the expiry time is a positive value, and has elapsed.
|
||||
$expiryseconds = (int) get_config('tool_dataprivacy', 'privacyrequestexpiry');
|
||||
if ($expiryseconds <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$expirytime = strtotime("-{$expiryseconds} second");
|
||||
$table = self::TABLE;
|
||||
$sqlwhere = 'type = :export_type AND status = :completestatus AND timemodified <= :expirytime';
|
||||
|
||||
@@ -54,6 +54,7 @@ Feature: Data delete from the privacy API
|
||||
And I should see "Awaiting approval" in the "Victim User 1" "table_row"
|
||||
And I open the action menu in "Victim User 1" "table_row"
|
||||
And I follow "Approve request"
|
||||
And I wait until "Approve request" "button" exists
|
||||
And I press "Approve request"
|
||||
And I should see "Approved" in the "Victim User 1" "table_row"
|
||||
And I run all adhoc tasks
|
||||
@@ -80,6 +81,7 @@ Feature: Data delete from the privacy API
|
||||
And I navigate to "Users > Privacy and policies > Data requests" in site administration
|
||||
And I open the action menu in "Victim User 1" "table_row"
|
||||
And I follow "Approve request"
|
||||
And I wait until "Approve request" "button" exists
|
||||
And I press "Approve request"
|
||||
|
||||
And I log out
|
||||
@@ -115,6 +117,7 @@ Feature: Data delete from the privacy API
|
||||
And I navigate to "Users > Privacy and policies > Data requests" in site administration
|
||||
And I open the action menu in "Victim User 1" "table_row"
|
||||
And I follow "Approve request"
|
||||
And I wait until "Approve request" "button" exists
|
||||
And I press "Approve request"
|
||||
|
||||
And I log out
|
||||
|
||||
@@ -48,13 +48,15 @@ Feature: Data export from the privacy API
|
||||
And I should see "Awaiting approval" in the "Victim User 1" "table_row"
|
||||
And I open the action menu in "Victim User 1" "table_row"
|
||||
And I follow "Approve request"
|
||||
And I wait until "Approve request" "button" exists
|
||||
And I press "Approve request"
|
||||
And I should see "Approved" in the "Victim User 1" "table_row"
|
||||
And I run all adhoc tasks
|
||||
And I reload the page
|
||||
And I should see "Download ready" in the "Victim User 1" "table_row"
|
||||
And I open the action menu in "Victim User 1" "table_row"
|
||||
And following "Download" should download between "1" and "200000" bytes
|
||||
And following "Download" should download a file that:
|
||||
| Contains file in zip | index.html |
|
||||
And the following config values are set as admin:
|
||||
| privacyrequestexpiry | 1 | tool_dataprivacy |
|
||||
And I wait "1" seconds
|
||||
@@ -89,7 +91,8 @@ Feature: Data export from the privacy API
|
||||
And I reload the page
|
||||
And I should see "Download ready" in the "Export all of my personal data" "table_row"
|
||||
And I open the action menu in "Victim User 1" "table_row"
|
||||
And following "Download" should download between "1" and "200000" bytes
|
||||
And following "Download" should download a file that:
|
||||
| Contains file in zip | index.html |
|
||||
|
||||
And the following config values are set as admin:
|
||||
| privacyrequestexpiry | 1 | tool_dataprivacy |
|
||||
@@ -115,6 +118,7 @@ Feature: Data export from the privacy API
|
||||
And I navigate to "Users > Privacy and policies > Data requests" in site administration
|
||||
And I open the action menu in "Victim User 1" "table_row"
|
||||
And I follow "Approve request"
|
||||
And I wait until "Approve request" "button" exists
|
||||
And I press "Approve request"
|
||||
|
||||
And I log out
|
||||
@@ -126,7 +130,8 @@ Feature: Data export from the privacy API
|
||||
And I reload the page
|
||||
And I should see "Download ready" in the "Victim User 1" "table_row"
|
||||
And I open the action menu in "Victim User 1" "table_row"
|
||||
And following "Download" should download between "1" and "200000" bytes
|
||||
And following "Download" should download a file that:
|
||||
| Contains file in zip | index.html |
|
||||
|
||||
And the following config values are set as admin:
|
||||
| privacyrequestexpiry | 1 | tool_dataprivacy |
|
||||
@@ -185,7 +190,8 @@ Feature: Data export from the privacy API
|
||||
And I reload the page
|
||||
And I should see "Download ready" in the "Victim User 1" "table_row"
|
||||
And I open the action menu in "Victim User 1" "table_row"
|
||||
And following "Download" should download between "1" and "170000" bytes
|
||||
And following "Download" should download a file that:
|
||||
| Contains file in zip | index.html |
|
||||
And the following config values are set as admin:
|
||||
| privacyrequestexpiry | 1 | tool_dataprivacy |
|
||||
And I wait "1" seconds
|
||||
@@ -229,7 +235,8 @@ Feature: Data export from the privacy API
|
||||
And I reload the page
|
||||
And I should see "Download ready" in the "Victim User 1" "table_row"
|
||||
And I open the action menu in "Victim User 1" "table_row"
|
||||
And following "Download" should download between "1" and "180000" bytes
|
||||
And following "Download" should download a file that:
|
||||
| Contains file in zip | index.html |
|
||||
|
||||
@javascript
|
||||
Scenario: Filter before export data for a user and download it in the view request action
|
||||
@@ -262,4 +269,5 @@ Feature: Data export from the privacy API
|
||||
And I reload the page
|
||||
And I should see "Download ready" in the "Victim User 1" "table_row"
|
||||
And I open the action menu in "Victim User 1" "table_row"
|
||||
And following "Download" should download between "1" and "180000" bytes
|
||||
And following "Download" should download a file that:
|
||||
| Contains file in zip | index.html |
|
||||
|
||||
@@ -26,10 +26,11 @@ require_once('data_privacy_testcase.php');
|
||||
* Expired data requests tests.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @covers \tool_dataprivacy\data_request
|
||||
* @copyright 2018 Michael Hawkins
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class expired_data_requests_test extends data_privacy_testcase {
|
||||
final class expired_data_requests_test extends data_privacy_testcase {
|
||||
|
||||
/**
|
||||
* Test tearDown.
|
||||
@@ -107,6 +108,52 @@ class expired_data_requests_test extends data_privacy_testcase {
|
||||
$this->assertEquals(0, $DB->count_records('files', $fileconditions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that data requests are not expired when expiration is disabled (set to zero)
|
||||
*/
|
||||
public function test_data_request_expiry_never(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
\core_privacy\local\request\writer::setup_real_writer_instance();
|
||||
|
||||
// Disable request expiry.
|
||||
set_config('privacyrequestexpiry', 0, 'tool_dataprivacy');
|
||||
|
||||
// Create and approve data request.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$usercontext = \context_user::instance($user->id);
|
||||
|
||||
$this->setUser($user->id);
|
||||
$datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT);
|
||||
$requestid = $datarequest->get('id');
|
||||
|
||||
$this->setAdminUser();
|
||||
api::approve_data_request($requestid);
|
||||
|
||||
ob_start();
|
||||
$this->runAdhocTasks('\tool_dataprivacy\task\process_data_request_task');
|
||||
ob_end_clean();
|
||||
|
||||
// Run expiry deletion - should not affect test export.
|
||||
$expiredrequests = data_request::get_expired_requests();
|
||||
$this->assertEmpty($expiredrequests);
|
||||
data_request::expire($expiredrequests);
|
||||
|
||||
// Confirm approved and exported.
|
||||
$request = new data_request($requestid);
|
||||
$this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status'));
|
||||
$fileconditions = [
|
||||
'userid' => $user->id,
|
||||
'component' => 'tool_dataprivacy',
|
||||
'filearea' => 'export',
|
||||
'itemid' => $requestid,
|
||||
'contextid' => $usercontext->id,
|
||||
];
|
||||
$this->assertEquals(2, $DB->count_records('files', $fileconditions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for \tool_dataprivacy\data_request::is_expired()
|
||||
* Tests for the expected request status to protect from false positive/negative,
|
||||
|
||||
@@ -57,7 +57,7 @@ class locale {
|
||||
}
|
||||
|
||||
// Store current locale.
|
||||
$currentlocale = $this->set_locale(LC_ALL, 0);
|
||||
$currentlocale = $this->get_locale();
|
||||
|
||||
$locale = get_string_manager()->get_string($stringtofetch, 'langconfig', $a = null, $langpackcode);
|
||||
|
||||
@@ -79,19 +79,16 @@ class locale {
|
||||
* @return string|false Returns the new current locale, or FALSE on error.
|
||||
*/
|
||||
protected function set_locale(int $category = LC_ALL, string $locale = '0') {
|
||||
if (strlen($locale) <= 255 || PHP_OS_FAMILY === 'BSD' || PHP_OS_FAMILY === 'Darwin') {
|
||||
// We can set the whole locale all together.
|
||||
return setlocale($category, $locale);
|
||||
}
|
||||
return \core\locale::set_locale($category, $locale);
|
||||
}
|
||||
|
||||
// Too long locale with linux or windows, let's split it into known and supported categories.
|
||||
$split = explode(';', $locale);
|
||||
foreach ($split as $element) {
|
||||
[$category, $value] = explode('=', $element);
|
||||
if (defined($category)) { // Only if the category exists, there are OS differences.
|
||||
setlocale(constant($category), $value);
|
||||
}
|
||||
}
|
||||
return setlocale(LC_ALL, 0); // Finally, return the complete configured locale.
|
||||
/**
|
||||
* Get the current locale.
|
||||
*
|
||||
* @param int $category
|
||||
* @return string|false
|
||||
*/
|
||||
protected function get_locale(int $category = LC_ALL): string|false {
|
||||
return \core\locale::get_locale($category);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,39 +21,59 @@ namespace tool_langimport;
|
||||
*
|
||||
* @package tool_langimport
|
||||
* @category test
|
||||
* @coversDefaultClass \tool_langimport\locale
|
||||
* @covers \tool_langimport\locale
|
||||
* @copyright 2018 Université Rennes 2 {@link https://www.univ-rennes2.fr}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class locale_test extends \advanced_testcase {
|
||||
final class locale_test extends \advanced_testcase {
|
||||
/** @var string Locale */
|
||||
protected string $locale;
|
||||
|
||||
#[\Override]
|
||||
public function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->locale = \core\locale::get_locale();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function tearDown(): void {
|
||||
parent::tearDown();
|
||||
\core\locale::set_locale(LC_ALL, $this->locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that \tool_langimport\locale::check_locale_availability() works as expected.
|
||||
*
|
||||
* @covers ::check_locale_availability
|
||||
* @return void
|
||||
*/
|
||||
public function test_check_locale_availability() {
|
||||
// Create a mock of set_locale() method to simulate :
|
||||
// - first setlocale() call which backup current locale
|
||||
// - second setlocale() call which try to set new 'es' locale
|
||||
// - third setlocale() call which restore locale.
|
||||
public function test_check_locale_availability(): void {
|
||||
// Create a mock of set_locale() method to simulate:
|
||||
// - get_locale() call which backup current locale
|
||||
// - first set_locale() call which try to set new 'es' locale
|
||||
// - second set_locale() call which restore locale.
|
||||
$mock = $this->getMockBuilder(locale::class)
|
||||
->onlyMethods(['set_locale'])
|
||||
->onlyMethods([
|
||||
'get_locale',
|
||||
'set_locale',
|
||||
])
|
||||
->getMock();
|
||||
$mock->method('set_locale')->will($this->onConsecutiveCalls('en', 'es', 'en'));
|
||||
$mock->method('get_locale')->will($this->onConsecutiveCalls('en'));
|
||||
$mock->method('set_locale')->will($this->onConsecutiveCalls('es', 'en'));
|
||||
|
||||
// Test what happen when locale is available on system.
|
||||
$result = $mock->check_locale_availability('en');
|
||||
$this->assertTrue($result);
|
||||
|
||||
// Create a mock of set_locale() method to simulate :
|
||||
// - first setlocale() call which backup current locale
|
||||
// - second setlocale() call which fail to set new locale
|
||||
// - third setlocale() call which restore locale.
|
||||
// Create a mock of set_locale() method to simulate:
|
||||
// - get_locale() call which backup current locale
|
||||
// - first set_locale() call which fail to set new locale
|
||||
// - second set_locale() call which restore locale.
|
||||
$mock = $this->getMockBuilder(locale::class)
|
||||
->onlyMethods(['set_locale'])
|
||||
->onlyMethods([
|
||||
'get_locale',
|
||||
'set_locale',
|
||||
])
|
||||
->getMock();
|
||||
$mock->method('set_locale')->will($this->onConsecutiveCalls('en', false, 'en'));
|
||||
$mock->method('get_locale')->will($this->onConsecutiveCalls('en'));
|
||||
$mock->method('set_locale')->will($this->onConsecutiveCalls(false, 'en'));
|
||||
|
||||
// Test what happen when locale is not available on system.
|
||||
$result = $mock->check_locale_availability('en');
|
||||
@@ -64,100 +84,4 @@ class locale_test extends \advanced_testcase {
|
||||
$this->expectException(\coding_exception::class);
|
||||
$locale->check_locale_availability('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test \tool_langimport\locale::set_locale() own logic.
|
||||
*
|
||||
* We have to explicitly test set_locale() own logic and results,
|
||||
* that effectively sets the current locale, so we need to restore
|
||||
* the original locale after every test (ugly, from a purist unit test
|
||||
* point of view, but needed).
|
||||
*
|
||||
* @dataProvider set_locale_provider
|
||||
* @covers ::set_locale
|
||||
*
|
||||
* @param string $set locale string to be set.
|
||||
* @param string $ret expected results returned after setting the locale.
|
||||
*/
|
||||
public function test_set_locale(string $set, string $ret) {
|
||||
// Make set_locale() public.
|
||||
$loc = new locale();
|
||||
$rc = new \ReflectionClass(locale::class);
|
||||
$rm = $rc->getMethod('set_locale');
|
||||
$rm->setAccessible(true);
|
||||
|
||||
// Capture current locale for later restore (funnily, using the set_locale() method itself.
|
||||
$originallocale = $rm->invokeArgs($loc, [LC_ALL, 0]);
|
||||
|
||||
// Assert we get the locale defined as expected.
|
||||
$this->assertEquals($ret, $rm->invokeArgs($loc, [LC_ALL, $set]));
|
||||
|
||||
// We have finished, restore the original locale, so this doesn't affect other tests at distance.
|
||||
// (again, funnily, using the very same set_locale() method).
|
||||
$rm->invokeArgs($loc, [LC_ALL, $originallocale]);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_set_locale().
|
||||
*
|
||||
* Provides a locale to be set (as 'set') and a expected return value (as 'ret'). Note that
|
||||
* some of the locales are OS dependent, so only the ones matching the OS will be provided.
|
||||
*
|
||||
* We make extensive use of the en_AU.UTF-8/English_Australia.1252 locale that is mandatory to
|
||||
* be installed in any system running PHPUnit tests.
|
||||
*/
|
||||
public function set_locale_provider(): array {
|
||||
// Let's list the allowed categories by OS.
|
||||
$bsdallowed = ['LC_COLLATE', 'LC_CTYPE', 'LC_MESSAGES', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME'];
|
||||
$winallowed = ['LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME'];
|
||||
$linuxallowed = [
|
||||
'LC_COLLATE', 'LC_CTYPE', 'LC_MESSAGES', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME',
|
||||
'LC_PAPER', 'LC_NAME', 'LC_ADDRESS', 'LC_TELEPHONE', 'LC_MEASUREMENT', 'LC_IDENTIFICATION'
|
||||
];
|
||||
|
||||
// The base locale name is also OS dependent.
|
||||
$baselocale = get_string('locale', 'langconfig');
|
||||
if (PHP_OS_FAMILY === 'Windows') {
|
||||
$baselocale = get_string('localewin', 'langconfig');
|
||||
}
|
||||
|
||||
// Here we'll go accumulating cases to be provided.
|
||||
$cases = [];
|
||||
|
||||
// First, the simplest case, just pass a locale name, without categories.
|
||||
$cases['rawlocale'] = [
|
||||
'set' => $baselocale,
|
||||
'ret' => $baselocale,
|
||||
];
|
||||
|
||||
// Now, let's fill ALL LC categories, we should get back the locale name if all them are set with same value.
|
||||
// Note that this case is the one that, under Linux only, covers the changes performed to the set_locale() method.
|
||||
// Pick the correct categories depending on the OS.
|
||||
$oscategories = $bsdallowed; // Default to BSD/Dawrwin ones because they are the standard 6 supported by PHP.
|
||||
if (PHP_OS_FAMILY === 'Windows') {
|
||||
$oscategories = $winallowed;
|
||||
} else if (PHP_OS_FAMILY === 'Linux') {
|
||||
$oscategories = $linuxallowed;
|
||||
}
|
||||
|
||||
$localestr = '';
|
||||
foreach ($oscategories as $category) {
|
||||
// Format is different by OS too, so let build the string conditionally.
|
||||
if (PHP_OS_FAMILY === 'BSD' || PHP_OS_FAMILY === 'Darwin') {
|
||||
// BSD uses slashes (/) separated list of the 6 values in exact order.
|
||||
$localestr .= '/' . $baselocale;
|
||||
} else {
|
||||
// Linux/Windows use semicolon (;) separated list of category=value pairs.
|
||||
$localestr .= ';' . $category . '=' . $baselocale;
|
||||
}
|
||||
}
|
||||
$cases['allcategories'] = [
|
||||
'set' => trim($localestr, ';/'),
|
||||
'ret' => $baselocale,
|
||||
];
|
||||
|
||||
// Return all the built cases.
|
||||
return $cases;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ Feature: Delete custom licenses
|
||||
| Licence version | ##1 March 2019## |
|
||||
And I press "Save changes"
|
||||
And I follow "Private files" in the user menu
|
||||
And I upload "lib/editor/atto/tests/fixtures/moodle-logo.png" file to "Files" filemanager
|
||||
And I click on "moodle-logo.png" "link"
|
||||
And I upload "lib/tests/fixtures/gd-logo.png" file to "Files" filemanager
|
||||
And I click on "gd-logo.png" "link"
|
||||
And I set the field "Choose licence" to "Test licence"
|
||||
And I press "Update"
|
||||
And I press "Save changes"
|
||||
|
||||
@@ -14,6 +14,6 @@ Feature: In a report, admin can see logstore visibility changes
|
||||
And I click on "Enable" "icon" in the "External database log" "table_row"
|
||||
And I navigate to "Reports > Config changes" in site administration
|
||||
Then the following should exist in the "reportbuilder-table" table:
|
||||
| User | Plugin | Setting | New value | Original value |
|
||||
| First name | Plugin | Setting | New value | Original value |
|
||||
| Admin User | logstore_standard | tool_logstore_visibility | 0 | 1 |
|
||||
| Admin User | logstore_database | tool_logstore_visibility | 1 | 0 |
|
||||
|
||||
@@ -170,6 +170,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
|
||||
And I click on "Actions" "link_or_button" in the "This privacy policy" "table_row"
|
||||
And I click on "Set status to \"Active\"" "link" in the "This privacy policy" "table_row"
|
||||
And I wait until "Activate" "button" exists
|
||||
And I press "Activate"
|
||||
And I log out
|
||||
When I log in as "manager"
|
||||
@@ -260,6 +261,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
|
||||
And I click on "Actions" "link_or_button" in the "This privacy policy" "table_row"
|
||||
And I click on "Set status to \"Active\"" "link" in the "This privacy policy" "table_row"
|
||||
And I wait until "Activate" "button" exists
|
||||
And I press "Activate"
|
||||
And I log out
|
||||
When I log in as "manager"
|
||||
|
||||
@@ -117,7 +117,9 @@ class category_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.
|
||||
$CFG->forced_plugin_settings['backup'] = ['backup_auto_storage' => 0, 'backup_auto_files' => 1];
|
||||
$forcedbackupsettings = $CFG->forced_plugin_settings['backup'] ?? [];
|
||||
$CFG->forced_plugin_settings['backup']['backup_auto_storage'] = 0;
|
||||
$CFG->forced_plugin_settings['backup']['backup_auto_files'] = 1;
|
||||
|
||||
// Backup the course.
|
||||
$user = get_admin();
|
||||
@@ -131,9 +133,9 @@ class category_bin extends base_bin {
|
||||
);
|
||||
$controller->execute_plan();
|
||||
|
||||
// We don't need the forced setting anymore, hence unsetting it.
|
||||
// We don't need the forced setting anymore, hence restore previous settings.
|
||||
// TODO: Remove this as part of MDL-65228.
|
||||
unset($CFG->forced_plugin_settings['backup']);
|
||||
$CFG->forced_plugin_settings['backup'] = $forcedbackupsettings;
|
||||
|
||||
// Grab the result.
|
||||
$result = $controller->get_results();
|
||||
@@ -251,7 +253,9 @@ class category_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.
|
||||
$CFG->forced_plugin_settings['restore'] = ['restore_general_users' => 1, 'restore_general_groups' => 1];
|
||||
$forcedrestoresettings = $CFG->forced_plugin_settings['restore'] ?? [];
|
||||
$CFG->forced_plugin_settings['restore']['restore_general_users'] = 1;
|
||||
$CFG->forced_plugin_settings['restore']['restore_general_groups'] = 1;
|
||||
|
||||
// Define the import.
|
||||
$controller = new \restore_controller(
|
||||
@@ -287,9 +291,9 @@ class category_bin extends base_bin {
|
||||
// Run the import.
|
||||
$controller->execute_plan();
|
||||
|
||||
// We don't need the forced setting anymore, hence unsetting it.
|
||||
// We don't need the forced setting anymore, hence restore previous settings.
|
||||
// TODO: Remove this as part of MDL-65228.
|
||||
unset($CFG->forced_plugin_settings['restore']);
|
||||
$CFG->forced_plugin_settings['restore'] = $forcedrestoresettings;
|
||||
|
||||
// Have finished with the controller, let's destroy it, freeing mem and resources.
|
||||
$controller->destroy();
|
||||
|
||||
@@ -121,7 +121,9 @@ 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.
|
||||
$CFG->forced_plugin_settings['backup'] = ['backup_auto_storage' => 0, 'backup_auto_files' => 1];
|
||||
$forcedbackupsettings = $CFG->forced_plugin_settings['backup'] ?? [];
|
||||
$CFG->forced_plugin_settings['backup']['backup_auto_storage'] = 0;
|
||||
$CFG->forced_plugin_settings['backup']['backup_auto_files'] = 1;
|
||||
|
||||
// Backup the activity.
|
||||
$user = get_admin();
|
||||
@@ -145,9 +147,9 @@ class course_bin extends base_bin {
|
||||
|
||||
$controller->execute_plan();
|
||||
|
||||
// We don't need the forced setting anymore, hence unsetting it.
|
||||
// We don't need the forced setting anymore, hence restore previous settings.
|
||||
// TODO: Remove this as part of MDL-65228.
|
||||
unset($CFG->forced_plugin_settings['backup']);
|
||||
$CFG->forced_plugin_settings['backup'] = $forcedbackupsettings;
|
||||
|
||||
// Grab the result.
|
||||
$result = $controller->get_results();
|
||||
@@ -252,7 +254,9 @@ 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.
|
||||
$CFG->forced_plugin_settings['restore'] = ['restore_general_users' => 1, 'restore_general_groups' => 1];
|
||||
$forcedrestoresettings = $CFG->forced_plugin_settings['restore'] ?? null;
|
||||
$CFG->forced_plugin_settings['restore']['restore_general_users'] = 1;
|
||||
$CFG->forced_plugin_settings['restore']['restore_general_groups'] = 1;
|
||||
|
||||
// Define the import.
|
||||
$controller = new \restore_controller(
|
||||
@@ -284,9 +288,9 @@ class course_bin extends base_bin {
|
||||
// Run the import.
|
||||
$controller->execute_plan();
|
||||
|
||||
// We don't need the forced setting anymore, hence unsetting it.
|
||||
// We don't need the forced setting anymore, hence restore previous settings.
|
||||
// TODO: Remove this as part of MDL-65228.
|
||||
unset($CFG->forced_plugin_settings['restore']);
|
||||
$CFG->forced_plugin_settings['restore'] = $forcedrestoresettings;
|
||||
|
||||
// Have finished with the controller, let's destroy it, freeing mem and resources.
|
||||
$controller->destroy();
|
||||
|
||||
@@ -57,7 +57,13 @@ list($options, $unrecognized) = cli_get_params(
|
||||
)
|
||||
);
|
||||
|
||||
if ($options['help'] || $options['search'] === null || $options['replace'] === null) {
|
||||
if ($unrecognized) {
|
||||
$unrecognized = implode("\n ", $unrecognized);
|
||||
cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
|
||||
}
|
||||
|
||||
// Ensure that user has populated both the search/replace parameters.
|
||||
if ($options['help'] || !is_string($options['search']) || !is_string($options['replace'])) {
|
||||
echo $help;
|
||||
exit(0);
|
||||
}
|
||||
|
||||
@@ -62,8 +62,8 @@ Feature: Manage scheduled tasks
|
||||
And I press "Save changes"
|
||||
Then I should see "Changes saved"
|
||||
And the following should not exist in the "admintable" table:
|
||||
| Name | Component | Minute | Hour | Day | Day of week | Month |
|
||||
| Log table cleanup | Standard log | */5 | 1 | 2 | 4 | 3 |
|
||||
| Name | Minute | Hour | Day | Day of week | Month |
|
||||
| Log table cleanup | */5 | 1 | 2 | 4 | 3 |
|
||||
And I should see "Log table cleanup" in the "tr.table-primary" "css_element"
|
||||
|
||||
Scenario: Disabled plugin's tasks are labelled as disabled too
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -37,6 +37,7 @@ import {eventTypes} from './events';
|
||||
import {getString} from 'core/str';
|
||||
import {prefetchStrings} from 'core/prefetch';
|
||||
import {notifyFilterContentUpdated} from 'core/event';
|
||||
import PendingPromise from 'core/pending';
|
||||
|
||||
/**
|
||||
* The minimum spacing for tour step to display.
|
||||
@@ -486,14 +487,22 @@ const Tour = class {
|
||||
return this.endTour();
|
||||
}
|
||||
|
||||
const pendingPromise = new PendingPromise(`tool_usertours/tour:_gotoStep-${stepConfig.stepNumber}`);
|
||||
|
||||
if (typeof stepConfig.delay !== 'undefined' && stepConfig.delay && !stepConfig.delayed) {
|
||||
stepConfig.delayed = true;
|
||||
window.setTimeout(this._gotoStep.bind(this), stepConfig.delay, stepConfig, direction);
|
||||
window.setTimeout(function(stepConfig, direction) {
|
||||
this._gotoStep(stepConfig, direction);
|
||||
pendingPromise.resolve();
|
||||
}, stepConfig.delay, stepConfig, direction);
|
||||
|
||||
return this;
|
||||
} else if (!stepConfig.orphan && !this.isStepActuallyVisible(stepConfig)) {
|
||||
let fn = direction == -1 ? 'getPreviousStepNumber' : 'getNextStepNumber';
|
||||
return this.gotoStep(this[fn](stepConfig.stepNumber), direction);
|
||||
const fn = direction == -1 ? 'getPreviousStepNumber' : 'getNextStepNumber';
|
||||
this.gotoStep(this[fn](stepConfig.stepNumber), direction);
|
||||
|
||||
pendingPromise.resolve();
|
||||
return this;
|
||||
}
|
||||
|
||||
this.hide();
|
||||
@@ -504,6 +513,7 @@ const Tour = class {
|
||||
this.dispatchEvent(eventTypes.stepRendered, {stepConfig});
|
||||
}
|
||||
|
||||
pendingPromise.resolve();
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -823,12 +833,14 @@ const Tour = class {
|
||||
left: 0,
|
||||
});
|
||||
|
||||
const pendingPromise = new PendingPromise(`tool_usertours/tour:addStepToPage-${stepConfig.stepNumber}`);
|
||||
animationTarget
|
||||
.animate({
|
||||
scrollTop: this.calculateScrollTop(stepConfig),
|
||||
}).promise().then(function() {
|
||||
this.positionStep(stepConfig);
|
||||
this.revealStep(stepConfig);
|
||||
pendingPromise.resolve();
|
||||
return;
|
||||
}.bind(this))
|
||||
.catch(function() {
|
||||
@@ -900,6 +912,7 @@ const Tour = class {
|
||||
*/
|
||||
revealStep(stepConfig) {
|
||||
// Fade the step in.
|
||||
const pendingPromise = new PendingPromise(`tool_usertours/tour:revealStep-${stepConfig.stepNumber}`);
|
||||
this.currentStepNode.fadeIn('', $.proxy(function() {
|
||||
// Announce via ARIA.
|
||||
this.announceStep(stepConfig);
|
||||
@@ -913,6 +926,7 @@ const Tour = class {
|
||||
if (this.currentStepNode) {
|
||||
this.currentStepNode.focus();
|
||||
}
|
||||
pendingPromise.resolve();
|
||||
}, this), 100);
|
||||
|
||||
}, this));
|
||||
@@ -1160,6 +1174,7 @@ const Tour = class {
|
||||
return this;
|
||||
}
|
||||
|
||||
const pendingPromise = new PendingPromise('tool_usertours/tour:hide');
|
||||
if (this.currentStepNode && this.currentStepNode.length) {
|
||||
this.currentStepNode.hide();
|
||||
if (this.currentStepPopper) {
|
||||
@@ -1194,17 +1209,22 @@ const Tour = class {
|
||||
this.currentStepConfig = null;
|
||||
}
|
||||
|
||||
let fadeTime = 0;
|
||||
if (transition) {
|
||||
fadeTime = 400;
|
||||
}
|
||||
|
||||
// Remove the backdrop features.
|
||||
$('[data-flexitour="step-background"]').remove();
|
||||
$('[data-flexitour="step-backdrop"]').removeAttr('data-flexitour');
|
||||
$('[data-flexitour="backdrop"]').fadeOut(fadeTime, function() {
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
const backdrop = $('[data-flexitour="backdrop"]');
|
||||
if (backdrop.length) {
|
||||
if (transition) {
|
||||
const backdropRemovalPromise = new PendingPromise('tool_usertours/tour:hide:backdrop');
|
||||
backdrop.fadeOut(400, function() {
|
||||
$(this).remove();
|
||||
backdropRemovalPromise.resolve();
|
||||
});
|
||||
} else {
|
||||
backdrop.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove aria-describedby and tabindex attributes.
|
||||
if (this.currentStepNode && this.currentStepNode.length) {
|
||||
@@ -1225,6 +1245,8 @@ const Tour = class {
|
||||
|
||||
this.currentStepNode = null;
|
||||
this.currentStepPopper = null;
|
||||
|
||||
pendingPromise.resolve();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -116,11 +116,9 @@ class editstep extends \moodleform {
|
||||
'changeformat' => 1,
|
||||
'trusttext' => true
|
||||
];
|
||||
$objs = $mform->createElement('editor', 'content', get_string('content', 'tool_usertours'), null, $editoroptions);
|
||||
// TODO: MDL-68540 We need to add the editor to a group element because editor element will not work with hideIf.
|
||||
$mform->addElement('group', 'contenthtmlgrp', get_string('content', 'tool_usertours'), [$objs], ' ', false);
|
||||
$mform->addHelpButton('contenthtmlgrp', 'content', 'tool_usertours');
|
||||
$mform->hideIf('contenthtmlgrp', 'contenttype', 'eq', static::CONTENTTYPE_LANGSTRING);
|
||||
$mform->addElement('editor', 'content', get_string('content', 'tool_usertours'), null, $editoroptions);
|
||||
$mform->addHelpButton('content', 'content', 'tool_usertours');
|
||||
$mform->hideIf('content', 'contenttype', 'eq', static::CONTENTTYPE_LANGSTRING);
|
||||
|
||||
// Add the step configuration.
|
||||
$mform->addElement('header', 'heading_options', get_string('options_heading', 'tool_usertours'));
|
||||
|
||||
@@ -572,7 +572,8 @@ class step {
|
||||
* @return $this
|
||||
*/
|
||||
public function persist($force = false) {
|
||||
global $DB;
|
||||
global $CFG, $DB;
|
||||
require_once("{$CFG->libdir}/filelib.php");
|
||||
|
||||
if (!$this->dirty && !$force) {
|
||||
return $this;
|
||||
@@ -729,6 +730,9 @@ class step {
|
||||
* @return object
|
||||
*/
|
||||
public function prepare_data_for_form() {
|
||||
global $CFG;
|
||||
require_once("{$CFG->libdir}/filelib.php");
|
||||
|
||||
$data = $this->to_record();
|
||||
foreach (self::get_config_keys() as $key) {
|
||||
$data->$key = $this->get_config($key, configuration::get_step_default_value($key));
|
||||
|
||||
@@ -27,9 +27,9 @@ Feature: Apply content type to a tour
|
||||
And the "Content type" select box should contain "Manual"
|
||||
And I select "Language string ID" from the "Content type" singleselect
|
||||
And I should see " Language string ID"
|
||||
And "#fgroup_id_contenthtmlgrp" "css_element" should not be visible
|
||||
And I should not see "Content" in the "#fitem_id_content" "css_element"
|
||||
And I select "Manual" from the "Content type" singleselect
|
||||
And "#fgroup_id_contenthtmlgrp" "css_element" should be visible
|
||||
And I should see "Content" in the "#fitem_id_content" "css_element"
|
||||
And I should not see "Language string ID" in the "#fitem_id_contentlangstring" "css_element"
|
||||
|
||||
@javascript
|
||||
|
||||
@@ -36,6 +36,11 @@ class new_table_from_mysql extends XMLDBAction {
|
||||
function init() {
|
||||
parent::init();
|
||||
|
||||
global $DB;
|
||||
if ($DB->get_dbfamily() !== 'mysql') {
|
||||
throw new moodle_exception('DB family not supported');
|
||||
}
|
||||
|
||||
// Set own custom attributes
|
||||
|
||||
// Get needed strings
|
||||
@@ -55,8 +60,6 @@ class new_table_from_mysql extends XMLDBAction {
|
||||
function invoke() {
|
||||
parent::invoke();
|
||||
|
||||
$result = true;
|
||||
|
||||
// Set own core attributes
|
||||
$this->does_generate = ACTION_GENERATE_HTML;
|
||||
|
||||
@@ -125,11 +128,15 @@ class new_table_from_mysql extends XMLDBAction {
|
||||
// If table, retrofit information and, if everything works,
|
||||
// go to the table edit action
|
||||
} else {
|
||||
// Get some params (table is mandatory here)
|
||||
$tableparam = required_param('table', PARAM_CLEAN);
|
||||
$afterparam = required_param('after', PARAM_CLEAN);
|
||||
// Get some params (table is mandatory here).
|
||||
$tableparam = required_param('table', PARAM_ALPHANUMEXT);
|
||||
$afterparam = required_param('after', PARAM_ALPHANUMEXT);
|
||||
|
||||
// Create one new xmldb_table
|
||||
if (empty($tableparam) || empty($afterparam)) {
|
||||
throw new moodle_exception('Invalid param value detected.');
|
||||
}
|
||||
|
||||
// Create one new xmldb_table.
|
||||
$table = new xmldb_table(strtolower(trim($tableparam)));
|
||||
$table->setComment($table->getName() . ' table retrofitted from MySQL');
|
||||
// Get fields info from ADODb
|
||||
@@ -147,7 +154,6 @@ class new_table_from_mysql extends XMLDBAction {
|
||||
// Get PK, UK and indexes info from ADODb
|
||||
$dbindexes = $DB->get_indexes($tableparam);
|
||||
if ($dbindexes) {
|
||||
$lastkey = NULL; //To temp store the last key processed
|
||||
foreach ($dbindexes as $indexname => $dbindex) {
|
||||
// Add the indexname to the array
|
||||
$dbindex['name'] = $indexname;
|
||||
@@ -156,9 +162,6 @@ class new_table_from_mysql extends XMLDBAction {
|
||||
$key = new xmldb_key(strtolower($dbindex['name']));
|
||||
// Set key with info retrofitted
|
||||
$key->setFromADOKey($dbindex);
|
||||
// Set default comment to PKs
|
||||
if ($key->getType() == XMLDB_KEY_PRIMARY) {
|
||||
}
|
||||
// Add key to the table
|
||||
$table->addKey($key);
|
||||
|
||||
@@ -172,18 +175,17 @@ class new_table_from_mysql extends XMLDBAction {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Finally, add the whole retroffited table to the structure
|
||||
// in the place specified
|
||||
// Finally, add the whole retrofitted table to the structure in the place specified.
|
||||
$structure->addTable($table, $afterparam);
|
||||
}
|
||||
|
||||
// Launch postaction if exists (leave this here!)
|
||||
if ($this->getPostAction() && $result) {
|
||||
if ($this->getPostAction()) {
|
||||
return $this->launch($this->getPostAction());
|
||||
}
|
||||
|
||||
// Return ok if arrived here
|
||||
return $result;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,6 +114,7 @@ Feature: Manage analytics models
|
||||
And I should see "Read actions amount"
|
||||
And I click on "Select Student 6 for bulk action" "checkbox" in the "Student 6" "table_row"
|
||||
And I click on "Accept" "button"
|
||||
And I wait until "Confirm" "button" exists
|
||||
And I click on "Confirm" "button" in the "Accept" "dialogue"
|
||||
And I click on "View prediction details" "icon" in the "Student 5" "table_row"
|
||||
And I click on "Select Student 5 for bulk action" "checkbox" in the "Student 5" "table_row"
|
||||
@@ -145,7 +146,8 @@ Feature: Manage analytics models
|
||||
When I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
|
||||
And I choose "Export" in the open action menu
|
||||
And I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
|
||||
And following "Export" should download between "100" and "500" bytes
|
||||
And following "Export" should download a file that:
|
||||
| Contains file in zip | model-config.json |
|
||||
|
||||
Scenario: Check invalid site elements
|
||||
When I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
|
||||
|
||||
+9
-11
@@ -14,31 +14,29 @@
|
||||
// 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 activities completed by classification.
|
||||
*
|
||||
* @package core_analytics
|
||||
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace core_analytics;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
use advanced_testcase;
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Unit tests for activities completed by classification.
|
||||
*
|
||||
* @package core_analytics
|
||||
* @covers \core_analytics\local\indicator\community_of_inquiry_activity
|
||||
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class community_of_inquiry_activities_completed_by_testcase extends advanced_testcase {
|
||||
final class community_of_inquiry_activities_completed_by_test extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* availability_levels
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function availability_levels() {
|
||||
public static function availability_levels(): array {
|
||||
return array(
|
||||
'activity' => array('activity'),
|
||||
'section' => array('section'),
|
||||
@@ -148,7 +146,7 @@ class community_of_inquiry_activities_completed_by_testcase extends advanced_tes
|
||||
$second = $startdate + WEEKSECS;
|
||||
$third = $startdate + (WEEKSECS * 2);
|
||||
$forth = $startdate + (WEEKSECS * 3);
|
||||
$this->assertCount(1, $method->invoke($indicator, $first, $first + WEEKSECS, $stu1));
|
||||
$this->assertCount(2, $method->invoke($indicator, $first, $first + WEEKSECS, $stu1));
|
||||
$this->assertCount(1, $method->invoke($indicator, $second, $second + WEEKSECS, $stu1));
|
||||
$this->assertCount(0, $method->invoke($indicator, $third, $third + WEEKSECS, $stu1));
|
||||
$this->assertCount(2, $method->invoke($indicator, $forth, $forth + WEEKSECS, $stu1));
|
||||
@@ -345,7 +345,7 @@ class api {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete linked login
|
||||
* Delete a users own linked login
|
||||
*
|
||||
* Requires auth/oauth2:managelinkedlogins capability at the user context.
|
||||
*
|
||||
@@ -353,14 +353,19 @@ class api {
|
||||
* @return boolean
|
||||
*/
|
||||
public static function delete_linked_login($linkedloginid) {
|
||||
$login = new linked_login($linkedloginid);
|
||||
$userid = $login->get('userid');
|
||||
global $USER;
|
||||
|
||||
if (\core\session\manager::is_loggedinas()) {
|
||||
throw new moodle_exception('notwhileloggedinas', 'auth_oauth2');
|
||||
}
|
||||
|
||||
$context = context_user::instance($userid);
|
||||
$login = linked_login::get_record([
|
||||
'id' => $linkedloginid,
|
||||
'userid' => $USER->id,
|
||||
'confirmtoken' => '',
|
||||
], MUST_EXIST);
|
||||
|
||||
$context = context_user::instance($login->get('userid'));
|
||||
require_capability('auth/oauth2:managelinkedlogins', $context);
|
||||
|
||||
$login->delete();
|
||||
|
||||
@@ -63,7 +63,6 @@ class auth extends \auth_plugin_base {
|
||||
public function __construct() {
|
||||
$this->authtype = 'oauth2';
|
||||
$this->config = get_config('auth_oauth2');
|
||||
$this->customfields = $this->get_custom_user_profile_fields();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -310,7 +309,7 @@ class auth extends \auth_plugin_base {
|
||||
return $userdata;
|
||||
}
|
||||
|
||||
$allfields = array_merge($this->userfields, $this->customfields);
|
||||
$allfields = array_merge($this->userfields, $this->get_custom_user_profile_fields());
|
||||
|
||||
// Go through each field from the external data.
|
||||
foreach ($externaldata as $fieldname => $value) {
|
||||
|
||||
@@ -137,6 +137,7 @@ class api_test extends \advanced_testcase {
|
||||
$issuer = \core\oauth2\api::create_standard_issuer('google');
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($user);
|
||||
|
||||
$info = [];
|
||||
$info['username'] = 'banana';
|
||||
@@ -169,6 +170,30 @@ class api_test extends \advanced_testcase {
|
||||
$this->assertEquals($newuser->id, $match->get('userid'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that we cannot deleted a linked login for another user
|
||||
*/
|
||||
public function test_delete_linked_login_other_user(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$this->setAdminUser();
|
||||
$issuer = \core\oauth2\api::create_standard_issuer('google');
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
api::link_login([
|
||||
'username' => 'banana',
|
||||
'email' => 'banana@example.com',
|
||||
], $issuer, $user->id);
|
||||
|
||||
/** @var linked_login $linkedlogin */
|
||||
$linkedlogin = api::get_linked_logins($user->id)[0];
|
||||
|
||||
// We are logged in as a different user, so cannot delete this.
|
||||
$this->expectException(\dml_missing_record_exception::class);
|
||||
api::delete_linked_login($linkedlogin->get('id'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that is_enabled correctly identifies when the plugin is enabled.
|
||||
*/
|
||||
|
||||
@@ -52,7 +52,7 @@ abstract class backup_block_task extends backup_task {
|
||||
}
|
||||
|
||||
$this->blockid = $blockid;
|
||||
$this->blockname = $block->blockname;
|
||||
$this->blockname = clean_param($block->blockname, PARAM_PLUGIN);
|
||||
$this->contextid = context_block::instance($this->blockid)->id;
|
||||
$this->moduleid = $moduleid;
|
||||
$this->modulename = null;
|
||||
|
||||
@@ -552,9 +552,12 @@ class backup_course_structure_step extends backup_structure_step {
|
||||
FROM {course_format_options}
|
||||
WHERE courseid = ? AND sectionid = 0', [ backup::VAR_PARENTID ]);
|
||||
|
||||
$handler = core_course\customfield\course_handler::create();
|
||||
$fieldsforbackup = $handler->get_instance_data_for_backup($this->task->get_courseid());
|
||||
$customfield->set_source_array($fieldsforbackup);
|
||||
// Custom fields.
|
||||
if ($this->get_setting_value('customfield')) {
|
||||
$handler = core_course\customfield\course_handler::create();
|
||||
$fieldsforbackup = $handler->get_instance_data_for_backup($this->task->get_courseid());
|
||||
$customfield->set_source_array($fieldsforbackup);
|
||||
}
|
||||
|
||||
// Some annotations
|
||||
|
||||
@@ -1403,14 +1406,19 @@ class backup_groups_structure_step extends backup_structure_step {
|
||||
FROM {groups} g
|
||||
JOIN {backup_ids_temp} bi ON g.id = bi.itemid
|
||||
WHERE bi.backupid = ?
|
||||
AND bi.itemname = 'groupfinal'", array(backup::VAR_BACKUPID));
|
||||
AND bi.itemname = 'groupfinal'",
|
||||
[backup_helper::is_sqlparam($this->get_backupid())]
|
||||
);
|
||||
|
||||
$grouping->set_source_sql("
|
||||
SELECT g.*
|
||||
FROM {groupings} g
|
||||
JOIN {backup_ids_temp} bi ON g.id = bi.itemid
|
||||
WHERE bi.backupid = ?
|
||||
AND bi.itemname = 'groupingfinal'", array(backup::VAR_BACKUPID));
|
||||
AND bi.itemname = 'groupingfinal'",
|
||||
[backup_helper::is_sqlparam($this->get_backupid())]
|
||||
);
|
||||
|
||||
$groupinggroup->set_source_table('groupings_groups', array('groupingid' => backup::VAR_PARENTID));
|
||||
|
||||
// This only happens if we are including users.
|
||||
@@ -1418,9 +1426,20 @@ class backup_groups_structure_step extends backup_structure_step {
|
||||
$member->set_source_table('groups_members', array('groupid' => backup::VAR_PARENTID));
|
||||
}
|
||||
|
||||
$courseid = $this->task->get_courseid();
|
||||
$groupcustomfield->set_source_array($this->get_group_custom_fields_for_backup($courseid));
|
||||
$groupingcustomfield->set_source_array($this->get_grouping_custom_fields_for_backup($courseid));
|
||||
// Custom fields.
|
||||
if ($this->get_setting_value('customfield')) {
|
||||
$groupcustomfieldarray = $this->get_group_custom_fields_for_backup(
|
||||
$group->get_source_sql(),
|
||||
[$this->get_backupid()]
|
||||
);
|
||||
$groupcustomfield->set_source_array($groupcustomfieldarray);
|
||||
|
||||
$groupingcustomfieldarray = $this->get_grouping_custom_fields_for_backup(
|
||||
$grouping->get_source_sql(),
|
||||
[$this->get_backupid()]
|
||||
);
|
||||
$groupingcustomfield->set_source_array($groupingcustomfieldarray);
|
||||
}
|
||||
}
|
||||
|
||||
// Define id annotations (as final)
|
||||
@@ -1439,14 +1458,16 @@ class backup_groups_structure_step extends backup_structure_step {
|
||||
|
||||
/**
|
||||
* Get custom fields array for group
|
||||
* @param int $courseid
|
||||
*
|
||||
* @param string $groupsourcesql
|
||||
* @param array $groupsourceparams
|
||||
* @return array
|
||||
*/
|
||||
protected function get_group_custom_fields_for_backup(int $courseid): array {
|
||||
protected function get_group_custom_fields_for_backup(string $groupsourcesql, array $groupsourceparams): array {
|
||||
global $DB;
|
||||
$handler = \core_group\customfield\group_handler::create();
|
||||
$fieldsforbackup = [];
|
||||
if ($groups = $DB->get_records('groups', ['courseid' => $courseid], '', 'id')) {
|
||||
if ($groups = $DB->get_records_sql($groupsourcesql, $groupsourceparams)) {
|
||||
foreach ($groups as $group) {
|
||||
$fieldsforbackup = array_merge($fieldsforbackup, $handler->get_instance_data_for_backup($group->id));
|
||||
}
|
||||
@@ -1456,14 +1477,16 @@ class backup_groups_structure_step extends backup_structure_step {
|
||||
|
||||
/**
|
||||
* Get custom fields array for grouping
|
||||
* @param int $courseid
|
||||
*
|
||||
* @param string $groupingsourcesql
|
||||
* @param array $groupingsourceparams
|
||||
* @return array
|
||||
*/
|
||||
protected function get_grouping_custom_fields_for_backup(int $courseid): array {
|
||||
protected function get_grouping_custom_fields_for_backup(string $groupingsourcesql, array $groupingsourceparams): array {
|
||||
global $DB;
|
||||
$handler = \core_group\customfield\grouping_handler::create();
|
||||
$fieldsforbackup = [];
|
||||
if ($groupings = $DB->get_records('groupings', ['courseid' => $courseid], '', 'id')) {
|
||||
if ($groupings = $DB->get_records_sql($groupingsourcesql, $groupingsourceparams)) {
|
||||
foreach ($groupings as $grouping) {
|
||||
$fieldsforbackup = array_merge($fieldsforbackup, $handler->get_instance_data_for_backup($grouping->id));
|
||||
}
|
||||
|
||||
@@ -300,8 +300,16 @@ class restore_root_task extends restore_task {
|
||||
$competencies->set_ui(new backup_setting_ui_checkbox($competencies, get_string('rootsettingcompetencies', 'backup')));
|
||||
$this->add_setting($competencies);
|
||||
|
||||
$customfields = new restore_customfield_setting('customfields', base_setting::IS_BOOLEAN, $defaultvalue);
|
||||
// Custom fields.
|
||||
$defaultvalue = false;
|
||||
$changeable = false;
|
||||
if (isset($rootsettings['customfield']) && $rootsettings['customfield']) { // Only enabled when available.
|
||||
$defaultvalue = true;
|
||||
$changeable = true;
|
||||
}
|
||||
$customfields = new restore_customfield_setting('customfield', base_setting::IS_BOOLEAN, $defaultvalue);
|
||||
$customfields->set_ui(new backup_setting_ui_checkbox($customfields, get_string('rootsettingcustomfield', 'backup')));
|
||||
$customfields->get_ui()->set_changeable($changeable);
|
||||
$this->add_setting($customfields);
|
||||
|
||||
// Define Content bank content.
|
||||
|
||||
+121
-115
@@ -1159,11 +1159,15 @@ class restore_groups_structure_step extends restore_structure_step {
|
||||
$groupinfo = $this->get_setting_value('groups');
|
||||
if ($groupinfo) {
|
||||
$paths[] = new restore_path_element('group', '/groups/group');
|
||||
$paths[] = new restore_path_element('groupcustomfield', '/groups/groupcustomfields/groupcustomfield');
|
||||
$paths[] = new restore_path_element('grouping', '/groups/groupings/grouping');
|
||||
$paths[] = new restore_path_element('groupingcustomfield',
|
||||
'/groups/groupings/groupingcustomfields/groupingcustomfield');
|
||||
$paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group');
|
||||
|
||||
// Custom fields.
|
||||
if ($this->get_setting_value('customfield')) {
|
||||
$paths[] = new restore_path_element('groupcustomfield', '/groups/groupcustomfields/groupcustomfield');
|
||||
$paths[] = new restore_path_element('groupingcustomfield',
|
||||
'/groups/groupings/groupingcustomfields/groupingcustomfield');
|
||||
}
|
||||
}
|
||||
return $paths;
|
||||
}
|
||||
@@ -1235,9 +1239,11 @@ class restore_groups_structure_step extends restore_structure_step {
|
||||
*/
|
||||
public function process_groupcustomfield($data) {
|
||||
$newgroup = $this->get_mapping('group', $data['groupid']);
|
||||
$data['groupid'] = $newgroup->newitemid ?? $data['groupid'];
|
||||
$handler = \core_group\customfield\group_handler::create();
|
||||
$handler->restore_instance_data_from_backup($this->task, $data);
|
||||
if ($newgroup && $newgroup->newitemid) {
|
||||
$data['groupid'] = $newgroup->newitemid;
|
||||
$handler = \core_group\customfield\group_handler::create();
|
||||
$handler->restore_instance_data_from_backup($this->task, $data);
|
||||
}
|
||||
}
|
||||
|
||||
public function process_grouping($data) {
|
||||
@@ -1291,10 +1297,12 @@ class restore_groups_structure_step extends restore_structure_step {
|
||||
* @return void
|
||||
*/
|
||||
public function process_groupingcustomfield($data) {
|
||||
$newgroup = $this->get_mapping('grouping', $data['groupingid']);
|
||||
$data['groupingid'] = $newgroup->newitemid ?? $data['groupingid'];
|
||||
$handler = \core_group\customfield\grouping_handler::create();
|
||||
$handler->restore_instance_data_from_backup($this->task, $data);
|
||||
$newgrouping = $this->get_mapping('grouping', $data['groupingid']);
|
||||
if ($newgrouping && $newgrouping->newitemid) {
|
||||
$data['groupingid'] = $newgrouping->newitemid;
|
||||
$handler = \core_group\customfield\grouping_handler::create();
|
||||
$handler->restore_instance_data_from_backup($this->task, $data);
|
||||
}
|
||||
}
|
||||
|
||||
public function process_grouping_group($data) {
|
||||
@@ -1827,12 +1835,19 @@ class restore_course_structure_step extends restore_structure_step {
|
||||
|
||||
protected function define_structure() {
|
||||
|
||||
$paths = [];
|
||||
|
||||
$course = new restore_path_element('course', '/course');
|
||||
$category = new restore_path_element('category', '/course/category');
|
||||
$tag = new restore_path_element('tag', '/course/tags/tag');
|
||||
$customfield = new restore_path_element('customfield', '/course/customfields/customfield');
|
||||
$courseformatoptions = new restore_path_element('course_format_option', '/course/courseformatoptions/courseformatoption');
|
||||
$allowedmodule = new restore_path_element('allowed_module', '/course/allowed_modules/module');
|
||||
$paths[] = $course;
|
||||
$paths[] = new restore_path_element('category', '/course/category');
|
||||
$paths[] = new restore_path_element('tag', '/course/tags/tag');
|
||||
$paths[] = new restore_path_element('course_format_option', '/course/courseformatoptions/courseformatoption');
|
||||
$paths[] = new restore_path_element('allowed_module', '/course/allowed_modules/module');
|
||||
|
||||
// Custom fields.
|
||||
if ($this->get_setting_value('customfield')) {
|
||||
$paths[] = new restore_path_element('customfield', '/course/customfields/customfield');
|
||||
}
|
||||
|
||||
// Apply for 'format' plugins optional paths at course level
|
||||
$this->add_plugin_structure('format', $course);
|
||||
@@ -1855,7 +1870,7 @@ class restore_course_structure_step extends restore_structure_step {
|
||||
// Apply for admin tool plugins optional paths at course level.
|
||||
$this->add_plugin_structure('tool', $course);
|
||||
|
||||
return array($course, $category, $tag, $customfield, $allowedmodule, $courseformatoptions);
|
||||
return $paths;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4912,6 +4927,12 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
/** @var array $cachedcategory store a question category */
|
||||
protected $cachedcategory = null;
|
||||
|
||||
/** @var stdClass the last question_bank_entry seen during the restore. Processed when we get to a question. */
|
||||
protected $latestqbe;
|
||||
|
||||
/** @var stdClass the last question_version seen during the restore. Processed when we get to a question. */
|
||||
protected $latestversion;
|
||||
|
||||
protected function define_structure() {
|
||||
|
||||
// Check if the backup is a pre 4.0 one.
|
||||
@@ -5037,45 +5058,25 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
}
|
||||
|
||||
/**
|
||||
* Process pre 4.0 question data where in creates the record for version and entry table.
|
||||
* Set up date to allow restore of questions from pre-4.0 backups.
|
||||
*
|
||||
* @param array $data the data from the XML file.
|
||||
* @param stdClass $data the data from the XML file.
|
||||
*/
|
||||
protected function process_question_legacy_data($data) {
|
||||
global $DB;
|
||||
$this->latestqbe = (object) [
|
||||
'id' => $data->id,
|
||||
'questioncategoryid' => $data->category,
|
||||
'ownerid' => $data->createdby,
|
||||
'idnumber' => $data->idnumber ?? null,
|
||||
];
|
||||
|
||||
$oldid = $data->id;
|
||||
// Process question bank entry.
|
||||
$entrydata = new stdClass();
|
||||
$entrydata->questioncategoryid = $data->category;
|
||||
$userid = $this->get_mappingid('user', $data->createdby);
|
||||
if ($userid) {
|
||||
$entrydata->ownerid = $userid;
|
||||
} else {
|
||||
if (!$this->task->is_samesite()) {
|
||||
$entrydata->ownerid = $this->task->get_userid();
|
||||
}
|
||||
}
|
||||
// The idnumber if it exists also needs to be unique within a category or reset it to null.
|
||||
if (isset($data->idnumber) && !$DB->record_exists('question_bank_entries',
|
||||
['idnumber' => $data->idnumber, 'questioncategoryid' => $data->category])) {
|
||||
$entrydata->idnumber = $data->idnumber;
|
||||
}
|
||||
|
||||
$newentryid = $DB->insert_record('question_bank_entries', $entrydata);
|
||||
// Process question versions.
|
||||
$versiondata = new stdClass();
|
||||
$versiondata->questionbankentryid = $newentryid;
|
||||
$versiondata->version = 1;
|
||||
// Question id is updated after inserting the question.
|
||||
$versiondata->questionid = 0;
|
||||
$versionstatus = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
if ((int)$data->hidden === 1) {
|
||||
$versionstatus = \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN;
|
||||
}
|
||||
$versiondata->status = $versionstatus;
|
||||
$newversionid = $DB->insert_record('question_versions', $versiondata);
|
||||
$this->set_mapping('question_version_created', $oldid, $newversionid);
|
||||
$this->latestversion = (object) [
|
||||
'id' => $data->id,
|
||||
'version' => 1,
|
||||
'status' => $data->hidden ?
|
||||
\core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN :
|
||||
\core_question\local\bank\question_version_status::QUESTION_STATUS_READY,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5084,37 +5085,9 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
* @param array $data the data from the XML file.
|
||||
*/
|
||||
protected function process_question_bank_entry($data) {
|
||||
global $DB;
|
||||
|
||||
$data = (object)$data;
|
||||
$oldid = $data->id;
|
||||
|
||||
$questioncreated = $this->get_mappingid('question_category_created', $data->questioncategoryid) ? true : false;
|
||||
$recordexist = $DB->record_exists('question_bank_entries', ['id' => $data->id,
|
||||
'questioncategoryid' => $data->questioncategoryid]);
|
||||
// Check we have category created.
|
||||
if (!$questioncreated && $recordexist) {
|
||||
return self::SKIP_ALL_CHILDREN;
|
||||
}
|
||||
|
||||
$data->questioncategoryid = $this->get_new_parentid('question_category');
|
||||
$userid = $this->get_mappingid('user', $data->ownerid);
|
||||
if ($userid) {
|
||||
$data->ownerid = $userid;
|
||||
} else {
|
||||
if (!$this->task->is_samesite()) {
|
||||
$data->ownerid = $this->task->get_userid();
|
||||
}
|
||||
}
|
||||
|
||||
// The idnumber if it exists also needs to be unique within a category or reset it to null.
|
||||
if (!empty($data->idnumber) && $DB->record_exists('question_bank_entries',
|
||||
['idnumber' => $data->idnumber, 'questioncategoryid' => $data->questioncategoryid])) {
|
||||
unset($data->idnumber);
|
||||
}
|
||||
|
||||
$newitemid = $DB->insert_record('question_bank_entries', $data);
|
||||
$this->set_mapping('question_bank_entry', $oldid, $newitemid);
|
||||
// We can only determine the right way to process this once we get to
|
||||
// process_question and have more information, so for now just store.
|
||||
$this->latestqbe = (object) $data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5123,16 +5096,9 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
* @param array $data the data from the XML file.
|
||||
*/
|
||||
protected function process_question_versions($data) {
|
||||
global $DB;
|
||||
|
||||
$data = (object)$data;
|
||||
$oldid = $data->id;
|
||||
|
||||
$data->questionbankentryid = $this->get_new_parentid('question_bank_entry');
|
||||
// Question id is updated after inserting the question.
|
||||
$data->questionid = 0;
|
||||
$newitemid = $DB->insert_record('question_versions', $data);
|
||||
$this->set_mapping('question_versions', $oldid, $newitemid);
|
||||
// We can only determine the right way to process this once we get to
|
||||
// process_question and have more information, so for now just store.
|
||||
$this->latestversion = (object) $data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5143,17 +5109,19 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
protected function process_question($data) {
|
||||
global $DB;
|
||||
|
||||
$data = (object)$data;
|
||||
$data = (object) $data;
|
||||
$oldid = $data->id;
|
||||
|
||||
// Check if the backup is a pre 4.0 one.
|
||||
// Check we have one mapping for this question.
|
||||
if (!$questionmapping = $this->get_mapping('question', $oldid)) {
|
||||
// No mapping = this question doesn't need to be created/mapped.
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a pre 4.0 backup, then there will not be a question bank entry
|
||||
// or question version in the file. So, we need to set up that data ready to be used below.
|
||||
$restoretask = $this->get_task();
|
||||
if ($restoretask->backup_release_compare('4.0', '<') || $restoretask->backup_version_compare(20220202, '<')) {
|
||||
// Check we have one mapping for this question.
|
||||
if (!$questionmapping = $this->get_mapping('question', $oldid)) {
|
||||
return; // No mapping = this question doesn't need to be created/mapped.
|
||||
}
|
||||
|
||||
// Get the mapped category (cannot use get_new_parentid() because not
|
||||
// all the categories have been created, so it is not always available
|
||||
// Instead we get the mapping for the question->parentitemid because
|
||||
@@ -5197,28 +5165,66 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
}
|
||||
}
|
||||
|
||||
$newitemid = $DB->insert_record('question', $data);
|
||||
$this->set_mapping('question', $oldid, $newitemid);
|
||||
// Also annotate them as question_created, we need
|
||||
// that later when remapping parents (keeping the old categoryid as parentid).
|
||||
$parentcatid = $this->get_old_parentid('question_category');
|
||||
$this->set_mapping('question_created', $oldid, $newitemid, false, null, $parentcatid);
|
||||
// Now update the question_versions table with the new question id. we dont need to do that for random qtypes.
|
||||
$legacyquestiondata = $this->get_mappingid('question_version_created', $oldid) ? true : false;
|
||||
if ($legacyquestiondata) {
|
||||
$parentitemid = $this->get_mappingid('question_version_created', $oldid);
|
||||
// With newitemid = 0, let's create the question.
|
||||
if (!$questionmapping->newitemid) {
|
||||
// Now we know we are inserting a question, we may need to insert the questionbankentry.
|
||||
if (empty($this->latestqbe->newid)) {
|
||||
$this->latestqbe->oldid = $this->latestqbe->id;
|
||||
|
||||
$this->latestqbe->questioncategoryid = $this->get_new_parentid('question_category');
|
||||
$userid = $this->get_mappingid('user', $this->latestqbe->ownerid);
|
||||
if ($userid) {
|
||||
$this->latestqbe->ownerid = $userid;
|
||||
} else {
|
||||
if (!$this->task->is_samesite()) {
|
||||
$this->latestqbe->ownerid = $this->task->get_userid();
|
||||
}
|
||||
}
|
||||
|
||||
// The idnumber if it exists also needs to be unique within a category or reset it to null.
|
||||
if (!empty($this->latestqbe->idnumber) && $DB->record_exists('question_bank_entries',
|
||||
['idnumber' => $this->latestqbe->idnumber, 'questioncategoryid' => $this->latestqbe->questioncategoryid])) {
|
||||
unset($this->latestqbe->idnumber);
|
||||
}
|
||||
|
||||
$this->latestqbe->newid = $DB->insert_record('question_bank_entries', $this->latestqbe);
|
||||
$this->set_mapping('question_bank_entry', $this->latestqbe->oldid, $this->latestqbe->newid);
|
||||
}
|
||||
|
||||
// Now store the question.
|
||||
$newitemid = $DB->insert_record('question', $data);
|
||||
$this->set_mapping('question', $oldid, $newitemid);
|
||||
// Also annotate them as question_created, we need
|
||||
// that later when remapping parents (keeping the old categoryid as parentid).
|
||||
$parentcatid = $this->get_old_parentid('question_category');
|
||||
$this->set_mapping('question_created', $oldid, $newitemid, false, null, $parentcatid);
|
||||
|
||||
// Also insert this question_version.
|
||||
$oldqvid = $this->latestversion->id;
|
||||
$this->latestversion->questionbankentryid = $this->latestqbe->newid;
|
||||
$this->latestversion->questionid = $newitemid;
|
||||
$newqvid = $DB->insert_record('question_versions', $this->latestversion);
|
||||
$this->set_mapping('question_versions', $oldqvid, $newqvid);
|
||||
|
||||
} else {
|
||||
$parentitemid = $this->get_new_parentid('question_versions');
|
||||
// By performing this set_mapping() we make get_old/new_parentid() to work for all the
|
||||
// children elements of the 'question' one (so qtype plugins will know the question they belong to).
|
||||
$this->set_mapping('question', $oldid, $questionmapping->newitemid);
|
||||
|
||||
// Also create the question_bank_entry and version mappings, if required.
|
||||
$newquestionversion = $DB->get_record('question_versions', ['questionid' => $questionmapping->newitemid]);
|
||||
$this->set_mapping('question_versions', $this->latestversion->id, $newquestionversion->id);
|
||||
if (empty($this->latestqbe->newid)) {
|
||||
$this->latestqbe->oldid = $this->latestqbe->id;
|
||||
$this->latestqbe->newid = $newquestionversion->questionbankentryid;
|
||||
$this->set_mapping('question_bank_entry', $this->latestqbe->oldid, $this->latestqbe->newid);
|
||||
}
|
||||
}
|
||||
$version = new stdClass();
|
||||
$version->id = $parentitemid;
|
||||
$version->questionid = $newitemid;
|
||||
$DB->update_record('question_versions', $version);
|
||||
|
||||
// Note, we don't restore any question files yet
|
||||
// as far as the CONTEXT_MODULE categories still
|
||||
// haven't their contexts to be restored to
|
||||
// The {@link restore_create_question_files}, executed in the final step
|
||||
// The {@see restore_create_question_files}, executed in the final
|
||||
// step will be in charge of restoring all the question files.
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ require_once($CFG->libdir . '/completionlib.php');
|
||||
* @copyright 2014 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class moodle2_test extends \advanced_testcase {
|
||||
final class moodle2_test extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* Tests the availability field on modules and sections is correctly
|
||||
@@ -1146,7 +1146,6 @@ class moodle2_test extends \advanced_testcase {
|
||||
'filearea' => 'package',
|
||||
'itemid' => 0,
|
||||
'filepath' => '/',
|
||||
'filepath' => '/',
|
||||
'filename' => 'dummy.h5p',
|
||||
'addxapistate' => true,
|
||||
];
|
||||
|
||||
@@ -77,7 +77,6 @@ abstract class backup_plan_dbops extends backup_dbops {
|
||||
// Get the course and sequence of the section
|
||||
$secrec = $DB->get_record('course_sections', array('id' => $sectionid), 'course, sequence');
|
||||
$courseid = $secrec->course;
|
||||
$sequence = $secrec->sequence;
|
||||
|
||||
// Get the section->sequence contents (it roots the activities order)
|
||||
// Get all course modules belonging to requested section
|
||||
@@ -89,7 +88,7 @@ abstract class backup_plan_dbops extends backup_dbops {
|
||||
WHERE cm.course = ?
|
||||
AND cm.section = ?
|
||||
AND cm.deletioninprogress <> 1", array($courseid, $sectionid));
|
||||
foreach (explode(',', $sequence) as $moduleid) {
|
||||
foreach (explode(',', (string) $secrec->sequence) as $moduleid) {
|
||||
if (isset($modules[$moduleid])) {
|
||||
$module = array('id' => $modules[$moduleid]->id, 'modname' => $modules[$moduleid]->modname);
|
||||
$modulesarr[] = (object)$module;
|
||||
|
||||
@@ -671,8 +671,7 @@ abstract class restore_dbops {
|
||||
$questions = self::restore_get_questions($restoreid, $category->id);
|
||||
|
||||
// Collect all the questions for this category into memory so we only talk to the DB once.
|
||||
$questioncache = $DB->get_records_sql_menu('SELECT q.id,
|
||||
q.stamp
|
||||
$questioncache = $DB->get_records_sql_menu('SELECT q.stamp, q.id
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv
|
||||
ON qv.questionid = q.id
|
||||
@@ -683,8 +682,8 @@ abstract class restore_dbops {
|
||||
WHERE qc.id = ?', array($matchcat->id));
|
||||
|
||||
foreach ($questions as $question) {
|
||||
if (isset($questioncache[$question->stamp." ".$question->version])) {
|
||||
$matchqid = $questioncache[$question->stamp." ".$question->version];
|
||||
if (isset($questioncache[$question->stamp])) {
|
||||
$matchqid = $questioncache[$question->stamp];
|
||||
} else {
|
||||
$matchqid = false;
|
||||
}
|
||||
|
||||
@@ -35,22 +35,35 @@ require_once($CFG->dirroot.'/backup/util/xml/parser/processors/grouped_parser_pr
|
||||
* TODO: Complete phpdocs
|
||||
*/
|
||||
class restore_questions_parser_processor extends grouped_parser_processor {
|
||||
/** @var string XML path in the questions.xml backup file to question categories. */
|
||||
protected const CATEGORY_PATH = '/question_categories/question_category';
|
||||
|
||||
protected $restoreid;
|
||||
protected $lastcatid;
|
||||
/** @var string XML path in the questions.xml to question elements within question_category (Moodle 4.0+). */
|
||||
protected const QUESTION_SUBPATH =
|
||||
'/question_bank_entries/question_bank_entry/question_version/question_versions/questions/question';
|
||||
|
||||
/** @var string XML path in the questions.xml to question elements within question_category (before Moodle 4.0). */
|
||||
protected const LEGACY_QUESTION_SUBPATH = '/questions/question';
|
||||
|
||||
/** @var string identifies the current restore. */
|
||||
protected string $restoreid;
|
||||
|
||||
/** @var int during the restore, this tracks the last category we saw. Any questions we see will be in here. */
|
||||
protected int $lastcatid;
|
||||
|
||||
public function __construct($restoreid) {
|
||||
$this->restoreid = $restoreid;
|
||||
$this->lastcatid = 0;
|
||||
parent::__construct(array());
|
||||
parent::__construct();
|
||||
// Set the paths we are interested on
|
||||
$this->add_path('/question_categories/question_category');
|
||||
$this->add_path('/question_categories/question_category/questions/question');
|
||||
$this->add_path(self::CATEGORY_PATH);
|
||||
$this->add_path(self::CATEGORY_PATH . self::QUESTION_SUBPATH);
|
||||
$this->add_path(self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH);
|
||||
}
|
||||
|
||||
protected function dispatch_chunk($data) {
|
||||
// Prepare question_category record
|
||||
if ($data['path'] == '/question_categories/question_category') {
|
||||
if ($data['path'] == self::CATEGORY_PATH) {
|
||||
$info = (object)$data['tags'];
|
||||
$itemname = 'question_category';
|
||||
$itemid = $info->id;
|
||||
@@ -58,7 +71,8 @@ class restore_questions_parser_processor extends grouped_parser_processor {
|
||||
$this->lastcatid = $itemid;
|
||||
|
||||
// Prepare question record
|
||||
} else if ($data['path'] == '/question_categories/question_category/questions/question') {
|
||||
} else if ($data['path'] == self::CATEGORY_PATH . self::QUESTION_SUBPATH ||
|
||||
$data['path'] == self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH) {
|
||||
$info = (object)$data['tags'];
|
||||
$itemname = 'question';
|
||||
$itemid = $info->id;
|
||||
|
||||
+11
-11
@@ -134,14 +134,14 @@ class core_backup_renderer extends plugin_renderer_base {
|
||||
$html .= $this->backup_detail_pair(get_string('backupmode', 'backup'), get_string('backupmode'.$details->mode, 'backup'));
|
||||
$html .= $this->backup_detail_pair(get_string('backupdate', 'backup'), userdate($details->backup_date));
|
||||
$html .= $this->backup_detail_pair(get_string('moodleversion', 'backup'),
|
||||
html_writer::tag('span', $details->moodle_release, array('class' => 'moodle_release')).
|
||||
html_writer::tag('span', '['.$details->moodle_version.']', array('class' => 'moodle_version sub-detail')));
|
||||
html_writer::tag('span', s($details->moodle_release), array('class' => 'moodle_release')).
|
||||
html_writer::tag('span', '[' . s($details->moodle_version) .']', array('class' => 'moodle_version sub-detail')));
|
||||
$html .= $this->backup_detail_pair(get_string('backupversion', 'backup'),
|
||||
html_writer::tag('span', $details->backup_release, array('class' => 'moodle_release')).
|
||||
html_writer::tag('span', '['.$details->backup_version.']', array('class' => 'moodle_version sub-detail')));
|
||||
html_writer::tag('span', s($details->backup_release), array('class' => 'moodle_release')).
|
||||
html_writer::tag('span', '[' . s($details->backup_version) . ']', array('class' => 'moodle_version sub-detail')));
|
||||
$html .= $this->backup_detail_pair(get_string('originalwwwroot', 'backup'),
|
||||
html_writer::tag('span', $details->original_wwwroot, array('class' => 'originalwwwroot')).
|
||||
html_writer::tag('span', '['.$details->original_site_identifier_hash.']', array('class' => 'sitehash sub-detail')));
|
||||
html_writer::tag('span', s($details->original_wwwroot), array('class' => 'originalwwwroot')).
|
||||
html_writer::tag('span', '[' . s($details->original_site_identifier_hash) . ']', array('class' => 'sitehash sub-detail')));
|
||||
if (!empty($details->include_file_references_to_external_content)) {
|
||||
$message = '';
|
||||
if (backup_general_helper::backup_is_samesite($details)) {
|
||||
@@ -169,8 +169,8 @@ class core_backup_renderer extends plugin_renderer_base {
|
||||
$html .= html_writer::start_tag('div', ['class' => 'backup-section',
|
||||
'role' => 'table', 'aria-labelledby' => 'backupcoursedetailsheader']);
|
||||
$html .= $this->output->heading(get_string('backupcoursedetails', 'backup'), 2, 'header', 'backupcoursedetailsheader');
|
||||
$html .= $this->backup_detail_pair(get_string('coursetitle', 'backup'), $details->course->title);
|
||||
$html .= $this->backup_detail_pair(get_string('courseid', 'backup'), $details->course->courseid);
|
||||
$html .= $this->backup_detail_pair(get_string('coursetitle', 'backup'), format_string($details->course->title));
|
||||
$html .= $this->backup_detail_pair(get_string('courseid', 'backup'), clean_param($details->course->courseid, PARAM_INT));
|
||||
|
||||
// Warning users about front page backups.
|
||||
if ($details->original_course_format === 'site') {
|
||||
@@ -188,7 +188,7 @@ class core_backup_renderer extends plugin_renderer_base {
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
$html .= $this->backup_detail_pair(get_string('backupcoursesection', 'backup', $section->title), $value);
|
||||
$html .= $this->backup_detail_pair(get_string('backupcoursesection', 'backup', format_string($section->title)), $value);
|
||||
$table = null;
|
||||
foreach ($details->activities as $activitykey => $activity) {
|
||||
if ($activity->sectionid != $section->sectionid) {
|
||||
@@ -206,7 +206,7 @@ class core_backup_renderer extends plugin_renderer_base {
|
||||
$icon = new image_icon('monologo', '', $activity->modulename, ['class' => 'iconlarge icon-pre']);
|
||||
$table->data[] = array(
|
||||
$this->output->render($icon).$name,
|
||||
$activity->title,
|
||||
format_string($activity->title),
|
||||
($activity->settings[$activitykey.'_userinfo']) ? $yestick : $notick,
|
||||
);
|
||||
}
|
||||
@@ -522,7 +522,7 @@ class core_backup_renderer extends plugin_renderer_base {
|
||||
public function substage_buttons($haserrors) {
|
||||
$output = html_writer::start_tag('div', array('continuebutton'));
|
||||
if (!$haserrors) {
|
||||
$attrs = array('type' => 'submit', 'value' => get_string('continue'), 'class' => 'btn btn-primary');
|
||||
$attrs = ['type' => 'submit', 'value' => get_string('continue'), 'class' => 'btn btn-primary mr-1'];
|
||||
$output .= html_writer::empty_tag('input', $attrs);
|
||||
}
|
||||
$attrs = array('type' => 'submit', 'name' => 'cancel', 'value' => get_string('cancel'), 'class' => 'btn btn-secondary');
|
||||
|
||||
@@ -23,6 +23,7 @@ use core_badges\reportbuilder\local\entities\badge;
|
||||
use core_reportbuilder\local\helpers\database;
|
||||
use core_reportbuilder\local\report\{action, column};
|
||||
use core_reportbuilder\system_report;
|
||||
use html_writer;
|
||||
use lang_string;
|
||||
use moodle_url;
|
||||
use pix_icon;
|
||||
@@ -42,6 +43,9 @@ require_once("{$CFG->libdir}/badgeslib.php");
|
||||
*/
|
||||
class badges extends system_report {
|
||||
|
||||
/** @var int $badgeid The ID of the current badge row */
|
||||
private int $badgeid;
|
||||
|
||||
/**
|
||||
* Initialise report, we need to set the main table, load our entities and set columns/filters
|
||||
*/
|
||||
@@ -73,8 +77,8 @@ 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.
|
||||
$this->set_downloadable(false);
|
||||
@@ -115,7 +119,6 @@ class badges extends system_report {
|
||||
$this->add_columns_from_entities($columns);
|
||||
|
||||
// Issued badges column.
|
||||
// TODO: Move this column to the entity when MDL-76392 is integrated.
|
||||
$tempbadgealias = database::generate_alias();
|
||||
$badgeentityalias = $badgeentity->get_table_alias('badge');
|
||||
$this->add_column((new column(
|
||||
@@ -130,7 +133,14 @@ class badges extends system_report {
|
||||
INNER JOIN {user} u
|
||||
ON {$tempbadgealias}.userid = u.id
|
||||
WHERE {$tempbadgealias}.badgeid = {$badgeentityalias}.id AND u.deleted = 0)", 'issued')
|
||||
->set_is_sortable(true));
|
||||
->set_is_sortable(true)
|
||||
->set_callback(function(int $count): string {
|
||||
if (!has_capability('moodle/badges:viewawarded', $this->get_context())) {
|
||||
return (string) $count;
|
||||
}
|
||||
|
||||
return html_writer::link(new moodle_url('/badges/recipients.php', ['id' => $this->badgeid]), $count);
|
||||
}));
|
||||
|
||||
// Remove title from image column.
|
||||
$this->get_column('badge:image')->set_title(null);
|
||||
@@ -146,11 +156,10 @@ class badges extends system_report {
|
||||
* unique identifier
|
||||
*/
|
||||
protected function add_filters(): void {
|
||||
$filters = [
|
||||
$this->add_filters_from_entities([
|
||||
'badge:name',
|
||||
'badge:status',
|
||||
];
|
||||
$this->add_filters_from_entities($filters);
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -286,6 +295,15 @@ class badges extends system_report {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the ID of the badge within each row
|
||||
*
|
||||
* @param stdClass $row
|
||||
*/
|
||||
public function row_callback(stdClass $row): void {
|
||||
$this->badgeid = (int) $row->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes to add to the row
|
||||
*
|
||||
|
||||
@@ -197,6 +197,7 @@ class award_criteria_courseset extends award_criteria {
|
||||
* @return bool Whether criteria is complete
|
||||
*/
|
||||
public function review($userid, $filtered = false) {
|
||||
$overall = false;
|
||||
foreach ($this->params as $param) {
|
||||
$course = new stdClass();
|
||||
$course->id = $param['course'];
|
||||
|
||||
+4
-3
@@ -89,11 +89,14 @@ if (!has_any_capability(array(
|
||||
}
|
||||
|
||||
$PAGE->set_title($hdr);
|
||||
|
||||
/** @var core_badges_renderer $output */
|
||||
$output = $PAGE->get_renderer('core', 'badges');
|
||||
|
||||
if (($delete || $archive) && has_capability('moodle/badges:deletebadge', $PAGE->context)) {
|
||||
if ($delete || $archive) {
|
||||
$badgeid = ($archive != 0) ? $archive : $delete;
|
||||
$badge = new badge($badgeid);
|
||||
require_capability('moodle/badges:deletebadge', $badge->get_context());
|
||||
if (!$confirm) {
|
||||
echo $output->header();
|
||||
// Archive this badge?
|
||||
@@ -158,8 +161,6 @@ if ($msg !== '') {
|
||||
}
|
||||
|
||||
$report = system_report_factory::create(badges::class, $PAGE->context);
|
||||
$report->set_default_no_results_notice(new lang_string('nobadges', 'badges'));
|
||||
|
||||
echo $report->output();
|
||||
|
||||
echo $OUTPUT->footer();
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ echo $OUTPUT->heading(print_badge_image($badge, $context, 'small') . ' ' . $badg
|
||||
|
||||
if ($awards == 'cron') {
|
||||
echo $OUTPUT->notification(get_string('awardoncron', 'badges'), 'notifysuccess');
|
||||
} else if ($awards != 0) {
|
||||
} else if ((int)$awards > 0) {
|
||||
echo $OUTPUT->notification(get_string('numawardstat', 'badges', $awards), 'notifysuccess');
|
||||
}
|
||||
echo $output->print_badge_status_box($badge);
|
||||
|
||||
@@ -757,6 +757,66 @@ class badgeslib_test extends advanced_testcase {
|
||||
$this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
|
||||
}
|
||||
|
||||
/**
|
||||
* Test badges observer when user_updated event is fired.
|
||||
* @covers \award_criteria_courseset
|
||||
*/
|
||||
public function test_badges_observer_courseset_criteria_review(): void {
|
||||
$this->preventResetByRollback(); // Messaging is not compatible with transactions.
|
||||
$badge = new badge($this->coursebadge);
|
||||
$this->assertFalse($badge->is_issued($this->user->id));
|
||||
|
||||
$additionalcourse = $this->getDataGenerator()->create_course(['enablecompletion' => true]);
|
||||
$this->getDataGenerator()->enrol_user($this->user->id, $additionalcourse->id);
|
||||
|
||||
$criteriaoverall = award_criteria::build(['criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id]);
|
||||
$criteriaoverall->save(['agg' => BADGE_CRITERIA_AGGREGATION_ANY]);
|
||||
$criteriaoverall1 = award_criteria::build(['criteriatype' => BADGE_CRITERIA_TYPE_COURSESET, 'badgeid' => $badge->id]);
|
||||
$criteriaoverall1->save(['agg' => BADGE_CRITERIA_AGGREGATION_ANY, 'course_' . $this->course->id => $this->course->id,
|
||||
'course_' . $additionalcourse->id => $additionalcourse->id]);
|
||||
|
||||
$ccompletion = new completion_completion(['course' => $this->course->id, 'userid' => $this->user->id]);
|
||||
$ccompletion2 = new completion_completion(['course' => $additionalcourse->id, 'userid' => $this->user->id]);
|
||||
// Assert the badge will not be issued to the user as is.
|
||||
$badge = new badge($this->coursebadge);
|
||||
$badge->review_all_criteria();
|
||||
$this->assertFalse($badge->is_issued($this->user->id));
|
||||
|
||||
// Mark course as complete.
|
||||
$sink = $this->redirectMessages();
|
||||
$ccompletion->mark_complete();
|
||||
$ccompletion2->mark_complete();
|
||||
// Thee messages are generated: Two for the course completed and the other one for the badge awarded.
|
||||
$messages = $sink->get_messages();
|
||||
$this->assertCount(3, $messages);
|
||||
$this->assertEquals('badgerecipientnotice', $messages[0]->eventtype);
|
||||
$this->assertEquals('coursecompleted', $messages[1]->eventtype);
|
||||
$sink->close();
|
||||
|
||||
// Check if badge is awarded.
|
||||
$this->assertDebuggingCalled('Error baking badge image!');
|
||||
$this->assertTrue($badge->is_issued($this->user->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the criteria review method for courseset
|
||||
* @covers \award_criteria_courseset::review
|
||||
*/
|
||||
public function test_badges_courseset_criteria_review_empty_courseset(): void {
|
||||
$this->preventResetByRollback(); // Messaging is not compatible with transactions.
|
||||
$badge = new badge($this->coursebadge);
|
||||
$this->assertFalse($badge->is_issued($this->user->id));
|
||||
|
||||
$criteriaoverall = award_criteria::build(['criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id]);
|
||||
$criteriaoverall->save(['agg' => BADGE_CRITERIA_AGGREGATION_ANY]);
|
||||
$criteriaoverall1 = award_criteria::build(['criteriatype' => BADGE_CRITERIA_TYPE_COURSESET, 'badgeid' => $badge->id]);
|
||||
$criteriaoverall1->save();
|
||||
// Assert the badge will not be issued to the user as is.
|
||||
$badge = new badge($this->coursebadge);
|
||||
$badge->review_all_criteria();
|
||||
$this->assertFalse($badge->is_issued($this->user->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test badges observer when cohort_member_added event is fired and user required to belong to multiple (all) cohorts.
|
||||
*
|
||||
|
||||
@@ -51,7 +51,7 @@ Feature: Add badges to the system
|
||||
And I should see "Math"
|
||||
And I should see "Physics"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I should not see "There are currently no badges available for users to earn."
|
||||
And I should not see "There are no matching badges available for users to earn."
|
||||
|
||||
@javascript @_file_upload
|
||||
Scenario: Add a badge related
|
||||
@@ -163,7 +163,7 @@ Feature: Add badges to the system
|
||||
And I should see "Alignments (0)"
|
||||
And I should not see "Create badge"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I should not see "There are currently no badges available for users to earn."
|
||||
And I should not see "There are no matching badges available for users to earn."
|
||||
# See buttons from the "Site badges" page.
|
||||
And I am on homepage
|
||||
When I click on "Site pages" "list_item" in the "Navigation" "block"
|
||||
|
||||
@@ -43,7 +43,7 @@ Feature: Manage badges
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I press "Delete" action in the "Badge #1" report row
|
||||
And I press "Delete and remove existing issued badges"
|
||||
Then I should see "There are currently no badges available for users to earn"
|
||||
Then I should see "There are no matching badges available for users to earn."
|
||||
|
||||
Scenario: Enable and disable access to a badge
|
||||
Given I log in as "admin"
|
||||
@@ -68,7 +68,10 @@ Feature: Manage badges
|
||||
| Badge #1 | Not available |
|
||||
|
||||
Scenario: Award a badge
|
||||
Given I log in as "admin"
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| user1 | User | One | user1@example.com |
|
||||
When I log in as "admin"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I press "Edit" action in the "Badge #1" report row
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
@@ -79,9 +82,14 @@ Feature: Manage badges
|
||||
And I press "Enable access" action in the "Badge #1" report row
|
||||
And I press "Continue"
|
||||
And I press "Award badge" action in the "Badge #1" report row
|
||||
And I set the field "potentialrecipients[]" to "Admin User (moodle@example.com)"
|
||||
And I set the field "potentialrecipients[]" to "Admin User (moodle@example.com),User One (user1@example.com)"
|
||||
And I press "Award badge"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
Then the following should exist in the "reportbuilder-table" table:
|
||||
Then the following should exist in the "Badges" table:
|
||||
| Name | Badge status | Recipients |
|
||||
| Badge #1 | Available | 1 |
|
||||
| Badge #1 | Available | 2 |
|
||||
And I click on "2" "link" in the "Badge #1" "table_row"
|
||||
And the following should exist in the "generaltable" table:
|
||||
| -1- |
|
||||
| Admin User |
|
||||
| User One |
|
||||
|
||||
@@ -4,7 +4,7 @@ Feature: Manage badges is not shown when there are no existing badges.
|
||||
Scenario: Check navigation at site level with no badges
|
||||
Given I log in as "admin"
|
||||
When I navigate to "Badges > Manage badges" in site administration
|
||||
And I should see "There are currently no badges available for users to earn"
|
||||
And I should see "There are no matching badges available for users to earn."
|
||||
Then "Manage badges" "button" should not exist
|
||||
|
||||
Scenario: Check navigation at course level with no badges
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
.block_accessreview_success,
|
||||
.block_accessreview.block_accessreview_success.hasinfo {
|
||||
color: #1e451e;
|
||||
background: #d7e6d7;
|
||||
border-color: #c8ddc8;
|
||||
background: #eff5ef;
|
||||
box-shadow: 0 0 2px 2px #619a61;
|
||||
}
|
||||
|
||||
.block_accessreview_danger,
|
||||
.block_accessreview.block_accessreview_danger.hasinfo {
|
||||
color: #6e211e;
|
||||
background: #f6d9d8;
|
||||
border-color: #f3c9c8;
|
||||
background: #fdf7f7;
|
||||
box-shadow: 0 0 2px 2px #da6960;
|
||||
}
|
||||
|
||||
.block_accessreview_warning,
|
||||
.block_accessreview.block_accessreview_warning.hasinfo {
|
||||
color: #7d5a29;
|
||||
background: #fcefdc;
|
||||
border-color: #fbe8cd;
|
||||
color: #694b21;
|
||||
background: #fdf2e3;
|
||||
box-shadow: 0 0 2px 2px #c97a0e;
|
||||
}
|
||||
|
||||
.block_accessreview_table {
|
||||
|
||||
@@ -56,6 +56,9 @@ class block_calendar_month extends block_base {
|
||||
$calendar = \calendar_information::create(time(), $courseid, $categoryid);
|
||||
list($data, $template) = calendar_get_view($calendar, 'monthblock', isloggedin());
|
||||
|
||||
// Add a flag that this is coming from calendar block.
|
||||
$data->iscalendarblock = true;
|
||||
|
||||
$renderer = $this->page->get_renderer('core_calendar');
|
||||
$this->content->text .= $renderer->render_from_template($template, $data);
|
||||
|
||||
|
||||
@@ -91,3 +91,22 @@ Feature: Enable the calendar block in a course and test it's functionality
|
||||
Then I should see "Group Event"
|
||||
And I am on the "Course 1" course page logged in as student2
|
||||
And I should not see "Group Event"
|
||||
|
||||
@javascript
|
||||
Scenario: Click on today's course event on the calendar view page's calendar block
|
||||
Given I log in as "admin"
|
||||
And I create a calendar event with form data:
|
||||
| id_eventtype | Site |
|
||||
| id_name | Site Event |
|
||||
And I am on site homepage
|
||||
And I turn editing mode on
|
||||
And I add the "Calendar" block
|
||||
And I configure the "Calendar" block
|
||||
And I set the following fields to these values:
|
||||
| Page contexts | Display throughout the entire site |
|
||||
And I press "Save changes"
|
||||
When I am on "Course 1" course homepage
|
||||
And I follow "Full calendar"
|
||||
And I click on today in the mini-calendar block to view the detail
|
||||
Then I should see "Site Event" in the "Calendar" "block"
|
||||
And ".popover" "css_element" should not exist
|
||||
|
||||
@@ -71,7 +71,7 @@ class block_base {
|
||||
|
||||
/**
|
||||
* An object to contain the information to be displayed in the block.
|
||||
* @var stdClass $content
|
||||
* @var stdClass|null $content
|
||||
*/
|
||||
var $content = NULL;
|
||||
|
||||
@@ -88,7 +88,7 @@ class block_base {
|
||||
public $page = NULL;
|
||||
|
||||
/**
|
||||
* This blocks's context.
|
||||
* This block's context.
|
||||
* @var context
|
||||
*/
|
||||
public $context = NULL;
|
||||
@@ -141,7 +141,7 @@ class block_base {
|
||||
* This should be implemented by the derived class to return
|
||||
* the content object.
|
||||
*
|
||||
* @return stdObject
|
||||
* @return stdClass
|
||||
*/
|
||||
function get_content() {
|
||||
// This should be implemented by the derived class.
|
||||
@@ -167,7 +167,7 @@ class block_base {
|
||||
* Intentionally doesn't check if content_type is set.
|
||||
* This is already done in {@link _self_test()}
|
||||
*
|
||||
* @return string $this->content_type
|
||||
* @return int $this->content_type
|
||||
*/
|
||||
function get_content_type() {
|
||||
// Intentionally doesn't check if a content_type is set. This is already done in _self_test()
|
||||
@@ -178,7 +178,7 @@ class block_base {
|
||||
* Returns true or false, depending on whether this block has any content to display
|
||||
* and whether the user has permission to view the block
|
||||
*
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
function is_empty() {
|
||||
if ( !has_capability('moodle/block:view', $this->context) ) {
|
||||
@@ -194,7 +194,7 @@ class block_base {
|
||||
* then calls the block's {@link get_content()} function
|
||||
* to set its value back.
|
||||
*
|
||||
* @return stdObject
|
||||
* @return stdClass
|
||||
*/
|
||||
function refresh_content() {
|
||||
// Nothing special here, depends on content()
|
||||
@@ -211,7 +211,7 @@ class block_base {
|
||||
* {@link html_attributes()}, {@link formatted_contents()} or {@link get_content()},
|
||||
* {@link hide_header()}, {@link (get_edit_controls)}, etc.
|
||||
*
|
||||
* @return block_contents a representation of the block, for rendering.
|
||||
* @return block_contents|null a representation of the block, for rendering.
|
||||
* @since Moodle 2.0.
|
||||
*/
|
||||
public function get_content_for_output($output) {
|
||||
|
||||
@@ -88,7 +88,7 @@ $string['sortbylastaccessed'] = 'Sort by last accessed';
|
||||
$string['sortbyshortname'] = 'Sort by short name';
|
||||
$string['privacy:request:preference:set'] = 'The value of the setting \'{$a->name}\' was \'{$a->value}\'';
|
||||
$string['viewquickstart'] = 'View Quickstart guide';
|
||||
$string['zero_default_title'] = 'You\'re not enrolled in any course';
|
||||
$string['zero_default_title'] = 'You\'re not enrolled in any courses.';
|
||||
$string['zero_default_intro'] = 'Once you\'re enrolled in a course, it will appear here.';
|
||||
$string['zero_request_title'] = 'Request your first course';
|
||||
$string['zero_request_intro'] = 'Need help getting started? Check out the <a href="{$a->dochref}" title="{$a->doctitle}" target="{$a->doctarget}">Moodle documentation</a> or take your first steps with our <a href="{$a->quickhref}" title="{$a->quicktitle}" target="{$a->quicktarget}">Quickstart guide</a>.';
|
||||
|
||||
@@ -35,7 +35,7 @@ Feature: My overview block pagination
|
||||
|
||||
Scenario: The pagination controls should be hidden if I am not enrolled in any courses
|
||||
When I am on the "My courses" page logged in as "student1"
|
||||
Then I should see "You're not enrolled in any course" in the "Course overview" "block"
|
||||
Then I should see "You're not enrolled in any courses." in the "Course overview" "block"
|
||||
And I should not see "Show" in the "Course overview" "block"
|
||||
And ".block_myoverview .dropdown-menu.show" "css_element" should not be visible
|
||||
And ".block_myoverview [data-control='next']" "css_element" should not be visible
|
||||
|
||||
@@ -40,7 +40,7 @@ Feature: My overview block searching
|
||||
|
||||
Scenario: There is no search if I am not enrolled in any course
|
||||
When I am on the "My courses" page logged in as "student2"
|
||||
Then I should see "You're not enrolled in any course" in the "Course overview" "block"
|
||||
Then I should see "You're not enrolled in any courses." in the "Course overview" "block"
|
||||
And "Search courses" "field" should not exist in the "Course overview" "block"
|
||||
And I log out
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Feature: Zero state on my overview block
|
||||
|
||||
Scenario: Users with no permissions don't see any CTA
|
||||
Given I am on the "My courses" page logged in as "user"
|
||||
When I should see "You're not enrolled in any course"
|
||||
When I should see "You're not enrolled in any courses."
|
||||
Then I should see "Once you're enrolled in a course, it will appear here."
|
||||
And I should not see "Create course"
|
||||
And I should not see "Request a course"
|
||||
@@ -46,7 +46,7 @@ Feature: Zero state on my overview block
|
||||
| fullname | Course 1 |
|
||||
| shortname | C1 |
|
||||
When I am on the "My courses" page logged in as "manager"
|
||||
Then I should see "You're not enrolled in any course"
|
||||
Then I should see "You're not enrolled in any courses."
|
||||
Then I should see "Once you're enrolled in a course, it will appear here."
|
||||
And "Manage courses" "button" should exist
|
||||
And "Create course" "button" should exist
|
||||
@@ -64,7 +64,7 @@ Feature: Zero state on my overview block
|
||||
| fullname | Course 1 |
|
||||
| shortname | C1 |
|
||||
When I am on the "My courses" page logged in as "manager"
|
||||
Then I should see "You're not enrolled in any course"
|
||||
Then I should see "You're not enrolled in any courses."
|
||||
Then I should not see "To view all courses on this sie, go to Manage courses"
|
||||
And "Manage courses" "button" should not exist
|
||||
And "Create course" "button" should exist
|
||||
|
||||
@@ -180,7 +180,18 @@ $PAGE->set_pagelayout('admin');
|
||||
|
||||
if ($rssid) {
|
||||
$isadding = false;
|
||||
$rssrecord = $DB->get_record('block_rss_client', array('id' => $rssid), '*', MUST_EXIST);
|
||||
|
||||
if ($managesharedfeeds) {
|
||||
$select = 'id = :id AND (userid = :userid OR shared = 1)';
|
||||
} else {
|
||||
$select = 'id = :id AND userid = :userid';
|
||||
}
|
||||
|
||||
$rssrecord = $DB->get_record_select('block_rss_client', $select, [
|
||||
'id' => $rssid,
|
||||
'userid' => $USER->id,
|
||||
], '*', MUST_EXIST);
|
||||
|
||||
} else {
|
||||
$isadding = true;
|
||||
$rssrecord = new stdClass;
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string['addfeed'] = 'Add a news feed URL:';
|
||||
$string['addfeed'] = 'Add RSS feed URL';
|
||||
$string['addheadlineblock'] = 'Add RSS headline block';
|
||||
$string['addnew'] = 'Add new';
|
||||
$string['addnewfeed'] = 'Add a new feed';
|
||||
@@ -30,16 +30,16 @@ $string['cannotmakemodification'] = 'You are not allowed to make modifications t
|
||||
$string['clientchannellink'] = 'Source site...';
|
||||
$string['clientnumentries'] = 'The default number of entries to show per feed.';
|
||||
$string['clientshowchannellinklabel'] = 'Should a link to the original site (channel link) be displayed? (Note that if no feed link is supplied in the news feed then no link will be shown) :';
|
||||
$string['clientshowimagelabel'] = 'Show channel image if available :';
|
||||
$string['clientshowimagelabel'] = 'Show channel image if available';
|
||||
$string['configblock'] = 'Configure this block';
|
||||
$string['couldnotfindfeed'] = 'Could not find feed with id';
|
||||
$string['couldnotfindfeed'] = 'Could not find feed with ID.';
|
||||
$string['couldnotfindloadrssfeed'] = 'Could not find or load the RSS feed.';
|
||||
$string['customtitlelabel'] = 'Custom title (leave blank to use title supplied by feed):';
|
||||
$string['customtitlelabel'] = 'Custom title (leave blank to use title supplied by feed)';
|
||||
$string['deletefeedconfirm'] = 'Are you sure you want to delete this feed?';
|
||||
$string['disabledrssfeeds'] = 'RSS feeds are disabled';
|
||||
$string['displaydescriptionlabel'] = 'Display each link\'s description?';
|
||||
$string['editafeed'] = 'Edit a feed';
|
||||
$string['editfeeds'] = 'Edit, subscribe or unsubscribe from RSS/Atom news feeds';
|
||||
$string['editfeeds'] = 'Edit, subscribe or unsubscribe from RSS feeds';
|
||||
$string['editnewsfeeds'] = 'Edit news feeds';
|
||||
$string['editrssblock'] = 'Edit RSS headline block';
|
||||
$string['enableautodiscovery'] = 'Enable auto-discovery of feeds?';
|
||||
@@ -47,21 +47,21 @@ $string['enableautodiscovery_help'] = 'If enabled, feeds on web pages are found
|
||||
$string['failedfeed'] = 'Feed failed to download - will retry after {$a}';
|
||||
$string['failedfeeds'] = 'One or more RSS feeds have failed';
|
||||
$string['feed'] = 'Feed';
|
||||
$string['feedadded'] = 'News feed added';
|
||||
$string['feeddeleted'] = 'News feed deleted';
|
||||
$string['feeds'] = 'News feeds';
|
||||
$string['feedadded'] = 'RSS feed added';
|
||||
$string['feeddeleted'] = 'RSS feed deleted';
|
||||
$string['feeds'] = 'RSS feeds';
|
||||
$string['feedsaddedit'] = 'Add/edit feeds';
|
||||
$string['feedsconfigurenewinstance'] = 'Click here to configure this block to display RSS feeds.';
|
||||
$string['feedsconfigurenewinstance2'] = 'Click the edit icon above to configure this block to display RSS feeds.';
|
||||
$string['feedupdated'] = 'News feed updated';
|
||||
$string['feedupdated'] = 'RSS feed updated';
|
||||
$string['feedurl'] = 'Feed URL';
|
||||
$string['findmorefeeds'] = 'Find more RSS feeds';
|
||||
$string['choosefeedlabel'] = 'Choose the feeds which you would like to make available in this block:';
|
||||
$string['managefeeds'] = 'Manage all my feeds';
|
||||
$string['managefeeds'] = 'Manage RSS feeds';
|
||||
$string['nofeeds'] = 'There are no RSS feeds defined for this site.';
|
||||
$string['numentries'] = 'Entries per feed';
|
||||
$string['pickfeed'] = 'Pick a news feed';
|
||||
$string['pluginname'] = 'Remote RSS feeds';
|
||||
$string['pickfeed'] = 'Pick a RSS feed';
|
||||
$string['pluginname'] = 'RSS feeds';
|
||||
$string['privacy:metadata:block_rss_client:description'] = 'The description of the RSS feed.';
|
||||
$string['privacy:metadata:block_rss_client:preferredtitle'] = 'The preferred (custom) title of the RSS feed.';
|
||||
$string['privacy:metadata:block_rss_client:shared'] = 'If the RSS feed is available to all courses.';
|
||||
@@ -71,14 +71,14 @@ $string['privacy:metadata:block_rss_client:tableexplanation'] = 'RSS block infor
|
||||
$string['privacy:metadata:block_rss_client:title'] = 'The title of the RSS feed.';
|
||||
$string['privacy:metadata:block_rss_client:url'] = 'The URL of the RSS feed.';
|
||||
$string['privacy:metadata:block_rss_client:userid'] = 'The ID of the user that added the RSS feed.';
|
||||
$string['remotenewsfeed'] = 'Remote news feed';
|
||||
$string['remotenewsfeed'] = 'RSS feed';
|
||||
$string['refreshfeedstask'] = 'Refresh RSS feeds task';
|
||||
$string['rss_client:addinstance'] = 'Add a new remote RSS feeds block';
|
||||
$string['rss_client:addinstance'] = 'Add new RSS feeds block';
|
||||
$string['rss_client:createprivatefeeds'] = 'Create private RSS feeds';
|
||||
$string['rss_client:createsharedfeeds'] = 'Create shared RSS feeds';
|
||||
$string['rss_client:manageanyfeeds'] = 'Manage any RSS feeds';
|
||||
$string['rss_client:manageownfeeds'] = 'Manage own RSS feeds';
|
||||
$string['rss_client:myaddinstance'] = 'Add a new Remote RSS feeds block to Dashboard';
|
||||
$string['rss_client:myaddinstance'] = 'Add new RSS feeds block to Dashboard';
|
||||
$string['seeallfeeds'] = 'See all feeds';
|
||||
$string['sharedfeed'] = 'Shared feed';
|
||||
$string['shownumentrieslabel'] = 'Max number entries to show per block.';
|
||||
@@ -87,5 +87,5 @@ $string['submitters2'] = 'Submitters';
|
||||
$string['timeout'] = 'Time in minutes before an RSS feed expires in cache. Note that this time defines the minimum time before expiry; the feed will be refreshed in cache on the next cron execution after expiry. Recommended values are 30 mins or greater.';
|
||||
$string['timeoutdesc'] = 'Time in minutes for an RSS feed to live in cache.';
|
||||
$string['timeout2'] = 'Timeout';
|
||||
$string['updatefeed'] = 'Update a news feed URL:';
|
||||
$string['updatefeed'] = 'Update RSS feed URL';
|
||||
$string['viewfeed'] = 'View feed';
|
||||
|
||||
@@ -61,20 +61,27 @@ if ($returnurl) {
|
||||
$baseurl = new moodle_url('/blocks/rss_client/managefeeds.php', $urlparams);
|
||||
$PAGE->set_url($baseurl);
|
||||
|
||||
if ($managesharedfeeds) {
|
||||
$select = '(userid = :userid OR shared = 1)';
|
||||
} else {
|
||||
$select = 'userid = :userid';
|
||||
}
|
||||
|
||||
// Process any actions
|
||||
if ($deleterssid && confirm_sesskey()) {
|
||||
$DB->delete_records('block_rss_client', array('id'=>$deleterssid));
|
||||
|
||||
$deleterssid = $DB->get_field_select('block_rss_client', 'id', "id = :id AND {$select}", [
|
||||
'id' => $deleterssid,
|
||||
'userid' => $USER->id
|
||||
], MUST_EXIST);
|
||||
|
||||
$DB->delete_records('block_rss_client', ['id' => $deleterssid]);
|
||||
|
||||
redirect($PAGE->url, get_string('feeddeleted', 'block_rss_client'));
|
||||
}
|
||||
|
||||
// Display the list of feeds.
|
||||
if ($managesharedfeeds) {
|
||||
$select = '(userid = ' . $USER->id . ' OR shared = 1)';
|
||||
} else {
|
||||
$select = 'userid = ' . $USER->id;
|
||||
}
|
||||
$feeds = $DB->get_records_select('block_rss_client', $select, null, $DB->sql_order_by_text('title'));
|
||||
$feeds = $DB->get_records_select('block_rss_client', $select, ['userid' => $USER->id], $DB->sql_order_by_text('title'));
|
||||
|
||||
$strmanage = get_string('managefeeds', 'block_rss_client');
|
||||
|
||||
|
||||
Vendored
+17
-8
@@ -759,18 +759,27 @@ class cache_helper {
|
||||
debugging('Cache stores used for session definitions should ideally be searchable.', DEBUG_DEVELOPER);
|
||||
continue;
|
||||
}
|
||||
// Get all of the keys.
|
||||
$keys = $store->find_by_prefix(cache_session::KEY_PREFIX);
|
||||
$todelete = array();
|
||||
// Get all of the last access keys.
|
||||
$keys = $store->find_by_prefix(cache_session::LASTACCESS);
|
||||
$todelete = [];
|
||||
foreach ($store->get_many($keys) as $key => $value) {
|
||||
if (strpos($key, cache_session::KEY_PREFIX) !== 0 || !is_array($value) || !isset($value['lastaccess'])) {
|
||||
continue;
|
||||
$expiresvalue = 0;
|
||||
if ($value instanceof cache_ttl_wrapper) {
|
||||
$expiresvalue = $value->data;
|
||||
} else if ($value instanceof cache_cached_object) {
|
||||
$expiresvalue = $value->restore_object();
|
||||
} else {
|
||||
$expiresvalue = $value;
|
||||
}
|
||||
if ((int)$value['lastaccess'] < $purgetime || true) {
|
||||
$todelete[] = $key;
|
||||
$expires = (int) $expiresvalue;
|
||||
|
||||
if ($expires > 0 && $expires < $purgetime) {
|
||||
$prefix = substr($key, strlen(cache_session::LASTACCESS));
|
||||
$foundbyprefix = $store->find_by_prefix($prefix);
|
||||
$todelete = array_merge($todelete, [$key], $foundbyprefix);
|
||||
}
|
||||
}
|
||||
if (count($todelete)) {
|
||||
if ($todelete) {
|
||||
$outcome = (int)$store->delete_many($todelete);
|
||||
if ($output) {
|
||||
$strdef = s($definition->get_id());
|
||||
|
||||
Vendored
+14
-3
@@ -63,6 +63,9 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
*/
|
||||
const TTL_EXPIRE_BATCH = 10000;
|
||||
|
||||
/** @var int The number of seconds to wait for a connection or response from the Redis server. */
|
||||
const CONNECTION_TIMEOUT = 10;
|
||||
|
||||
/**
|
||||
* Name of this store.
|
||||
*
|
||||
@@ -241,8 +244,16 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
}
|
||||
|
||||
try {
|
||||
if ($redis->connect($server, $port, 1, null, 100, 1, $opts)) {
|
||||
|
||||
$connection = $redis->connect(
|
||||
$server,
|
||||
$port,
|
||||
self::CONNECTION_TIMEOUT, // Timeout.
|
||||
null,
|
||||
100, // Retry interval.
|
||||
self::CONNECTION_TIMEOUT, // Read timeout.
|
||||
$opts,
|
||||
);
|
||||
if ($connection) {
|
||||
if (!empty($password)) {
|
||||
$redis->auth($password);
|
||||
}
|
||||
@@ -355,7 +366,7 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
* @return array An array of the values of the given keys.
|
||||
*/
|
||||
public function get_many($keys) {
|
||||
$values = $this->redis->hMGet($this->hash, $keys);
|
||||
$values = $this->redis->hMGet($this->hash, $keys) ?: [];
|
||||
|
||||
if ($this->compressor == self::COMPRESSOR_NONE) {
|
||||
return $values;
|
||||
|
||||
Vendored
+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
@@ -5,6 +5,6 @@
|
||||
* @copyright 2017 Simey Lameze <simey@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define("core_calendar/calendar_view",["jquery","core/notification","core_calendar/selectors","core_calendar/events","core_calendar/view_manager","core_calendar/crud"],(function($,Notification,CalendarSelectors,CalendarEvents,CalendarViewManager,CalendarCrud){return{init:function(root,type){root=$(root),CalendarViewManager.init(root,type),function(root,type){var body=$("body");CalendarCrud.registerRemove(root);var reloadFunction="reloadCurrent"+type.charAt(0).toUpperCase()+type.slice(1);body.on(CalendarEvents.created,(function(){CalendarViewManager[reloadFunction](root)})),body.on(CalendarEvents.deleted,(function(){CalendarViewManager[reloadFunction](root)})),body.on(CalendarEvents.updated,(function(){CalendarViewManager[reloadFunction](root)})),root.on("change",CalendarSelectors.courseSelector,(function(){var courseId=$(this).val();CalendarViewManager[reloadFunction](root,courseId,null).then((function(){return root.find(CalendarSelectors.courseSelector).val(courseId)})).then((function(){CalendarViewManager.updateUrl("?view=upcoming&course="+courseId)})).fail(Notification.exception)})),body.on(CalendarEvents.filterChanged,(function(e,data){var daysWithEvent=root.find(CalendarSelectors.eventType[data.type]);1==data.hidden?daysWithEvent.addClass("hidden"):daysWithEvent.removeClass("hidden"),CalendarViewManager.foldDayEvents(root)}));var eventFormPromise=CalendarCrud.registerEventFormModal(root);CalendarCrud.registerEditListeners(root,eventFormPromise)}(root,type)}}}));
|
||||
define("core_calendar/calendar_view",["jquery","core/notification","core_calendar/selectors","core_calendar/events","core_calendar/view_manager","core_calendar/crud"],(function($,Notification,CalendarSelectors,CalendarEvents,CalendarViewManager,CalendarCrud){var registerEventListeners=function(root,type){var body=$("body");CalendarCrud.registerRemove(root);var reloadFunction="reloadCurrent"+type.charAt(0).toUpperCase()+type.slice(1);body.on(CalendarEvents.created,(function(){CalendarViewManager[reloadFunction](root)})),body.on(CalendarEvents.deleted,(function(){CalendarViewManager[reloadFunction](root)})),body.on(CalendarEvents.updated,(function(){CalendarViewManager[reloadFunction](root)})),root.on("change",CalendarSelectors.courseSelector,(function(){var courseId=$(this).val();CalendarViewManager[reloadFunction](root,courseId,null).then((function(){return root.find(CalendarSelectors.courseSelector).val(courseId)})).then((function(){CalendarViewManager.updateUrl("?view=upcoming&course="+courseId)})).fail(Notification.exception)})),body.on(CalendarEvents.filterChanged,(function(e,data){var daysWithEvent=root.find(CalendarSelectors.eventType[data.type]);1==data.hidden?daysWithEvent.addClass("hidden"):daysWithEvent.removeClass("hidden"),CalendarViewManager.foldDayEvents(root)}));var eventFormPromise=CalendarCrud.registerEventFormModal(root);CalendarCrud.registerEditListeners(root,eventFormPromise)};return{init:function(root,type){let isCalendarBlock=arguments.length>2&&void 0!==arguments[2]&&arguments[2];root=$(root),CalendarViewManager.init(root,type,isCalendarBlock),registerEventListeners(root,type)}}}));
|
||||
|
||||
//# sourceMappingURL=calendar_view.min.js.map
|
||||
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -5,6 +5,6 @@
|
||||
* @copyright 2017 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define("core_calendar/event_form",["jquery","core_calendar/repository","core/notification"],(function($,CalendarRepository,Notification){var SELECTORS_EVENT_GROUP_COURSE_ID='[name="groupcourseid"]',SELECTORS_EVENT_GROUP_ID='[name="groupid"]',SELECTORS_SELECT_OPTION="option",addCourseGroupSelectListeners=function(formElement){var courseGroupSelect=formElement.find(SELECTORS_EVENT_GROUP_COURSE_ID);courseGroupSelect.on("change",(function(){var courseId=formElement.find(SELECTORS_EVENT_GROUP_COURSE_ID).val();CalendarRepository.getCourseGroupsData(courseId).then((function(groups){return function(groups){var groupSelect=formElement.find(SELECTORS_EVENT_GROUP_ID),groupSelectOptions=groupSelect.find(SELECTORS_SELECT_OPTION),courseGroups=$(groups);groupSelectOptions.remove(),groupSelect.prop("disabled",!1),courseGroups.each((function(id,group){$(groupSelect).append($("<option></option>").attr("value",group.id).text(group.name))}))}(groups)})).catch(Notification.exception)}))};return{init:function(formId){var formElement=$("#"+formId);addCourseGroupSelectListeners(formElement)}}}));
|
||||
define("core_calendar/event_form",["jquery","core_calendar/repository","core/notification"],(function($,CalendarRepository,Notification){var SELECTORS_EVENT_GROUP_COURSE_ID='[name="groupcourseid"]',SELECTORS_EVENT_GROUP_ID='[name="groupid"]',SELECTORS_SELECT_OPTION="option";return{init:function(formId){!function(formElement){var courseGroupSelect=formElement.find(SELECTORS_EVENT_GROUP_COURSE_ID),loadGroupSelectOptions=function(groups){var groupSelect=formElement.find(SELECTORS_EVENT_GROUP_ID),groupSelectOptions=groupSelect.find(SELECTORS_SELECT_OPTION),courseGroups=$(groups);groupSelectOptions.remove(),groupSelect.prop("disabled",!1),courseGroups.each((function(id,group){$(groupSelect).append($("<option></option>").attr("value",group.id).text(group.name))}))};courseGroupSelect.on("change",(function(){var courseId=formElement.find(SELECTORS_EVENT_GROUP_COURSE_ID).val();isNaN(courseId)||courseId<=0?loadGroupSelectOptions([]):CalendarRepository.getCourseGroupsData(courseId).then((function(groups){return loadGroupSelectOptions(groups)})).catch(Notification.exception)}))}($("#"+formId))}}}));
|
||||
|
||||
//# sourceMappingURL=event_form.min.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"event_form.min.js","sources":["../src/event_form.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * A javascript module to enhance the event form.\n *\n * @module core_calendar/event_form\n * @copyright 2017 Ryan Wyllie <ryan@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core_calendar/repository', 'core/notification'], function($, CalendarRepository, Notification) {\n\n var SELECTORS = {\n EVENT_GROUP_COURSE_ID: '[name=\"groupcourseid\"]',\n EVENT_GROUP_ID: '[name=\"groupid\"]',\n SELECT_OPTION: 'option'\n };\n\n /**\n * Listen for when the user changes the group course when configuring\n * a group event and filter the options in the group select to only\n * show the groups available within the course the user has selected.\n *\n * @method addCourseGroupSelectListeners\n * @param {object} formElement The root form element\n */\n var addCourseGroupSelectListeners = function(formElement) {\n var courseGroupSelect = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID);\n\n var loadGroupSelectOptions = function(groups) {\n var groupSelect = formElement.find(SELECTORS.EVENT_GROUP_ID),\n groupSelectOptions = groupSelect.find(SELECTORS.SELECT_OPTION),\n courseGroups = $(groups);\n\n // Let's clear all options first.\n groupSelectOptions.remove();\n groupSelect.prop(\"disabled\", false);\n courseGroups.each(function(id, group) {\n $(groupSelect).append($(\"<option></option>\").attr(\"value\", group.id).text(group.name));\n });\n };\n\n // If the user choose a course in the selector do a WS request to get groups.\n courseGroupSelect.on('change', function() {\n var courseId = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID).val();\n CalendarRepository.getCourseGroupsData(courseId)\n .then(function(groups) {\n return loadGroupSelectOptions(groups);\n })\n .catch(Notification.exception);\n });\n };\n\n /**\n * Initialise all of the form enhancements.\n *\n * @method init\n * @param {string} formId The value of the form's id attribute\n */\n var init = function(formId) {\n var formElement = $('#' + formId);\n addCourseGroupSelectListeners(formElement);\n };\n\n return {\n init: init,\n };\n});\n"],"names":["define","$","CalendarRepository","Notification","SELECTORS","addCourseGroupSelectListeners","formElement","courseGroupSelect","find","on","courseId","val","getCourseGroupsData","then","groups","groupSelect","groupSelectOptions","courseGroups","remove","prop","each","id","group","append","attr","text","name","loadGroupSelectOptions","catch","exception","init","formId"],"mappings":";;;;;;;AAsBAA,kCAAO,CAAC,SAAU,2BAA4B,sBAAsB,SAASC,EAAGC,mBAAoBC,kBAE5FC,gCACuB,yBADvBA,yBAEgB,mBAFhBA,wBAGe,SAWfC,8BAAgC,SAASC,iBACrCC,kBAAoBD,YAAYE,KAAKJ,iCAgBzCG,kBAAkBE,GAAG,UAAU,eACvBC,SAAWJ,YAAYE,KAAKJ,iCAAiCO,MACjET,mBAAmBU,oBAAoBF,UAClCG,MAAK,SAASC,eAjBM,SAASA,YAC9BC,YAAcT,YAAYE,KAAKJ,0BAC/BY,mBAAqBD,YAAYP,KAAKJ,yBACtCa,aAAehB,EAAEa,QAGrBE,mBAAmBE,SACnBH,YAAYI,KAAK,YAAY,GAC7BF,aAAaG,MAAK,SAASC,GAAIC,OAC3BrB,EAAEc,aAAaQ,OAAOtB,EAAE,qBAAqBuB,KAAK,QAASF,MAAMD,IAAII,KAAKH,MAAMI,UASrEC,CAAuBb,WAEjCc,MAAMzB,aAAa0B,qBAezB,CACHC,KANO,SAASC,YACZzB,YAAcL,EAAE,IAAM8B,QAC1B1B,8BAA8BC"}
|
||||
{"version":3,"file":"event_form.min.js","sources":["../src/event_form.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * A javascript module to enhance the event form.\n *\n * @module core_calendar/event_form\n * @copyright 2017 Ryan Wyllie <ryan@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core_calendar/repository', 'core/notification'], function($, CalendarRepository, Notification) {\n\n var SELECTORS = {\n EVENT_GROUP_COURSE_ID: '[name=\"groupcourseid\"]',\n EVENT_GROUP_ID: '[name=\"groupid\"]',\n SELECT_OPTION: 'option'\n };\n\n /**\n * Listen for when the user changes the group course when configuring\n * a group event and filter the options in the group select to only\n * show the groups available within the course the user has selected.\n *\n * @method addCourseGroupSelectListeners\n * @param {object} formElement The root form element\n */\n var addCourseGroupSelectListeners = function(formElement) {\n var courseGroupSelect = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID);\n\n var loadGroupSelectOptions = function(groups) {\n var groupSelect = formElement.find(SELECTORS.EVENT_GROUP_ID),\n groupSelectOptions = groupSelect.find(SELECTORS.SELECT_OPTION),\n courseGroups = $(groups);\n\n // Let's clear all options first.\n groupSelectOptions.remove();\n groupSelect.prop(\"disabled\", false);\n courseGroups.each(function(id, group) {\n $(groupSelect).append($(\"<option></option>\").attr(\"value\", group.id).text(group.name));\n });\n };\n\n // If the user choose a course in the selector do a WS request to get groups.\n courseGroupSelect.on('change', function() {\n var courseId = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID).val();\n if (isNaN(courseId) || courseId <= 0) {\n loadGroupSelectOptions([]);\n } else {\n CalendarRepository.getCourseGroupsData(courseId)\n .then(function(groups) {\n return loadGroupSelectOptions(groups);\n })\n .catch(Notification.exception);\n }\n });\n };\n\n /**\n * Initialise all of the form enhancements.\n *\n * @method init\n * @param {string} formId The value of the form's id attribute\n */\n var init = function(formId) {\n var formElement = $('#' + formId);\n addCourseGroupSelectListeners(formElement);\n };\n\n return {\n init: init,\n };\n});\n"],"names":["define","$","CalendarRepository","Notification","SELECTORS","init","formId","formElement","courseGroupSelect","find","loadGroupSelectOptions","groups","groupSelect","groupSelectOptions","courseGroups","remove","prop","each","id","group","append","attr","text","name","on","courseId","val","isNaN","getCourseGroupsData","then","catch","exception","addCourseGroupSelectListeners"],"mappings":";;;;;;;AAsBAA,kCAAO,CAAC,SAAU,2BAA4B,sBAAsB,SAASC,EAAGC,mBAAoBC,kBAE5FC,gCACuB,yBADvBA,yBAEgB,mBAFhBA,wBAGe,eAqDZ,CACHC,KANO,SAASC,SArCgB,SAASC,iBACrCC,kBAAoBD,YAAYE,KAAKL,iCAErCM,uBAAyB,SAASC,YAC9BC,YAAcL,YAAYE,KAAKL,0BAC/BS,mBAAqBD,YAAYH,KAAKL,yBACtCU,aAAeb,EAAEU,QAGrBE,mBAAmBE,SACnBH,YAAYI,KAAK,YAAY,GAC7BF,aAAaG,MAAK,SAASC,GAAIC,OAC3BlB,EAAEW,aAAaQ,OAAOnB,EAAE,qBAAqBoB,KAAK,QAASF,MAAMD,IAAII,KAAKH,MAAMI,WAKxFf,kBAAkBgB,GAAG,UAAU,eACvBC,SAAWlB,YAAYE,KAAKL,iCAAiCsB,MAC7DC,MAAMF,WAAaA,UAAY,EAC/Bf,uBAAuB,IAEvBR,mBAAmB0B,oBAAoBH,UAClCI,MAAK,SAASlB,eACJD,uBAAuBC,WAEjCmB,MAAM3B,aAAa4B,cAahCC,CADkB/B,EAAE,IAAMK"}
|
||||
Vendored
+1
-1
@@ -6,6 +6,6 @@ define("core_calendar/popover",["theme_boost/popover","jquery","core_calendar/se
|
||||
* @copyright 2021 Huong Nguyen <huongnv13@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @since 4.0
|
||||
*/function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj},CalendarSelectors=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CalendarSelectors);const isPopoverConfigured=new Map,showPopover=target=>{const dateContainer=target.closest(CalendarSelectors.elements.dateContainer);if(!isPopoverConfigured.has(dateContainer)){(0,_jquery.default)(target).popover({trigger:"manual",placement:"top",html:!0,title:dateContainer.dataset.title,content:()=>{const source=(0,_jquery.default)(dateContainer).find(CalendarSelectors.elements.dateContent),content=(0,_jquery.default)("<div>");if(source.length){const temptContent=source.find(".hidden").clone(!1);content.html(temptContent.html())}return content.html()}}),isPopoverConfigured.set(dateContainer,!0)}(dateContainer=>"none"===window.getComputedStyle(dateContainer.querySelector(CalendarSelectors.elements.dateContent)).display)(dateContainer)&&((0,_jquery.default)(target).popover("show"),target.addEventListener("mouseleave",hidePopover),target.addEventListener("focusout",hidePopover))},hidePopover=e=>{const target=e.target,dateContainer=e.target.closest(CalendarSelectors.elements.dateContainer);if(dateContainer&&isPopoverConfigured.has(dateContainer)){const isTargetActive=target.contains(document.activeElement),isTargetHover=target.matches(":hover");isTargetActive||isTargetHover||((0,_jquery.default)(target).popover("hide"),target.removeEventListener("mouseleave",hidePopover),target.removeEventListener("focusout",hidePopover))}};let listenersRegistered=!1;listenersRegistered||((()=>{const showPopoverHandler=e=>{const dayLink=e.target.closest(CalendarSelectors.links.dayLink);dayLink&&(e.preventDefault(),showPopover(dayLink))};document.addEventListener("mouseover",showPopoverHandler),document.addEventListener("focusin",showPopoverHandler)})(),listenersRegistered=!0)}));
|
||||
*/function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj},CalendarSelectors=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CalendarSelectors);const isPopoverConfigured=new Map,showPopover=target=>{const dateContainer=target.closest(CalendarSelectors.elements.dateContainer);if(!isPopoverConfigured.has(dateContainer)){(0,_jquery.default)(target).popover({trigger:"manual",placement:"top",html:!0,title:dateContainer.dataset.title,content:()=>{const source=(0,_jquery.default)(dateContainer).find(CalendarSelectors.elements.dateContent),content=(0,_jquery.default)("<div>");if(source.length){const temptContent=source.find(".hidden").clone(!1);content.html(temptContent.html())}return content.html()},animation:!1}),isPopoverConfigured.set(dateContainer,!0)}(dateContainer=>"none"===window.getComputedStyle(dateContainer.querySelector(CalendarSelectors.elements.dateContent)).display)(dateContainer)&&((0,_jquery.default)(target).popover("show"),target.addEventListener("mouseleave",hidePopover),target.addEventListener("focusout",hidePopover),target.addEventListener("click",hidePopover))},hidePopover=e=>{const target=e.target,dateContainer=e.target.closest(CalendarSelectors.elements.dateContainer);if(dateContainer&&isPopoverConfigured.has(dateContainer)){const isTargetActive=target.contains(document.activeElement),isTargetHover=target.matches(":hover"),isTargetClicked=document.activeElement.contains(target);let removeListener=!0;isTargetActive||isTargetHover?isTargetClicked?(0,_jquery.default)(document.activeElement).popover("hide"):removeListener=!1:(0,_jquery.default)(target).popover("hide"),removeListener&&(target.removeEventListener("mouseleave",hidePopover),target.removeEventListener("focusout",hidePopover),target.removeEventListener("click",hidePopover))}};let listenersRegistered=!1;listenersRegistered||((()=>{const showPopoverHandler=e=>{const dayLink=e.target.closest(CalendarSelectors.links.dayLink);dayLink&&(e.preventDefault(),showPopover(dayLink))};document.addEventListener("mouseover",showPopoverHandler),document.addEventListener("focusin",showPopoverHandler)})(),listenersRegistered=!0)}));
|
||||
|
||||
//# sourceMappingURL=popover.min.js.map
|
||||
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -5,6 +5,6 @@
|
||||
* @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define("core_calendar/selectors",[],(function(){return{eventFilterItem:"[data-action='filter-event-type']",eventType:{site:"[data-eventtype-site]",category:"[data-eventtype-category]",course:"[data-eventtype-course]",group:"[data-eventtype-group]",user:"[data-eventtype-user]",other:"[data-eventtype-other]"},popoverType:{site:"[data-popover-eventtype-site]",category:"[data-popover-eventtype-category]",course:"[data-popover-eventtype-course]",group:"[data-popover-eventtype-group]",user:"[data-popover-eventtype-user]",other:"[data-popover-eventtype-other]"},calendarPeriods:{month:"[data-period='month']"},courseSelector:'select[name="course"]',viewSelector:'div[data-region="view-selector"]',actions:{create:'[data-action="new-event-button"]',edit:'[data-action="edit"]',remove:'[data-action="delete"]',viewEvent:'[data-action="view-event"]',deleteSubscription:'[data-action="delete-subscription"]'},elements:{courseSelector:'select[name="course"]',dateContainer:".clickable.hasevent",dateContent:'[data-region="day-content"]',monthDetailed:".calendarmonth.calendartable"},today:".today",day:'[data-region="day"]',calendarMain:'[data-region="calendar"]',wrapper:".calendarwrapper",eventItem:'[data-type="event"]',links:{navLink:".calendarwrapper .arrow_link",eventLink:"[data-region='event-item']",miniDayLink:"[data-region='mini-day-link']",dayLink:"[data-action='view-day-link']"},containers:{loadingIcon:'[data-region="overlay-icon-container"]'},mainCalendar:".maincalendar .heightcontainer",fullCalendarView:"page-calendar-view"}}));
|
||||
define("core_calendar/selectors",[],(function(){return{eventFilterItem:"[data-action='filter-event-type']",eventType:{site:"[data-eventtype-site]",category:"[data-eventtype-category]",course:"[data-eventtype-course]",group:"[data-eventtype-group]",user:"[data-eventtype-user]",other:"[data-eventtype-other]"},popoverType:{site:"[data-popover-eventtype-site]",category:"[data-popover-eventtype-category]",course:"[data-popover-eventtype-course]",group:"[data-popover-eventtype-group]",user:"[data-popover-eventtype-user]",other:"[data-popover-eventtype-other]"},calendarPeriods:{month:"[data-period='month']"},courseSelector:'select[name="course"]',viewSelector:'div[data-region="view-selector"]',actions:{create:'[data-action="new-event-button"]',edit:'[data-action="edit"]',remove:'[data-action="delete"]',viewEvent:'[data-action="view-event"]',deleteSubscription:'[data-action="delete-subscription"]'},elements:{courseSelector:'select[name="course"]',dateContainer:".clickable.hasevent",dateContent:'[data-region="day-content"]',monthDetailed:".calendarmonth.calendartable"},today:".today",day:'[data-region="day"]',calendarMain:'[data-region="calendar"]',wrapper:".calendarwrapper",eventItem:'[data-type="event"]',links:{navLink:".calendarwrapper .arrow_link",eventLink:"[data-region='event-item']",miniDayLink:"[data-region='mini-day-link']",dayLink:"[data-action='view-day-link']"},containers:{loadingIcon:'[data-region="overlay-icon-container"]'},mainCalendar:".maincalendar .heightcontainer",fullCalendarView:"page-calendar-view",pageHeaderHeadings:".page-header-headings h1"}}));
|
||||
|
||||
//# sourceMappingURL=selectors.min.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"selectors.min.js","sources":["../src/selectors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * CSS selectors for the calendar.\n *\n * @module core_calendar/selectors\n * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([], function() {\n return {\n eventFilterItem: \"[data-action='filter-event-type']\",\n eventType: {\n site: \"[data-eventtype-site]\",\n category: \"[data-eventtype-category]\",\n course: \"[data-eventtype-course]\",\n group: \"[data-eventtype-group]\",\n user: \"[data-eventtype-user]\",\n other: \"[data-eventtype-other]\",\n },\n popoverType: {\n site: \"[data-popover-eventtype-site]\",\n category: \"[data-popover-eventtype-category]\",\n course: \"[data-popover-eventtype-course]\",\n group: \"[data-popover-eventtype-group]\",\n user: \"[data-popover-eventtype-user]\",\n other: \"[data-popover-eventtype-other]\",\n },\n calendarPeriods: {\n month: \"[data-period='month']\",\n },\n courseSelector: 'select[name=\"course\"]',\n viewSelector: 'div[data-region=\"view-selector\"]',\n actions: {\n create: '[data-action=\"new-event-button\"]',\n edit: '[data-action=\"edit\"]',\n remove: '[data-action=\"delete\"]',\n viewEvent: '[data-action=\"view-event\"]',\n deleteSubscription: '[data-action=\"delete-subscription\"]',\n },\n elements: {\n courseSelector: 'select[name=\"course\"]',\n dateContainer: '.clickable.hasevent',\n dateContent: '[data-region=\"day-content\"]',\n monthDetailed: '.calendarmonth.calendartable',\n },\n today: '.today',\n day: '[data-region=\"day\"]',\n calendarMain: '[data-region=\"calendar\"]',\n wrapper: '.calendarwrapper',\n eventItem: '[data-type=\"event\"]',\n links: {\n navLink: '.calendarwrapper .arrow_link',\n eventLink: \"[data-region='event-item']\",\n miniDayLink: \"[data-region='mini-day-link']\",\n dayLink: \"[data-action='view-day-link']\",\n },\n containers: {\n loadingIcon: '[data-region=\"overlay-icon-container\"]',\n },\n mainCalendar: '.maincalendar .heightcontainer',\n fullCalendarView: 'page-calendar-view',\n };\n});\n"],"names":["define","eventFilterItem","eventType","site","category","course","group","user","other","popoverType","calendarPeriods","month","courseSelector","viewSelector","actions","create","edit","remove","viewEvent","deleteSubscription","elements","dateContainer","dateContent","monthDetailed","today","day","calendarMain","wrapper","eventItem","links","navLink","eventLink","miniDayLink","dayLink","containers","loadingIcon","mainCalendar","fullCalendarView"],"mappings":";;;;;;;AAsBAA,iCAAO,IAAI,iBACA,CACHC,gBAAiB,oCACjBC,UAAW,CACPC,KAAM,wBACNC,SAAU,4BACVC,OAAQ,0BACRC,MAAO,yBACPC,KAAM,wBACNC,MAAO,0BAEXC,YAAa,CACTN,KAAM,gCACNC,SAAU,oCACVC,OAAQ,kCACRC,MAAO,iCACPC,KAAM,gCACNC,MAAO,kCAEXE,gBAAiB,CACbC,MAAO,yBAEXC,eAAgB,wBAChBC,aAAc,mCACdC,QAAS,CACLC,OAAQ,mCACRC,KAAM,uBACNC,OAAQ,yBACRC,UAAW,6BACXC,mBAAoB,uCAExBC,SAAU,CACNR,eAAgB,wBAChBS,cAAe,sBACfC,YAAa,8BACbC,cAAe,gCAEnBC,MAAO,SACPC,IAAK,sBACLC,aAAc,2BACdC,QAAS,mBACTC,UAAW,sBACXC,MAAO,CACHC,QAAS,+BACTC,UAAW,6BACXC,YAAa,gCACbC,QAAS,iCAEbC,WAAY,CACRC,YAAa,0CAEjBC,aAAc,iCACdC,iBAAkB"}
|
||||
{"version":3,"file":"selectors.min.js","sources":["../src/selectors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * CSS selectors for the calendar.\n *\n * @module core_calendar/selectors\n * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([], function() {\n return {\n eventFilterItem: \"[data-action='filter-event-type']\",\n eventType: {\n site: \"[data-eventtype-site]\",\n category: \"[data-eventtype-category]\",\n course: \"[data-eventtype-course]\",\n group: \"[data-eventtype-group]\",\n user: \"[data-eventtype-user]\",\n other: \"[data-eventtype-other]\",\n },\n popoverType: {\n site: \"[data-popover-eventtype-site]\",\n category: \"[data-popover-eventtype-category]\",\n course: \"[data-popover-eventtype-course]\",\n group: \"[data-popover-eventtype-group]\",\n user: \"[data-popover-eventtype-user]\",\n other: \"[data-popover-eventtype-other]\",\n },\n calendarPeriods: {\n month: \"[data-period='month']\",\n },\n courseSelector: 'select[name=\"course\"]',\n viewSelector: 'div[data-region=\"view-selector\"]',\n actions: {\n create: '[data-action=\"new-event-button\"]',\n edit: '[data-action=\"edit\"]',\n remove: '[data-action=\"delete\"]',\n viewEvent: '[data-action=\"view-event\"]',\n deleteSubscription: '[data-action=\"delete-subscription\"]',\n },\n elements: {\n courseSelector: 'select[name=\"course\"]',\n dateContainer: '.clickable.hasevent',\n dateContent: '[data-region=\"day-content\"]',\n monthDetailed: '.calendarmonth.calendartable',\n },\n today: '.today',\n day: '[data-region=\"day\"]',\n calendarMain: '[data-region=\"calendar\"]',\n wrapper: '.calendarwrapper',\n eventItem: '[data-type=\"event\"]',\n links: {\n navLink: '.calendarwrapper .arrow_link',\n eventLink: \"[data-region='event-item']\",\n miniDayLink: \"[data-region='mini-day-link']\",\n dayLink: \"[data-action='view-day-link']\",\n },\n containers: {\n loadingIcon: '[data-region=\"overlay-icon-container\"]',\n },\n mainCalendar: '.maincalendar .heightcontainer',\n fullCalendarView: 'page-calendar-view',\n pageHeaderHeadings: '.page-header-headings h1',\n };\n});\n"],"names":["define","eventFilterItem","eventType","site","category","course","group","user","other","popoverType","calendarPeriods","month","courseSelector","viewSelector","actions","create","edit","remove","viewEvent","deleteSubscription","elements","dateContainer","dateContent","monthDetailed","today","day","calendarMain","wrapper","eventItem","links","navLink","eventLink","miniDayLink","dayLink","containers","loadingIcon","mainCalendar","fullCalendarView","pageHeaderHeadings"],"mappings":";;;;;;;AAsBAA,iCAAO,IAAI,iBACA,CACHC,gBAAiB,oCACjBC,UAAW,CACPC,KAAM,wBACNC,SAAU,4BACVC,OAAQ,0BACRC,MAAO,yBACPC,KAAM,wBACNC,MAAO,0BAEXC,YAAa,CACTN,KAAM,gCACNC,SAAU,oCACVC,OAAQ,kCACRC,MAAO,iCACPC,KAAM,gCACNC,MAAO,kCAEXE,gBAAiB,CACbC,MAAO,yBAEXC,eAAgB,wBAChBC,aAAc,mCACdC,QAAS,CACLC,OAAQ,mCACRC,KAAM,uBACNC,OAAQ,yBACRC,UAAW,6BACXC,mBAAoB,uCAExBC,SAAU,CACNR,eAAgB,wBAChBS,cAAe,sBACfC,YAAa,8BACbC,cAAe,gCAEnBC,MAAO,SACPC,IAAK,sBACLC,aAAc,2BACdC,QAAS,mBACTC,UAAW,sBACXC,MAAO,CACHC,QAAS,+BACTC,UAAW,6BACXC,YAAa,gCACbC,QAAS,iCAEbC,WAAY,CACRC,YAAa,0CAEjBC,aAAc,iCACdC,iBAAkB,qBAClBC,mBAAoB"}
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -169,8 +169,9 @@ function(
|
||||
* Register event listeners for the module.
|
||||
*
|
||||
* @param {object} root The calendar root element
|
||||
* @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block.
|
||||
*/
|
||||
var registerEventListeners = function(root) {
|
||||
var registerEventListeners = function(root, isCalendarBlock) {
|
||||
const viewingFullCalendar = document.getElementById(CalendarSelectors.fullCalendarView);
|
||||
// Listen the click on the day link to render the day view.
|
||||
root.on('click', SELECTORS.VIEW_DAY_LINK, function(e) {
|
||||
@@ -183,9 +184,13 @@ function(
|
||||
const url = '?view=day&time=' + dayLink.data('timestamp');
|
||||
if (viewingFullCalendar) {
|
||||
CalendarViewManager.refreshDayContent(root, year, month, day, courseId, categoryId, root,
|
||||
'core_calendar/calendar_day').then(function() {
|
||||
'core_calendar/calendar_day', isCalendarBlock).then(function() {
|
||||
e.preventDefault();
|
||||
return CalendarViewManager.updateUrl(url);
|
||||
// Update the URL if it's not calendar block.
|
||||
if (!isCalendarBlock) {
|
||||
CalendarViewManager.updateUrl(url);
|
||||
}
|
||||
return;
|
||||
}).catch(Notification.exception);
|
||||
} else {
|
||||
window.location.assign(Config.wwwroot + '/calendar/view.php' + url);
|
||||
@@ -244,10 +249,16 @@ function(
|
||||
};
|
||||
|
||||
return {
|
||||
init: function(root) {
|
||||
/**
|
||||
* Initializes the calendar view manager and registers event listeners.
|
||||
*
|
||||
* @param {HTMLElement} root - The root element where the calendar view manager and event listeners will be attached.
|
||||
* @param {boolean} [isCalendarBlock=false] - A flag indicating whether this is a calendar block.
|
||||
*/
|
||||
init: function(root, isCalendarBlock = false) {
|
||||
root = $(root);
|
||||
CalendarViewManager.init(root);
|
||||
registerEventListeners(root);
|
||||
CalendarViewManager.init(root, 'month', isCalendarBlock);
|
||||
registerEventListeners(root, isCalendarBlock);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -83,11 +83,11 @@ function(
|
||||
};
|
||||
|
||||
return {
|
||||
init: function(root, type) {
|
||||
init: function(root, type, isCalendarBlock = false) {
|
||||
root = $(root);
|
||||
|
||||
CalendarViewManager.init(root, type);
|
||||
registerEventListeners(root, type);
|
||||
CalendarViewManager.init(root, type, isCalendarBlock);
|
||||
registerEventListeners(root, type, isCalendarBlock);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -55,11 +55,15 @@ define(['jquery', 'core_calendar/repository', 'core/notification'], function($,
|
||||
// If the user choose a course in the selector do a WS request to get groups.
|
||||
courseGroupSelect.on('change', function() {
|
||||
var courseId = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID).val();
|
||||
CalendarRepository.getCourseGroupsData(courseId)
|
||||
.then(function(groups) {
|
||||
return loadGroupSelectOptions(groups);
|
||||
})
|
||||
.catch(Notification.exception);
|
||||
if (isNaN(courseId) || courseId <= 0) {
|
||||
loadGroupSelectOptions([]);
|
||||
} else {
|
||||
CalendarRepository.getCourseGroupsData(courseId)
|
||||
.then(function(groups) {
|
||||
return loadGroupSelectOptions(groups);
|
||||
})
|
||||
.catch(Notification.exception);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -54,7 +54,8 @@ const showPopover = target => {
|
||||
content.html(temptContent.html());
|
||||
}
|
||||
return content.html();
|
||||
}
|
||||
},
|
||||
'animation': false,
|
||||
});
|
||||
|
||||
isPopoverConfigured.set(dateContainer, true);
|
||||
@@ -64,6 +65,8 @@ const showPopover = target => {
|
||||
jQuery(target).popover('show');
|
||||
target.addEventListener('mouseleave', hidePopover);
|
||||
target.addEventListener('focusout', hidePopover);
|
||||
// Set up the hide function to the click event type.
|
||||
target.addEventListener('click', hidePopover);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -76,10 +79,23 @@ const hidePopover = e => {
|
||||
if (isPopoverConfigured.has(dateContainer)) {
|
||||
const isTargetActive = target.contains(document.activeElement);
|
||||
const isTargetHover = target.matches(':hover');
|
||||
|
||||
// Checks if a target element is clicked or pressed.
|
||||
const isTargetClicked = document.activeElement.contains(target);
|
||||
|
||||
let removeListener = true;
|
||||
if (!isTargetActive && !isTargetHover) {
|
||||
jQuery(target).popover('hide');
|
||||
} else if (isTargetClicked) {
|
||||
jQuery(document.activeElement).popover('hide');
|
||||
} else {
|
||||
removeListener = false;
|
||||
}
|
||||
|
||||
if (removeListener) {
|
||||
target.removeEventListener('mouseleave', hidePopover);
|
||||
target.removeEventListener('focusout', hidePopover);
|
||||
target.removeEventListener('click', hidePopover);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -73,5 +73,6 @@ define([], function() {
|
||||
},
|
||||
mainCalendar: '.maincalendar .heightcontainer',
|
||||
fullCalendarView: 'page-calendar-view',
|
||||
pageHeaderHeadings: '.page-header-headings h1',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -143,8 +143,9 @@ export const registerEventListenersForMonthDetailed = (pendingId) => {
|
||||
* Register event listeners for the module.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block.
|
||||
*/
|
||||
const registerEventListeners = (root) => {
|
||||
const registerEventListeners = (root, isCalendarBlock) => {
|
||||
root = $(root);
|
||||
|
||||
// Bind click events to event links.
|
||||
@@ -190,10 +191,12 @@ const registerEventListeners = (root) => {
|
||||
const link = e.currentTarget;
|
||||
|
||||
if (view === 'month' || view === 'monthblock') {
|
||||
changeMonth(root, link.href, link.dataset.year, link.dataset.month, courseId, categoryId, link.dataset.day);
|
||||
changeMonth(root, link.href, link.dataset.year, link.dataset.month,
|
||||
courseId, categoryId, link.dataset.day, isCalendarBlock);
|
||||
e.preventDefault();
|
||||
} else if (view === 'day') {
|
||||
changeDay(root, link.href, link.dataset.year, link.dataset.month, link.dataset.day, courseId, categoryId);
|
||||
changeDay(root, link.href, link.dataset.year, link.dataset.month, link.dataset.day,
|
||||
courseId, categoryId, isCalendarBlock);
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
@@ -287,18 +290,19 @@ export const refreshMonthContent = (root, year, month, courseId, categoryId, tar
|
||||
* @param {number} courseId The id of the course whose events are shown
|
||||
* @param {number} categoryId The id of the category whose events are shown
|
||||
* @param {number} day Day (optional)
|
||||
* @param {boolean} [isCalendarBlock=false] - A flag indicating whether this is a calendar block.
|
||||
* @return {promise}
|
||||
*/
|
||||
export const changeMonth = (root, url, year, month, courseId, categoryId, day = 1) => {
|
||||
export const changeMonth = (root, url, year, month, courseId, categoryId, day = 1, isCalendarBlock = false) => {
|
||||
return refreshMonthContent(root, year, month, courseId, categoryId, null, '', day)
|
||||
.then((...args) => {
|
||||
if (url.length && url !== '#') {
|
||||
if (url.length && url !== '#' && !isCalendarBlock) {
|
||||
updateUrl(url);
|
||||
}
|
||||
return args;
|
||||
})
|
||||
.then((...args) => {
|
||||
$('body').trigger(CalendarEvents.monthChanged, [year, month, courseId, categoryId]);
|
||||
$('body').trigger(CalendarEvents.monthChanged, [year, month, courseId, categoryId, day, isCalendarBlock]);
|
||||
return args;
|
||||
});
|
||||
};
|
||||
@@ -338,10 +342,12 @@ export const reloadCurrentMonth = (root, courseId = 0, categoryId = 0) => {
|
||||
* @param {number} categoryId The id of the category whose events are shown
|
||||
* @param {object} target The element being replaced. If not specified, the calendarwrapper is used.
|
||||
* @param {string} template The template to be rendered.
|
||||
* @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block.
|
||||
*
|
||||
* @return {promise}
|
||||
*/
|
||||
export const refreshDayContent = (root, year, month, day, courseId, categoryId, target = null, template = '') => {
|
||||
export const refreshDayContent = (root, year, month, day, courseId, categoryId,
|
||||
target = null, template = '', isCalendarBlock = false) => {
|
||||
startLoading(root);
|
||||
|
||||
if (!target || target.length == 0){
|
||||
@@ -354,6 +360,7 @@ export const refreshDayContent = (root, year, month, day, courseId, categoryId,
|
||||
.then((context) => {
|
||||
context.viewingday = true;
|
||||
context.showviewselector = true;
|
||||
context.iscalendarblock = isCalendarBlock;
|
||||
return Templates.render(template, context);
|
||||
})
|
||||
.then((html, js) => {
|
||||
@@ -400,18 +407,19 @@ export const reloadCurrentDay = (root, courseId = 0, categoryId = 0) => {
|
||||
* @param {Number} day Day
|
||||
* @param {Number} courseId The id of the course whose events are shown
|
||||
* @param {Number} categoryId The id of the category whose events are shown
|
||||
* @param {boolean} [isCalendarBlock=false] - A flag indicating whether this is a calendar block.
|
||||
* @return {promise}
|
||||
*/
|
||||
export const changeDay = (root, url, year, month, day, courseId, categoryId) => {
|
||||
return refreshDayContent(root, year, month, day, courseId, categoryId)
|
||||
export const changeDay = (root, url, year, month, day, courseId, categoryId, isCalendarBlock = false) => {
|
||||
return refreshDayContent(root, year, month, day, courseId, categoryId, null, '', isCalendarBlock)
|
||||
.then((...args) => {
|
||||
if (url.length && url !== '#') {
|
||||
if (url.length && url !== '#' && !isCalendarBlock) {
|
||||
updateUrl(url);
|
||||
}
|
||||
return args;
|
||||
})
|
||||
.then((...args) => {
|
||||
$('body').trigger(CalendarEvents.dayChanged, [year, month, courseId, categoryId]);
|
||||
$('body').trigger(CalendarEvents.dayChanged, [year, month, courseId, categoryId, isCalendarBlock]);
|
||||
return args;
|
||||
});
|
||||
};
|
||||
@@ -557,10 +565,18 @@ const renderEventSummaryModal = (eventId) => {
|
||||
.catch(Notification.exception);
|
||||
};
|
||||
|
||||
export const init = (root, view) => {
|
||||
/**
|
||||
* Initializes the calendar component by prefetching strings, folding day events,
|
||||
* and registering event listeners.
|
||||
*
|
||||
* @param {HTMLElement} root - The root element where the calendar view manager and event listeners will be attached.
|
||||
* @param {string} view - A flag indicating whether this is a calendar block.
|
||||
* @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block.
|
||||
*/
|
||||
export const init = (root, view, isCalendarBlock) => {
|
||||
prefetchStrings('calendar', ['moreevents']);
|
||||
foldDayEvents();
|
||||
registerEventListeners(root, view);
|
||||
registerEventListeners(root, isCalendarBlock);
|
||||
const calendarTable = root.find(CalendarSelectors.elements.monthDetailed);
|
||||
if (calendarTable.length) {
|
||||
const pendingId = `month-detailed-${calendarTable.id}-filterChanged`;
|
||||
|
||||
+17
-19
@@ -2177,29 +2177,27 @@ function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false, s
|
||||
}
|
||||
|
||||
if (!empty($courseeventsfrom) && (calendar_show_event_type(CALENDAR_EVENT_GROUP, $user) || $ignorefilters)) {
|
||||
|
||||
if (count($courseeventsfrom) == 1) {
|
||||
$course = reset($courseeventsfrom);
|
||||
if (has_any_capability($allgroupscaps, \context_course::instance($course->id))) {
|
||||
$coursegroups = groups_get_all_groups($course->id, 0, 0, 'g.id');
|
||||
$group = array_keys($coursegroups);
|
||||
}
|
||||
}
|
||||
if ($group === false) {
|
||||
if (!empty($CFG->calendar_adminseesall) && has_any_capability($allgroupscaps, \context_system::instance())) {
|
||||
$group = true;
|
||||
} else if ($isvaliduser) {
|
||||
$groupids = array();
|
||||
foreach ($courseeventsfrom as $courseid => $course) {
|
||||
if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) {
|
||||
// If this course has groups, show events from all of those related to the current user.
|
||||
if (!empty($CFG->calendar_adminseesall) && has_any_capability($allgroupscaps, \context_system::instance())) {
|
||||
$group = true;
|
||||
} else if ($isvaliduser) {
|
||||
$groupids = [];
|
||||
foreach ($courseeventsfrom as $courseid => $course) {
|
||||
if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) {
|
||||
if (has_all_capabilities($allgroupscaps, \context_course::instance($courseid))) {
|
||||
// User can access all groups in this course.
|
||||
// Get all the groups in this course.
|
||||
$coursegroups = groups_get_all_groups($course->id, 0, 0, 'g.id');
|
||||
$groupids = array_merge($groupids, array_keys($coursegroups));
|
||||
} else {
|
||||
// User can only access their own groups.
|
||||
// Get the groups the user is in.
|
||||
$coursegroups = groups_get_user_groups($course->id, $user->id);
|
||||
$groupids = array_merge($groupids, $coursegroups['0']);
|
||||
}
|
||||
}
|
||||
if (!empty($groupids)) {
|
||||
$group = $groupids;
|
||||
}
|
||||
}
|
||||
if (!empty($groupids)) {
|
||||
$group = $groupids;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,9 +351,8 @@ class core_calendar_renderer extends plugin_renderer_base {
|
||||
*/
|
||||
public function render_no_calendar_subscriptions(): string {
|
||||
$output = html_writer::start_div('mt-5');
|
||||
$importlink = html_writer::link((new moodle_url('/calendar/import.php', calendar_get_export_import_link_params()))->out(),
|
||||
get_string('importcalendarexternal', 'calendar'));
|
||||
$output .= get_string('nocalendarsubscriptions', 'calendar', $importlink);
|
||||
$importlink = (new moodle_url('/calendar/import.php', calendar_get_export_import_link_params()))->out();
|
||||
$output .= get_string('nocalendarsubscriptionsimportexternal', 'core_calendar', $importlink);
|
||||
$output .= html_writer::end_div();
|
||||
|
||||
return $output;
|
||||
@@ -380,7 +379,8 @@ class core_calendar_renderer extends plugin_renderer_base {
|
||||
$table->id = 'subscription_details_table';
|
||||
|
||||
if (empty($subscriptions)) {
|
||||
$cell = new html_table_cell(get_string('nocalendarsubscriptions', 'calendar'));
|
||||
$importlink = (new moodle_url('/calendar/import.php', calendar_get_export_import_link_params()))->out();
|
||||
$cell = new html_table_cell(get_string('nocalendarsubscriptionsimportexternal', 'core_calendar', $importlink));
|
||||
$cell->colspan = 5;
|
||||
$table->data[] = new html_table_row(array($cell));
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"iscalendarblock": false
|
||||
}
|
||||
}}
|
||||
<div id="calendar-day-{{uniqid}}" data-template="core_calendar/day_detailed">
|
||||
@@ -37,6 +38,7 @@
|
||||
</div>
|
||||
{{#js}}
|
||||
require(['jquery', 'core_calendar/calendar_view'], function($, CalendarView) {
|
||||
CalendarView.init($("#calendar-day-{{uniqid}}"), 'day');
|
||||
const isCalendarBlock = {{#iscalendarblock}}true{{/iscalendarblock}}{{^iscalendarblock}}false{{/iscalendarblock}};
|
||||
CalendarView.init($("#calendar-day-{{uniqid}}"), 'day', isCalendarBlock);
|
||||
});
|
||||
{{/js}}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"iscalendarblock": false
|
||||
}
|
||||
}}
|
||||
<div id="calendar-month-{{uniqid}}-{{calendarinstanceid}}" data-template="core_calendar/month_detailed">
|
||||
@@ -37,6 +38,7 @@
|
||||
</div>
|
||||
{{#js}}
|
||||
require(['jquery', 'core_calendar/calendar', 'core_calendar/popover'], function($, Calendar, calendarPopover) {
|
||||
Calendar.init($("#calendar-month-{{uniqid}}-{{calendarinstanceid}}"));
|
||||
const isCalendarBlock = {{#iscalendarblock}}true{{/iscalendarblock}}{{^iscalendarblock}}false{{/iscalendarblock}};
|
||||
Calendar.init($("#calendar-month-{{uniqid}}-{{calendarinstanceid}}"), isCalendarBlock);
|
||||
});
|
||||
{{/js}}
|
||||
|
||||
@@ -48,10 +48,19 @@ class behat_calendar extends behat_base {
|
||||
new behat_component_named_selector('mini calendar block', [".//*[@data-block='calendar_month']"]),
|
||||
new behat_component_named_selector('full calendar page', [".//*[@id='page-calendar-view']"]),
|
||||
new behat_component_named_selector('calendar day', [".//*[@data-region='day'][@data-day=%locator%]"]),
|
||||
new behat_component_named_selector(
|
||||
'calendar day detail',
|
||||
[".//*[@data-region='day'][@data-day=%locator%]//a[@data-action='view-day-link']"]
|
||||
),
|
||||
new behat_component_named_selector(
|
||||
'responsive calendar day',
|
||||
[".//*[@data-region='day'][@data-day=%locator%]/div[contains(@class, 'hidden-desktop')]"]
|
||||
),
|
||||
new behat_component_named_selector(
|
||||
'responsive calendar day detail',
|
||||
[".//*[@data-region='day'][@data-day=%locator%]" .
|
||||
"/div[contains(@class, 'hidden-desktop')]//a[@data-action='view-day-link']"]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -132,6 +141,39 @@ class behat_calendar extends behat_base {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on a specific day in the mini-calendar.
|
||||
*
|
||||
* @Given /^I click on day "(?P<dayofmonth>\d+)" of this month in the mini-calendar block(?P<responsive> responsive view|)$/
|
||||
*
|
||||
* @param int $day The day of the current month.
|
||||
* @param string $responsive If not null, find the responsive version of the link.
|
||||
* @param string $detail If not null, find the detail version of the link.
|
||||
*/
|
||||
public function i_click_on_day_of_this_month_in_mini_calendar_block(
|
||||
int $day,
|
||||
string $responsive = '',
|
||||
string $detail = '',
|
||||
): void {
|
||||
$selectortype = 'core_calendar >';
|
||||
if (!empty($responsive)) {
|
||||
$selectortype .= ' responsive';
|
||||
}
|
||||
$selectortype .= ' calendar day';
|
||||
if (!empty($detail)) {
|
||||
$selectortype .= ' detail';
|
||||
}
|
||||
$this->execute(
|
||||
contextapi: "behat_general::i_click_on_in_the",
|
||||
params: [
|
||||
$day,
|
||||
$selectortype,
|
||||
'',
|
||||
'core_calendar > mini calendar block',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hover over today in the mini-calendar.
|
||||
*
|
||||
@@ -154,6 +196,22 @@ class behat_calendar extends behat_base {
|
||||
return $this->i_hover_over_day_of_this_month_in_calendar($todaysday);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on today in the mini-calendar.
|
||||
*
|
||||
* @Given /^I click on today in the mini-calendar block( responsive view|)( to view the detail|)$/
|
||||
*
|
||||
* @param string $responsive If not empty, use the responsive calendar link.
|
||||
* @param string $detail If not empty, use the detail view calendar link.
|
||||
*/
|
||||
public function i_click_on_today_in_mini_calendar_block(string $responsive = '', string $detail = ''): void {
|
||||
$this->i_click_on_day_of_this_month_in_mini_calendar_block(
|
||||
day: date('j'),
|
||||
responsive: $responsive,
|
||||
detail: $detail,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a specific month in the calendar.
|
||||
*
|
||||
|
||||
@@ -896,6 +896,182 @@ class lib_test extends \advanced_testcase {
|
||||
$this->assertEquals($users[1]->id, $userid);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function tests calendar_set_filters for courses with separate group mode.
|
||||
*/
|
||||
public function test_calendar_set_filters_with_separate_group_mode(): void {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator();
|
||||
|
||||
// Create users.
|
||||
$student1 = $generator->create_user();
|
||||
$student2 = $generator->create_user();
|
||||
$teacher1 = $generator->create_user();
|
||||
$teacher2 = $generator->create_user();
|
||||
|
||||
// Create courses.
|
||||
$course1 = $generator->create_course([
|
||||
'shortname' => 'C1',
|
||||
'groupmode' => 1,
|
||||
'groupmodeforce' => 1,
|
||||
]);
|
||||
$course2 = $generator->create_course([
|
||||
'shortname' => 'C2',
|
||||
'groupmode' => 1,
|
||||
'groupmodeforce' => 1,
|
||||
]);
|
||||
$course1context = \context_course::instance($course1->id);
|
||||
$course2context = \context_course::instance($course2->id);
|
||||
|
||||
// Create groups.
|
||||
$group1 = $generator->create_group([
|
||||
'name' => 'G1-C1',
|
||||
'courseid' => $course1->id,
|
||||
]);
|
||||
$group2 = $generator->create_group([
|
||||
'name' => 'G1-C2',
|
||||
'courseid' => $course2->id,
|
||||
]);
|
||||
$group3 = $generator->create_group([
|
||||
'name' => 'G2-C2',
|
||||
'courseid' => $course2->id,
|
||||
]);
|
||||
|
||||
// Modify the capabilities.
|
||||
$editingteacherroleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']);
|
||||
assign_capability(
|
||||
'moodle/site:accessallgroups',
|
||||
CAP_PREVENT,
|
||||
$editingteacherroleid,
|
||||
$course1context->id,
|
||||
true
|
||||
);
|
||||
assign_capability(
|
||||
'moodle/site:accessallgroups',
|
||||
CAP_PREVENT,
|
||||
$editingteacherroleid,
|
||||
$course2context->id,
|
||||
true
|
||||
);
|
||||
|
||||
// Enrol users.
|
||||
$generator->enrol_user($student1->id, $course1->id, 'student');
|
||||
$generator->enrol_user($teacher1->id, $course1->id, 'editingteacher');
|
||||
$generator->enrol_user($student1->id, $course2->id, 'student');
|
||||
$generator->enrol_user($student2->id, $course2->id, 'student');
|
||||
$generator->enrol_user($teacher1->id, $course2->id, 'editingteacher');
|
||||
$generator->enrol_user($teacher2->id, $course2->id, 'editingteacher');
|
||||
|
||||
// Group memberships.
|
||||
$generator->create_group_member([
|
||||
'groupid' => $group1->id,
|
||||
'userid' => $student1->id,
|
||||
]);
|
||||
$generator->create_group_member([
|
||||
'groupid' => $group1->id,
|
||||
'userid' => $teacher1->id,
|
||||
]);
|
||||
$generator->create_group_member([
|
||||
'groupid' => $group2->id,
|
||||
'userid' => $student1->id,
|
||||
]);
|
||||
$generator->create_group_member([
|
||||
'groupid' => $group2->id,
|
||||
'userid' => $teacher1->id,
|
||||
]);
|
||||
$generator->create_group_member([
|
||||
'groupid' => $group3->id,
|
||||
'userid' => $student2->id,
|
||||
]);
|
||||
$generator->create_group_member([
|
||||
'groupid' => $group3->id,
|
||||
'userid' => $teacher2->id,
|
||||
]);
|
||||
|
||||
// Test teacher1.
|
||||
$this->setUser($teacher1);
|
||||
$defaultcourses = calendar_get_default_courses(
|
||||
null,
|
||||
'*',
|
||||
false,
|
||||
$teacher1->id
|
||||
);
|
||||
[$courseids, $groupids] = calendar_set_filters(
|
||||
$defaultcourses,
|
||||
false,
|
||||
$teacher1
|
||||
);
|
||||
// Teacher1 can see SITE, C1, G1-C1, C2, G1-C2.
|
||||
$this->assertCount(3, $courseids); // SITE, C1, C2.
|
||||
$this->assertCount(2, $groupids); // G1-C1, G1-C2.
|
||||
|
||||
$courseidskey = array_fill_keys($courseids, null);
|
||||
$this->assertArrayHasKey(SITEID, $courseidskey);
|
||||
$this->assertArrayHasKey($course1->id, $courseidskey);
|
||||
$this->assertArrayHasKey($course2->id, $courseidskey);
|
||||
|
||||
$groupidskey = array_fill_keys($groupids, null);
|
||||
$this->assertArrayHasKey($group1->id, $groupidskey);
|
||||
$this->assertArrayHasKey($group2->id, $groupidskey);
|
||||
$this->assertArrayNotHasKey($group3->id, $groupidskey);
|
||||
|
||||
// Test teacher2.
|
||||
$this->setUser($teacher2);
|
||||
$defaultcourses = calendar_get_default_courses(
|
||||
null,
|
||||
'*',
|
||||
false,
|
||||
$teacher2->id
|
||||
);
|
||||
[$courseids, $groupids] = calendar_set_filters(
|
||||
$defaultcourses,
|
||||
false,
|
||||
$teacher2
|
||||
);
|
||||
// Teacher2 can see SITE, C2, G2-C2.
|
||||
$this->assertCount(2, $courseids); // SITE, C2.
|
||||
$this->assertCount(1, $groupids); // G2-C2.
|
||||
|
||||
$courseidskey = array_fill_keys($courseids, null);
|
||||
$this->assertArrayHasKey(SITEID, $courseidskey);
|
||||
$this->assertArrayHasKey($course2->id, $courseidskey);
|
||||
|
||||
$groupidskey = array_fill_keys($groupids, null);
|
||||
$this->assertArrayHasKey($group3->id, $groupidskey);
|
||||
$this->assertArrayNotHasKey($group1->id, $groupidskey);
|
||||
$this->assertArrayNotHasKey($group2->id, $groupidskey);
|
||||
|
||||
// Modify the capabilities.
|
||||
assign_capability(
|
||||
'moodle/site:accessallgroups',
|
||||
CAP_ALLOW,
|
||||
$editingteacherroleid,
|
||||
$course2context->id,
|
||||
true
|
||||
);
|
||||
|
||||
$defaultcourses = calendar_get_default_courses(
|
||||
null,
|
||||
'*',
|
||||
false,
|
||||
$teacher2->id
|
||||
);
|
||||
[$courseids, $groupids] = calendar_set_filters(
|
||||
$defaultcourses,
|
||||
false,
|
||||
$teacher2
|
||||
);
|
||||
// Teacher2 can see SITE, C2, G1-C2, G2-C2.
|
||||
$this->assertCount(2, $courseids); // SITE, C2.
|
||||
$this->assertCount(2, $groupids); // G1-C2, G2-C2.
|
||||
|
||||
$groupidskey = array_fill_keys($groupids, null);
|
||||
$this->assertArrayHasKey($group2->id, $groupidskey);
|
||||
$this->assertArrayHasKey($group3->id, $groupidskey);
|
||||
$this->assertArrayNotHasKey($group1->id, $groupidskey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for calendar_view_event_allowed for course event types.
|
||||
*/
|
||||
|
||||
+9
-9
@@ -100,9 +100,9 @@ M.core_comment = {
|
||||
var newcomment = Y.Node.create(result.html);
|
||||
container.appendChild(newcomment);
|
||||
var ids = result.ids;
|
||||
var linkText = Y.one('#comment-link-text-' + cid);
|
||||
if (linkText) {
|
||||
linkText.set('innerHTML', M.util.get_string('commentscount', 'moodle', obj.count));
|
||||
var linkTextCount = Y.one('#comment-link-text-' + cid + ' .comment-link-count');
|
||||
if (linkTextCount) {
|
||||
linkTextCount.set('innerHTML', obj.count);
|
||||
}
|
||||
for(var i in ids) {
|
||||
var attributes = {
|
||||
@@ -249,9 +249,9 @@ M.core_comment = {
|
||||
scope: scope,
|
||||
params: params,
|
||||
callback: async function(id, ret, args) {
|
||||
var linkText = Y.one('#comment-link-text-' + scope.client_id);
|
||||
if (ret.count && linkText) {
|
||||
linkText.set('innerHTML', M.util.get_string('commentscount', 'moodle', ret.count));
|
||||
var linkTextCount = Y.one('#comment-link-text-' + scope.client_id + ' .comment-link-count');
|
||||
if (linkTextCount) {
|
||||
linkTextCount.set('innerHTML', ret.count);
|
||||
}
|
||||
var container = Y.one('#comment-list-'+scope.client_id);
|
||||
var pagination = Y.one('#comment-pagination-'+scope.client_id);
|
||||
@@ -284,10 +284,10 @@ M.core_comment = {
|
||||
params = {'commentid': id};
|
||||
function remove_dom(type, anim, cmt) {
|
||||
cmt.remove();
|
||||
var linkText = Y.one('#comment-link-text-' + cid),
|
||||
var linkTextCount = Y.one('#comment-link-text-' + cid + ' .comment-link-count'),
|
||||
comments = Y.all('#comment-list-' + cid + ' li');
|
||||
if (linkText && comments) {
|
||||
linkText.set('innerHTML', M.util.get_string('commentscount', 'moodle', comments.size()));
|
||||
if (linkTextCount) {
|
||||
linkTextCount.set('innerHTML', comments.size());
|
||||
}
|
||||
}
|
||||
this.request({
|
||||
|
||||
+1
-2
@@ -263,7 +263,6 @@ class comment {
|
||||
$page->requires->strings_for_js(array(
|
||||
'addcomment',
|
||||
'comments',
|
||||
'commentscount',
|
||||
'commentsrequirelogin',
|
||||
'deletecommentbyon'
|
||||
),
|
||||
@@ -454,7 +453,7 @@ class comment {
|
||||
// comments open and closed
|
||||
$countstring = '';
|
||||
if ($this->displaytotalcount) {
|
||||
$countstring = '('.$this->count().')';
|
||||
$countstring = '(' . html_writer::span($this->count(), 'comment-link-count') . ')';
|
||||
}
|
||||
$collapsedimage= 't/collapsed';
|
||||
if (right_to_left()) {
|
||||
|
||||
@@ -17,7 +17,7 @@ Feature: Manage comments made by users
|
||||
Scenario: View and filter site comments
|
||||
When I navigate to "Reports > Comments" in site administration
|
||||
And the following should exist in the "reportbuilder-table" table:
|
||||
| -0- | Content | Context URL |
|
||||
| First name | Content | Context URL |
|
||||
| Admin User | Uno | Course: Course 1 |
|
||||
| Admin User | Dos | Course: Course 1 |
|
||||
| Admin User | Tres | Course: Course 1 |
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user