Compare commits
393 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d56aa41e9e | |||
| 5778558dae | |||
| b7bf868042 | |||
| 06c20c4675 | |||
| f30a2bb18e | |||
| 92a2dcc96a | |||
| 3a694fd6bf | |||
| bb6a65da42 | |||
| a11ffea697 | |||
| 90d7307872 | |||
| 453dfc4f94 | |||
| 75bffe9d96 | |||
| 22dc6301f0 | |||
| 7266fa1524 | |||
| e0a09f31b1 | |||
| 72526c6f49 | |||
| 5c0f33209e | |||
| 943ee4d484 | |||
| acd31294f4 | |||
| b409287479 | |||
| 13d98b9f6e | |||
| 2b19e45d49 | |||
| 7bde04468b | |||
| 80b23a2afd | |||
| da8e201e17 | |||
| d6950db0d9 | |||
| 661d66b452 | |||
| 74a5cc0965 | |||
| 768a82b583 | |||
| 04d3db34ee | |||
| c871b4d5f8 | |||
| d3749a82ab | |||
| d7610a257f | |||
| 62b840648d | |||
| 5f93e6ab03 | |||
| 69beec3d2a | |||
| d2c9c79c0e | |||
| 1ce15ab9b1 | |||
| a24d8a0d2a | |||
| 8835ecef16 | |||
| 86c79ed091 | |||
| d4f5e92ee1 | |||
| 2c58f9533b | |||
| 34077a9190 | |||
| 42726d645c | |||
| 02d08d2e4d | |||
| 9b90cf6584 | |||
| bdbc077efc | |||
| fb06894e75 | |||
| 6b4946e0a8 | |||
| 057f63909f | |||
| ea84a13152 | |||
| 9639feb9a3 | |||
| f009e2621e | |||
| 3449379bca | |||
| c77d6a79ec | |||
| d8558b1d42 | |||
| b8614dee98 | |||
| b852d34285 | |||
| 250ef211f0 | |||
| 2ebcdda606 | |||
| ce89c63b0b | |||
| 8acc343fb8 | |||
| 15a54747b9 | |||
| 7666131199 | |||
| 8b54213ad0 | |||
| 561a371423 | |||
| 6cc39f8e94 | |||
| 58e54a3d33 | |||
| eb1c1e79a1 | |||
| e7c973a195 | |||
| a982ebad0c | |||
| 27692e70b5 | |||
| 05db16c24d | |||
| 0aaf7fe953 | |||
| aea9770cfc | |||
| 05fe39a88f | |||
| 44305df587 | |||
| 638caa1355 | |||
| 6d29dc587e | |||
| 0f03ac03b5 | |||
| a11b849ebe | |||
| 4cfa5bca7c | |||
| 19ad8459d7 | |||
| 3f6919adc7 | |||
| 5d2ce59559 | |||
| e08dd90c32 | |||
| 26d7f16686 | |||
| 5b168b055f | |||
| 7ad936dcbf | |||
| 29ab56c4d2 | |||
| 6c640dacf4 | |||
| 234d24dfea | |||
| 835505681c | |||
| e930abfcf8 | |||
| cb06a0a271 | |||
| f8f19a4d8e | |||
| e2a8d063ee | |||
| 1fd2f0364c | |||
| 7f450dcea3 | |||
| c39a3357ed | |||
| cc10212366 | |||
| df2b3113d9 | |||
| d4f428311a | |||
| e2c677c3e8 | |||
| 80b3cb4951 | |||
| dfa5124909 | |||
| d3a6c97a43 | |||
| 316a50f2b7 | |||
| 2883fd28e4 | |||
| aa7f8f9e09 | |||
| c4b56bb393 | |||
| 156d214635 | |||
| 31b07a6298 | |||
| 2c82466444 | |||
| 91e70280a2 | |||
| 68e93dbfee | |||
| c12d07d9d4 | |||
| a29ef48500 | |||
| 0fae681a7c | |||
| 149df44e29 | |||
| da1fafe59d | |||
| 3821c707b8 | |||
| 1c7a5d9972 | |||
| 0fdbf26fd0 | |||
| 4f3c3432cd | |||
| b6715bb1ae | |||
| 40094ddbe0 | |||
| 8535c32bf3 | |||
| 6b90c69bda | |||
| 6f3337dcba | |||
| ff33f82ac2 | |||
| bf6cf572ca | |||
| 433109e2ac | |||
| 07bfca9041 | |||
| b2320ccd71 | |||
| 5e4ba1f036 | |||
| 7e17945088 | |||
| 535e970ad3 | |||
| dd8595035f | |||
| 06ae387ca5 | |||
| df48a73c5c | |||
| 70bbb44030 | |||
| 70ab78478f | |||
| 8a66b2b99a | |||
| ee5ac3f1f3 | |||
| 9e71503ab3 | |||
| 651ecb52d9 | |||
| 85845b13d7 | |||
| 7b0946129c | |||
| 7d7cfee0b7 | |||
| 6751ba6500 | |||
| 6d387e4b2b | |||
| ed3b73d9fc | |||
| f95b010ec5 | |||
| 97060da478 | |||
| 9715fd56da | |||
| 7e3d7fb02d | |||
| a9cc2ebb6e | |||
| 1a7ec4cf3d | |||
| 7715c626ad | |||
| 1c9098aeb8 | |||
| 5d18431087 | |||
| 0d27c76e34 | |||
| 8deeed9771 | |||
| 7893a34171 | |||
| e6966b7ae7 | |||
| 3e81ebecb6 | |||
| f9c5802f4e | |||
| e6ee7a5b1c | |||
| 17081ee902 | |||
| e817dd70db | |||
| bc30a904f4 | |||
| b2d9c6ff81 | |||
| 18dae18e20 | |||
| 18c7f3cdf3 | |||
| f0b462209a | |||
| 974db25816 | |||
| 7a02ae5644 | |||
| a9f24ce125 | |||
| a68fcabc65 | |||
| 2978ffc4a5 | |||
| a694661d5c | |||
| 7d77854fd6 | |||
| 17124e62a9 | |||
| d9ae012d15 | |||
| c03706fafb | |||
| f77aa8c4a7 | |||
| 4044abd156 | |||
| 6164556c85 | |||
| 69a8f920d4 | |||
| ff263b6dbc | |||
| 763c71194b | |||
| 19ac0728f2 | |||
| 39cb2a03e5 | |||
| c8c84b4af1 | |||
| 5ec01a7226 | |||
| f878e05d62 | |||
| aa0e2cffdd | |||
| c18b59808c | |||
| a0d8c025f7 | |||
| 137d311fd1 | |||
| 57f20b6cb3 | |||
| a10506b8d7 | |||
| 28b8fc9896 | |||
| f55b829f44 | |||
| 836b2c23a2 | |||
| 0068701bc6 | |||
| 83a978d1cd | |||
| d05795db8e | |||
| 629a7066be | |||
| 058b11e57b | |||
| aa230649d9 | |||
| f8cce391b2 | |||
| a7d476c35f | |||
| 5425fb3d79 | |||
| 587d737690 | |||
| 31b68a2b5d | |||
| a48f00d940 | |||
| f7da739ea0 | |||
| ebb346cd8e | |||
| b39d8d5027 | |||
| cf48f359ad | |||
| 8ac29c9422 | |||
| 7f3617860c | |||
| b35f70ce14 | |||
| 77d0c0caa9 | |||
| c2193308a6 | |||
| 9456911fe9 | |||
| bd2c8fdd54 | |||
| 234d81e39b | |||
| 9747b5b26b | |||
| 508d462fa7 | |||
| f687e02dec | |||
| 72c3038925 | |||
| d393030c58 | |||
| d68ea84924 | |||
| 85825dcc55 | |||
| 80f152296c | |||
| 48aa9aae56 | |||
| 025065daf5 | |||
| 49be0febdc | |||
| 73f57aab98 | |||
| 457f0e9384 | |||
| 4b15c803e2 | |||
| 50aff515b7 | |||
| 5b33398f07 | |||
| 47568f31c5 | |||
| eba83406da | |||
| 137c6f36bb | |||
| d9043b324d | |||
| e57e560385 | |||
| eebd5742b6 | |||
| 0837a24c05 | |||
| f2e48b39f6 | |||
| 5060c53176 | |||
| 3f392e934e | |||
| c1e4679ed5 | |||
| 9254b7e75a | |||
| 1d609ca0ee | |||
| 61fabd3e73 | |||
| e8d03fdae4 | |||
| c8922328b2 | |||
| 19635ef720 | |||
| 9d5a4746ab | |||
| e4406472f0 | |||
| cf2f67272a | |||
| 2ebac5604c | |||
| c5f48698da | |||
| c2a043f50d | |||
| 001c78285f | |||
| af90bb572e | |||
| 882a660f7e | |||
| adc1de569d | |||
| 607716f206 | |||
| 5358ce28ee | |||
| fef8dda150 | |||
| b049731dfb | |||
| 5277dca108 | |||
| 4be4d0d489 | |||
| 3d0bcb7172 | |||
| 7dac3707fd | |||
| 037a7d5d6c | |||
| f5587cf881 | |||
| a18c0b7c8b | |||
| 097a879e1a | |||
| 58acb96ee6 | |||
| 1901cd3fdf | |||
| 2c9a5c009f | |||
| b564a40d74 | |||
| 179bfdf5c5 | |||
| eb975467bd | |||
| 5efbd6e0e1 | |||
| fa20b1790f | |||
| 7d579dd4bb | |||
| 6b0661b9ea | |||
| afd57ca9a0 | |||
| c14e75a017 | |||
| 318e2344c0 | |||
| 17ed404ccc | |||
| 5b54a87858 | |||
| ea49be1e61 | |||
| 0f3d3b2d77 | |||
| dee41e0648 | |||
| c11b1c6b3f | |||
| e2362b0a7a | |||
| d63721cb15 | |||
| 66b1733bda | |||
| 81821e6833 | |||
| 965a5fdb27 | |||
| 6aa2828161 | |||
| d46a37ebd4 | |||
| 8c358aefd9 | |||
| 40aceb0256 | |||
| f64240ce94 | |||
| 10c7926c0f | |||
| 8a26315686 | |||
| 0e8d4d8e2a | |||
| 5ac11701ac | |||
| dc65317ba6 | |||
| f0ae7c4343 | |||
| b52c1855fa | |||
| 6e1e63cc6b | |||
| 50ae525be9 | |||
| a700d119df | |||
| 9ccd794b36 | |||
| 49673645ea | |||
| e6316e6b9b | |||
| 5003afbe48 | |||
| 7e1bbaaf20 | |||
| d1111bc3e8 | |||
| 8de03684ac | |||
| 2db07c4d0a | |||
| 3ac3d942dd | |||
| 84308c7dfc | |||
| 24a0e76ebf | |||
| 2444b2b787 | |||
| b704fa7dda | |||
| 9d3d193f29 | |||
| 43c7557e84 | |||
| da74ac4987 | |||
| 1e5bc3a393 | |||
| 7c20cd1b81 | |||
| edf1a1ac8c | |||
| 5a2acac1ed | |||
| 719c95009e | |||
| 860b4b941a | |||
| 26ed48ec40 | |||
| 58ba1a9448 | |||
| 40d2b4de39 | |||
| bce1d7ff85 | |||
| 3f439ff9d4 | |||
| 0c34355c85 | |||
| ab118d48f5 | |||
| 6df3213c9b | |||
| 6dbd0e938f | |||
| 8889f6f26f | |||
| 36aa09726b | |||
| 196c2ae7a5 | |||
| 4d217e3a1a | |||
| 09e3db1aa6 | |||
| 8815ecb3fd | |||
| 8482210209 | |||
| 281ef7ef8d | |||
| 6970580b4c | |||
| 652633b467 | |||
| 5a7963d9f5 | |||
| 56ff5e78f1 | |||
| e31db928b5 | |||
| 03fcd81892 | |||
| 05a41b6e32 | |||
| b3867e7422 | |||
| 8c298f14bb | |||
| 58bd2ed8e4 | |||
| 36ca488c06 | |||
| bca28b03a9 | |||
| a6216bb890 | |||
| 94b14df11d | |||
| 0dc60026c8 | |||
| 63a1ec81c4 | |||
| 150fc0837a | |||
| 72958879c6 | |||
| d8d560b7fe | |||
| 917dd49db2 | |||
| 80691cf5a3 | |||
| e20720ce0e | |||
| f976349b95 | |||
| 312a166f1e | |||
| f16ca8fa7a | |||
| 846a8b6568 | |||
| 692d3090fa | |||
| 8092eb95c4 | |||
| 2b864ceec3 |
@@ -1,8 +1,12 @@
|
||||
{
|
||||
'plugins': [
|
||||
'babel',
|
||||
'@babel',
|
||||
'promise',
|
||||
'jsdoc',
|
||||
'jsdoc'
|
||||
],
|
||||
'extends': [
|
||||
'eslint:recommended',
|
||||
'plugin:promise/recommended'
|
||||
],
|
||||
'env': {
|
||||
'browser': true,
|
||||
@@ -16,42 +20,19 @@
|
||||
'M': true,
|
||||
'Y': true
|
||||
},
|
||||
// Warn about unused eslint-disable statements.
|
||||
'reportUnusedDisableDirectives': true,
|
||||
'rules': {
|
||||
// See http://eslint.org/docs/rules/ for all rules and explanations of all
|
||||
// rules.
|
||||
|
||||
// === Possible Errors ===
|
||||
'comma-dangle': 'off',
|
||||
'no-compare-neg-zero': 'error',
|
||||
'no-cond-assign': 'error',
|
||||
'no-console': 'error',
|
||||
'no-constant-condition': 'error',
|
||||
'no-control-regex': 'error',
|
||||
'no-debugger': 'error',
|
||||
'no-dupe-args': 'error',
|
||||
'no-dupe-keys': 'error',
|
||||
'no-duplicate-case': 'error',
|
||||
'no-empty': 'warn',
|
||||
'no-empty-character-class': 'error',
|
||||
'no-ex-assign': 'error',
|
||||
'no-extra-boolean-cast': 'error',
|
||||
'no-extra-parens': 'off',
|
||||
'no-extra-semi': 'error',
|
||||
'no-func-assign': 'error',
|
||||
'no-inner-declarations': 'error',
|
||||
'no-invalid-regexp': 'error',
|
||||
'no-irregular-whitespace': 'error',
|
||||
'no-obj-calls': 'error',
|
||||
'no-prototype-builtins': 'off',
|
||||
'no-regex-spaces': 'error',
|
||||
'no-sparse-arrays': 'error',
|
||||
'no-unexpected-multiline': 'error',
|
||||
'no-unreachable': 'warn',
|
||||
'no-unsafe-finally': 'error',
|
||||
'no-unsafe-negation': 'error',
|
||||
'use-isnan': 'error',
|
||||
'valid-jsdoc': ['warn', { 'requireReturn': false, 'requireParamDescription': false, 'requireReturnDescription': false }],
|
||||
'valid-typeof': 'error',
|
||||
|
||||
// === Best Practices ===
|
||||
// (these mostly match our jshint config)
|
||||
@@ -63,15 +44,12 @@
|
||||
'dot-notation': 'warn',
|
||||
'no-alert': 'warn',
|
||||
'no-caller': 'error',
|
||||
'no-case-declarations': 'error',
|
||||
'no-div-regex': 'error',
|
||||
'no-empty-pattern': 'error',
|
||||
'no-empty-function': 'warn',
|
||||
'no-eq-null': 'error',
|
||||
'no-eval': 'error',
|
||||
'no-extend-native': 'error',
|
||||
'no-extra-bind': 'warn',
|
||||
'no-fallthrough': 'error',
|
||||
'no-floating-decimal': 'warn',
|
||||
'no-global-assign': 'warn',
|
||||
'no-implied-eval': 'error',
|
||||
@@ -83,28 +61,22 @@
|
||||
'no-multi-str': 'error',
|
||||
'no-new-func': 'error',
|
||||
'no-new-wrappers': 'error',
|
||||
'no-octal': 'error',
|
||||
'no-octal-escape': 'error',
|
||||
'no-proto': 'error',
|
||||
'no-redeclare': 'warn',
|
||||
'no-restricted-globals': ['error', { 'name': 'Notification' }],
|
||||
'no-return-assign': 'error',
|
||||
'no-script-url': 'error',
|
||||
'no-self-assign': 'error',
|
||||
'no-self-compare': 'error',
|
||||
'no-sequences': 'warn',
|
||||
'no-throw-literal': 'warn',
|
||||
'no-unmodified-loop-condition': 'error',
|
||||
'no-unused-expressions': 'error',
|
||||
'no-unused-labels': 'error',
|
||||
'no-useless-call': 'warn',
|
||||
'no-useless-escape': 'warn',
|
||||
'no-with': 'error',
|
||||
'wrap-iife': ['error', 'any'],
|
||||
|
||||
// === Variables ===
|
||||
'no-delete-var': 'error',
|
||||
'no-undef': 'error',
|
||||
'no-undef-init': 'error',
|
||||
'no-unused-vars': ['error', { 'caughtErrors': 'none' }],
|
||||
|
||||
@@ -137,8 +109,6 @@
|
||||
'max-statements-per-line': ['warn', { max: 2 }],
|
||||
'new-cap': ['warn', { 'properties': false }],
|
||||
'new-parens': 'warn',
|
||||
'newline-after-var': 'off',
|
||||
'newline-before-return': 'off',
|
||||
'newline-per-chained-call': 'off',
|
||||
'no-array-constructor': 'off',
|
||||
'no-bitwise': 'error',
|
||||
@@ -146,7 +116,6 @@
|
||||
'no-inline-comments': 'off',
|
||||
'no-lonely-if': 'off',
|
||||
'no-mixed-operators': 'off',
|
||||
'no-mixed-spaces-and-tabs': 'error',
|
||||
'no-multiple-empty-lines': 'warn',
|
||||
'no-negated-condition': 'off',
|
||||
'no-nested-ternary': 'warn',
|
||||
@@ -168,7 +137,6 @@
|
||||
'padded-blocks': 'off',
|
||||
'quote-props': ['warn', 'as-needed', {'unnecessary': false, 'keywords': true, 'numbers': true}],
|
||||
'quotes': 'off',
|
||||
'require-jsdoc': 'warn',
|
||||
'semi': 'error',
|
||||
'semi-spacing': ['warn', {'before': false, 'after': true}],
|
||||
'sort-vars': 'off',
|
||||
@@ -181,21 +149,19 @@
|
||||
'unicode-bom': 'error',
|
||||
'wrap-regex': 'off',
|
||||
|
||||
// === Promises ===
|
||||
// === Promises (override plugin:promise/recommended) ===
|
||||
'promise/always-return': 'warn',
|
||||
'promise/no-return-wrap': 'warn',
|
||||
'promise/param-names': 'warn',
|
||||
'promise/catch-or-return': ['warn', {terminationMethod: ['catch', 'fail']}],
|
||||
'promise/no-native': 'warn',
|
||||
'promise/no-promise-in-callback': 'warn',
|
||||
'promise/no-callback-in-promise': 'warn',
|
||||
'promise/avoid-new': 'warn',
|
||||
|
||||
// === Deprecations ===
|
||||
"no-restricted-properties": ['warn', {
|
||||
'object': 'M',
|
||||
'property': 'str',
|
||||
'message': 'Use AMD module "core/str" or M.util.get_string()'
|
||||
'message': 'Use "core/str" module or M.util.get_string()'
|
||||
}],
|
||||
},
|
||||
overrides: [
|
||||
@@ -235,7 +201,6 @@
|
||||
parser: '@babel/eslint-parser',
|
||||
// Check AMD with some slightly stricter rules.
|
||||
rules: {
|
||||
'no-unused-vars': 'error',
|
||||
'no-implicit-globals': 'error',
|
||||
// Disable all of the rules that have babel versions.
|
||||
'new-cap': 'off',
|
||||
@@ -247,65 +212,37 @@
|
||||
'semi': 'off',
|
||||
'no-unused-expressions': 'off',
|
||||
// Enable all of the babel version of these rules.
|
||||
'babel/new-cap': ['warn', { 'properties': false }],
|
||||
'@babel/new-cap': ['warn', { 'properties': false }],
|
||||
// Not using this rule for the time being because it isn't
|
||||
// compatible with jQuery and ES6.
|
||||
'babel/no-invalid-this': 'off',
|
||||
'babel/object-curly-spacing': 'warn',
|
||||
// This is off in the original style int.
|
||||
'babel/quotes': 'off',
|
||||
'babel/semi': 'error',
|
||||
'babel/no-unused-expressions': 'error',
|
||||
'@babel/no-invalid-this': 'off',
|
||||
'@babel/object-curly-spacing': 'warn',
|
||||
'@babel/semi': 'error',
|
||||
'@babel/no-unused-expressions': 'error',
|
||||
// === Promises ===
|
||||
// We have Promise now that we're using ES6.
|
||||
'promise/no-native': 'off',
|
||||
'promise/avoid-new': 'off',
|
||||
|
||||
// === JSDocs ===
|
||||
"jsdoc/check-access": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/check-alignment": 1, // Recommended.
|
||||
"jsdoc/check-param-names": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/check-property-names": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/empty-tags": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/implements-on-classes": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/multiline-blocks": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/require-jsdoc": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/require-param": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/require-param-name": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/require-param-type": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/require-property": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/require-property-name": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/require-property-type": [
|
||||
'error',
|
||||
],
|
||||
'jsdoc/check-access': 'error',
|
||||
'jsdoc/check-alignment': 1, // Recommended.
|
||||
'jsdoc/check-param-names': 'error',
|
||||
'jsdoc/check-property-names': 'error',
|
||||
'jsdoc/empty-tags': 'error',
|
||||
'jsdoc/implements-on-classes': 'error',
|
||||
'jsdoc/multiline-blocks': 'error',
|
||||
'jsdoc/require-jsdoc': 'error',
|
||||
'jsdoc/require-param': 'error',
|
||||
'jsdoc/require-param-name': 'error',
|
||||
'jsdoc/require-param-type': 'error',
|
||||
'jsdoc/require-property': 'error',
|
||||
'jsdoc/require-property-name': 'error',
|
||||
'jsdoc/require-property-type': 'error'
|
||||
},
|
||||
parserOptions: {
|
||||
'sourceType': 'module',
|
||||
'requireConfigFile': false,
|
||||
'requireConfigFile': false
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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]+*
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Function to generate the destination for the uglify task
|
||||
* Function to generate the destination for the minification task
|
||||
* (e.g. build/file.min.js). This function will be passed to
|
||||
* the rename property of files array when building dynamically:
|
||||
* http://gruntjs.com/configuring-tasks#building-the-files-object-dynamically
|
||||
@@ -60,7 +60,6 @@ module.exports = grunt => {
|
||||
grunt.registerTask('js', ['amd', 'yui']);
|
||||
|
||||
// Register NPM tasks.
|
||||
grunt.loadNpmTasks('grunt-contrib-uglify');
|
||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||
grunt.loadNpmTasks('grunt-rollup');
|
||||
|
||||
@@ -104,6 +103,7 @@ module.exports = grunt => {
|
||||
// The queue runner will run the next `size` items in the queue.
|
||||
const runQueue = (size = 1) => {
|
||||
queue.splice(0, size).forEach(resolve => {
|
||||
grunt.log.debug(`Item resolved. Kicking off next one.`);
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
@@ -113,15 +113,17 @@ module.exports = grunt => {
|
||||
|
||||
// The options hook is run in parallel.
|
||||
// We can return an unresolved Promise which is queued for later resolution.
|
||||
options: async() => {
|
||||
options: async(options) => {
|
||||
return new Promise(resolve => {
|
||||
queue.push(resolve);
|
||||
startQueue();
|
||||
return options;
|
||||
});
|
||||
},
|
||||
|
||||
// When an item in the queue completes, start the next item in the queue.
|
||||
buildEnd: () => {
|
||||
generateBundle: (options, bundle) => {
|
||||
grunt.log.debug(`Finished output phase for ${Object.keys(bundle).join(', ')}`);
|
||||
runQueue();
|
||||
},
|
||||
};
|
||||
@@ -151,24 +153,10 @@ module.exports = grunt => {
|
||||
//
|
||||
// It also adds the Moodle plugin name to the AMD module definition
|
||||
// so that it can be imported as expected in other modules.
|
||||
path.resolve('.grunt/babel-plugin-add-module-to-define.js'),
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
'@babel/plugin-syntax-import-meta',
|
||||
['@babel/plugin-proposal-class-properties', {'loose': false}],
|
||||
'@babel/plugin-proposal-json-strings'
|
||||
path.resolve('.grunt/babel-plugin-add-module-to-define.js')
|
||||
],
|
||||
presets: [
|
||||
['@babel/preset-env', {
|
||||
targets: {
|
||||
browsers: [
|
||||
">0.3%",
|
||||
"last 2 versions",
|
||||
"not ie >= 0",
|
||||
"not op_mini all",
|
||||
"not Opera > 0",
|
||||
"not dead"
|
||||
]
|
||||
},
|
||||
modules: false,
|
||||
useBuiltIns: false
|
||||
}]
|
||||
|
||||
+28
-12
@@ -20,17 +20,33 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
module.exports = grunt => {
|
||||
// Project configuration.
|
||||
grunt.config.merge({
|
||||
jsdoc: {
|
||||
dist: {
|
||||
options: {
|
||||
configure: ".grunt/jsdoc/jsdoc.conf.js",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
module.exports = (grunt) => {
|
||||
const path = require('path');
|
||||
|
||||
grunt.loadNpmTasks('grunt-jsdoc');
|
||||
grunt.registerTask('jsdoc', 'Generate JavaScript documentation using jsdoc', function() {
|
||||
const done = this.async();
|
||||
const configuration = path.resolve('.grunt/jsdoc/jsdoc.conf.js');
|
||||
|
||||
grunt.util.spawn({
|
||||
cmd: 'jsdoc',
|
||||
args: [
|
||||
'--configure',
|
||||
configuration,
|
||||
]
|
||||
}, function(error, result, code) {
|
||||
if (result.stdout) {
|
||||
grunt.log.write(result.stdout);
|
||||
}
|
||||
|
||||
if (result.stderr) {
|
||||
grunt.log.error(result.stderr);
|
||||
}
|
||||
if (error) {
|
||||
grunt.fail.fatal(`JSDoc failed with error code ${code}`);
|
||||
} else {
|
||||
grunt.log.write('JSDoc completed successfully'.green);
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ module.exports = grunt => {
|
||||
// Use a fully-qualified path.
|
||||
src: files,
|
||||
options: {
|
||||
quietDeprecationWarnings: true,
|
||||
configOverrides: {
|
||||
rules: {
|
||||
// These rules have to be disabled in .stylelintrc for scss compat.
|
||||
@@ -45,7 +46,10 @@ module.exports = grunt => {
|
||||
return {
|
||||
stylelint: {
|
||||
scss: {
|
||||
options: {syntax: 'scss'},
|
||||
options: {
|
||||
quietDeprecationWarnings: true,
|
||||
customSyntax: 'postcss-scss',
|
||||
},
|
||||
src: files,
|
||||
},
|
||||
},
|
||||
|
||||
+16
-4
@@ -1,9 +1,14 @@
|
||||
{
|
||||
"customSyntax": "postcss-scss",
|
||||
"plugins": [
|
||||
"stylelint-csstree-validator"
|
||||
],
|
||||
"rules": {
|
||||
"csstree/validator": true,
|
||||
"csstree/validator": {
|
||||
"syntaxExtensions": [
|
||||
"sass"
|
||||
]
|
||||
},
|
||||
"at-rule-empty-line-before": [ "always",
|
||||
{"except": [ "blockless-after-blockless"], ignore: ["after-comment", "inside-block"]}
|
||||
],
|
||||
@@ -45,7 +50,7 @@
|
||||
"function-name-case": "lower",
|
||||
"function-parentheses-newline-inside": "always-multi-line",
|
||||
"function-parentheses-space-inside": "never-single-line",
|
||||
"function-url-scheme-blacklist": ["data"],
|
||||
"function-url-scheme-disallowed-list": ["data"],
|
||||
"function-whitespace-after": "always",
|
||||
"indentation": 4,
|
||||
"keyframe-declaration-no-important": true,
|
||||
@@ -84,12 +89,19 @@
|
||||
"selector-type-no-unknown": true,
|
||||
"string-no-newline": true,
|
||||
"time-min-milliseconds": 100,
|
||||
"unit-blacklist": ["pt"],
|
||||
"unit-disallowed-list": ["pt"],
|
||||
"unit-case": "lower",
|
||||
"unit-no-unknown": true,
|
||||
"value-keyword-case": ["lower", {"ignoreKeywords": ["/(@|$)/"]}],
|
||||
"value-list-comma-newline-after": "always-multi-line",
|
||||
"value-list-comma-space-after": "always-single-line",
|
||||
"value-list-comma-space-before": "never",
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/yui/**/*.css"],
|
||||
"rules": {
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3923,6 +3923,8 @@
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_xmlrpc_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_oracle_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="4.2" requires="3.11.8">
|
||||
@@ -3939,6 +3941,7 @@
|
||||
<VENDOR name="oracle" version="19" />
|
||||
</DATABASE>
|
||||
<PHP version="8.0.0" level="required">
|
||||
<RESTRICT function="restrict_php_version_83" message="unsupportedphpversion83" />
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
@@ -4112,6 +4115,8 @@
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mod_assignment" level="required">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_oracle_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="4.3" requires="3.11.8">
|
||||
@@ -4128,6 +4133,7 @@
|
||||
<VENDOR name="oracle" version="19" />
|
||||
</DATABASE>
|
||||
<PHP version="8.0.0" level="required">
|
||||
<RESTRICT function="restrict_php_version_83" message="unsupportedphpversion83" />
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
@@ -4303,6 +4309,8 @@
|
||||
</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_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="4.4" requires="4.1.2">
|
||||
@@ -4494,6 +4502,205 @@
|
||||
</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>
|
||||
<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>
|
||||
|
||||
+5
-2
@@ -2052,8 +2052,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') {
|
||||
@@ -2084,7 +2086,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,8 @@ 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';
|
||||
$temp->add(new admin_setting_configtextarea('adminpresets/sensiblesettings',
|
||||
get_string('sensiblesettings', 'core_adminpresets'),
|
||||
get_string('sensiblesettingstext', 'core_adminpresets'),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -64,7 +64,7 @@ class delete extends base {
|
||||
* Delete the DB preset
|
||||
*/
|
||||
public function execute(): void {
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
$this->manager->delete_preset($this->id);
|
||||
|
||||
|
||||
@@ -52,8 +52,6 @@ class export extends base {
|
||||
* Stores a preset into the DB.
|
||||
*/
|
||||
public function execute(): void {
|
||||
confirm_sesskey();
|
||||
|
||||
$url = new \moodle_url('/admin/tool/admin_presets/index.php', ['action' => 'export', 'mode' => 'execute']);
|
||||
$this->moodleform = new export_form($url);
|
||||
|
||||
@@ -87,7 +85,7 @@ class export extends base {
|
||||
* @throws xml_writer_exception
|
||||
*/
|
||||
public function download_xml(): void {
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
list($xmlstr, $filename) = $this->manager->download_preset($this->id);
|
||||
|
||||
|
||||
@@ -40,8 +40,6 @@ class import extends base {
|
||||
* Imports the xmlfile into DB
|
||||
*/
|
||||
public function execute(): void {
|
||||
confirm_sesskey();
|
||||
|
||||
$url = new \moodle_url('/admin/tool/admin_presets/index.php', ['action' => 'import', 'mode' => 'execute']);
|
||||
$this->moodleform = new import_form($url);
|
||||
|
||||
|
||||
@@ -38,8 +38,6 @@ class load extends base {
|
||||
public function execute(): void {
|
||||
global $OUTPUT;
|
||||
|
||||
confirm_sesskey();
|
||||
|
||||
$url = new \moodle_url('/admin/tool/admin_presets/index.php', ['action' => 'load', 'mode' => 'execute']);
|
||||
$this->moodleform = new load_form($url);
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ class rollback extends base {
|
||||
public function execute(): void {
|
||||
global $OUTPUT;
|
||||
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
list($presetapp, $rollback, $failures) = $this->manager->revert_preset($this->id);
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -97,28 +97,28 @@ if ($onlycli === false) {
|
||||
switch ($action) {
|
||||
|
||||
case 'enable':
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
$model->enable();
|
||||
redirect($returnurl);
|
||||
break;
|
||||
|
||||
case 'disable':
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
$model->update(0, false, false);
|
||||
redirect($returnurl);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
$model->delete();
|
||||
redirect($returnurl);
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
$invalidcurrenttimesplitting = $model->invalid_timesplitting_selected();
|
||||
$potentialtimesplittings = $model->get_potential_timesplittings();
|
||||
@@ -183,7 +183,7 @@ switch ($action) {
|
||||
break;
|
||||
|
||||
case 'evaluate':
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
if ($model->is_static()) {
|
||||
throw new moodle_exception('errornostaticevaluate', 'tool_analytics');
|
||||
@@ -226,7 +226,7 @@ switch ($action) {
|
||||
break;
|
||||
|
||||
case 'scheduledanalysis':
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
if ($onlycli) {
|
||||
throw new moodle_exception('erroronlycli', 'tool_analytics');
|
||||
@@ -292,7 +292,7 @@ switch ($action) {
|
||||
break;
|
||||
|
||||
case 'clear':
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
$model->clear();
|
||||
redirect($returnurl);
|
||||
|
||||
@@ -155,9 +155,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;
|
||||
}
|
||||
@@ -178,7 +178,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 "150000" 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 "155000" 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 "150000" 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 |
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 |
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -71,14 +71,12 @@ define(['jquery'], function($) {
|
||||
return null;
|
||||
};
|
||||
|
||||
// eslint-disable-line valid-jsdoc
|
||||
/**
|
||||
* Return the type of the module.
|
||||
*
|
||||
* @return {String}
|
||||
* @method getType
|
||||
*/
|
||||
// eslint-enable-line valid-jsdoc
|
||||
Rule.prototype.getType = function() {
|
||||
throw new Error('Not implemented');
|
||||
};
|
||||
|
||||
@@ -419,13 +419,13 @@ class api {
|
||||
public static function get_qrlogin_key(stdClass $mobilesettings) {
|
||||
global $USER;
|
||||
// Delete previous keys.
|
||||
delete_user_key('tool_mobile', $USER->id);
|
||||
delete_user_key('tool_mobile/qrlogin', $USER->id);
|
||||
|
||||
// Create a new key.
|
||||
$iprestriction = !empty($mobilesettings->qrsameipcheck) ? getremoteaddr(null) : null;
|
||||
$qrkeyttl = !empty($mobilesettings->qrkeyttl) ? $mobilesettings->qrkeyttl : self::LOGIN_QR_KEY_TTL;
|
||||
$validuntil = time() + $qrkeyttl;
|
||||
return create_user_key('tool_mobile', $USER->id, null, $iprestriction, $validuntil);
|
||||
return create_user_key('tool_mobile/qrlogin', $USER->id, null, $iprestriction, $validuntil);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace tool_mobile;
|
||||
|
||||
use core\session\utility\cookie_helper;
|
||||
use core\event\user_loggedin;
|
||||
|
||||
/**
|
||||
* Event handler for tool_mobile.
|
||||
*
|
||||
* @package tool_mobile
|
||||
* @copyright 2024 Juan Leyva
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class event_handler {
|
||||
|
||||
/**
|
||||
* Allows the plugin to augment Set-Cookie headers when the user_loggedin event is fired as part of complete_user_login() calls.
|
||||
*
|
||||
* @param user_loggedin $event the event
|
||||
* @return void
|
||||
*/
|
||||
public static function handle_user_loggedin(user_loggedin $event): void {
|
||||
global $CFG;
|
||||
|
||||
// Set Partitioned and Secure attributes to the MoodleSession cookie if the user is using the Moodle app.
|
||||
if (\core_useragent::is_moodle_app()) {
|
||||
cookie_helper::add_attributes_to_cookie_response_header('MoodleSession'.$CFG->sessioncookie, ['Secure', 'Partitioned']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -649,8 +649,8 @@ class external extends external_api {
|
||||
api::check_autologin_prerequisites($params['userid']); // Checks https, avoid site admins using this...
|
||||
|
||||
// Validate and delete the key.
|
||||
$key = validate_user_key($params['qrloginkey'], 'tool_mobile', null);
|
||||
delete_user_key('tool_mobile', $params['userid']);
|
||||
$key = validate_user_key($params['qrloginkey'], 'tool_mobile/qrlogin', null);
|
||||
delete_user_key('tool_mobile/qrlogin', $params['userid']);
|
||||
|
||||
// Double check key belong to user.
|
||||
if ($key->userid != $params['userid']) {
|
||||
|
||||
@@ -66,7 +66,7 @@ class provider implements
|
||||
FROM {user_private_key} k
|
||||
JOIN {user} u ON k.userid = u.id
|
||||
JOIN {context} ctx ON ctx.instanceid = u.id AND ctx.contextlevel = :contextlevel
|
||||
WHERE k.userid = :userid AND k.script = 'tool_mobile'";
|
||||
WHERE k.userid = :userid AND (k.script = 'tool_mobile' OR k.script = 'tool_mobile/qrlogin')";
|
||||
$params = ['userid' => $userid, 'contextlevel' => CONTEXT_USER];
|
||||
$contextlist = new contextlist();
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
@@ -88,6 +88,7 @@ class provider implements
|
||||
|
||||
// Add users based on userkey.
|
||||
\core_userkey\privacy\provider::get_user_contexts_with_script($userlist, $context, 'tool_mobile');
|
||||
\core_userkey\privacy\provider::get_user_contexts_with_script($userlist, $context, 'tool_mobile/qrlogin');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,6 +109,7 @@ class provider implements
|
||||
}
|
||||
// Export associated userkeys.
|
||||
\core_userkey\privacy\provider::export_userkeys($context, [], 'tool_mobile');
|
||||
\core_userkey\privacy\provider::export_userkeys($context, [], 'tool_mobile/qrlogin');
|
||||
}
|
||||
/**
|
||||
* Export all user preferences for the plugin.
|
||||
@@ -138,6 +140,7 @@ class provider implements
|
||||
$userid = $context->instanceid;
|
||||
// Delete all the userkeys.
|
||||
\core_userkey\privacy\provider::delete_userkeys('tool_mobile', $userid);
|
||||
\core_userkey\privacy\provider::delete_userkeys('tool_mobile/qrlogin', $userid);
|
||||
}
|
||||
/**
|
||||
* Delete all user data for the specified user, in the specified contexts.
|
||||
@@ -158,6 +161,7 @@ class provider implements
|
||||
$userid = $context->instanceid;
|
||||
// Delete all the userkeys.
|
||||
\core_userkey\privacy\provider::delete_userkeys('tool_mobile', $userid);
|
||||
\core_userkey\privacy\provider::delete_userkeys('tool_mobile/qrlogin', $userid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,5 +182,6 @@ class provider implements
|
||||
|
||||
// Delete all the userkeys.
|
||||
\core_userkey\privacy\provider::delete_userkeys('tool_mobile', $userid);
|
||||
\core_userkey\privacy\provider::delete_userkeys('tool_mobile/qrlogin', $userid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* tool_mobile plugin event handler definition.
|
||||
*
|
||||
* @package tool_mobile
|
||||
* @category event
|
||||
* @copyright 2024 Juan Leyva
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$observers = [
|
||||
[
|
||||
'eventname' => '\core\event\user_loggedin',
|
||||
'callback' => '\tool_mobile\event_handler::handle_user_loggedin',
|
||||
],
|
||||
];
|
||||
@@ -69,6 +69,14 @@ if (empty($service)) {
|
||||
throw new moodle_exception('servicenotavailable', 'webservice');
|
||||
}
|
||||
|
||||
// If the user is using the inapp (embedded) browser, we need to set the Secure and Partitioned attributes to the session cookie.
|
||||
if (\core_useragent::is_moodle_app()) {
|
||||
\core\session\utility\cookie_helper::add_attributes_to_cookie_response_header(
|
||||
"MoodleSession{$CFG->sessioncookie}",
|
||||
['Secure', 'Partitioned'],
|
||||
);
|
||||
}
|
||||
|
||||
require_login(0, false);
|
||||
|
||||
// Require an active user: not guest, not suspended.
|
||||
|
||||
@@ -265,3 +265,16 @@ function tool_mobile_pre_processor_message_send($procname, $data) {
|
||||
$data->fullmessagehtml .= html_writer::tag('p', get_string('readingthisemailgettheapp', 'tool_mobile', $url->out()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to add headers before the HTTP headers are sent.
|
||||
*
|
||||
*/
|
||||
function tool_mobile_before_http_headers() {
|
||||
global $CFG;
|
||||
|
||||
// Set Partitioned and Secure attributes to the MoodleSession cookie if the user is using the Moodle app.
|
||||
if (\core_useragent::is_moodle_app()) {
|
||||
\core\session\utility\cookie_helper::add_attributes_to_cookie_response_header('MoodleSession'.$CFG->sessioncookie, ['Secure', 'Partitioned']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,8 @@ class provider_test extends \core_privacy\tests\provider_testcase {
|
||||
$context1 = \context_user::instance($user1->id);
|
||||
$context2 = \context_user::instance($user2->id);
|
||||
$key1 = get_user_key('tool_mobile', $user1->id);
|
||||
$key2 = get_user_key('tool_mobile', $user2->id);
|
||||
$key2 = get_user_key('tool_mobile/qrlogin', $user1->id);
|
||||
$key3 = get_user_key('tool_mobile', $user2->id);
|
||||
|
||||
// Ensure only user1 is found in context1.
|
||||
$userlist = new \core_privacy\local\request\userlist($context1, $component);
|
||||
@@ -174,12 +175,15 @@ class provider_test extends \core_privacy\tests\provider_testcase {
|
||||
$context1 = \context_user::instance($user1->id);
|
||||
$context2 = \context_user::instance($user2->id);
|
||||
$keyvalue1 = get_user_key('tool_mobile', $user1->id);
|
||||
$keyvalue2 = get_user_key('tool_mobile', $user2->id);
|
||||
$keyvalue2 = get_user_key('tool_mobile/qrlogin', $user1->id);
|
||||
$keyvalue3 = get_user_key('tool_mobile', $user2->id);
|
||||
$key1 = $DB->get_record('user_private_key', ['value' => $keyvalue1]);
|
||||
|
||||
// Before deletion, we should have 2 user_private_keys.
|
||||
// Before deletion, we should have 2 user_private_keys for tool_mobile and one for tool_mobile/qrlogin.
|
||||
$count = $DB->count_records('user_private_key', ['script' => 'tool_mobile']);
|
||||
$this->assertEquals(2, $count);
|
||||
$count = $DB->count_records('user_private_key', ['script' => 'tool_mobile/qrlogin']);
|
||||
$this->assertEquals(1, $count);
|
||||
|
||||
// Ensure deleting wrong user in the user context does nothing.
|
||||
$approveduserids = [$user2->id];
|
||||
@@ -197,6 +201,8 @@ class provider_test extends \core_privacy\tests\provider_testcase {
|
||||
// Ensure only user1's data is deleted, user2's remains.
|
||||
$count = $DB->count_records('user_private_key', ['script' => 'tool_mobile']);
|
||||
$this->assertEquals(1, $count);
|
||||
$count = $DB->count_records('user_private_key', ['script' => 'tool_mobile/qrlogin']);
|
||||
$this->assertEquals(0, $count);
|
||||
|
||||
$params = ['script' => $component];
|
||||
$userid = $DB->get_field_select('user_private_key', 'userid', 'script = :script', $params);
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
$plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2022112801; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2022111800; // Requires this Moodle version.
|
||||
$plugin->component = 'tool_mobile'; // Full name of the plugin (used for diagnostics).
|
||||
$plugin->dependencies = array(
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -121,7 +121,6 @@ define(['tool_moodlenet/validator',
|
||||
});
|
||||
// Trigger the transition between 'pages'.
|
||||
carousel.carousel(2);
|
||||
// eslint-disable-next-line max-len
|
||||
modal.setFooter(Templates.render('tool_moodlenet/chooser_footer_close_mnet', {}));
|
||||
};
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ if ($cancel) {
|
||||
}
|
||||
redirect($url);
|
||||
} else if ($continue) {
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
// Handle backups.
|
||||
if (strtolower($importinfo->get_resource()->get_extension()) == 'mbz') {
|
||||
|
||||
@@ -81,7 +81,7 @@ switch ($config->type) {
|
||||
}
|
||||
|
||||
if ($import && $module) {
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
$handlerinfo = $handlerregistry->get_resource_handler_for_mod_and_strategy($importinfo->get_resource(), $module, $strategy);
|
||||
if (is_null($handlerinfo)) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -36,6 +36,7 @@ import {dispatchEvent} from 'core/event_dispatcher';
|
||||
import {eventTypes} from './events';
|
||||
import {get_string as getString} from 'core/str';
|
||||
import {prefetchStrings} from 'core/prefetch';
|
||||
import PendingPromise from 'core/pending';
|
||||
|
||||
/**
|
||||
* The minimum spacing for tour step to display.
|
||||
@@ -485,14 +486,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();
|
||||
@@ -503,6 +512,7 @@ const Tour = class {
|
||||
this.dispatchEvent(eventTypes.stepRendered, {stepConfig});
|
||||
}
|
||||
|
||||
pendingPromise.resolve();
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -820,12 +830,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() {
|
||||
@@ -897,6 +909,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);
|
||||
@@ -910,6 +923,7 @@ const Tour = class {
|
||||
if (this.currentStepNode) {
|
||||
this.currentStepNode.focus();
|
||||
}
|
||||
pendingPromise.resolve();
|
||||
}, this), 100);
|
||||
|
||||
}, this));
|
||||
@@ -1157,6 +1171,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) {
|
||||
@@ -1191,17 +1206,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) {
|
||||
@@ -1222,6 +1242,8 @@ const Tour = class {
|
||||
|
||||
this.currentStepNode = null;
|
||||
this.currentStepPopper = null;
|
||||
|
||||
pendingPromise.resolve();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
+1
-1
@@ -116,7 +116,7 @@ class auth_plugin_lti extends \auth_plugin_base {
|
||||
if (isloggedin()) {
|
||||
// If a different user is currently logged in, authenticate the linked user instead.
|
||||
global $USER;
|
||||
if ((int) $USER->id !== $user->id) {
|
||||
if ($USER->id !== $user->id) {
|
||||
complete_user_login($user);
|
||||
}
|
||||
// If the linked user is already logged in, skip the call to complete_user_login() because this affects deep linking
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace auth_lti\local\ltiadvantage\event;
|
||||
|
||||
use auth_lti\local\ltiadvantage\utility\cookie_helper;
|
||||
use core\event\user_loggedin;
|
||||
|
||||
/**
|
||||
* Event handler for auth_lti.
|
||||
*
|
||||
* @package auth_lti
|
||||
* @copyright 2024 Jake Dallimore <jrhdallimore@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class event_handler {
|
||||
|
||||
/**
|
||||
* Allows the plugin to augment Set-Cookie headers when the user_loggedin event is fired as part of complete_user_login() calls.
|
||||
*
|
||||
* @param user_loggedin $event the event
|
||||
* @return void
|
||||
*/
|
||||
public static function handle_user_loggedin(user_loggedin $event): void {
|
||||
// The event data isn't important here. The intent of this listener is to ensure that the MoodleSession cookie is set up
|
||||
// properly during LTI launches + login. This means two things:
|
||||
// i) it's set with SameSite=None; Secure; where possible (since OIDC needs HTTPS this will almost always be possible).
|
||||
// ii) it set with the 'Partitioned' attribute, when required.
|
||||
// The former ensures cross-site cookies are sent for embedded launches. The latter is an opt-in flag needed to use Chrome's
|
||||
// partitioning mechanism, CHIPS.
|
||||
if (cookie_helper::cookies_supported()) {
|
||||
cookie_helper::setup_session_cookie();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace auth_lti\local\ltiadvantage\utility;
|
||||
use core\session\utility\cookie_helper as core_cookie_helper;
|
||||
|
||||
/**
|
||||
* Helper class providing utils dealing with cookies in LTI, particularly 3rd party cookies.
|
||||
*
|
||||
* @package auth_lti
|
||||
* @copyright 2024 Jake Dallimore <jrhdallimore@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
final class cookie_helper {
|
||||
|
||||
/** @var int Cookies are not supported. */
|
||||
public const COOKIE_METHOD_NOT_SUPPORTED = 0;
|
||||
|
||||
/** @var int Cookies are supported without explicit partitioning. */
|
||||
public const COOKIE_METHOD_NO_PARTITIONING = 1;
|
||||
|
||||
/** @var int Cookies are supported via explicit partitioning. */
|
||||
public const COOKIE_METHOD_EXPLICIT_PARTITIONING = 2;
|
||||
|
||||
/**
|
||||
* Check whether cookies can be used with the current user agent and, if so, via what method they are set.
|
||||
*
|
||||
* Currently, this tries 2 modes of setting a test cookie:
|
||||
* 1. Setting a SameSite=None, Secure cookie. This will work in any first party context, and in 3rd party contexts for
|
||||
* any browsers supporting automatic partitioning of 3rd party cookies (E.g. Firefox, Brave).
|
||||
* 2. If 1 fails, setting a cookie with the Chrome 'Partitioned' attribute included, opting that cookie into CHIPS. This will
|
||||
* work for Chrome.
|
||||
*
|
||||
* Upon completion of the cookie check, the check sets a SESSION flag indicating the method used to set the cookie, and upgrades
|
||||
* the session cookie ('MoodleSession') using the respective method. This ensure the session cookie will continue to be sent.
|
||||
*
|
||||
* Then, the following methods can be used by client code to query whether the UA supports cookies, and how:
|
||||
* @see self::cookies_supported() - whether it could be set at all.
|
||||
* @see self::get_cookies_supported_mode() - if a cookie could be set, what mode was used to set it.
|
||||
*
|
||||
* This permits client code to make sure it's setting its cookies appropriately (via the advertised method), and allows it to
|
||||
* present notices - such as in the case where a given UA is found to be lacking the requisite cookie support.
|
||||
* E.g.
|
||||
* cookie_helper::do_cookie_check($mypageurl);
|
||||
* if (!cookie_helper::cookies_supported()) {
|
||||
* // Print a notice stating that cookie support is required.
|
||||
* }
|
||||
* // Elsewhere in other client code...
|
||||
* if (cookie_helper::get_cookies_supported_mode() === cookie_helper::COOKIE_METHOD_EXPLICIT_PARTITIONING) {
|
||||
* // Set a cookie, making sure to use the helper to also opt-in to partitioning.
|
||||
* setcookie('myauthcookie', 'myauthcookievalue', ['samesite' => 'None', 'secure' => true]);
|
||||
* cookie_helper::add_partitioning_to_cookie('myauthcookie');
|
||||
* }
|
||||
*
|
||||
* @param \moodle_url $pageurl the URL of the page making the check, used to redirect back to after setting test cookies.
|
||||
* @return void
|
||||
*/
|
||||
public static function do_cookie_check(\moodle_url $pageurl): void {
|
||||
global $_COOKIE, $SESSION, $CFG;
|
||||
$cookiecheck1 = optional_param('cookiecheck1', null, PARAM_INT);
|
||||
$cookiecheck2 = optional_param('cookiecheck2', null, PARAM_INT);
|
||||
|
||||
if (empty($cookiecheck1)) {
|
||||
// Start the cookie check. Set two test cookies - one samesite none, and one partitioned - and redirect.
|
||||
// Set cookiecheck to show the check has started.
|
||||
self::set_test_cookie('cookiecheck1', self::COOKIE_METHOD_NO_PARTITIONING);
|
||||
self::set_test_cookie('cookiecheck2', self::COOKIE_METHOD_EXPLICIT_PARTITIONING, true);
|
||||
$pageurl->params([
|
||||
'cookiecheck1' => self::COOKIE_METHOD_NO_PARTITIONING,
|
||||
'cookiecheck2' => self::COOKIE_METHOD_EXPLICIT_PARTITIONING,
|
||||
]);
|
||||
|
||||
// LTI needs to guarantee the 'SameSite=None', 'Secure' (and sometimes 'Partitioned') attributes are set on the
|
||||
// MoodleSession cookie. This is done via manipulation of the outgoing headers after the cookie check redirect. To
|
||||
// guarantee these outgoing Set-Cookie headers will be created after the redirect, expire the current cookie.
|
||||
core_cookie_helper::expire_moodlesession();
|
||||
|
||||
redirect($pageurl);
|
||||
} else {
|
||||
// Have already started a cookie check, so check the result.
|
||||
$cookie1received = isset($_COOKIE['cookiecheck1']) && $_COOKIE['cookiecheck1'] == $cookiecheck1;
|
||||
$cookie2received = isset($_COOKIE['cookiecheck2']) && $_COOKIE['cookiecheck2'] == $cookiecheck2;
|
||||
|
||||
if ($cookie1received || $cookie2received) {
|
||||
// The test cookie could be set and received.
|
||||
// Set a session flag storing the method used to set it, and make sure the session cookie uses this method.
|
||||
$cookiemethod = $cookie1received ? self::COOKIE_METHOD_NO_PARTITIONING : self::COOKIE_METHOD_EXPLICIT_PARTITIONING;
|
||||
$SESSION->auth_lti_cookie_method = $cookiemethod;
|
||||
|
||||
self::setup_session_cookie();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If a cookie check has been made, returns whether cookies could be set or not.
|
||||
*
|
||||
* @return bool whether cookies are supported or not.
|
||||
*/
|
||||
public static function cookies_supported(): bool {
|
||||
return self::get_cookies_supported_method() !== self::COOKIE_METHOD_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a cookie check has been made, gets the method used to set a cookie, or self::COOKIE_METHOD_NOT_SUPPORTED if not supported.
|
||||
*
|
||||
* For cookie methods:
|
||||
* @see self::COOKIE_METHOD_NOT_SUPPORTED
|
||||
* @see self::COOKIE_METHOD_NO_PARTITIONING
|
||||
* @see self::COOKIE_METHOD_EXPLICIT_PARTITIONING
|
||||
*
|
||||
* @return int the constant representing the method by which the cookie was set, or not.
|
||||
*/
|
||||
public static function get_cookies_supported_method(): int {
|
||||
global $SESSION;
|
||||
return $SESSION->auth_lti_cookie_method ?? self::COOKIE_METHOD_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the session cookie according to the method used in the cookie check, and with SameSite=None; Secure attributes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function setup_session_cookie(): void {
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/sessionlib.php');
|
||||
|
||||
if (is_moodle_cookie_secure()) {
|
||||
$atts = ['SameSite=None', 'Secure'];
|
||||
if (self::get_cookies_supported_method() == self::COOKIE_METHOD_EXPLICIT_PARTITIONING) {
|
||||
$atts[] = 'Partitioned';
|
||||
}
|
||||
core_cookie_helper::add_attributes_to_cookie_response_header('MoodleSession' . $CFG->sessioncookie, $atts);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a test cookie, using SameSite=None; Secure; attributes if possible, and with or without partitioning opt-in.
|
||||
*
|
||||
* @param string $name cookie name
|
||||
* @param string $value cookie value
|
||||
* @param bool $partitioned whether to try to add partitioning opt-in, which requires secure cookies (https sites).
|
||||
* @return void
|
||||
*/
|
||||
private static function set_test_cookie(string $name, string $value, bool $partitioned = false): void {
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/sessionlib.php');
|
||||
|
||||
$atts = ['expires' => time() + 30];
|
||||
if (is_moodle_cookie_secure()) {
|
||||
$atts['samesite'] = 'none';
|
||||
$atts['secure'] = true;
|
||||
}
|
||||
setcookie($name, $value, $atts);
|
||||
|
||||
if (is_moodle_cookie_secure() && $partitioned) {
|
||||
core_cookie_helper::add_attributes_to_cookie_response_header($name, ['Partitioned']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,33 +33,32 @@ class renderer extends \plugin_renderer_base {
|
||||
* @return string the html.
|
||||
*/
|
||||
public function render_account_binding_options_page(int $provisioningmode): string {
|
||||
|
||||
$formaction = new \moodle_url('/auth/lti/login.php');
|
||||
$notification = new notification(get_string('firstlaunchnotice', 'auth_lti'), \core\notification::INFO, false);
|
||||
$noauthnotice = new notification(get_string('firstlaunchnoauthnotice', 'auth_lti', get_docs_url('Publish_as_LTI_tool')),
|
||||
\core\notification::WARNING, false);
|
||||
$cancreateaccounts = !get_config('moodle', 'authpreventaccountcreation');
|
||||
if ($provisioningmode == \auth_plugin_lti::PROVISIONING_MODE_PROMPT_EXISTING_ONLY) {
|
||||
$cancreateaccounts = false;
|
||||
}
|
||||
|
||||
$accountinfo = ['isloggedin' => isloggedin()];
|
||||
$accountinfo = [];
|
||||
if (isloggedin()) {
|
||||
global $USER;
|
||||
$accountinfo = array_merge($accountinfo, [
|
||||
$accountinfo = [
|
||||
'firstname' => $USER->firstname,
|
||||
'lastname' => $USER->lastname,
|
||||
'email' => $USER->email,
|
||||
'picturehtml' => $this->output->user_picture($USER, ['size' => 35, 'class' => 'round']),
|
||||
]);
|
||||
];
|
||||
}
|
||||
|
||||
$context = [
|
||||
'isloggedin' => isloggedin(),
|
||||
'info' => $notification->export_for_template($this),
|
||||
'formaction' => $formaction->out(),
|
||||
'sesskey' => sesskey(),
|
||||
'accountinfo' => $accountinfo,
|
||||
'cancreateaccounts' => $cancreateaccounts,
|
||||
'noauthnotice' => $noauthnotice->export_for_template($this)
|
||||
];
|
||||
return parent::render_from_template('auth_lti/local/ltiadvantage/login', $context);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* LTI Auth plugin event handler definition.
|
||||
*
|
||||
* @package auth_lti
|
||||
* @category event
|
||||
* @copyright 2024 Jake Dallimore <jrhdallimore@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$observers = [
|
||||
[
|
||||
'eventname' => '\core\event\user_loggedin',
|
||||
'callback' => '\auth_lti\local\ltiadvantage\event\event_handler::handle_user_loggedin',
|
||||
],
|
||||
];
|
||||
@@ -35,7 +35,7 @@ $string['firstlaunchnoauthnotice'] = 'To link your existing account you must be
|
||||
$string['getstartedwithnewaccount'] = 'Get started with a new account';
|
||||
$string['haveexistingaccount'] = 'I have an existing account';
|
||||
$string['linkthisaccount'] = 'Link this account';
|
||||
$string['mustbeloggedin'] = 'You need to be logged in to your existing account';
|
||||
$string['mustbeloggedin'] = 'Sign in to link your existing account';
|
||||
$string['pluginname'] = 'LTI';
|
||||
$string['privacy:metadata:auth_lti'] = 'LTI authentication';
|
||||
$string['privacy:metadata:auth_lti:authsubsystem'] = 'This plugin is connected to the authentication subsystem.';
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
* info - a notification describing the first launch options
|
||||
* cancreateaccounts - whether or not the user is allowed to create auth_lti accounts
|
||||
* accountinfo - information about the user, importantly whether they are logged in or not.
|
||||
* noauthnotice - a notification telling the user they must be authenticated to link accounts. Only relevant when not logged in.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
@@ -46,19 +45,12 @@
|
||||
"issuccess": true
|
||||
},
|
||||
"cancreateaccounts": true,
|
||||
"isloggedin": true,
|
||||
"accountinfo": {
|
||||
"isloggedin": true,
|
||||
"firstname": "John",
|
||||
"lastname": "Smith",
|
||||
"email": "john@example.com",
|
||||
"picturehtml": "<img src=\"http://site.example.com/pluginfile.php/5/user/icon/boost/f2?rev=99\" class=\"round\" alt=\"\" width=\"35\" height=\"35\">"
|
||||
},
|
||||
"noauthnotice": {
|
||||
"message": "To link your existing account you must be logged in to the site...",
|
||||
"extraclasses": "",
|
||||
"announce": false,
|
||||
"closebutton": false,
|
||||
"iswarning": true
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -79,8 +71,8 @@
|
||||
<div class="card-body text-center d-flex flex-column">
|
||||
<i class="fa fa-user-circle-o fa-2x link"></i>
|
||||
<h4 class="card-title">{{#str}} useexistingaccount, auth_lti {{/str}}</h4>
|
||||
{{#accountinfo}}
|
||||
{{#isloggedin}}
|
||||
{{#accountinfo}}
|
||||
<p class="card-text mt-2">
|
||||
<span class="text-muted">
|
||||
{{#str}} currentlyloggedinas, auth_lti {{/str}}
|
||||
@@ -90,14 +82,12 @@
|
||||
{{firstname}} {{lastname}} ({{email}})
|
||||
</p>
|
||||
<input type="submit" class="btn btn-primary mt-auto" name="existing_account" value="{{#str}} linkthisaccount, auth_lti {{/str}}">
|
||||
{{/accountinfo}}
|
||||
{{/isloggedin}}
|
||||
{{^isloggedin}}
|
||||
<p class="card-text text-muted">{{#str}} mustbeloggedin, auth_lti {{/str}}</p>
|
||||
{{#noauthnotice}}
|
||||
{{> core/notification}}
|
||||
{{/noauthnotice}}
|
||||
<input type="submit" class="btn btn-primary mt-auto" name="existing_account" value="{{#str}} login, moodle {{/str}}">
|
||||
{{/isloggedin}}
|
||||
{{/accountinfo}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2022112801; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2022111800; // Requires this Moodle version.
|
||||
$plugin->component = 'auth_lti'; // Full name of the plugin (used for diagnostics).
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
+10
-10
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
+2
-1
@@ -107,9 +107,10 @@ if (!has_any_capability(array(
|
||||
$PAGE->set_title($hdr);
|
||||
$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?
|
||||
|
||||
@@ -119,3 +119,97 @@ Feature: Award badges with separate groups
|
||||
And I press "Award badge"
|
||||
# Teacher 2 shouldn't be able to go further
|
||||
Then I should see "Sorry, but you need to be part of a group to see this page."
|
||||
|
||||
@javascript
|
||||
Scenario: Editing teacher can award badge to members of separate groups
|
||||
Given I am on the "Course 1" course page logged in as teacher1
|
||||
And I navigate to "Badges > Manage badges" in current page administration
|
||||
And I follow "Course Badge"
|
||||
And I press "Award badge"
|
||||
When I set the field "role" to "Teacher"
|
||||
# Confirm that editing teacher sees a separate groups dropdown menu.
|
||||
Then "Separate groups" "select" should exist
|
||||
And I should see "All participants" in the "Separate groups" "select"
|
||||
# Confirm that all participants are displayed when All participants is selected.
|
||||
And I should see "Student 1" in the "potentialrecipients[]" "select"
|
||||
And I should see "Student 2" in the "potentialrecipients[]" "select"
|
||||
And I should see "Teacher 1" in the "potentialrecipients[]" "select"
|
||||
And I should see "Teacher 2" in the "potentialrecipients[]" "select"
|
||||
And I set the field "Separate groups" to "Class A"
|
||||
# Confirm that only members of selected group are displayed
|
||||
And I should not see "Student 1" in the "potentialrecipients[]" "select"
|
||||
And I should not see "Teacher 1" in the "potentialrecipients[]" "select"
|
||||
And I should see "Student 2" in the "potentialrecipients[]" "select"
|
||||
And I should see "Teacher 2" in the "potentialrecipients[]" "select"
|
||||
And I set the field "Separate groups" to "Class B"
|
||||
And I should not see "Student 2" in the "potentialrecipients[]" "select"
|
||||
And I should not see "Teacher 2" in the "potentialrecipients[]" "select"
|
||||
And I should see "Student 1" in the "potentialrecipients[]" "select"
|
||||
And I should see "Teacher 1" in the "potentialrecipients[]" "select"
|
||||
|
||||
@javascript
|
||||
Scenario Outline: Teacher can award badge to members of visible groups
|
||||
Given I am on the "Course 1" "course editing" page logged in as teacher1
|
||||
And I expand all fieldsets
|
||||
# Set the group mode to visible groups.
|
||||
And I set the field "Group mode" to "Visible groups"
|
||||
And I press "Save and display"
|
||||
When I am on the "Course 1" course page logged in as <loggedinuser>
|
||||
And I navigate to "Badges > Manage badges" in current page administration
|
||||
And I follow "Course Badge"
|
||||
And I press "Award badge"
|
||||
And I set the field "role" to "<awarder>"
|
||||
# Confirm that teachers see a visible groups dropdown menu.
|
||||
Then "Visible groups" "select" should exist
|
||||
# Confirm that My groups option group exists.
|
||||
And "optgroup[label='My groups']" "css_element" should exist in the "select[name='group']" "css_element"
|
||||
# Confirm that Other groups option group exists.
|
||||
And "optgroup[label='Other groups']" "css_element" should exist in the "select[name='group']" "css_element"
|
||||
# Confirm that all participants are displayed when All participants is selected.
|
||||
And I set the field "Visible groups" to "All participants"
|
||||
And I should see "Student 1" in the "potentialrecipients[]" "select"
|
||||
And I should see "Student 2" in the "potentialrecipients[]" "select"
|
||||
And I should see "Teacher 1" in the "potentialrecipients[]" "select"
|
||||
And I should see "Teacher 2" in the "potentialrecipients[]" "select"
|
||||
# Confirm that only members of selected group are displayed.
|
||||
And I set the field "Visible groups" to "Class A"
|
||||
And I should not see "Student 1" in the "potentialrecipients[]" "select"
|
||||
And I should not see "Teacher 1" in the "potentialrecipients[]" "select"
|
||||
And I should see "Student 2" in the "potentialrecipients[]" "select"
|
||||
And I should see "Teacher 2" in the "potentialrecipients[]" "select"
|
||||
And I set the field "Visible groups" to "Class B"
|
||||
And I should not see "Student 2" in the "potentialrecipients[]" "select"
|
||||
And I should not see "Teacher 2" in the "potentialrecipients[]" "select"
|
||||
And I should see "Student 1" in the "potentialrecipients[]" "select"
|
||||
And I should see "Teacher 1" in the "potentialrecipients[]" "select"
|
||||
|
||||
Examples:
|
||||
| loggedinuser | awarder |
|
||||
| teacher1 | Teacher |
|
||||
| teacher2 | Non-editing teacher |
|
||||
|
||||
@javascript
|
||||
Scenario Outline: Teacher can award badge to members when group mode is set to no groups
|
||||
Given I am on the "Course 1" "course editing" page logged in as teacher1
|
||||
And I expand all fieldsets
|
||||
# Set the group mode to no groups.
|
||||
And I set the field "Group mode" to "No groups"
|
||||
And I press "Save and display"
|
||||
When I am on the "Course 1" course page logged in as <loggedinuser>
|
||||
And I navigate to "Badges > Manage badges" in current page administration
|
||||
And I follow "Course Badge"
|
||||
And I press "Award badge"
|
||||
And I set the field "role" to "<awarder>"
|
||||
# Confirm that no group dropdowns don't exist.
|
||||
Then "Separate groups" "select" should not exist
|
||||
And "Visible groups" "select" should not exist
|
||||
# Confirm all participants are displayed.
|
||||
And I should see "Student 1" in the "potentialrecipients[]" "select"
|
||||
And I should see "Student 2" in the "potentialrecipients[]" "select"
|
||||
And I should see "Teacher 1" in the "potentialrecipients[]" "select"
|
||||
And I should see "Teacher 2" in the "potentialrecipients[]" "select"
|
||||
|
||||
Examples:
|
||||
| loggedinuser | awarder |
|
||||
| teacher1 | Teacher |
|
||||
| teacher2 | Non-editing teacher |
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -58,7 +58,7 @@ class helper {
|
||||
FROM {block_recentlyaccesseditems} rai
|
||||
JOIN {course} c ON c.id = rai.courseid
|
||||
WHERE userid = :userid
|
||||
ORDER BY rai.timeaccess DESC";
|
||||
ORDER BY rai.timeaccess DESC, rai.id DESC";
|
||||
$records = $DB->get_records_sql($sql, $paramsql);
|
||||
$order = 0;
|
||||
|
||||
|
||||
@@ -27,9 +27,8 @@
|
||||
<div class="text-xs-center text-center mt-4" data-region="empty-message">
|
||||
<img
|
||||
src="{{noitemsimgurl}}"
|
||||
alt="{{#str}} noitems, block_recentlyaccesseditems {{/str}}"
|
||||
role="presentation"
|
||||
alt=""
|
||||
style="height: 70px; width: 70px;"
|
||||
>
|
||||
<p class="text-muted mt-3 mb-0">{{#str}} noitems, block_recentlyaccesseditems {{/str}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ Feature: Edit activities in main menu block
|
||||
I need to add and edit activities there
|
||||
|
||||
@javascript
|
||||
Scenario: Edit name of acitivity in-place in site main menu block
|
||||
Scenario: Edit name of activity in-place in site main menu block
|
||||
Given the following "activity" exists:
|
||||
| activity | forum |
|
||||
| course | Acceptance test site |
|
||||
@@ -36,6 +36,7 @@ Feature: Edit activities in main menu block
|
||||
| forum | Acceptance test site | 0 | Visible forum |
|
||||
| forum | Acceptance test site | 0 | My forum name |
|
||||
And I log in as "admin"
|
||||
And I change window size to "large"
|
||||
And I am on site homepage
|
||||
And I turn editing mode on
|
||||
And "My forum name" activity in site main menu block should have "Hide" editing icon
|
||||
|
||||
@@ -101,13 +101,14 @@ class block_tag_flickr extends block_base {
|
||||
$request .= '&api_key='.FLICKR_DEV_KEY;
|
||||
$request .= '&photoset_id='.$this->config->photoset;
|
||||
$request .= '&per_page='.$numberofphotos;
|
||||
$request .= '&format=php_serial';
|
||||
$request .= '&format=json';
|
||||
// We need to add nojsoncallback=? here, otherwise, Flickr will return the jsonFlickrApi object.
|
||||
$request .= '&nojsoncallback=?';
|
||||
|
||||
$response = $this->fetch_request($request);
|
||||
|
||||
$search = @unserialize($response);
|
||||
if ($search === false && $search != serialize(false)) {
|
||||
// The response didn't appear to be anything serialized, exit...
|
||||
$search = @json_decode($response, true);
|
||||
if (!is_array($search) || json_last_error() !== JSON_ERROR_NONE) {
|
||||
// The response didn't appear to be in correct format.
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -126,13 +127,14 @@ class block_tag_flickr extends block_base {
|
||||
$request .= '&tags='.$tagscsv;
|
||||
$request .= '&per_page='.$numberofphotos;
|
||||
$request .= '&sort='.$sortby;
|
||||
$request .= '&format=php_serial';
|
||||
$request .= '&format=json';
|
||||
// We need to add nojsoncallback=? here, otherwise, Flickr will return the jsonFlickrApi object.
|
||||
$request .= '&nojsoncallback=?';
|
||||
|
||||
$response = $this->fetch_request($request);
|
||||
|
||||
$search = @unserialize($response);
|
||||
if ($search === false && $search != serialize(false)) {
|
||||
// The response didn't appear to be anything serialized, exit...
|
||||
$search = @json_decode($response, true);
|
||||
if (!is_array($search) || json_last_error() !== JSON_ERROR_NONE) {
|
||||
// The response didn't appear to be in correct format.
|
||||
return;
|
||||
}
|
||||
$photos = array_values($search['photos']['photo']);
|
||||
|
||||
Vendored
+2
-1
@@ -608,7 +608,8 @@ class cache implements cache_loader {
|
||||
// store; parent method will have set it to all stores if needed.
|
||||
if ($setaftervalidation) {
|
||||
$lock = null;
|
||||
if (!empty($this->requirelockingbeforewrite)) {
|
||||
// Only try to acquire a lock for this cache if we do not already have one.
|
||||
if (!empty($this->requirelockingbeforewrite) && !$this->check_lock_state($key)) {
|
||||
$lock = $this->acquire_lock($key);
|
||||
}
|
||||
if ($requiredversion === self::VERSION_NONE) {
|
||||
|
||||
@@ -78,7 +78,8 @@ trait eventtype {
|
||||
if (!empty($eventtypes['user']) && count($options) == 1) {
|
||||
$mform->addElement('hidden', 'eventtype');
|
||||
$mform->setType('eventtype', PARAM_TEXT);
|
||||
$mform->setDefault('eventtype', 'user');
|
||||
$mform->hardFreeze('eventtype');
|
||||
$mform->setConstant('eventtype', 'user');
|
||||
return;
|
||||
} else {
|
||||
$mform->addElement('select', 'eventtype', get_string('eventkind', 'calendar'), $options);
|
||||
|
||||
@@ -106,7 +106,7 @@ class managesubscriptions extends \moodleform {
|
||||
$courseid = (!empty($data[$coursekey])) ? $data[$coursekey] : null;
|
||||
$eventtypes = calendar_get_allowed_event_types($courseid);
|
||||
|
||||
if (empty($eventtype) || !isset($eventtypes[$eventtype])) {
|
||||
if (empty($eventtype) || !isset($eventtypes[$eventtype]) || $eventtypes[$eventtype] == false) {
|
||||
$errors['eventtype'] = get_string('invalideventtype', 'calendar');
|
||||
}
|
||||
|
||||
|
||||
@@ -898,7 +898,17 @@ class core_calendar_external extends external_api {
|
||||
$courseid = (!empty($data[$coursekey])) ? $data[$coursekey] : null;
|
||||
$editoroptions = \core_calendar\local\event\forms\create::build_editor_options($context);
|
||||
$formoptions = ['editoroptions' => $editoroptions, 'courseid' => $courseid];
|
||||
$formoptions['eventtypes'] = calendar_get_allowed_event_types($courseid);
|
||||
$allowedeeventtypes = calendar_get_allowed_event_types($courseid);
|
||||
|
||||
// Event type validation.
|
||||
if (in_array(true, $allowedeeventtypes, true) === false) {
|
||||
throw new \moodle_exception('nopermissiontoupdatecalendar');
|
||||
}
|
||||
if (empty($eventtype) || !isset($allowedeeventtypes[$eventtype]) || $allowedeeventtypes[$eventtype] == false) {
|
||||
return ['validationerror' => true];
|
||||
}
|
||||
|
||||
$formoptions['eventtypes'] = $allowedeeventtypes;
|
||||
if ($courseid) {
|
||||
require_once($CFG->libdir . '/grouplib.php');
|
||||
$groupcoursedata = groups_get_course_data($courseid);
|
||||
|
||||
@@ -3555,6 +3555,18 @@ function calendar_get_view(\calendar_information $calendar, $view, $includenavig
|
||||
}
|
||||
}
|
||||
|
||||
// Check if $data has events.
|
||||
if (isset($data->events)) {
|
||||
// Let's check and sanitize all "name" in $data->events before it's sent to front end.
|
||||
foreach ($data->events as $d) {
|
||||
$name = $d->name ?? null;
|
||||
// Encode special characters if our decoded name does not match the original name.
|
||||
if ($name && (html_entity_decode($name) !== $name)) {
|
||||
$d->name = htmlspecialchars(html_entity_decode($name), ENT_QUOTES, 'utf-8');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [$data, $template];
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ Feature: Allow to mark course as completed without cron for activity completion
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Completion course | CC1 | 0 |
|
||||
| fullname | shortname | category | enablecompletion |
|
||||
| Completion course | CC1 | 0 | 1 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student1 | Student | First | student1@example.com |
|
||||
@@ -25,11 +25,7 @@ Feature: Allow to mark course as completed without cron for activity completion
|
||||
And the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| completionstatus | Course | CC1 | course-view-* | side-pre |
|
||||
And I am on the "Completion course" "course editing" page logged in as admin
|
||||
And I expand all fieldsets
|
||||
And I set the field "Enable completion tracking" to "Yes"
|
||||
And I click on "Save and display" "button"
|
||||
And I am on the "Test assignment name" "assign activity editing" page
|
||||
And I am on the "Test assignment name" "assign activity editing" page logged in as admin
|
||||
And I follow "Expand all"
|
||||
And I set the field "Completion tracking" to "Show activity as complete when conditions are met"
|
||||
And I set the field "completionusegrade" to "1"
|
||||
|
||||
@@ -6,8 +6,8 @@ Feature: Show activity completion status or activity completion configuration on
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
| fullname | shortname | category | enablecompletion |
|
||||
| Course 1 | C1 | 0 | 1 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | First | teacher1@example.com |
|
||||
@@ -18,12 +18,6 @@ Feature: Show activity completion status or activity completion configuration on
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| teacher2 | C1 | teacher |
|
||||
| student1 | C1 | student |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I navigate to "Settings" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Enable completion tracking | Yes |
|
||||
And I press "Save and display"
|
||||
And the following "activities" exist:
|
||||
| activity | course | idnumber | name | intro | completion | completionview | completionexpected |
|
||||
| assign | C1 | assign1 | Test assignment name | Test assignment description | 2 | 1 | 0 |
|
||||
|
||||
@@ -7,8 +7,8 @@ Feature: Allow students to manually mark an activity as complete
|
||||
@javascript
|
||||
Scenario: Mark an activity as completed
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
| fullname | shortname | category | enablecompletion |
|
||||
| Course 1 | C1 | 0 | 1 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | First | teacher1@example.com |
|
||||
@@ -18,18 +18,11 @@ Feature: Allow students to manually mark an activity as complete
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
And the following "activity" exists:
|
||||
| activity | forum |
|
||||
| course | C1 |
|
||||
| name | Test forum name |
|
||||
| activity | forum |
|
||||
| course | C1 |
|
||||
| name | Test forum name |
|
||||
| completion | 1 |
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I navigate to "Settings" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Enable completion tracking | Yes |
|
||||
And I press "Save and display"
|
||||
And I am on the "Test forum name" "forum activity editing" page
|
||||
And I set the following fields to these values:
|
||||
| completion | 1 |
|
||||
And I press "Save and return to course"
|
||||
And "Student First" user has not completed "Test forum name" activity
|
||||
And I am on the "Course 1" course page logged in as student1
|
||||
When I toggle the manual completion state of "Test forum name"
|
||||
|
||||
@@ -6,8 +6,8 @@ Feature: Restrict sections availability through completion or grade conditions
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
| fullname | shortname | category | enablecompletion |
|
||||
| Course 1 | C1 | 0 | 1 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | First | teacher1@example.com |
|
||||
@@ -23,15 +23,11 @@ Feature: Restrict sections availability through completion or grade conditions
|
||||
|
||||
@javascript
|
||||
Scenario: Show section greyed-out to student when completion condition is not satisfied
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I navigate to "Settings" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Enable completion tracking | Yes |
|
||||
And I press "Save and display"
|
||||
And the following "activities" exist:
|
||||
Given the following "activities" exist:
|
||||
| activity | course | section | intro | completion | idnumber |
|
||||
| label | C1 | 1 | Test label | 1 | 1 |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
When I edit the section "2"
|
||||
And I expand all fieldsets
|
||||
And I click on "Add restriction..." "button"
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
@core @core_contentbank @core_h5p @contenttype_h5p @_file_upload @_switch_iframe @javascript
|
||||
@core @core_contentbank @core_h5p @contenttype_h5p @_switch_iframe @javascript
|
||||
Feature: H5P file upload to content bank for admins
|
||||
In order import new H5P content to content bank
|
||||
As an admin
|
||||
I need to be able to upload a new .h5p file to content bank
|
||||
|
||||
Background:
|
||||
Given the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| private_files | System | 1 | my-index | side-post |
|
||||
Given the following "user private file" exists:
|
||||
| user | admin |
|
||||
| filepath | h5p/tests/fixtures/filltheblanks.h5p |
|
||||
And I log in as "admin"
|
||||
And I follow "Manage private files..."
|
||||
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager
|
||||
And I click on "Save changes" "button"
|
||||
And I turn editing mode on
|
||||
And the following config values are set as admin:
|
||||
| unaddableblocks | | theme_boost|
|
||||
|
||||
@@ -19,13 +19,10 @@ Feature: H5P file upload to content bank for non admins
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| teacher2 | C1 | editingteacher |
|
||||
And the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| private_files | System | 1 | my-index | side-post |
|
||||
And the following "user private file" exists:
|
||||
| user | teacher1 |
|
||||
| filepath | h5p/tests/fixtures/filltheblanks.h5p |
|
||||
And I log in as "teacher1"
|
||||
And I follow "Manage private files..."
|
||||
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager
|
||||
And I click on "Save changes" "button"
|
||||
|
||||
Scenario: Teachers can not access system level content bank
|
||||
Given I turn editing mode on
|
||||
@@ -128,11 +125,10 @@ Feature: H5P file upload to content bank for non admins
|
||||
And I should not see "filltheblanks.h5p"
|
||||
|
||||
Scenario: Teachers can not see existing contents when libraries are not installed
|
||||
Given I log out
|
||||
Given the following "user private file" exists:
|
||||
| user | admin |
|
||||
| filepath | h5p/tests/fixtures/filltheblanks.h5p |
|
||||
And I log in as "admin"
|
||||
And I follow "Manage private files..."
|
||||
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager
|
||||
And I click on "Save changes" "button"
|
||||
And I navigate to "H5P > Manage H5P content types" in site administration
|
||||
And I should not see "Fill in the Blanks"
|
||||
When I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
|
||||
|
||||
@@ -1,28 +1,14 @@
|
||||
@core @core_contentbank @core_h5p @contentbank_h5p @_file_upload @javascript
|
||||
@core @core_contentbank @core_h5p @contentbank_h5p @javascript
|
||||
Feature: Delete H5P file from the content bank
|
||||
In order remove H5P content from the content bank
|
||||
As an admin
|
||||
I need to be able to delete any H5P content from the content bank
|
||||
|
||||
Background:
|
||||
Given the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| private_files | System | 1 | my-index | side-post |
|
||||
And I log in as "admin"
|
||||
And I follow "Manage private files..."
|
||||
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager
|
||||
And I click on "Save changes" "button"
|
||||
And I am on site homepage
|
||||
And I turn editing mode on
|
||||
And the following config values are set as admin:
|
||||
| unaddableblocks | | theme_boost|
|
||||
And I add the "Navigation" block if not present
|
||||
And I configure the "Navigation" block
|
||||
And I set the following fields to these values:
|
||||
| Page contexts | Display throughout the entire site |
|
||||
And I press "Save changes"
|
||||
And I click on "Site pages" "list_item" in the "Navigation" "block"
|
||||
And I click on "Content bank" "link" in the "Navigation" "block"
|
||||
Given the following "user private file" exists:
|
||||
| user | admin |
|
||||
| filepath | h5p/tests/fixtures/filltheblanks.h5p |
|
||||
And I am on the "Content bank" page logged in as "admin"
|
||||
And I click on "Upload" "link"
|
||||
And I click on "Choose a file..." "button"
|
||||
And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
|
||||
@@ -32,14 +18,16 @@ Feature: Delete H5P file from the content bank
|
||||
And I click on "Save changes" "button"
|
||||
|
||||
Scenario: Admins can delete content from the content bank
|
||||
Given I click on "More" "button"
|
||||
Given I wait "2" seconds
|
||||
And I click on "More" "button"
|
||||
And I should see "Delete"
|
||||
And I click on "Delete" "link" in the ".cb-toolbar-container" "css_element"
|
||||
And I click on "Delete" "link"
|
||||
And I should see "Are you sure you want to delete the content 'content2delete.h5p'"
|
||||
And I should not see "The content will only be deleted from the content bank"
|
||||
And I click on "Cancel" "button" in the "Delete content" "dialogue"
|
||||
Then I should see "content2delete.h5p"
|
||||
And I click on "More" "button"
|
||||
And I wait "2" seconds
|
||||
And I click on "More" "button"
|
||||
And I click on "Delete" "link" in the ".cb-toolbar-container" "css_element"
|
||||
And I click on "Delete" "button" in the "Delete content" "dialogue"
|
||||
And I wait until the page is ready
|
||||
@@ -56,40 +44,38 @@ Feature: Delete H5P file from the content bank
|
||||
And the following "role assigns" exist:
|
||||
| user | role | contextlevel | reference |
|
||||
| manager | manager | System | |
|
||||
And I log out
|
||||
And I log in as "manager"
|
||||
And I follow "Manage private files..."
|
||||
And I upload "h5p/tests/fixtures/find-the-words.h5p" file to "Files" filemanager
|
||||
And I click on "Save changes" "button"
|
||||
When I click on "Site pages" "list_item" in the "Navigation" "block"
|
||||
And I click on "Content bank" "link" in the "Navigation" "block"
|
||||
And the following "user private file" exists:
|
||||
| user | manager |
|
||||
| filepath | h5p/tests/fixtures/find-the-words.h5p |
|
||||
When I am on the "Content bank" page logged in as "manager"
|
||||
And I should see "content2delete.h5p"
|
||||
And I follow "content2delete.h5p"
|
||||
And I wait "2" seconds
|
||||
And I click on "More" "button"
|
||||
Then I should not see "Delete"
|
||||
And I click on "Content bank" "link"
|
||||
And I am on the "Content bank" page
|
||||
And I click on "Upload" "link"
|
||||
And I click on "Choose a file..." "button"
|
||||
And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
|
||||
And I click on "find-the-words.h5p" "link"
|
||||
And I click on "Select this file" "button"
|
||||
And I click on "Save changes" "button"
|
||||
And I click on "More" "button"
|
||||
And I wait "2" seconds
|
||||
And I click on "More" "button"
|
||||
And I should see "Delete"
|
||||
|
||||
Scenario: The number of times a content is used is displayed before removing it
|
||||
Given I follow "Dashboard"
|
||||
And I follow "Manage private files..."
|
||||
Given I am on the "My private files" page
|
||||
And I click on "Add..." "button"
|
||||
And I select "Content bank" repository in file picker
|
||||
And I click on "content2delete.h5p" "file" in repository content area
|
||||
And I click on "Link to the file" "radio"
|
||||
And I click on "Select this file" "button"
|
||||
And I click on "Save changes" "button"
|
||||
When I click on "Site pages" "list_item" in the "Navigation" "block"
|
||||
And I click on "Content bank" "link" in the "Navigation" "block"
|
||||
And I am on the "Content bank" page
|
||||
And I follow "content2delete.h5p"
|
||||
And I click on "More" "button"
|
||||
And I wait "2" seconds
|
||||
And I click on "More" "button"
|
||||
And I click on "Delete" "link" in the ".cb-toolbar-container" "css_element"
|
||||
Then I should see "Are you sure you want to delete the content 'content2delete.h5p'"
|
||||
And I should see "The content will only be deleted from the content bank"
|
||||
|
||||
@@ -5,10 +5,7 @@ Feature: Content bank use editor feature
|
||||
I need to be able to access the edition options
|
||||
|
||||
Background:
|
||||
Given the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| private_files | System | 1 | my-index | side-post |
|
||||
And I log in as "admin"
|
||||
Given I log in as "admin"
|
||||
And I am on site homepage
|
||||
And I turn editing mode on
|
||||
And the following config values are set as admin:
|
||||
@@ -25,10 +22,9 @@ Feature: Content bank use editor feature
|
||||
Then the "[data-action=Add-content]" "css_element" should be disabled
|
||||
|
||||
Scenario: Users can see the Add button if there is content type available for creation
|
||||
Given I follow "Dashboard"
|
||||
And I follow "Manage private files..."
|
||||
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager
|
||||
And I click on "Save changes" "button"
|
||||
Given the following "user private file" exists:
|
||||
| user | admin |
|
||||
| filepath | h5p/tests/fixtures/filltheblanks.h5p |
|
||||
And I click on "Site pages" "list_item" in the "Navigation" "block"
|
||||
And I click on "Content bank" "link" in the "Navigation" "block"
|
||||
And I click on "Upload" "link"
|
||||
@@ -44,10 +40,9 @@ Feature: Content bank use editor feature
|
||||
And I should see "Fill in the Blanks"
|
||||
|
||||
Scenario: Users can edit content if they have the required permission
|
||||
Given I follow "Dashboard"
|
||||
And I follow "Manage private files..."
|
||||
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager
|
||||
And I click on "Save changes" "button"
|
||||
Given the following "user private file" exists:
|
||||
| user | admin |
|
||||
| filepath | h5p/tests/fixtures/filltheblanks.h5p |
|
||||
And I click on "Site pages" "list_item" in the "Navigation" "block"
|
||||
And I click on "Content bank" "link" in the "Navigation" "block"
|
||||
And I click on "Upload" "link"
|
||||
|
||||
@@ -1,30 +1,23 @@
|
||||
@core @core_contentbank @core_h5p @contentbank_h5p @_file_upload @_switch_iframe @javascript
|
||||
@core @core_contentbank @core_h5p @contentbank_h5p @_switch_iframe @javascript
|
||||
Feature: Confirm content bank events are triggered
|
||||
In order to log content bank actions
|
||||
As an admin
|
||||
I need to be able to check triggered events
|
||||
|
||||
Background:
|
||||
Given the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| private_files | System | 1 | my-index | side-post |
|
||||
And I log in as "admin"
|
||||
And I navigate to "H5P > Manage H5P content types" in site administration
|
||||
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
|
||||
And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
|
||||
And the following "courses" exist:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "contentbank content" exist:
|
||||
| contextlevel | reference | contenttype | user | contentname | filepath |
|
||||
| Course | C1 | contenttype_h5p | admin | Existing | /h5p/tests/fixtures/filltheblanks.h5p |
|
||||
And I follow "Dashboard"
|
||||
And I follow "Manage private files..."
|
||||
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager
|
||||
And I click on "Save changes" "button"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And the following "user private file" exists:
|
||||
| user | admin |
|
||||
| filepath | h5p/tests/fixtures/filltheblanks.h5p |
|
||||
And the following config values are set as admin:
|
||||
| unaddableblocks | | theme_boost|
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add the "Navigation" block if not present
|
||||
|
||||
Scenario: Content created and uploaded events when uploading a content file
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
@core @core_contentbank @core_h5p @contentbank_h5p @_file_upload @javascript
|
||||
@core @core_contentbank @core_h5p @contentbank_h5p @javascript
|
||||
Feature: Store the content bank view preference
|
||||
In order to consistantly view the content bank in icons or details view
|
||||
As an admin
|
||||
I need to be able to store my view preference
|
||||
|
||||
Background:
|
||||
Given the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| private_files | System | 1 | my-index | side-post |
|
||||
Given the following "user private files" exist:
|
||||
| user | filepath |
|
||||
| admin | h5p/tests/fixtures/filltheblanks.h5p |
|
||||
| admin | h5p/tests/fixtures/greeting-card.h5p |
|
||||
And I log in as "admin"
|
||||
And I follow "Manage private files..."
|
||||
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager
|
||||
And I upload "h5p/tests/fixtures/greeting-card.h5p" file to "Files" filemanager
|
||||
And I click on "Save changes" "button"
|
||||
And I am on site homepage
|
||||
And I turn editing mode on
|
||||
And the following config values are set as admin:
|
||||
@@ -55,8 +52,7 @@ Feature: Store the content bank view preference
|
||||
And I should not see "Last modified"
|
||||
|
||||
Scenario: Display the number of times a content is used in file details view
|
||||
Given I follow "Dashboard"
|
||||
And I follow "Manage private files..."
|
||||
Given I am on the "My private files" page
|
||||
And I click on "Add..." "button"
|
||||
And I select "Content bank" repository in file picker
|
||||
And I click on "fib.h5p" "file" in repository content area
|
||||
|
||||
@@ -46,7 +46,7 @@ $filename = str_replace('/', '', str_replace(' ', '_', $courseinfo->shortname))
|
||||
|
||||
// If download confirmed, prepare and start the zipstream of the course download content.
|
||||
if ($isdownload) {
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
$exportoptions = null;
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
@@ -8,6 +8,6 @@ define("core_courseformat/local/courseindex/cm",["exports","core_courseformat/lo
|
||||
* @class core_courseformat/local/courseindex/cm
|
||||
* @copyright 2021 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_dndcmitem=_interopRequireDefault(_dndcmitem),_templates=_interopRequireDefault(_templates),_prefetch=_interopRequireDefault(_prefetch),_config=_interopRequireDefault(_config);_prefetch.default.prefetchTemplate("core_courseformat/local/courseindex/cmcompletion");class Component extends _dndcmitem.default{create(){this.name="courseindex_cm",this.selectors={CM_NAME:"[data-for='cm_name']",CM_COMPLETION:"[data-for='cm_completion']"},this.classes={CMHIDDEN:"dimmed",LOCKED:"editinprogress",RESTRICTIONS:"restrictions",PAGEITEM:"pageitem",INDENTED:"indented"},this.id=this.element.dataset.id}static init(target,selectors){return new Component({element:document.getElementById(target),selectors:selectors})}stateReady(state){this.configDragDrop(this.id);const cm=state.cm.get(this.id),course=state.course;this._refreshCompletion({state:state,element:cm});const anchor=new URL(window.location.href).hash.replace("#","");(window.location.href==cm.url||window.location.href.includes(course.baseurl)&&anchor==cm.anchor)&&(this.reactive.dispatch("setPageItem","cm",this.id),this.element.scrollIntoView({block:"center"})),_config.default.contextid!=_config.default.courseContextId&&_config.default.contextInstanceId==this.id&&(this.reactive.dispatch("setPageItem","cm",this.id,!0),this.element.scrollIntoView({block:"center"})),cm.uservisible||this.addEventListener(this.getElement(this.selectors.CM_NAME),"click",this._activityAnchor)}getWatchers(){return[{watch:"cm[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"cm[".concat(this.id,"]:updated"),handler:this._refreshCm},{watch:"cm[".concat(this.id,"].completionstate:updated"),handler:this._refreshCompletion},{watch:"course.pageItem:updated",handler:this._refreshPageItem}]}_refreshCm(_ref){var _element$dragging,_element$locked,_element$hascmrestric;let{element:element}=_ref;this.element.classList.toggle(this.classes.CMHIDDEN,!element.visible),this.getElement(this.selectors.CM_NAME).innerHTML=element.name,this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.element.classList.toggle(this.classes.RESTRICTIONS,null!==(_element$hascmrestric=element.hascmrestrictions)&&void 0!==_element$hascmrestric&&_element$hascmrestric),this.element.classList.toggle(this.classes.INDENTED,element.indent),this.locked=element.locked}_refreshPageItem(_ref2){let{element:element}=_ref2;if(!element.pageItem)return;const isPageId="cm"==element.pageItem.type&&element.pageItem.id==this.id;this.element.classList.toggle(this.classes.PAGEITEM,isPageId),isPageId&&!this.reactive.isEditing&&this.element.scrollIntoView({block:"nearest"})}async _refreshCompletion(_ref3){let{state:state,element:element}=_ref3;if(this.reactive.isEditing||!element.istrackeduser)return;const completionElement=this.getElement(this.selectors.CM_COMPLETION);if(completionElement.dataset.value==element.completionstate)return;const data=this.reactive.getExporter().cmCompletion(state,element);try{const{html:html,js:js}=await _templates.default.renderForPromise("core_courseformat/local/courseindex/cmcompletion",data);_templates.default.replaceNode(completionElement,html,js)}catch(error){throw error}}_activityAnchor(event){const cm=this.reactive.get("cm",this.id);if(document.getElementById(cm.anchor))return void setTimeout((()=>{this.reactive.dispatch("setPageItem","cm",cm.id)}),50);const course=this.reactive.get("course"),section=this.reactive.get("section",cm.sectionid);if(!section)return;const url="".concat(course.baseurl,"§ion=").concat(section.number,"#").concat(cm.anchor);event.preventDefault(),window.location=url}}return _exports.default=Component,_exports.default}));
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_dndcmitem=_interopRequireDefault(_dndcmitem),_templates=_interopRequireDefault(_templates),_prefetch=_interopRequireDefault(_prefetch),_config=_interopRequireDefault(_config);_prefetch.default.prefetchTemplate("core_courseformat/local/courseindex/cmcompletion");class Component extends _dndcmitem.default{create(){this.name="courseindex_cm",this.selectors={CM_NAME:"[data-for='cm_name']",CM_COMPLETION:"[data-for='cm_completion']"},this.classes={CMHIDDEN:"dimmed",LOCKED:"editinprogress",RESTRICTIONS:"restrictions",PAGEITEM:"pageitem",INDENTED:"indented"},this.id=this.element.dataset.id}static init(target,selectors){return new Component({element:document.getElementById(target),selectors:selectors})}stateReady(state){this.configDragDrop(this.id);const cm=state.cm.get(this.id),course=state.course;this._refreshCompletion({state:state,element:cm});const anchor=new URL(window.location.href).hash.replace("#","");(window.location.href==cm.url||window.location.href.includes(course.baseurl)&&anchor==cm.anchor)&&(this.reactive.dispatch("setPageItem","cm",this.id),this.element.scrollIntoView({block:"center"})),_config.default.contextid!=_config.default.courseContextId&&_config.default.contextInstanceId==this.id&&(this.reactive.dispatch("setPageItem","cm",this.id,!0),this.element.scrollIntoView({block:"center"})),cm.uservisible||this.addEventListener(this.getElement(this.selectors.CM_NAME),"click",this._activityAnchor)}getWatchers(){return[{watch:"cm[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"cm[".concat(this.id,"]:updated"),handler:this._refreshCm},{watch:"cm[".concat(this.id,"].completionstate:updated"),handler:this._refreshCompletion},{watch:"course.pageItem:updated",handler:this._refreshPageItem}]}_refreshCm(_ref){var _element$dragging,_element$locked,_element$hascmrestric;let{element:element}=_ref;this.element.classList.toggle(this.classes.CMHIDDEN,!element.visible),this.getElement(this.selectors.CM_NAME).innerHTML=element.name,this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.element.classList.toggle(this.classes.RESTRICTIONS,null!==(_element$hascmrestric=element.hascmrestrictions)&&void 0!==_element$hascmrestric&&_element$hascmrestric),this.element.classList.toggle(this.classes.INDENTED,element.indent),this.locked=element.locked}_refreshPageItem(_ref2){let{element:element}=_ref2;if(!element.pageItem)return;const isPageId="cm"==element.pageItem.type&&element.pageItem.id==this.id;this.element.classList.toggle(this.classes.PAGEITEM,isPageId),isPageId&&!this.reactive.isEditing&&this.element.scrollIntoView({block:"nearest"})}async _refreshCompletion(_ref3){let{state:state,element:element}=_ref3;if(this.reactive.isEditing||!element.istrackeduser)return;const completionElement=this.getElement(this.selectors.CM_COMPLETION);if(completionElement.dataset.value==element.completionstate)return;const data=this.reactive.getExporter().cmCompletion(state,element),{html:html,js:js}=await _templates.default.renderForPromise("core_courseformat/local/courseindex/cmcompletion",data);_templates.default.replaceNode(completionElement,html,js)}_activityAnchor(event){const cm=this.reactive.get("cm",this.id);if(document.getElementById(cm.anchor))return void setTimeout((()=>{this.reactive.dispatch("setPageItem","cm",cm.id)}),50);const course=this.reactive.get("course"),section=this.reactive.get("section",cm.sectionid);if(!section)return;const url="".concat(course.baseurl,"§ion=").concat(section.number,"#").concat(cm.anchor);event.preventDefault(),window.location=url}}return _exports.default=Component,_exports.default}));
|
||||
|
||||
//# sourceMappingURL=cm.min.js.map
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -173,15 +173,12 @@ export default class Component extends BaseComponent {
|
||||
const toggler = section.querySelector(this.selectors.COLLAPSE);
|
||||
const isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;
|
||||
|
||||
if (isChevron || isCollapsed) {
|
||||
// Update the state.
|
||||
const sectionId = section.getAttribute('data-id');
|
||||
this.reactive.dispatch(
|
||||
'sectionContentCollapsed',
|
||||
[sectionId],
|
||||
!isCollapsed
|
||||
);
|
||||
}
|
||||
const sectionId = section.getAttribute('data-id');
|
||||
this.reactive.dispatch(
|
||||
'sectionContentCollapsed',
|
||||
[sectionId],
|
||||
!isCollapsed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,549 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* The course file uploader.
|
||||
*
|
||||
* This module is used to upload files directly into the course.
|
||||
*
|
||||
* @module core_courseformat/local/courseeditor/fileuploader
|
||||
* @copyright 2022 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Handler
|
||||
* @property {String} extension the handled extension or * for any
|
||||
* @property {String} message the handler message
|
||||
* @property {String} module the module name
|
||||
*/
|
||||
|
||||
import Config from 'core/config';
|
||||
import ModalFactory from 'core/modal_factory';
|
||||
import ModalEvents from 'core/modal_events';
|
||||
import Templates from 'core/templates';
|
||||
import {getFirst} from 'core/normalise';
|
||||
import {prefetchStrings} from 'core/prefetch';
|
||||
import {get_string as getString, get_strings as getStrings} from 'core/str';
|
||||
import {getCourseEditor} from 'core_courseformat/courseeditor';
|
||||
import {processMonitor} from 'core/process_monitor';
|
||||
import {debounce} from 'core/utils';
|
||||
|
||||
// Uploading url.
|
||||
const UPLOADURL = Config.wwwroot + '/course/dndupload.php';
|
||||
const DEBOUNCETIMER = 500;
|
||||
const USERCANIGNOREFILESIZELIMITS = -1;
|
||||
|
||||
/** @var {ProcessQueue} uploadQueue the internal uploadQueue instance. */
|
||||
let uploadQueue = null;
|
||||
/** @var {Object} handlerManagers the courseId indexed loaded handler managers. */
|
||||
let handlerManagers = {};
|
||||
/** @var {Map} courseUpdates the pending course sections updates. */
|
||||
let courseUpdates = new Map();
|
||||
/** @var {Object} errors the error messages. */
|
||||
let errors = null;
|
||||
|
||||
// Load global strings.
|
||||
prefetchStrings('moodle', ['addresourceoractivity', 'upload']);
|
||||
prefetchStrings('core_error', ['dndmaxbytes', 'dndread', 'dndupload', 'dndunkownfile']);
|
||||
|
||||
/**
|
||||
* Class to upload a file into the course.
|
||||
* @private
|
||||
*/
|
||||
class FileUploader {
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param {number} courseId the course id
|
||||
* @param {number} sectionId the section id
|
||||
* @param {number} sectionNum the section number
|
||||
* @param {File} fileInfo the file information object
|
||||
* @param {Handler} handler the file selected file handler
|
||||
*/
|
||||
constructor(courseId, sectionId, sectionNum, fileInfo, handler) {
|
||||
this.courseId = courseId;
|
||||
this.sectionId = sectionId;
|
||||
this.sectionNum = sectionNum;
|
||||
this.fileInfo = fileInfo;
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the file upload and update the state in the given process.
|
||||
*
|
||||
* @param {LoadingProcess} process the process to store the upload result
|
||||
*/
|
||||
execute(process) {
|
||||
const fileInfo = this.fileInfo;
|
||||
const xhr = this._createXhrRequest(process);
|
||||
const formData = this._createUploadFormData();
|
||||
|
||||
// Try reading the file to check it is not a folder, before sending it to the server.
|
||||
const reader = new FileReader();
|
||||
reader.onload = function() {
|
||||
// File was read OK - send it to the server.
|
||||
xhr.open("POST", UPLOADURL, true);
|
||||
xhr.send(formData);
|
||||
};
|
||||
reader.onerror = function() {
|
||||
// Unable to read the file (it is probably a folder) - display an error message.
|
||||
process.setError(errors.dndread);
|
||||
};
|
||||
if (fileInfo.size > 0) {
|
||||
// If this is a non-empty file, try reading the first few bytes.
|
||||
// This will trigger reader.onerror() for folders and reader.onload() for ordinary, readable files.
|
||||
reader.readAsText(fileInfo.slice(0, 5));
|
||||
} else {
|
||||
// If you call slice() on a 0-byte folder, before calling readAsText, then Firefox triggers reader.onload(),
|
||||
// instead of reader.onerror().
|
||||
// So, for 0-byte files, just call readAsText on the whole file (and it will trigger load/error functions as expected).
|
||||
reader.readAsText(fileInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bind version of execute function.
|
||||
*
|
||||
* This method is used to queue the process into a ProcessQueue instance.
|
||||
*
|
||||
* @returns {Function} the bind function to execute the process
|
||||
*/
|
||||
getExecutionFunction() {
|
||||
return this.execute.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a upload XHR file request.
|
||||
*
|
||||
* @param {LoadingProcess} process the current process
|
||||
* @return {XMLHttpRequest} the XHR request
|
||||
*/
|
||||
_createXhrRequest(process) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
// Update the progress bar as the file is uploaded.
|
||||
xhr.upload.addEventListener(
|
||||
'progress',
|
||||
(event) => {
|
||||
if (event.lengthComputable) {
|
||||
const percent = Math.round((event.loaded * 100) / event.total);
|
||||
process.setPercentage(percent);
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
// Wait for the AJAX call to complete.
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState == 1) {
|
||||
// Add a 1% just to indicate that it is uploading.
|
||||
process.setPercentage(1);
|
||||
}
|
||||
// State 4 is DONE. Otherwise the connection is still ongoing.
|
||||
if (xhr.readyState != 4) {
|
||||
return;
|
||||
}
|
||||
if (xhr.status == 200) {
|
||||
var result = JSON.parse(xhr.responseText);
|
||||
if (result && result.error == 0) {
|
||||
// All OK.
|
||||
this._finishProcess(process);
|
||||
} else {
|
||||
process.setError(result.error);
|
||||
}
|
||||
} else {
|
||||
process.setError(errors.dndupload);
|
||||
}
|
||||
};
|
||||
return xhr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file into the course.
|
||||
*
|
||||
* @return {FormData|null} the new form data object
|
||||
*/
|
||||
_createUploadFormData() {
|
||||
const formData = new FormData();
|
||||
try {
|
||||
formData.append('repo_upload_file', this.fileInfo);
|
||||
} catch (error) {
|
||||
throw Error(error.dndread);
|
||||
}
|
||||
formData.append('sesskey', Config.sesskey);
|
||||
formData.append('course', this.courseId);
|
||||
formData.append('section', this.sectionNum);
|
||||
formData.append('module', this.handler.module);
|
||||
formData.append('type', 'Files');
|
||||
return formData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finishes the current process.
|
||||
* @param {LoadingProcess} process the process
|
||||
*/
|
||||
_finishProcess(process) {
|
||||
addRefreshSection(this.courseId, this.sectionId);
|
||||
process.setPercentage(100);
|
||||
process.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The file handler manager class.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
class HandlerManager {
|
||||
|
||||
/** @var {Object} lastHandlers the last handlers selected per each file extension. */
|
||||
lastHandlers = {};
|
||||
|
||||
/** @var {Handler[]|null} allHandlers all the available handlers. */
|
||||
allHandlers = null;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param {Number} courseId
|
||||
*/
|
||||
constructor(courseId) {
|
||||
this.courseId = courseId;
|
||||
this.lastUploadId = 0;
|
||||
this.courseEditor = getCourseEditor(courseId);
|
||||
if (!this.courseEditor) {
|
||||
throw Error('Unkown course editor');
|
||||
}
|
||||
this.maxbytes = this.courseEditor.get('course')?.maxbytes ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the course file handlers.
|
||||
*/
|
||||
async loadHandlers() {
|
||||
this.allHandlers = await this.courseEditor.getFileHandlersPromise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the file extension from a fileInfo.
|
||||
*
|
||||
* @param {File} fileInfo
|
||||
* @returns {String} the file extension or an empty string.
|
||||
*/
|
||||
getFileExtension(fileInfo) {
|
||||
let extension = '';
|
||||
const dotpos = fileInfo.name.lastIndexOf('.');
|
||||
if (dotpos != -1) {
|
||||
extension = fileInfo.name.substring(dotpos + 1, fileInfo.name.length).toLowerCase();
|
||||
}
|
||||
return extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the file is valid.
|
||||
*
|
||||
* @param {File} fileInfo the file info
|
||||
*/
|
||||
validateFile(fileInfo) {
|
||||
if (this.maxbytes !== USERCANIGNOREFILESIZELIMITS && fileInfo.size > this.maxbytes) {
|
||||
throw Error(errors.dndmaxbytes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file handlers of an specific file.
|
||||
*
|
||||
* @param {File} fileInfo the file indo
|
||||
* @return {Array} Array of handlers
|
||||
*/
|
||||
filterHandlers(fileInfo) {
|
||||
const extension = this.getFileExtension(fileInfo);
|
||||
return this.allHandlers.filter(handler => handler.extension == '*' || handler.extension == extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Handler to upload a specific file.
|
||||
*
|
||||
* It will ask the used if more than one handler is available.
|
||||
*
|
||||
* @param {File} fileInfo the file info
|
||||
* @returns {Promise<Handler|null>} the selected handler or null if the user cancel
|
||||
*/
|
||||
async getFileHandler(fileInfo) {
|
||||
const fileHandlers = this.filterHandlers(fileInfo);
|
||||
if (fileHandlers.length == 0) {
|
||||
throw Error(errors.dndunkownfile);
|
||||
}
|
||||
let fileHandler = null;
|
||||
if (fileHandlers.length == 1) {
|
||||
fileHandler = fileHandlers[0];
|
||||
} else {
|
||||
fileHandler = await this.askHandlerToUser(fileHandlers, fileInfo);
|
||||
}
|
||||
return fileHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user to select a specific handler.
|
||||
*
|
||||
* @param {Handler[]} fileHandlers
|
||||
* @param {File} fileInfo the file info
|
||||
* @return {Promise<Handler>} the selected handler
|
||||
*/
|
||||
async askHandlerToUser(fileHandlers, fileInfo) {
|
||||
const extension = this.getFileExtension(fileInfo);
|
||||
// Build the modal parameters from the event data.
|
||||
const modalParams = {
|
||||
title: getString('addresourceoractivity', 'moodle'),
|
||||
body: Templates.render(
|
||||
'core_courseformat/fileuploader',
|
||||
this.getModalData(
|
||||
fileHandlers,
|
||||
fileInfo,
|
||||
this.lastHandlers[extension] ?? null
|
||||
)
|
||||
),
|
||||
type: ModalFactory.types.SAVE_CANCEL,
|
||||
saveButtonText: getString('upload', 'moodle'),
|
||||
};
|
||||
// Create the modal.
|
||||
const modal = await this.modalBodyRenderedPromise(modalParams);
|
||||
const selectedHandler = await this.modalUserAnswerPromise(modal, fileHandlers);
|
||||
// Cancel action.
|
||||
if (selectedHandler === null) {
|
||||
return null;
|
||||
}
|
||||
// Save last selected handler.
|
||||
this.lastHandlers[extension] = selectedHandler.module;
|
||||
return selectedHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generated the modal template data.
|
||||
*
|
||||
* @param {Handler[]} fileHandlers
|
||||
* @param {File} fileInfo the file info
|
||||
* @param {String|null} defaultModule the default module if any
|
||||
* @return {Object} the modal template data.
|
||||
*/
|
||||
getModalData(fileHandlers, fileInfo, defaultModule) {
|
||||
const data = {
|
||||
filename: fileInfo.name,
|
||||
uploadid: ++this.lastUploadId,
|
||||
handlers: [],
|
||||
};
|
||||
let hasDefault = false;
|
||||
fileHandlers.forEach((handler, index) => {
|
||||
const isDefault = (defaultModule == handler.module);
|
||||
data.handlers.push({
|
||||
...handler,
|
||||
selected: isDefault,
|
||||
labelid: `fileuploader_${data.uploadid}`,
|
||||
value: index,
|
||||
});
|
||||
hasDefault = hasDefault || isDefault;
|
||||
});
|
||||
if (!hasDefault && data.handlers.length > 0) {
|
||||
const lastHandler = data.handlers.pop();
|
||||
lastHandler.selected = true;
|
||||
data.handlers.push(lastHandler);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user handler choice.
|
||||
*
|
||||
* Wait for the user answer in the modal and resolve with the selected index.
|
||||
*
|
||||
* @param {Modal} modal the modal instance
|
||||
* @param {Handler[]} fileHandlers the availabvle file handlers
|
||||
* @return {Promise} with the option selected by the user.
|
||||
*/
|
||||
modalUserAnswerPromise(modal, fileHandlers) {
|
||||
const modalBody = getFirst(modal.getBody());
|
||||
return new Promise((resolve, reject) => {
|
||||
modal.getRoot().on(
|
||||
ModalEvents.save,
|
||||
event => {
|
||||
// Get the selected option.
|
||||
const index = modalBody.querySelector('input:checked').value;
|
||||
event.preventDefault();
|
||||
modal.destroy();
|
||||
if (!fileHandlers[index]) {
|
||||
reject('Invalid handler selected');
|
||||
}
|
||||
resolve(fileHandlers[index]);
|
||||
|
||||
}
|
||||
);
|
||||
modal.getRoot().on(
|
||||
ModalEvents.cancel,
|
||||
() => {
|
||||
resolve(null);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new modal and return a Promise to the body rendered.
|
||||
*
|
||||
* @param {Object} modalParams the modal params
|
||||
* @returns {Promise} the modal body rendered promise
|
||||
*/
|
||||
modalBodyRenderedPromise(modalParams) {
|
||||
return new Promise((resolve, reject) => {
|
||||
ModalFactory.create(modalParams).then((modal) => {
|
||||
modal.setRemoveOnClose(true);
|
||||
// Handle body loading event.
|
||||
modal.getRoot().on(ModalEvents.bodyRendered, () => {
|
||||
resolve(modal);
|
||||
});
|
||||
// Configure some extra modal params.
|
||||
if (modalParams.saveButtonText !== undefined) {
|
||||
modal.setSaveButtonText(modalParams.saveButtonText);
|
||||
}
|
||||
modal.show();
|
||||
return;
|
||||
}).catch(() => {
|
||||
reject(`Cannot load modal content`);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a section to refresh.
|
||||
*
|
||||
* @param {number} courseId the course id
|
||||
* @param {number} sectionId the seciton id
|
||||
*/
|
||||
function addRefreshSection(courseId, sectionId) {
|
||||
let refresh = courseUpdates.get(courseId);
|
||||
if (!refresh) {
|
||||
refresh = new Set();
|
||||
}
|
||||
refresh.add(sectionId);
|
||||
courseUpdates.set(courseId, refresh);
|
||||
refreshCourseEditors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounced processing all pending course refreshes.
|
||||
* @private
|
||||
*/
|
||||
const refreshCourseEditors = debounce(
|
||||
() => {
|
||||
const refreshes = courseUpdates;
|
||||
courseUpdates = new Map();
|
||||
refreshes.forEach((sectionIds, courseId) => {
|
||||
const courseEditor = getCourseEditor(courseId);
|
||||
if (!courseEditor) {
|
||||
return;
|
||||
}
|
||||
courseEditor.dispatch('sectionState', [...sectionIds]);
|
||||
});
|
||||
},
|
||||
DEBOUNCETIMER
|
||||
);
|
||||
|
||||
/**
|
||||
* Load and return the course handler manager instance.
|
||||
*
|
||||
* @param {Number} courseId the course Id to load
|
||||
* @returns {Promise<HandlerManager>} promise of the the loaded handleManager
|
||||
*/
|
||||
async function loadCourseHandlerManager(courseId) {
|
||||
if (handlerManagers[courseId] !== undefined) {
|
||||
return handlerManagers[courseId];
|
||||
}
|
||||
const handlerManager = new HandlerManager(courseId);
|
||||
await handlerManager.loadHandlers();
|
||||
handlerManagers[courseId] = handlerManager;
|
||||
return handlerManagers[courseId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all the erros messages at once in the module "errors" variable.
|
||||
* @param {Number} courseId the course id
|
||||
*/
|
||||
async function loadErrorStrings(courseId) {
|
||||
if (errors !== null) {
|
||||
return;
|
||||
}
|
||||
const courseEditor = getCourseEditor(courseId);
|
||||
const maxbytestext = courseEditor.get('course')?.maxbytestext ?? '0';
|
||||
|
||||
errors = {};
|
||||
const allStrings = [
|
||||
{key: 'dndmaxbytes', component: 'core_error', param: {size: maxbytestext}},
|
||||
{key: 'dndread', component: 'core_error'},
|
||||
{key: 'dndupload', component: 'core_error'},
|
||||
{key: 'dndunkownfile', component: 'core_error'},
|
||||
];
|
||||
window.console.log(allStrings);
|
||||
const loadedStrings = await getStrings(allStrings);
|
||||
allStrings.forEach(({key}, index) => {
|
||||
errors[key] = loadedStrings[index];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a batch file uploading into the course.
|
||||
*
|
||||
* @private
|
||||
* @param {number} courseId the course id.
|
||||
* @param {number} sectionId the section id.
|
||||
* @param {number} sectionNum the section number.
|
||||
* @param {File} fileInfo the file information object
|
||||
* @param {HandlerManager} handlerManager the course handler manager
|
||||
*/
|
||||
const queueFileUpload = async function(courseId, sectionId, sectionNum, fileInfo, handlerManager) {
|
||||
let handler;
|
||||
uploadQueue = await processMonitor.createProcessQueue();
|
||||
try {
|
||||
handlerManager.validateFile(fileInfo);
|
||||
handler = await handlerManager.getFileHandler(fileInfo);
|
||||
} catch (error) {
|
||||
uploadQueue.addError(fileInfo.name, error.message);
|
||||
return;
|
||||
}
|
||||
// If we don't have a handler means the user cancel the upload.
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
const fileProcessor = new FileUploader(courseId, sectionId, sectionNum, fileInfo, handler);
|
||||
uploadQueue.addPending(fileInfo.name, fileProcessor.getExecutionFunction());
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload a file to the course.
|
||||
*
|
||||
* This method will show any necesary modal to handle the request.
|
||||
*
|
||||
* @param {number} courseId the course id
|
||||
* @param {number} sectionId the section id
|
||||
* @param {number} sectionNum the section number
|
||||
* @param {Array} files and array of files
|
||||
*/
|
||||
export const uploadFilesToCourse = async function(courseId, sectionId, sectionNum, files) {
|
||||
// Get the course handlers.
|
||||
const handlerManager = await loadCourseHandlerManager(courseId);
|
||||
await loadErrorStrings(courseId);
|
||||
for (let index = 0; index < files.length; index++) {
|
||||
const fileInfo = files[index];
|
||||
await queueFileUpload(courseId, sectionId, sectionNum, fileInfo, handlerManager);
|
||||
}
|
||||
};
|
||||
@@ -473,12 +473,16 @@ export default class {
|
||||
* @param {boolean} collapsed the new collapsed value
|
||||
*/
|
||||
async sectionIndexCollapsed(stateManager, sectionIds, collapsed) {
|
||||
const collapsedIds = this._updateStateSectionPreference(stateManager, 'indexcollapsed', sectionIds, collapsed);
|
||||
if (!collapsedIds) {
|
||||
const affectedSections = this._updateStateSectionPreference(stateManager, 'indexcollapsed', sectionIds, collapsed);
|
||||
if (!affectedSections) {
|
||||
return;
|
||||
}
|
||||
const course = stateManager.get('course');
|
||||
await this._callEditWebservice('section_index_collapsed', course.id, collapsedIds);
|
||||
let actionName = 'section_index_collapsed';
|
||||
if (!collapsed) {
|
||||
actionName = 'section_index_expanded';
|
||||
}
|
||||
await this._callEditWebservice(actionName, course.id, affectedSections);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -489,12 +493,16 @@ export default class {
|
||||
* @param {boolean} collapsed the new collapsed value
|
||||
*/
|
||||
async sectionContentCollapsed(stateManager, sectionIds, collapsed) {
|
||||
const collapsedIds = this._updateStateSectionPreference(stateManager, 'contentcollapsed', sectionIds, collapsed);
|
||||
if (!collapsedIds) {
|
||||
const affectedSections = this._updateStateSectionPreference(stateManager, 'contentcollapsed', sectionIds, collapsed);
|
||||
if (!affectedSections) {
|
||||
return;
|
||||
}
|
||||
const course = stateManager.get('course');
|
||||
await this._callEditWebservice('section_content_collapsed', course.id, collapsedIds);
|
||||
let actionName = 'section_content_collapsed';
|
||||
if (!collapsed) {
|
||||
actionName = 'section_content_expanded';
|
||||
}
|
||||
await this._callEditWebservice(actionName, course.id, affectedSections);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -508,32 +516,22 @@ export default class {
|
||||
*/
|
||||
_updateStateSectionPreference(stateManager, preferenceName, sectionIds, preferenceValue) {
|
||||
stateManager.setReadOnly(false);
|
||||
const affectedSections = new Set();
|
||||
const affectedSections = [];
|
||||
// Check if we need to update preferences.
|
||||
sectionIds.forEach(sectionId => {
|
||||
const section = stateManager.get('section', sectionId);
|
||||
if (section === undefined) {
|
||||
stateManager.setReadOnly(true);
|
||||
return null;
|
||||
}
|
||||
const newValue = preferenceValue ?? section[preferenceName];
|
||||
if (section[preferenceName] != newValue) {
|
||||
section[preferenceName] = newValue;
|
||||
affectedSections.add(section.id);
|
||||
affectedSections.push(section.id);
|
||||
}
|
||||
});
|
||||
stateManager.setReadOnly(true);
|
||||
if (affectedSections.size == 0) {
|
||||
return null;
|
||||
}
|
||||
// Get all collapsed section ids.
|
||||
const collapsedSectionIds = [];
|
||||
const state = stateManager.state;
|
||||
state.section.forEach(section => {
|
||||
if (section[preferenceName]) {
|
||||
collapsedSectionIds.push(section.id);
|
||||
}
|
||||
});
|
||||
return collapsedSectionIds;
|
||||
return affectedSections;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -180,12 +180,8 @@ export default class Component extends DndCmItem {
|
||||
const exporter = this.reactive.getExporter();
|
||||
const data = exporter.cmCompletion(state, element);
|
||||
|
||||
try {
|
||||
const {html, js} = await Templates.renderForPromise(completionTemplate, data);
|
||||
Templates.replaceNode(completionElement, html, js);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const {html, js} = await Templates.renderForPromise(completionTemplate, data);
|
||||
Templates.replaceNode(completionElement, html, js);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -131,9 +131,16 @@ export default class Component extends BaseComponent {
|
||||
const toggler = section.querySelector(this.selectors.COLLAPSE);
|
||||
const isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;
|
||||
|
||||
if (isChevron || isCollapsed) {
|
||||
// Update the state.
|
||||
const sectionId = section.getAttribute('data-id');
|
||||
// Update the state.
|
||||
const sectionId = section.getAttribute('data-id');
|
||||
if ((!sectionlink)) {
|
||||
this.reactive.dispatch(
|
||||
'sectionIndexCollapsed',
|
||||
[sectionId],
|
||||
!isCollapsed
|
||||
);
|
||||
} else if (isCollapsed) {
|
||||
// Always expand the section when clicking on section name.
|
||||
this.reactive.dispatch(
|
||||
'sectionIndexCollapsed',
|
||||
[sectionId],
|
||||
|
||||
@@ -617,8 +617,9 @@ abstract class base {
|
||||
global $USER;
|
||||
$course = $this->get_course();
|
||||
try {
|
||||
$sectionpreferences = (array) json_decode(
|
||||
get_user_preferences("coursesectionspreferences_{$course->id}", '', $USER->id)
|
||||
$sectionpreferences = json_decode(
|
||||
get_user_preferences("coursesectionspreferences_{$course->id}", '', $USER->id),
|
||||
true
|
||||
);
|
||||
if (empty($sectionpreferences)) {
|
||||
$sectionpreferences = [];
|
||||
@@ -637,10 +638,65 @@ abstract class base {
|
||||
*
|
||||
*/
|
||||
public function set_sections_preference(string $preferencename, array $sectionids) {
|
||||
global $USER;
|
||||
$course = $this->get_course();
|
||||
$sectionpreferences = $this->get_sections_preferences_by_preference();
|
||||
$sectionpreferences[$preferencename] = $sectionids;
|
||||
$this->persist_to_user_preference($sectionpreferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add section preference ids.
|
||||
*
|
||||
* @param string $preferencename preference name
|
||||
* @param array $sectionids affected section ids
|
||||
*/
|
||||
public function add_section_preference_ids(
|
||||
string $preferencename,
|
||||
array $sectionids
|
||||
): void {
|
||||
$sectionpreferences = $this->get_sections_preferences_by_preference();
|
||||
if (!isset($sectionpreferences[$preferencename])) {
|
||||
$sectionpreferences[$preferencename] = [];
|
||||
}
|
||||
foreach ($sectionids as $sectionid) {
|
||||
if (!in_array($sectionid, $sectionpreferences[$preferencename])) {
|
||||
$sectionpreferences[$preferencename][] = $sectionid;
|
||||
}
|
||||
}
|
||||
$this->persist_to_user_preference($sectionpreferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove section preference ids.
|
||||
*
|
||||
* @param string $preferencename preference name
|
||||
* @param array $sectionids affected section ids
|
||||
*/
|
||||
public function remove_section_preference_ids(
|
||||
string $preferencename,
|
||||
array $sectionids
|
||||
): void {
|
||||
$sectionpreferences = $this->get_sections_preferences_by_preference();
|
||||
if (!isset($sectionpreferences[$preferencename])) {
|
||||
$sectionpreferences[$preferencename] = [];
|
||||
}
|
||||
foreach ($sectionids as $sectionid) {
|
||||
if (($key = array_search($sectionid, $sectionpreferences[$preferencename])) !== false) {
|
||||
unset($sectionpreferences[$preferencename][$key]);
|
||||
}
|
||||
}
|
||||
$this->persist_to_user_preference($sectionpreferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the section preferences to the user preferences.
|
||||
*
|
||||
* @param array $sectionpreferences the section preferences
|
||||
*/
|
||||
private function persist_to_user_preference(
|
||||
array $sectionpreferences
|
||||
): void {
|
||||
global $USER;
|
||||
$course = $this->get_course();
|
||||
set_user_preference('coursesectionspreferences_' . $course->id, json_encode($sectionpreferences), $USER->id);
|
||||
// Invalidate section preferences cache.
|
||||
$coursesectionscache = cache::make('core', 'coursesectionspreferences');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user