Compare commits
381 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a1fcb3050 | |||
| cff8e8dc6a | |||
| 4cea1c74ac | |||
| 8f9d3179c3 | |||
| 5708639d32 | |||
| 63ba5e591d | |||
| 4b09e99595 | |||
| 9c4131f03d | |||
| 915e3007a8 | |||
| 1d0f426d97 | |||
| 2a54d31352 | |||
| 251b89743d | |||
| 6885687464 | |||
| 43bb50931d | |||
| 6c0e29e944 | |||
| 7a05e29d3d | |||
| 0f6571f1cd | |||
| 97090433d6 | |||
| 127da6652a | |||
| 6ba9ca190c | |||
| 394fa437a5 | |||
| a50d789801 | |||
| a1eec364e5 | |||
| efa2c5881f | |||
| aa93afad1a | |||
| 42f39c6ced | |||
| b708b37a63 | |||
| 367f0e5b8c | |||
| 25e2df4fe0 | |||
| 138ffb4719 | |||
| 8a525dcce6 | |||
| fac456676f | |||
| 582b55df77 | |||
| 87bacf1750 | |||
| dd29c50196 | |||
| 7ad48d998d | |||
| de9b088216 | |||
| c726557be5 | |||
| 6a4307c700 | |||
| 80c7835f1b | |||
| a1c9eec784 | |||
| 1b2fb9e004 | |||
| b245e9e36e | |||
| 2c5df73e65 | |||
| 5b33d3ed96 | |||
| e92d302968 | |||
| 49f208613a | |||
| 0cabf7439f | |||
| 2bf465589e | |||
| 926a41ff15 | |||
| b083d20b02 | |||
| ef609b5559 | |||
| 3ab5b63463 | |||
| 88290b1968 | |||
| a72c3d6f6b | |||
| 92408cf43a | |||
| cf4b8bec22 | |||
| 578d35e81f | |||
| 073735a431 | |||
| 28f26a66c7 | |||
| 77f610789d | |||
| 708caba53b | |||
| ba4f0a0b3f | |||
| 50950c8001 | |||
| f56dfeb5d2 | |||
| 883ebde06f | |||
| 5c94593816 | |||
| 59103ae80e | |||
| 5a13faf0cf | |||
| 41d94f98a0 | |||
| 7f18665575 | |||
| 112625d571 | |||
| a6f26b66e2 | |||
| 13d6489f7c | |||
| e7d21fd9c0 | |||
| ac5524575b | |||
| b7ef78c4e6 | |||
| d740bb09e3 | |||
| 7e5b98ea3e | |||
| eb8ff555d5 | |||
| fe4c9df572 | |||
| 9a1b4a4278 | |||
| e40e5dc513 | |||
| 53e9136176 | |||
| f67e87ec7a | |||
| f7e5353863 | |||
| 151edb5949 | |||
| c856ee60bf | |||
| 37fc36554b | |||
| 6e3e69b0cd | |||
| 32e3c9102f | |||
| 10df0d8122 | |||
| 17b57e06e0 | |||
| 5ede213279 | |||
| e62918e4fd | |||
| 83197cee22 | |||
| c0038b965e | |||
| e179c272e3 | |||
| a8ff93575d | |||
| 7611878d8d | |||
| 899dc30959 | |||
| 13efde9b95 | |||
| 481715aff9 | |||
| a398bbadc5 | |||
| 666cf9927d | |||
| fa504af178 | |||
| 366e8bd8c1 | |||
| 1bc2cc0bca | |||
| 02379cc237 | |||
| 7a1cd8700a | |||
| c0e8e44bf6 | |||
| 6a008f13b5 | |||
| 1cce6e50c9 | |||
| f2c3a62385 | |||
| 7fe5a9c323 | |||
| f6e27c5dfa | |||
| ea15eb265b | |||
| 0a2120ab19 | |||
| 443efa8df3 | |||
| 187d53de0a | |||
| fce7f84cca | |||
| 4a35061172 | |||
| e72422e42c | |||
| 7197b66d58 | |||
| b459df584c | |||
| b838c9142c | |||
| eee7268bd7 | |||
| bf9d201af9 | |||
| fa4a79d6d7 | |||
| adf53426bf | |||
| 41d73d71b0 | |||
| f4ba69224f | |||
| 64325bc560 | |||
| c10d1a30da | |||
| 8406fd5cc2 | |||
| ff76a52fd1 | |||
| 054d15a42f | |||
| bd4c8e3a70 | |||
| d265739f7d | |||
| 4fcf69eb18 | |||
| f512681b3b | |||
| 23b110b28b | |||
| 14f699d4d9 | |||
| f6240d0346 | |||
| fdf6f2b5b4 | |||
| 3f5ca666b5 | |||
| 7dc2cc4074 | |||
| 4f1db606cd | |||
| f7c5f34883 | |||
| 8da80a17b2 | |||
| 2dc10f656b | |||
| d225ecddbf | |||
| e58ca2edf4 | |||
| 036c4e3a9c | |||
| c3597e30ae | |||
| 06f12a2027 | |||
| 4262ddd629 | |||
| 31ff0d1e3a | |||
| 793487edcf | |||
| 929cd807d6 | |||
| be192b5b27 | |||
| 419930f35d | |||
| c7104868f3 | |||
| 4aea02a526 | |||
| e14310ea26 | |||
| 6b11cd340c | |||
| e4621f3ba5 | |||
| 76298ee081 | |||
| 17da4aa635 | |||
| 7e28d5386f | |||
| 801b64899f | |||
| b4963516fe | |||
| 511371a6d4 | |||
| cfdd746569 | |||
| acc40ef803 | |||
| 9f11b1fe13 | |||
| ce2d73bf6a | |||
| fe65233fd1 | |||
| b6d6d75095 | |||
| 5dd9626ff7 | |||
| 313c6d80a0 | |||
| 1300ddb017 | |||
| b34c1981b9 | |||
| 9f769b0fa1 | |||
| 87a08ab157 | |||
| cf7e8e194d | |||
| bce2299e75 | |||
| 6fd3a9054f | |||
| bb057e53af | |||
| 7b9d71254b | |||
| d0dc438919 | |||
| a6a84d6fe8 | |||
| 1b445aa197 | |||
| 315ad4b289 | |||
| cf5e25259d | |||
| 2f2c6408c9 | |||
| 7dbecdbacc | |||
| 4ffded4880 | |||
| 0c20c4f178 | |||
| f1dc10573f | |||
| 2f826c7407 | |||
| 1a42f253de | |||
| d2295281fa | |||
| 0ff19ca287 | |||
| b200b26636 | |||
| 7f7ed47336 | |||
| ae51dc65aa | |||
| c59fdffc2f | |||
| d1cd5d9c59 | |||
| 27b9963490 | |||
| b2e39b8a7f | |||
| b2bc5b2849 | |||
| 3de04dae67 | |||
| 2233454466 | |||
| db79dde840 | |||
| 63f1f94a76 | |||
| 58d0ae35cb | |||
| 85b832ff8a | |||
| 55473ab11d | |||
| 7884d5b779 | |||
| 6ed0b36d3b | |||
| 58e84e19b2 | |||
| 1132a4e7ba | |||
| e68c43c3df | |||
| 3f86d722c0 | |||
| c3a943edd2 | |||
| 8f7b882630 | |||
| 91609a45cf | |||
| 66ff5a87d0 | |||
| fc6ad45ecd | |||
| b014a4b944 | |||
| 722cb7e4f8 | |||
| 2d7b5f4859 | |||
| 07aaf4b0a4 | |||
| d54cef26cd | |||
| 725e64fbf5 | |||
| e797f1e087 | |||
| 9242d13483 | |||
| 3d2d1e04c0 | |||
| 5ab684e657 | |||
| 337244bfb4 | |||
| 2948abf1e3 | |||
| c2b6290ed1 | |||
| 15a77e7519 | |||
| 5ef6dccc02 | |||
| 94e7bfbd81 | |||
| 534e33bf0f | |||
| 127fb43d1f | |||
| 82644cb997 | |||
| c516fe9aac | |||
| 7e1ddefbd8 | |||
| 46ae3ce27a | |||
| 147605bd6b | |||
| 82fa5ad309 | |||
| b543b65a20 | |||
| 5118d7a0d4 | |||
| 5b28ac18a6 | |||
| 680ebc8d36 | |||
| 109b3ddee7 | |||
| 8e7b8281c0 | |||
| 3d3fff3787 | |||
| cf46667e43 | |||
| cd4e7f3fe6 | |||
| e881931be0 | |||
| 0881357dc4 | |||
| da41805b5e | |||
| c6f6e1a496 | |||
| bf93a4c590 | |||
| b60d42e92a | |||
| d53011ed8c | |||
| 7c0e7bd762 | |||
| 93102af0dd | |||
| d1e414d3bb | |||
| 563081cc8f | |||
| d3cbf29cc6 | |||
| 51f170fc64 | |||
| 0e3205a159 | |||
| c8d990de64 | |||
| 05a7705fb2 | |||
| af1e548adc | |||
| ce125ae0e6 | |||
| 8f995f9adc | |||
| 7a6e47070d | |||
| fed4c20d68 | |||
| 6de5117a81 | |||
| 2d0c5c46b4 | |||
| 8c81fa2cd3 | |||
| 5fff172a6e | |||
| 294db8a3cb | |||
| be46d2ee2a | |||
| 18acd24914 | |||
| 3b08277ed4 | |||
| b9c2b4317f | |||
| 714906f0b6 | |||
| 9070c5dfcc | |||
| 567e171318 | |||
| fec7eb792a | |||
| e0c3a64ecd | |||
| b488c2459b | |||
| dc5f698fa4 | |||
| 0fc876444a | |||
| 793e2d7e05 | |||
| b47dcbb0cd | |||
| 2d51b3dd97 | |||
| 8fbbb3f4e2 | |||
| e41340c435 | |||
| d389c06894 | |||
| 2a439d0fae | |||
| cb8c663829 | |||
| e52dae90cc | |||
| 387a3fdae0 | |||
| 22eb4c9fe1 | |||
| b13eae06db | |||
| ce9d8dcd88 | |||
| a1326ff471 | |||
| 6df2a49db1 | |||
| bb2ad26f15 | |||
| f5db45abdf | |||
| c22b8986ba | |||
| e8fa6d43a2 | |||
| d6fc90e1d8 | |||
| f97fe5840e | |||
| 70b7730827 | |||
| 751f1f36b2 | |||
| 75a5a43750 | |||
| fbbb39ca62 | |||
| 8f7376df56 | |||
| cc96965b3b | |||
| 91b453eb1d | |||
| c1e7ef97bc | |||
| acaf6c5e6e | |||
| 17193ae01b | |||
| 207bac77ab | |||
| f0bbfa22a2 | |||
| e5ead3c03a | |||
| 208298d00d | |||
| 0f8e328e75 | |||
| 66c760f644 | |||
| 9cf5d56172 | |||
| 15c708addc | |||
| 080a06ebb7 | |||
| 8a51a07d11 | |||
| f623bf2770 | |||
| a7311b0c05 | |||
| 15b96af24d | |||
| 3ff231c1d4 | |||
| 7b243f53cc | |||
| 6d474f0170 | |||
| ddcae16efb | |||
| 69cf7cf56a | |||
| 363150d771 | |||
| a1d797b2d8 | |||
| 116322c4c6 | |||
| 81cac56362 | |||
| 7bfd7fe95f | |||
| 67cbdbcf0b | |||
| da92cd6e76 | |||
| 2fb1311389 | |||
| d8bec7a090 | |||
| da0f7544fa | |||
| 1091ff1f12 | |||
| 3bea8ad014 | |||
| bb64ff0b0f | |||
| 3bb4bc5bb5 | |||
| e48494696d | |||
| b3f2e372c4 | |||
| 53f53e2424 | |||
| 4c10eb14d8 | |||
| b4858a4237 | |||
| 6c80395a44 | |||
| 00a7b14c0e | |||
| 527944c274 | |||
| 95458aa709 | |||
| deb801fadc | |||
| b3445f0c83 | |||
| 266083b359 | |||
| 70e3dc44ef | |||
| 1e242956cc | |||
| 47bc3254c8 | |||
| 8b85e34fbf | |||
| 25c5ed6ea3 |
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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,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>
|
||||
</COMPATIBILITY_MATRIX>
|
||||
|
||||
@@ -38,6 +38,22 @@ if (!is_siteadmin()) {
|
||||
}
|
||||
|
||||
$admisselector = new core_role_admins_existing_selector();
|
||||
|
||||
if (array_key_exists('siteadmins', $CFG->config_php_settings)) {
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->heading(get_string('manageadmins', 'core_role'), 3);
|
||||
echo $OUTPUT->notification(get_string('siteadministratorsconfigphp', 'core_role'), \core\output\notification::NOTIFY_INFO);
|
||||
echo $OUTPUT->box_start();
|
||||
echo html_writer::tag(
|
||||
'p',
|
||||
get_string('existingadmins', 'core_role'),
|
||||
);
|
||||
$admisselector->display();
|
||||
echo $OUTPUT->box_end();
|
||||
echo $OUTPUT->footer();
|
||||
die();
|
||||
}
|
||||
|
||||
$potentialadmisselector = new core_role_admins_potential_selector();
|
||||
|
||||
if ($addusersaction) {
|
||||
|
||||
@@ -41,4 +41,18 @@ if (!empty($defaulth5plib)) {
|
||||
|
||||
$settings->add(new admin_settings_h5plib_handler_select('h5plibraryhandler', new lang_string('h5plibraryhandler', 'core_h5p'),
|
||||
new lang_string('h5plibraryhandler_help', 'core_h5p'), $defaulth5plib));
|
||||
|
||||
$setting = new admin_setting_configtextarea(
|
||||
'core_h5p/h5pcustomcss',
|
||||
new lang_string('h5pcustomcss', 'core_h5p'),
|
||||
new lang_string('h5pcustomcss_help', 'core_h5p'),
|
||||
'',
|
||||
PARAM_NOTAGS
|
||||
);
|
||||
$setting->set_updatedcallback(function () {
|
||||
// Enables use of file_storage constants.
|
||||
\core_h5p\local\library\autoloader::register();
|
||||
\core_h5p\file_storage::generate_custom_styles();
|
||||
});
|
||||
$settings->add($setting);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -45,7 +45,7 @@ $string['cachedef_purpose'] = 'Data purposes';
|
||||
$string['cachedef_purpose_overrides'] = 'Purpose overrides in the Data privacy tool';
|
||||
$string['cachedef_contextlevel'] = 'Context levels purpose and category';
|
||||
$string['cancelrequest'] = 'Cancel request';
|
||||
$string['cancelrequestconfirmation'] = 'Do you really want cancel this data request?';
|
||||
$string['cancelrequestconfirmation'] = 'Do you want to cancel this data request?';
|
||||
$string['cannotreset'] = 'Unable to reset this request. Only rejected requests can be reset.';
|
||||
$string['categories'] = 'Categories';
|
||||
$string['category'] = 'Category';
|
||||
|
||||
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');
|
||||
};
|
||||
|
||||
@@ -75,7 +75,7 @@ class manage_competency_frameworks_page implements renderable, templatable {
|
||||
);
|
||||
$this->navigation[] = $addpage;
|
||||
$competenciesrepository = new single_button(
|
||||
new moodle_url('https://archive.moodle.net/competencies'),
|
||||
new moodle_url('https://moodle.net/search', ['q' => 'competency frameworks']),
|
||||
get_string('competencyframeworksrepository', 'tool_lp'),
|
||||
'get'
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
}
|
||||
}}
|
||||
<span>
|
||||
<img height="12" src="{{profileimageurlsmall}}" alt="" role="presentation">
|
||||
<img height="12" src="{{profileimageurlsmall}}" alt="">
|
||||
<span>{{fullname}}</span>
|
||||
{{#hasidentity}}
|
||||
<span><small>{{identity}}</small></span>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<p>
|
||||
<ul class="inline list-inline">
|
||||
{{#coursemodules}}
|
||||
<li class="list-inline-item"><a href="{{url}}"><img class="icon" src="{{iconurl}}"> {{name}} </a></li>
|
||||
<li class="list-inline-item"><a href="{{url}}"><img class="icon" src="{{iconurl}}" alt=""> {{name}} </a></li>
|
||||
{{/coursemodules}}
|
||||
{{^coursemodules}}
|
||||
<li class="list-inline-item"><span class="alert">{{#str}}noactivities, tool_lp{{/str}}</span></li>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
}}
|
||||
<span>
|
||||
<a href="{{profileurl}}" title="{{#str}}viewprofile{{/str}}">
|
||||
<img height="18" src="{{profileimageurlsmall}}" alt="" role="presentation">
|
||||
<img height="18" src="{{profileimageurlsmall}}" alt="">
|
||||
<span>{{fullname}}</span>
|
||||
{{#hasidentity}}
|
||||
<span><small>{{identity}}</small></span>
|
||||
|
||||
@@ -369,6 +369,14 @@ class api {
|
||||
$settings->forcetimezone = $CFG->forcetimezone;
|
||||
}
|
||||
|
||||
if (empty($section) || $section === 'h5psettings') {
|
||||
\core_h5p\local\library\autoloader::register();
|
||||
$customcss = \core_h5p\file_storage::get_custom_styles();
|
||||
if (!empty($customcss)) {
|
||||
$settings->h5pcustomcssurl = $customcss['cssurl']->out() . '?ver=' . $customcss['cssversion'];
|
||||
}
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
],
|
||||
];
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,6 +260,18 @@ class externallib_test extends externallib_advanced_testcase {
|
||||
$this->assertCount(0, $result['warnings']);
|
||||
$this->assertEquals($expected, $result['settings']);
|
||||
|
||||
// H5P custom CSS.
|
||||
set_config('h5pcustomcss', '.debug { color: #fab; }', 'core_h5p');
|
||||
\core_h5p\local\library\autoloader::register();
|
||||
\core_h5p\file_storage::generate_custom_styles();
|
||||
$result = external::get_config();
|
||||
$result = external_api::clean_returnvalue(external::get_config_returns(), $result);
|
||||
|
||||
$customcss = \core_h5p\file_storage::get_custom_styles();
|
||||
$expected[] = ['name' => 'h5pcustomcssurl', 'value' => $customcss['cssurl']->out() . '?ver=' . $customcss['cssversion']];
|
||||
$this->assertCount(0, $result['warnings']);
|
||||
$this->assertEquals($expected, $result['settings']);
|
||||
|
||||
// Change a value and retrieve filtering by section.
|
||||
set_config('commentsperpage', 1);
|
||||
$expected[10]['value'] = 1;
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
$plugin->version = 2023042400; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2023042401; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2023041800; // 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', {}));
|
||||
};
|
||||
|
||||
|
||||
@@ -688,6 +688,17 @@ class process {
|
||||
break;
|
||||
|
||||
case UU_USER_ADD_UPDATE:
|
||||
if ($this->get_match_on_email()) {
|
||||
if ($usersbyname = $DB->get_records('user', ['username' => $user->username])) {
|
||||
foreach ($usersbyname as $userbyname) {
|
||||
if (strtolower($userbyname->email) != strtolower($user->email)) {
|
||||
$this->usersskipped++;
|
||||
$this->upt->track('status', get_string('usernotaddedusernameexists', 'error'), 'warning');
|
||||
$skip = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case UU_USER_UPDATE:
|
||||
|
||||
@@ -50,7 +50,7 @@ The example text file may also be used for testing, as you are able to preview u
|
||||
$string['infoprefix'] = 'Info:';
|
||||
$string['invalidupdatetype'] = 'This option cannot be selected with the chosen upload type.';
|
||||
$string['invaliduserdata'] = 'Invalid data detected for user {$a} and it has been automatically cleaned.';
|
||||
$string['invaliduserdatavalues'] = 'Invalid data detected for user \'{$a->username}\' ({$a->values}), which has been automatically cleaned.';
|
||||
$string['invaliduserdatavalues'] = 'Incorrect data ({$a->values}) found for user {$a->username}. This data has been corrected or deleted.';
|
||||
$string['invalidtheme'] = 'Theme "{$a}" is not installed and will be ignored.';
|
||||
$string['linex'] = 'Line {$a}';
|
||||
$string['matchemail'] = 'Match on email address';
|
||||
|
||||
@@ -296,3 +296,25 @@ Feature: Upload users
|
||||
And I navigate to "Users > Accounts > Browse list of users" in site administration
|
||||
And I should see "Bilbo Baggins"
|
||||
And I should not see "Frodo Baggins"
|
||||
|
||||
@javascript
|
||||
Scenario: Create a new user when matching them on email where where the username already exists
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| bilbob | Samwise | Gamgee | samwise@example.com |
|
||||
| frodob | Frodeo | Baspins | frodo@example.com |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Users > Accounts > Upload users" in site administration
|
||||
When I upload "lib/tests/fixtures/upload_users_email_matching.csv" file to "File" filemanager
|
||||
And I press "Upload users"
|
||||
Then I should see "Upload users preview"
|
||||
And I set the following fields to these values:
|
||||
| Upload type | Add new and update existing users |
|
||||
| Existing user details | Override with file |
|
||||
| Match on email address | Yes |
|
||||
And I press "Upload users"
|
||||
And I should see "User not added - username already exists under a different email"
|
||||
And I press "Continue"
|
||||
And I navigate to "Users > Accounts > Browse list of users" in site administration
|
||||
And I should see "Samwise Gamgee"
|
||||
And I should see "Frodo Baggins"
|
||||
|
||||
@@ -202,8 +202,8 @@ EOF;
|
||||
|
||||
// We should get the debugging from the user class itself, as well as warning in the output regarding the same.
|
||||
$this->assertDebuggingCalled('The property \'country\' has invalid data and has been cleaned.');
|
||||
$this->assertStringContainsString('Invalid data detected for user \'student1\' (country), ' .
|
||||
'which has been automatically cleaned.', $output);
|
||||
$this->assertStringContainsString('Incorrect data (country) found for user student1. ' .
|
||||
'This data has been corrected or deleted.', $output);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -283,7 +283,7 @@ class manager {
|
||||
'title' => get_string('importtour', 'tool_usertours'),
|
||||
],
|
||||
(object) [
|
||||
'link' => new \moodle_url('https://archive.moodle.net/tours'),
|
||||
'link' => new \moodle_url('https://moodle.net/search', ['q' => 'user tours']),
|
||||
'linkproperties' => [
|
||||
'target' => '_blank',
|
||||
],
|
||||
|
||||
@@ -91,12 +91,12 @@ class edit_field_save extends XMLDBAction {
|
||||
$comment = trim($comment);
|
||||
|
||||
$type = required_param('type', PARAM_INT);
|
||||
$length = strtolower(optional_param('length', NULL, PARAM_ALPHANUM));
|
||||
$length = optional_param('length', null, PARAM_INT);
|
||||
$decimals = optional_param('decimals', NULL, PARAM_INT);
|
||||
$notnull = optional_param('notnull', false, PARAM_BOOL);
|
||||
$sequence = optional_param('sequence', false, PARAM_BOOL);
|
||||
$default = optional_param('default', NULL, PARAM_PATH);
|
||||
$default = trim($default);
|
||||
$default = is_null($default) ? $default : trim($default);
|
||||
|
||||
$editeddir = $XMLDB->editeddirs[$dirpath];
|
||||
$structure = $editeddir->xml_file->getStructure();
|
||||
|
||||
+1
-1
@@ -115,7 +115,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'] = 'Log 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 = 2023042400; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2023042401; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2023041800; // Requires this Moodle version.
|
||||
$plugin->component = 'auth_lti'; // Full name of the plugin (used for diagnostics).
|
||||
|
||||
@@ -49,6 +49,9 @@ abstract class info {
|
||||
/** @var tree Availability configuration, decoded from JSON; null if unset */
|
||||
protected $availabilitytree;
|
||||
|
||||
/** @var array The groups the current user belongs to. */
|
||||
protected $groups;
|
||||
|
||||
/** @var array|null Array of information about current restore if any */
|
||||
protected static $restoreinfo = null;
|
||||
|
||||
@@ -65,6 +68,7 @@ abstract class info {
|
||||
$this->course = $course;
|
||||
$this->visible = (bool)$visible;
|
||||
$this->availability = $availability;
|
||||
$this->groups = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -800,4 +804,28 @@ abstract class info {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns groups that the current user belongs to on the course. Note: If not already
|
||||
* available, this may make a database query.
|
||||
*
|
||||
* This will include groups the user is not allowed to see themselves, so check visibility
|
||||
* before displaying groups to the user.
|
||||
*
|
||||
* @param int $groupingid Grouping ID or 0 (default) for all groups
|
||||
* @return int[] Array of int (group id) => int (same group id again); empty array if none
|
||||
*/
|
||||
public function get_groups(int $groupingid = 0): array {
|
||||
global $USER;
|
||||
if (is_null($this->groups)) {
|
||||
$allgroups = groups_get_user_groups($this->course->id, $USER->id, true);
|
||||
$this->groups = $allgroups;
|
||||
} else {
|
||||
$allgroups = $this->groups;
|
||||
}
|
||||
if (!isset($allgroups[$groupingid])) {
|
||||
return [];
|
||||
}
|
||||
return $allgroups[$groupingid];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ class condition extends \core_availability\condition {
|
||||
$allow = true;
|
||||
if (!has_capability('moodle/site:accessallgroups', $context, $userid)) {
|
||||
// Get all groups the user belongs to.
|
||||
$groups = $info->get_modinfo()->get_groups();
|
||||
$groups = $info->get_groups();
|
||||
if ($this->groupid) {
|
||||
$allow = in_array($this->groupid, $groups);
|
||||
} else {
|
||||
|
||||
@@ -108,3 +108,49 @@ Feature: availability_group
|
||||
When I am on the "C1" "Course" page logged in as "student1"
|
||||
Then I should see "Not available unless: You belong to G-One"
|
||||
And I should not see "G-Un"
|
||||
|
||||
@javascript
|
||||
Scenario: Condition using a hidden group
|
||||
Given the following "groups" exist:
|
||||
| name | course | idnumber | visibility |
|
||||
| Hidden Group | C1 | GA | 3 |
|
||||
And I log in as "teacher1"
|
||||
And I add a page activity to course "Course 1" section "1"
|
||||
And I expand all fieldsets
|
||||
|
||||
# Page P1 any group.
|
||||
And I am on the "P1" "page activity editing" page
|
||||
And I expand all fieldsets
|
||||
And I click on "Add restriction..." "button"
|
||||
And "Group" "button" should exist in the "Add restriction..." "dialogue"
|
||||
And I click on "Group" "button" in the "Add restriction..." "dialogue"
|
||||
And I set the field "Group" to "(Any group)"
|
||||
And I click on ".availability-item .availability-eye img" "css_element"
|
||||
And I click on "Save and return to course" "button"
|
||||
|
||||
# Page P2 with hidden group.
|
||||
And I am on the "P2" "page activity editing" page
|
||||
And I expand all fieldsets
|
||||
And I click on "Add restriction..." "button"
|
||||
And I click on "Group" "button" in the "Add restriction..." "dialogue"
|
||||
And I set the field "Group" to "Hidden Group"
|
||||
And I click on "Save and return to course" "button"
|
||||
|
||||
# Log back in as student.
|
||||
When I am on the "Course 1" "course" page logged in as "student1"
|
||||
|
||||
# No pages should appear yet.
|
||||
Then I should not see "P1" in the "region-main" "region"
|
||||
And I should not see "P2" in the "region-main" "region"
|
||||
And I should not see "Hidden Group"
|
||||
|
||||
# Add to groups and log out/in again.
|
||||
And the following "group members" exist:
|
||||
| user | group |
|
||||
| student1 | GA |
|
||||
And I am on "Course 1" course homepage
|
||||
|
||||
# P1 (any groups) and P2 should show. The user should not see the hidden group mentioned anywhere.
|
||||
And I should see "P1" in the "region-main" "region"
|
||||
And I should see "P2" in the "region-main" "region"
|
||||
And I should not see "Hidden Group"
|
||||
|
||||
@@ -49,6 +49,7 @@ class condition_test extends \advanced_testcase {
|
||||
$course = $generator->create_course();
|
||||
$user = $generator->create_user();
|
||||
$generator->enrol_user($user->id, $course->id);
|
||||
$this->setUser($user);
|
||||
$info = new \core_availability\mock_info($course, $user->id);
|
||||
|
||||
// Make 2 test groups, one in a grouping and one not.
|
||||
@@ -70,7 +71,7 @@ class condition_test extends \advanced_testcase {
|
||||
// Add user to groups and refresh cache.
|
||||
groups_add_member($group1, $user);
|
||||
groups_add_member($group2, $user);
|
||||
get_fast_modinfo($course->id, 0, true);
|
||||
$info = new \core_availability\mock_info($course, $user->id);
|
||||
|
||||
// Recheck.
|
||||
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
|
||||
|
||||
@@ -168,6 +168,7 @@ class backup_course_task extends backup_task {
|
||||
$content = self::encode_links_helper($content, 'BADGESVIEWBYID', '/badges/view.php?type=2&id=');
|
||||
$content = self::encode_links_helper($content, 'USERINDEXVIEWBYID', '/user/index.php?id=');
|
||||
$content = self::encode_links_helper($content, 'PLUGINFILEBYCONTEXT', '/pluginfile.php/');
|
||||
$content = self::encode_links_helper($content, 'PLUGINFILEBYCONTEXTURLENCODED', '/pluginfile.php/', true);
|
||||
|
||||
return $content;
|
||||
}
|
||||
@@ -178,17 +179,26 @@ class backup_course_task extends backup_task {
|
||||
* @param string $name the name of this type of encoded link.
|
||||
* @param string $path the path that identifies this type of link, up
|
||||
* to the ?paramname= bit.
|
||||
* @param bool $urlencoded whether to use urlencode() before replacing the path.
|
||||
* @return string content with one type of link encoded.
|
||||
*/
|
||||
static private function encode_links_helper($content, $name, $path) {
|
||||
private static function encode_links_helper(string $content, string $name, string $path, bool $urlencoded = false) {
|
||||
global $CFG;
|
||||
// We want to convert both http and https links.
|
||||
$root = $CFG->wwwroot;
|
||||
$httpsroot = str_replace('http://', 'https://', $root);
|
||||
$httproot = str_replace('https://', 'http://', $root);
|
||||
|
||||
$httpsbase = preg_quote($httpsroot . $path, '/');
|
||||
$httpbase = preg_quote($httproot . $path, '/');
|
||||
$httpsbase = $httpsroot . $path;
|
||||
$httpbase = $httproot . $path;
|
||||
|
||||
if ($urlencoded) {
|
||||
$httpsbase = urlencode($httpsbase);
|
||||
$httpbase = urlencode($httpbase);
|
||||
}
|
||||
|
||||
$httpsbase = preg_quote($httpsbase, '/');
|
||||
$httpbase = preg_quote($httpbase, '/');
|
||||
|
||||
$return = preg_replace('/(' . $httpsbase . ')([0-9]+)/', '$@' . $name . '*$2@$', $content);
|
||||
$return = preg_replace('/(' . $httpbase . ')([0-9]+)/', '$@' . $name . '*$2@$', $return);
|
||||
|
||||
@@ -167,6 +167,7 @@ class restore_course_task extends restore_task {
|
||||
$rules[] = new restore_decode_rule('BADGESVIEWBYID', '/badges/view.php?type=2&id=$1', 'course');
|
||||
$rules[] = new restore_decode_rule('USERINDEXVIEWBYID', '/user/index.php?id=$1', 'course');
|
||||
$rules[] = new restore_decode_rule('PLUGINFILEBYCONTEXT', '/pluginfile.php/$1', 'context');
|
||||
$rules[] = new restore_decode_rule('PLUGINFILEBYCONTEXTURLENCODED', '/pluginfile.php/$1', 'context', true);
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
@@ -3749,7 +3749,7 @@ class restore_activity_competencies_structure_step extends restore_structure_ste
|
||||
// Sortorder is ignored by precaution, anyway we should walk through the records in the right order.
|
||||
$record = (object) $params;
|
||||
$record->ruleoutcome = $data->ruleoutcome;
|
||||
$record->overridegrade = $data->overridegrade;
|
||||
$record->overridegrade = $data->overridegrade ?? 0;
|
||||
$coursemodulecompetency = new \core_competency\course_module_competency(0, $record);
|
||||
$coursemodulecompetency->create();
|
||||
}
|
||||
@@ -5390,6 +5390,25 @@ class restore_move_module_questions_categories extends restore_execution_step {
|
||||
];
|
||||
$params += $categoryidparams;
|
||||
$DB->execute($sqlupdate, $params);
|
||||
|
||||
// As explained in {@see restore_quiz_activity_structure_step::process_quiz_question_legacy_instance()}
|
||||
// question_set_references relating to random questions restored from old backups,
|
||||
// which pick from context_module question_categores, will have been restored with the wrong questioncontextid.
|
||||
// So, now, we need to find those, and updated the questioncontextid.
|
||||
// We can only find them by picking apart the filter conditions, and seeign which categories they refer to.
|
||||
|
||||
// We need to check all the question_set_references belonging to this context_module.
|
||||
$references = $DB->get_records('question_set_references', ['usingcontextid' => $newcontext->newitemid]);
|
||||
foreach ($references as $reference) {
|
||||
$filtercondition = json_decode($reference->filtercondition);
|
||||
if (!empty($filtercondition->questioncategoryid) &&
|
||||
in_array($filtercondition->questioncategoryid, $categoryids)) {
|
||||
// This is one of ours, update the questionscontextid.
|
||||
$DB->set_field('question_set_references',
|
||||
'questionscontextid', $newcontext->newitemid,
|
||||
['id' => $reference->id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now set the parent id for the question categories that were in the top category in the course context
|
||||
@@ -6187,6 +6206,11 @@ trait restore_question_set_reference_data_trait {
|
||||
$data->filtercondition = json_encode($filtercondition);
|
||||
if ($context = $this->get_mappingid('context', $data->questionscontextid)) {
|
||||
$data->questionscontextid = $context;
|
||||
} else {
|
||||
$this->log('question_set_reference with old id ' . $data->id .
|
||||
' referenced question context ' . $data->questionscontextid .
|
||||
' which was not included in the backup. Therefore, this has been ' .
|
||||
' restored with the old questionscontextid.', backup::LOG_WARNING);
|
||||
}
|
||||
|
||||
$DB->insert_record('question_set_references', $data);
|
||||
|
||||
@@ -44,7 +44,18 @@ class restore_decode_rule {
|
||||
|
||||
protected $cregexp; // Calculated regular expresion we'll be looking for matches
|
||||
|
||||
public function __construct($linkname, $urltemplate, $mappings) {
|
||||
/** @var bool $urlencoded Whether to use urlencode() on the final URL. */
|
||||
protected bool $urlencoded;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $linkname How the link has been encoded in backup (CHOICEVIEWBYID, COURSEVIEWBYID...)
|
||||
* @param string $urltemplate How the original URL looks like, with dollar placeholders
|
||||
* @param array|string $mappings Which backup_ids mappings do we need to apply for replacing the placeholders
|
||||
* @param bool $urlencoded Whether to use urlencode() on the final URL (defaults to false)
|
||||
*/
|
||||
public function __construct(string $linkname, string $urltemplate, $mappings, bool $urlencoded = false) {
|
||||
// Validate all the params are ok
|
||||
$this->mappings = $this->validate_params($linkname, $urltemplate, $mappings);
|
||||
$this->linkname = $linkname;
|
||||
@@ -52,6 +63,7 @@ class restore_decode_rule {
|
||||
$this->restoreid = 0;
|
||||
$this->sourcewwwroot = '';
|
||||
$this->targetwwwroot = ''; // yes, uses to be $CFG->wwwroot, and? ;-)
|
||||
$this->urlencoded = $urlencoded;
|
||||
$this->cregexp = $this->get_calculated_regexp();
|
||||
}
|
||||
|
||||
@@ -96,6 +108,9 @@ class restore_decode_rule {
|
||||
} else { // All mappings found, apply target values to the template
|
||||
$toreplace = str_replace($placeholdersarr, $mappingstargetarr, $toreplace);
|
||||
}
|
||||
if ($this->urlencoded) {
|
||||
$toreplace = urlencode($toreplace);
|
||||
}
|
||||
// Finally, perform the replacement in original content
|
||||
$content = str_replace($tosearch, $toreplace, $content);
|
||||
}
|
||||
|
||||
@@ -50,14 +50,18 @@ class backup_encode_content_test extends \basic_testcase {
|
||||
// HTTPS root and links of both types in content.
|
||||
$CFG->wwwroot = $httpsroot;
|
||||
$encoded = backup_course_task::encode_content_links(
|
||||
$httproot . '/course/view.php?id=123, ' .
|
||||
$httpsroot . '/course/view.php?id=123, ' .
|
||||
$httpsroot . '/grade/index.php?id=123, ' .
|
||||
$httpsroot . '/grade/report/index.php?id=123, ' .
|
||||
$httpsroot . '/badges/view.php?type=2&id=123 and ' .
|
||||
$httpsroot . '/user/index.php?id=123.');
|
||||
$httproot . '/course/view.php?id=123, ' .
|
||||
$httpsroot . '/course/view.php?id=123, ' .
|
||||
$httpsroot . '/grade/index.php?id=123, ' .
|
||||
$httpsroot . '/grade/report/index.php?id=123, ' .
|
||||
$httpsroot . '/badges/view.php?type=2&id=123, ' .
|
||||
$httpsroot . '/user/index.php?id=123, ' .
|
||||
$httpsroot . '/pluginfile.php/123 and ' .
|
||||
urlencode($httpsroot . '/pluginfile.php/123') . '.'
|
||||
);
|
||||
$this->assertEquals('$@COURSEVIEWBYID*123@$, $@COURSEVIEWBYID*123@$, $@GRADEINDEXBYID*123@$, ' .
|
||||
'$@GRADEREPORTINDEXBYID*123@$, $@BADGESVIEWBYID*123@$ and $@USERINDEXVIEWBYID*123@$.', $encoded);
|
||||
'$@GRADEREPORTINDEXBYID*123@$, $@BADGESVIEWBYID*123@$, $@USERINDEXVIEWBYID*123@$, ' .
|
||||
'$@PLUGINFILEBYCONTEXT*123@$ and $@PLUGINFILEBYCONTEXTURLENCODED*123@$.', $encoded);
|
||||
|
||||
// HTTP root and links of both types in content.
|
||||
$CFG->wwwroot = $httproot;
|
||||
@@ -66,10 +70,14 @@ class backup_encode_content_test extends \basic_testcase {
|
||||
$httpsroot . '/course/view.php?id=123, ' .
|
||||
$httproot . '/grade/index.php?id=123, ' .
|
||||
$httproot . '/grade/report/index.php?id=123, ' .
|
||||
$httproot . '/badges/view.php?type=2&id=123 and ' .
|
||||
$httproot . '/user/index.php?id=123.');
|
||||
$httproot . '/badges/view.php?type=2&id=123, ' .
|
||||
$httproot . '/user/index.php?id=123, ' .
|
||||
$httproot . '/pluginfile.php/123 and ' .
|
||||
urlencode($httproot . '/pluginfile.php/123') . '.'
|
||||
);
|
||||
$this->assertEquals('$@COURSEVIEWBYID*123@$, $@COURSEVIEWBYID*123@$, $@GRADEINDEXBYID*123@$, ' .
|
||||
'$@GRADEREPORTINDEXBYID*123@$, $@BADGESVIEWBYID*123@$ and $@USERINDEXVIEWBYID*123@$.', $encoded);
|
||||
'$@GRADEREPORTINDEXBYID*123@$, $@BADGESVIEWBYID*123@$, $@USERINDEXVIEWBYID*123@$, ' .
|
||||
'$@PLUGINFILEBYCONTEXT*123@$ and $@PLUGINFILEBYCONTEXTURLENCODED*123@$.', $encoded);
|
||||
$CFG->wwwroot = $oldroot;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -10,8 +10,8 @@ Feature: View a site event on the dashboard
|
||||
| student1 | Student | 1 | student1@example.com | S1 |
|
||||
And I log in as "admin"
|
||||
And I create a calendar event with form data:
|
||||
| id_eventtype | Site |
|
||||
| id_name | Site Event |
|
||||
| id_eventtype | Site |
|
||||
| id_name | Site Event |
|
||||
And I log out
|
||||
|
||||
@javascript
|
||||
@@ -29,5 +29,5 @@ Feature: View a site event on the dashboard
|
||||
# We need to give the browser a couple seconds to re-render the page after the screen has been resized.
|
||||
And I wait "1" seconds
|
||||
And I should not see "Site Event"
|
||||
And I hover over today in the mini-calendar block
|
||||
When I hover over today in the mini-calendar block responsive view
|
||||
And I should see "Site Event"
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -551,7 +551,11 @@ const itemsPerPageFunc = (pagingLimit, root) => {
|
||||
// Filter out all pagination options which are too large for the amount of courses user is enrolled in.
|
||||
const totalCourseCount = parseInt(root.find(SELECTORS.courseView.region).attr('data-totalcoursecount'), 10);
|
||||
return itemsPerPage.filter(pagingOption => {
|
||||
return pagingOption.value < totalCourseCount || pagingOption.value === 0;
|
||||
if (pagingOption.value === 0 && totalCourseCount > 100) {
|
||||
// To minimise performance issues, do not show the "All" option if the user is enrolled in more than 100 courses.
|
||||
return false;
|
||||
}
|
||||
return pagingOption.value < totalCourseCount;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -363,7 +363,14 @@ class main implements renderable, templatable {
|
||||
if (!$this->displaygroupingcustomfield) {
|
||||
return [];
|
||||
}
|
||||
$fieldid = $DB->get_field('customfield_field', 'id', ['shortname' => $this->customfiltergrouping]);
|
||||
|
||||
// Get the relevant customfield ID within the core_course/course component/area.
|
||||
$fieldid = $DB->get_field_sql("
|
||||
SELECT f.id
|
||||
FROM {customfield_field} f
|
||||
JOIN {customfield_category} c ON c.id = f.categoryid
|
||||
WHERE f.shortname = :shortname AND c.component = 'core_course' AND c.area = 'course'
|
||||
", ['shortname' => $this->customfiltergrouping]);
|
||||
if (!$fieldid) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -51,8 +51,7 @@
|
||||
<div class="text-xs-center text-center mt-3" data-region="empty-message">
|
||||
<img class="empty-placeholder-image-lg mt-1"
|
||||
src="{{nocoursesimg}}"
|
||||
alt="{{{ title }}}"
|
||||
role="presentation"
|
||||
alt=""
|
||||
>
|
||||
<h5 class="h5 mt-3 mb-0">{{{ title }}}</h5>
|
||||
<p class="mt-3 mb-0">{{{ intro }}}</p>
|
||||
|
||||
@@ -70,3 +70,8 @@ Feature: Zero state on my overview block
|
||||
And "Create course" "button" should exist
|
||||
And I click on "Create course" "button"
|
||||
And I should see "Add a new course"
|
||||
|
||||
@accessibility
|
||||
Scenario: Evaluate the accessibility of the My courses (zero state)
|
||||
When I am on the "My courses" page logged in as "manager"
|
||||
Then the page should meet accessibility standards
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -42,19 +42,21 @@
|
||||
|
||||
<div class="card-deck dashboard-card-deck one-row" data-region="items-list" role="list">
|
||||
{{#items}}
|
||||
<a class="card dashboard-card mb-1" href="{{{viewurl}}}" title="{{{name}}}" role="listitem">
|
||||
<div class="card-body p-2 m-1">
|
||||
<div class="d-flex text-truncate">
|
||||
<div class="d-flex align-self-center activityiconcontainer {{purpose}}">
|
||||
{{{icon}}}
|
||||
</div>
|
||||
<div class="w-100 line-height-3 text-truncate ml-2">
|
||||
<h6 class="mb-0 text-truncate"><span class="clickable">{{{name}}}</span></h6>
|
||||
<small class="text-truncate mb-0">{{{coursename}}}</small>
|
||||
<div class="card dashboard-card" role="listitem">
|
||||
<a class="mb-1" href="{{{viewurl}}}" title="{{{name}}}">
|
||||
<div class="card-body p-2 m-1">
|
||||
<div class="d-flex text-truncate">
|
||||
<div class="d-flex align-self-center activityiconcontainer {{purpose}}">
|
||||
{{{icon}}}
|
||||
</div>
|
||||
<div class="w-100 line-height-3 text-truncate ml-2">
|
||||
<h6 class="mb-0 text-truncate"><span class="clickable">{{{name}}}</span></h6>
|
||||
<small class="text-truncate mb-0">{{{coursename}}}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
{{/items}}
|
||||
</div>
|
||||
{{#hasmoreitems}}
|
||||
|
||||
@@ -23,15 +23,19 @@ Feature: The recently accessed items block allows users to easily access their m
|
||||
| name | Test forum name |
|
||||
And I log in as "student1"
|
||||
|
||||
@accessibility
|
||||
Scenario: User has not accessed any item
|
||||
Then I should see "No recent items" in the "Recently accessed items" "block"
|
||||
And the page should meet accessibility standards
|
||||
|
||||
@accessibility
|
||||
Scenario: User has accessed some items
|
||||
Given I change window size to "large"
|
||||
When I am on the "Test forum name" "forum activity" page
|
||||
And I follow "Dashboard"
|
||||
Then I should see "Test forum name" in the "Recently accessed items" "block"
|
||||
And I should not see "Show more items" in the "Recently accessed items" "block"
|
||||
And the page should meet accessibility standards
|
||||
|
||||
Scenario: User has accessed more than 3 items
|
||||
Given the following "activities" exist:
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
@block_starredcourses
|
||||
Feature: Starred courses
|
||||
In order for me to quickly navigate to my favourite courses
|
||||
As a user
|
||||
I must be able to add them to the Starred courses block
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
And the following "courses" exist:
|
||||
| shortname | fullname |
|
||||
| C1 | Course 1 |
|
||||
| C2 | Course 2 |
|
||||
| C3 | Course 3 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
| student1 | C2 | student |
|
||||
| student1 | C3 | student |
|
||||
And the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| starredcourses | User | student1 | my-index | content |
|
||||
|
||||
@accessibility @javascript
|
||||
Scenario: User has no starred courses
|
||||
Given I log in as "student1"
|
||||
Then I should see "No starred courses"
|
||||
And the page should meet accessibility standards
|
||||
|
||||
@accessibility @javascript
|
||||
Scenario: User has starred courses
|
||||
Given I am on the "My courses" page logged in as "student1"
|
||||
And I click on "Actions for course Course 1" "button"
|
||||
And I click on "Star for Course 1" "link"
|
||||
And I click on "Actions for course Course 3" "button"
|
||||
And I click on "Star for Course 3" "link"
|
||||
When I follow "Dashboard"
|
||||
Then the page should meet accessibility standards
|
||||
And I should see "Course 1" in the "Starred courses" "block"
|
||||
And I should see "Course 3" in the "Starred courses" "block"
|
||||
But I should not see "Course 2" in the "Starred courses" "block"
|
||||
@@ -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
@@ -621,7 +621,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) {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -6,6 +6,6 @@ define("core_calendar/popover",["theme_boost/popover","jquery","core_calendar/se
|
||||
* @copyright 2021 Huong Nguyen <huongnv13@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @since 4.0
|
||||
*/function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj},CalendarSelectors=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CalendarSelectors);const isPopoverConfigured=new Map,showPopover=target=>{if(!isPopoverConfigured.has(target)){const dateEle=(0,_jquery.default)(target);dateEle.popover({trigger:"manual",placement:"top",html:!0,content:()=>{const source=dateEle.find(CalendarSelectors.elements.dateContent),content=(0,_jquery.default)("<div>");if(source.length){const temptContent=source.find(".hidden").clone(!1);content.html(temptContent.html())}return content.html()}}),isPopoverConfigured.set(target,!0)}var dateContainer;dateContainer=target,"none"===window.getComputedStyle(dateContainer.querySelector(CalendarSelectors.elements.dateContent)).display&&((0,_jquery.default)(target).popover("show"),target.addEventListener("mouseleave",hidePopover),target.addEventListener("focusout",hidePopover))},hidePopover=e=>{const target=e.target,dateContainer=e.target.closest(CalendarSelectors.elements.dateContainer);if(dateContainer&&isPopoverConfigured.has(dateContainer)){const isTargetActive=target.contains(document.activeElement),isTargetHover=target.matches(":hover");isTargetActive||isTargetHover||((0,_jquery.default)(dateContainer).popover("hide"),dateContainer.removeEventListener("mouseleave",hidePopover),dateContainer.removeEventListener("focusout",hidePopover))}};let listenersRegistered=!1;listenersRegistered||((()=>{const showPopoverHandler=e=>{const dateContainer=e.target.closest(CalendarSelectors.elements.dateContainer);dateContainer&&(e.preventDefault(),showPopover(dateContainer))};document.addEventListener("mouseover",showPopoverHandler),document.addEventListener("focusin",showPopoverHandler)})(),listenersRegistered=!0)}));
|
||||
*/function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj},CalendarSelectors=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CalendarSelectors);const isPopoverConfigured=new Map,showPopover=target=>{const dateContainer=target.closest(CalendarSelectors.elements.dateContainer);if(!isPopoverConfigured.has(dateContainer)){(0,_jquery.default)(target).popover({trigger:"manual",placement:"top",html:!0,title:dateContainer.dataset.title,content:()=>{const source=(0,_jquery.default)(dateContainer).find(CalendarSelectors.elements.dateContent),content=(0,_jquery.default)("<div>");if(source.length){const temptContent=source.find(".hidden").clone(!1);content.html(temptContent.html())}return content.html()}}),isPopoverConfigured.set(dateContainer,!0)}(dateContainer=>"none"===window.getComputedStyle(dateContainer.querySelector(CalendarSelectors.elements.dateContent)).display)(dateContainer)&&((0,_jquery.default)(target).popover("show"),target.addEventListener("mouseleave",hidePopover),target.addEventListener("focusout",hidePopover))},hidePopover=e=>{const target=e.target,dateContainer=e.target.closest(CalendarSelectors.elements.dateContainer);if(dateContainer&&isPopoverConfigured.has(dateContainer)){const isTargetActive=target.contains(document.activeElement),isTargetHover=target.matches(":hover");isTargetActive||isTargetHover||((0,_jquery.default)(target).popover("hide"),target.removeEventListener("mouseleave",hidePopover),target.removeEventListener("focusout",hidePopover))}};let listenersRegistered=!1;listenersRegistered||((()=>{const showPopoverHandler=e=>{const dayLink=e.target.closest(CalendarSelectors.links.dayLink);dayLink&&(e.preventDefault(),showPopover(dayLink))};document.addEventListener("mouseover",showPopoverHandler),document.addEventListener("focusin",showPopoverHandler)})(),listenersRegistered=!0)}));
|
||||
|
||||
//# sourceMappingURL=popover.min.js.map
|
||||
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -5,6 +5,6 @@
|
||||
* @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define("core_calendar/selectors",[],(function(){return{eventFilterItem:"[data-action='filter-event-type']",eventType:{site:"[data-eventtype-site]",category:"[data-eventtype-category]",course:"[data-eventtype-course]",group:"[data-eventtype-group]",user:"[data-eventtype-user]",other:"[data-eventtype-other]"},popoverType:{site:"[data-popover-eventtype-site]",category:"[data-popover-eventtype-category]",course:"[data-popover-eventtype-course]",group:"[data-popover-eventtype-group]",user:"[data-popover-eventtype-user]",other:"[data-popover-eventtype-other]"},calendarPeriods:{month:"[data-period='month']"},courseSelector:'select[name="course"]',viewSelector:'div[data-region="view-selector"]',actions:{create:'[data-action="new-event-button"]',edit:'[data-action="edit"]',remove:'[data-action="delete"]',viewEvent:'[data-action="view-event"]',deleteSubscription:'[data-action="delete-subscription"]'},elements:{courseSelector:'select[name="course"]',dateContainer:".clickable.hasevent",dateContent:'[data-region="day-content"]',monthDetailed:".calendarmonth.calendartable"},today:".today",day:'[data-region="day"]',calendarMain:'[data-region="calendar"]',wrapper:".calendarwrapper",eventItem:'[data-type="event"]',links:{navLink:".calendarwrapper .arrow_link",eventLink:"[data-region='event-item']",miniDayLink:"[data-region='mini-day-link']"},containers:{loadingIcon:'[data-region="overlay-icon-container"]'},mainCalendar:".maincalendar .heightcontainer",fullCalendarView:"page-calendar-view"}}));
|
||||
define("core_calendar/selectors",[],(function(){return{eventFilterItem:"[data-action='filter-event-type']",eventType:{site:"[data-eventtype-site]",category:"[data-eventtype-category]",course:"[data-eventtype-course]",group:"[data-eventtype-group]",user:"[data-eventtype-user]",other:"[data-eventtype-other]"},popoverType:{site:"[data-popover-eventtype-site]",category:"[data-popover-eventtype-category]",course:"[data-popover-eventtype-course]",group:"[data-popover-eventtype-group]",user:"[data-popover-eventtype-user]",other:"[data-popover-eventtype-other]"},calendarPeriods:{month:"[data-period='month']"},courseSelector:'select[name="course"]',viewSelector:'div[data-region="view-selector"]',actions:{create:'[data-action="new-event-button"]',edit:'[data-action="edit"]',remove:'[data-action="delete"]',viewEvent:'[data-action="view-event"]',deleteSubscription:'[data-action="delete-subscription"]'},elements:{courseSelector:'select[name="course"]',dateContainer:".clickable.hasevent",dateContent:'[data-region="day-content"]',monthDetailed:".calendarmonth.calendartable"},today:".today",day:'[data-region="day"]',calendarMain:'[data-region="calendar"]',wrapper:".calendarwrapper",eventItem:'[data-type="event"]',links:{navLink:".calendarwrapper .arrow_link",eventLink:"[data-region='event-item']",miniDayLink:"[data-region='mini-day-link']",dayLink:"[data-action='view-day-link']"},containers:{loadingIcon:'[data-region="overlay-icon-container"]'},mainCalendar:".maincalendar .heightcontainer",fullCalendarView:"page-calendar-view"}}));
|
||||
|
||||
//# sourceMappingURL=selectors.min.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"selectors.min.js","sources":["../src/selectors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * CSS selectors for the calendar.\n *\n * @module core_calendar/selectors\n * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([], function() {\n return {\n eventFilterItem: \"[data-action='filter-event-type']\",\n eventType: {\n site: \"[data-eventtype-site]\",\n category: \"[data-eventtype-category]\",\n course: \"[data-eventtype-course]\",\n group: \"[data-eventtype-group]\",\n user: \"[data-eventtype-user]\",\n other: \"[data-eventtype-other]\",\n },\n popoverType: {\n site: \"[data-popover-eventtype-site]\",\n category: \"[data-popover-eventtype-category]\",\n course: \"[data-popover-eventtype-course]\",\n group: \"[data-popover-eventtype-group]\",\n user: \"[data-popover-eventtype-user]\",\n other: \"[data-popover-eventtype-other]\",\n },\n calendarPeriods: {\n month: \"[data-period='month']\",\n },\n courseSelector: 'select[name=\"course\"]',\n viewSelector: 'div[data-region=\"view-selector\"]',\n actions: {\n create: '[data-action=\"new-event-button\"]',\n edit: '[data-action=\"edit\"]',\n remove: '[data-action=\"delete\"]',\n viewEvent: '[data-action=\"view-event\"]',\n deleteSubscription: '[data-action=\"delete-subscription\"]',\n },\n elements: {\n courseSelector: 'select[name=\"course\"]',\n dateContainer: '.clickable.hasevent',\n dateContent: '[data-region=\"day-content\"]',\n monthDetailed: '.calendarmonth.calendartable',\n },\n today: '.today',\n day: '[data-region=\"day\"]',\n calendarMain: '[data-region=\"calendar\"]',\n wrapper: '.calendarwrapper',\n eventItem: '[data-type=\"event\"]',\n links: {\n navLink: '.calendarwrapper .arrow_link',\n eventLink: \"[data-region='event-item']\",\n miniDayLink: \"[data-region='mini-day-link']\",\n },\n containers: {\n loadingIcon: '[data-region=\"overlay-icon-container\"]',\n },\n mainCalendar: '.maincalendar .heightcontainer',\n fullCalendarView: 'page-calendar-view',\n };\n});\n"],"names":["define","eventFilterItem","eventType","site","category","course","group","user","other","popoverType","calendarPeriods","month","courseSelector","viewSelector","actions","create","edit","remove","viewEvent","deleteSubscription","elements","dateContainer","dateContent","monthDetailed","today","day","calendarMain","wrapper","eventItem","links","navLink","eventLink","miniDayLink","containers","loadingIcon","mainCalendar","fullCalendarView"],"mappings":";;;;;;;AAsBAA,iCAAO,IAAI,iBACA,CACHC,gBAAiB,oCACjBC,UAAW,CACPC,KAAM,wBACNC,SAAU,4BACVC,OAAQ,0BACRC,MAAO,yBACPC,KAAM,wBACNC,MAAO,0BAEXC,YAAa,CACTN,KAAM,gCACNC,SAAU,oCACVC,OAAQ,kCACRC,MAAO,iCACPC,KAAM,gCACNC,MAAO,kCAEXE,gBAAiB,CACbC,MAAO,yBAEXC,eAAgB,wBAChBC,aAAc,mCACdC,QAAS,CACLC,OAAQ,mCACRC,KAAM,uBACNC,OAAQ,yBACRC,UAAW,6BACXC,mBAAoB,uCAExBC,SAAU,CACNR,eAAgB,wBAChBS,cAAe,sBACfC,YAAa,8BACbC,cAAe,gCAEnBC,MAAO,SACPC,IAAK,sBACLC,aAAc,2BACdC,QAAS,mBACTC,UAAW,sBACXC,MAAO,CACHC,QAAS,+BACTC,UAAW,6BACXC,YAAa,iCAEjBC,WAAY,CACRC,YAAa,0CAEjBC,aAAc,iCACdC,iBAAkB"}
|
||||
{"version":3,"file":"selectors.min.js","sources":["../src/selectors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * CSS selectors for the calendar.\n *\n * @module core_calendar/selectors\n * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([], function() {\n return {\n eventFilterItem: \"[data-action='filter-event-type']\",\n eventType: {\n site: \"[data-eventtype-site]\",\n category: \"[data-eventtype-category]\",\n course: \"[data-eventtype-course]\",\n group: \"[data-eventtype-group]\",\n user: \"[data-eventtype-user]\",\n other: \"[data-eventtype-other]\",\n },\n popoverType: {\n site: \"[data-popover-eventtype-site]\",\n category: \"[data-popover-eventtype-category]\",\n course: \"[data-popover-eventtype-course]\",\n group: \"[data-popover-eventtype-group]\",\n user: \"[data-popover-eventtype-user]\",\n other: \"[data-popover-eventtype-other]\",\n },\n calendarPeriods: {\n month: \"[data-period='month']\",\n },\n courseSelector: 'select[name=\"course\"]',\n viewSelector: 'div[data-region=\"view-selector\"]',\n actions: {\n create: '[data-action=\"new-event-button\"]',\n edit: '[data-action=\"edit\"]',\n remove: '[data-action=\"delete\"]',\n viewEvent: '[data-action=\"view-event\"]',\n deleteSubscription: '[data-action=\"delete-subscription\"]',\n },\n elements: {\n courseSelector: 'select[name=\"course\"]',\n dateContainer: '.clickable.hasevent',\n dateContent: '[data-region=\"day-content\"]',\n monthDetailed: '.calendarmonth.calendartable',\n },\n today: '.today',\n day: '[data-region=\"day\"]',\n calendarMain: '[data-region=\"calendar\"]',\n wrapper: '.calendarwrapper',\n eventItem: '[data-type=\"event\"]',\n links: {\n navLink: '.calendarwrapper .arrow_link',\n eventLink: \"[data-region='event-item']\",\n miniDayLink: \"[data-region='mini-day-link']\",\n dayLink: \"[data-action='view-day-link']\",\n },\n containers: {\n loadingIcon: '[data-region=\"overlay-icon-container\"]',\n },\n mainCalendar: '.maincalendar .heightcontainer',\n fullCalendarView: 'page-calendar-view',\n };\n});\n"],"names":["define","eventFilterItem","eventType","site","category","course","group","user","other","popoverType","calendarPeriods","month","courseSelector","viewSelector","actions","create","edit","remove","viewEvent","deleteSubscription","elements","dateContainer","dateContent","monthDetailed","today","day","calendarMain","wrapper","eventItem","links","navLink","eventLink","miniDayLink","dayLink","containers","loadingIcon","mainCalendar","fullCalendarView"],"mappings":";;;;;;;AAsBAA,iCAAO,IAAI,iBACA,CACHC,gBAAiB,oCACjBC,UAAW,CACPC,KAAM,wBACNC,SAAU,4BACVC,OAAQ,0BACRC,MAAO,yBACPC,KAAM,wBACNC,MAAO,0BAEXC,YAAa,CACTN,KAAM,gCACNC,SAAU,oCACVC,OAAQ,kCACRC,MAAO,iCACPC,KAAM,gCACNC,MAAO,kCAEXE,gBAAiB,CACbC,MAAO,yBAEXC,eAAgB,wBAChBC,aAAc,mCACdC,QAAS,CACLC,OAAQ,mCACRC,KAAM,uBACNC,OAAQ,yBACRC,UAAW,6BACXC,mBAAoB,uCAExBC,SAAU,CACNR,eAAgB,wBAChBS,cAAe,sBACfC,YAAa,8BACbC,cAAe,gCAEnBC,MAAO,SACPC,IAAK,sBACLC,aAAc,2BACdC,QAAS,mBACTC,UAAW,sBACXC,MAAO,CACHC,QAAS,+BACTC,UAAW,6BACXC,YAAa,gCACbC,QAAS,iCAEbC,WAAY,CACRC,YAAa,0CAEjBC,aAAc,iCACdC,iBAAkB"}
|
||||
@@ -71,7 +71,9 @@ define([
|
||||
CALENDAR_MONTH_WRAPPER: ".calendarwrapper",
|
||||
TODAY: '.today',
|
||||
DAY_NUMBER_CIRCLE: '.day-number-circle',
|
||||
DAY_NUMBER: '.day-number'
|
||||
DAY_NUMBER: '.day-number',
|
||||
SCREEN_READER_ANNOUNCEMENTS: '.calendar-announcements',
|
||||
CURRENT_MONTH: '.calendar-controls .current'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -165,6 +167,12 @@ define([
|
||||
body.on(CalendarEvents.eventMoved, function() {
|
||||
CalendarViewManager.reloadCurrentMonth(root);
|
||||
});
|
||||
// Announce the newly loaded month to screen readers.
|
||||
body.on(CalendarEvents.monthChanged, root, async function() {
|
||||
const monthName = body.find(SELECTORS.CURRENT_MONTH).text();
|
||||
const monthAnnoucement = await Str.get_string('newmonthannouncement', 'calendar', monthName);
|
||||
body.find(SELECTORS.SCREEN_READER_ANNOUNCEMENTS).html(monthAnnoucement);
|
||||
});
|
||||
|
||||
CalendarCrud.registerEditListeners(root, eventFormModalPromise);
|
||||
};
|
||||
|
||||
+12
-10
@@ -38,14 +38,16 @@ const isPopoverAvailable = (dateContainer) => {
|
||||
const isPopoverConfigured = new Map();
|
||||
|
||||
const showPopover = target => {
|
||||
if (!isPopoverConfigured.has(target)) {
|
||||
const dateContainer = target.closest(CalendarSelectors.elements.dateContainer);
|
||||
if (!isPopoverConfigured.has(dateContainer)) {
|
||||
const dateEle = jQuery(target);
|
||||
dateEle.popover({
|
||||
trigger: 'manual',
|
||||
placement: 'top',
|
||||
html: true,
|
||||
title: dateContainer.dataset.title,
|
||||
content: () => {
|
||||
const source = dateEle.find(CalendarSelectors.elements.dateContent);
|
||||
const source = jQuery(dateContainer).find(CalendarSelectors.elements.dateContent);
|
||||
const content = jQuery('<div>');
|
||||
if (source.length) {
|
||||
const temptContent = source.find('.hidden').clone(false);
|
||||
@@ -55,10 +57,10 @@ const showPopover = target => {
|
||||
}
|
||||
});
|
||||
|
||||
isPopoverConfigured.set(target, true);
|
||||
isPopoverConfigured.set(dateContainer, true);
|
||||
}
|
||||
|
||||
if (isPopoverAvailable(target)) {
|
||||
if (isPopoverAvailable(dateContainer)) {
|
||||
jQuery(target).popover('show');
|
||||
target.addEventListener('mouseleave', hidePopover);
|
||||
target.addEventListener('focusout', hidePopover);
|
||||
@@ -75,9 +77,9 @@ const hidePopover = e => {
|
||||
const isTargetActive = target.contains(document.activeElement);
|
||||
const isTargetHover = target.matches(':hover');
|
||||
if (!isTargetActive && !isTargetHover) {
|
||||
jQuery(dateContainer).popover('hide');
|
||||
dateContainer.removeEventListener('mouseleave', hidePopover);
|
||||
dateContainer.removeEventListener('focusout', hidePopover);
|
||||
jQuery(target).popover('hide');
|
||||
target.removeEventListener('mouseleave', hidePopover);
|
||||
target.removeEventListener('focusout', hidePopover);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -87,13 +89,13 @@ const hidePopover = e => {
|
||||
*/
|
||||
const registerEventListeners = () => {
|
||||
const showPopoverHandler = (e) => {
|
||||
const dateContainer = e.target.closest(CalendarSelectors.elements.dateContainer);
|
||||
if (!dateContainer) {
|
||||
const dayLink = e.target.closest(CalendarSelectors.links.dayLink);
|
||||
if (!dayLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
showPopover(dateContainer);
|
||||
showPopover(dayLink);
|
||||
};
|
||||
|
||||
document.addEventListener('mouseover', showPopoverHandler);
|
||||
|
||||
@@ -66,6 +66,7 @@ define([], function() {
|
||||
navLink: '.calendarwrapper .arrow_link',
|
||||
eventLink: "[data-region='event-item']",
|
||||
miniDayLink: "[data-region='mini-day-link']",
|
||||
dayLink: "[data-action='view-day-link']",
|
||||
},
|
||||
containers: {
|
||||
loadingIcon: '[data-region="overlay-icon-container"]',
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -118,9 +118,9 @@ class provider implements
|
||||
(e.courseid = ctx.instanceid AND e.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR
|
||||
(e.courseid = ctx.instanceid AND e.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR
|
||||
(e.userid = ctx.instanceid AND e.eventtype = 'user' AND ctx.contextlevel = :usercontext)
|
||||
WHERE e.userid = :cuserid
|
||||
UNION
|
||||
SELECT ctx.id
|
||||
WHERE e.userid = :cuserid";
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "SELECT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :modulecontext
|
||||
JOIN {modules} m ON m.id = cm.module
|
||||
|
||||
@@ -39,12 +39,9 @@ if (empty($CFG->enablecalendarexport)) {
|
||||
die('no export');
|
||||
}
|
||||
|
||||
// Fetch basic user information to correctly log the user.
|
||||
$fields = 'id,username,password,firstname,lastname';
|
||||
|
||||
$checkuserid = !empty($userid) && $user = $DB->get_record('user', array('id' => $userid), $fields);
|
||||
$checkuserid = !empty($userid) && $user = \core_user::get_user($userid);
|
||||
// Allowing for fallback check of old url - MDL-27542.
|
||||
$checkusername = !empty($username) && $user = $DB->get_record('user', array('username' => $username), $fields);
|
||||
$checkusername = !empty($username) && $user = \core_user::get_user_by_username($username);
|
||||
if ((!$checkuserid && !$checkusername) || !$user) {
|
||||
//No such user
|
||||
die('Invalid authentication');
|
||||
|
||||
@@ -902,7 +902,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);
|
||||
|
||||
@@ -39,4 +39,5 @@
|
||||
{{{filter_selector}}}
|
||||
{{/filter_selector}}
|
||||
{{> core_calendar/add_event_button}}
|
||||
</div>
|
||||
</div>
|
||||
<div aria-live="polite" class="sr-only calendar-announcements"></div>
|
||||
|
||||
@@ -48,6 +48,10 @@ class behat_calendar extends behat_base {
|
||||
new behat_component_named_selector('mini calendar block', [".//*[@data-block='calendar_month']"]),
|
||||
new behat_component_named_selector('full calendar page', [".//*[@id='page-calendar-view']"]),
|
||||
new behat_component_named_selector('calendar day', [".//*[@data-region='day'][@data-day=%locator%]"]),
|
||||
new behat_component_named_selector(
|
||||
'responsive calendar day',
|
||||
[".//*[@data-region='day'][@data-day=%locator%]/div[contains(@class, 'hidden-desktop')]"]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -93,33 +97,51 @@ class behat_calendar extends behat_base {
|
||||
/**
|
||||
* Hover over a specific day in the mini-calendar.
|
||||
*
|
||||
* @Given /^I hover over day "(?P<dayofmonth>\d+)" of this month in the mini-calendar block$/
|
||||
* @Given /^I hover over day "(?P<dayofmonth>\d+)" of this month in the mini-calendar block(?P<responsive> responsive view|)$/
|
||||
* @param int $day The day of the current month
|
||||
* @param string $responsive If not null, find the responsive version of the link.
|
||||
*/
|
||||
public function i_hover_over_day_of_this_month_in_mini_calendar_block(int $day): void {
|
||||
$this->execute("behat_general::i_hover_in_the",
|
||||
[$day, 'core_calendar > calendar day', '', 'core_calendar > mini calendar block']);
|
||||
public function i_hover_over_day_of_this_month_in_mini_calendar_block(int $day, string $responsive = ''): void {
|
||||
$this->execute(
|
||||
"behat_general::i_hover_in_the",
|
||||
[
|
||||
$day,
|
||||
empty($responsive) ? 'core_calendar > calendar day' : 'core_calendar > responsive calendar day',
|
||||
'',
|
||||
'core_calendar > mini calendar block',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hover over a specific day in the full calendar page.
|
||||
*
|
||||
* @Given /^I hover over day "(?P<dayofmonth>\d+)" of this month in the full calendar page$/
|
||||
* @Given /^I hover over day "(?P<dayofmonth>\d+)" of this month in the full calendar page(?P<responsive> responsive view|)$/
|
||||
* @param int $day The day of the current month
|
||||
* @param string $responsive If not empty, use the repsonsive view.
|
||||
*/
|
||||
public function i_hover_over_day_of_this_month_in_full_calendar_page(int $day): void {
|
||||
$this->execute("behat_general::i_hover_in_the",
|
||||
[$day, 'core_calendar > calendar day', '', 'core_calendar > full calendar page']);
|
||||
public function i_hover_over_day_of_this_month_in_full_calendar_page(int $day, string $responsive = ''): void {
|
||||
$this->execute(
|
||||
"behat_general::i_hover_in_the",
|
||||
[
|
||||
$day,
|
||||
empty($responsive) ? 'core_calendar > calendar day' : 'core_calendar > responsive calendar day',
|
||||
'',
|
||||
'core_calendar > full calendar page',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hover over today in the mini-calendar.
|
||||
*
|
||||
* @Given /^I hover over today in the mini-calendar block$/
|
||||
* @Given /^I hover over today in the mini-calendar block( responsive view|)$/
|
||||
*
|
||||
* @param string $responsive If not empty, use the responsive calendar link.
|
||||
*/
|
||||
public function i_hover_over_today_in_mini_calendar_block(): void {
|
||||
public function i_hover_over_today_in_mini_calendar_block(string $responsive = ''): void {
|
||||
$todaysday = date('j');
|
||||
$this->i_hover_over_day_of_this_month_in_mini_calendar_block($todaysday);
|
||||
$this->i_hover_over_day_of_this_month_in_mini_calendar_block($todaysday, $responsive);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -278,7 +278,7 @@ Feature: Perform basic calendar functionality
|
||||
# We need to give the browser a couple seconds to re-render the page after the screen has been resized.
|
||||
And I wait "1" seconds
|
||||
And I should not see "Event 1:1"
|
||||
And I hover over day "1" of this month in the full calendar page
|
||||
And I hover over day "1" of this month in the full calendar page responsive view
|
||||
And I should see "Event 1:1"
|
||||
|
||||
@javascript
|
||||
|
||||
@@ -42,6 +42,7 @@ use core_privacy\local\request\approved_userlist;
|
||||
*
|
||||
* @copyright 2018 Zig Tan <zig@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \core_calendar\privacy\provider
|
||||
*/
|
||||
class provider_test extends provider_testcase {
|
||||
|
||||
|
||||
@@ -402,7 +402,7 @@ class core_cohort_external extends external_api {
|
||||
$results = cohort_get_cohorts($context->id, $limitfrom, $limitnum, $query);
|
||||
$results = $results['cohorts'];
|
||||
if (!$context instanceof context_system) {
|
||||
$results = array_merge($results, cohort_get_available_cohorts($context, COHORT_ALL, $limitfrom, $limitnum, $query));
|
||||
$results = $results + cohort_get_available_cohorts($context, COHORT_ALL, $limitfrom, $limitnum, $query);
|
||||
}
|
||||
} else if ($includes == 'all') {
|
||||
$results = cohort_get_all_cohorts($limitfrom, $limitnum, $query);
|
||||
@@ -414,7 +414,7 @@ class core_cohort_external extends external_api {
|
||||
$cohorts = array();
|
||||
|
||||
if (!empty($results)) {
|
||||
$cohortids = array_keys($results);
|
||||
$cohortids = array_column($results, 'id');
|
||||
$customfieldsdata = self::get_custom_fields_data($cohortids);
|
||||
}
|
||||
|
||||
|
||||
@@ -716,11 +716,16 @@ class externallib_test extends externallib_advanced_testcase {
|
||||
$this->assertCount(1, $result['cohorts']);
|
||||
$this->assertEquals('Cohortsearch 4', $result['cohorts'][$cohort4->id]->name);
|
||||
|
||||
$result = core_cohort_external::search_cohorts("Cohortsearch 4", $syscontext, 'parents');
|
||||
// A user with permissions in the system, searching category context.
|
||||
$result = core_cohort_external::search_cohorts("Cohortsearch 4", $catcontext, 'parents');
|
||||
$this->assertCount(1, $result['cohorts']);
|
||||
$this->assertEquals('Cohortsearch 4', $result['cohorts'][$cohort4->id]->name);
|
||||
$this->assertIsArray($result['cohorts'][$cohort4->id]->customfields);
|
||||
$this->assertCount(2, $result['cohorts'][$cohort4->id]->customfields);
|
||||
|
||||
$this->assertEqualsCanonicalizing([
|
||||
'Test value 1',
|
||||
'Test value 2',
|
||||
], array_column($result['cohorts'][$cohort4->id]->customfields, 'value'));
|
||||
|
||||
$actual = [];
|
||||
foreach ($result['cohorts'][$cohort4->id]->customfields as $customfield) {
|
||||
$this->assertArrayHasKey('name', $customfield);
|
||||
@@ -730,8 +735,6 @@ class externallib_test extends externallib_advanced_testcase {
|
||||
$this->assertArrayHasKey('value', $customfield);
|
||||
$actual[$customfield['shortname']] = $customfield;
|
||||
}
|
||||
$this->assertEquals('Test value 1', $actual['testfield1']['value']);
|
||||
$this->assertEquals('Test value 2', $actual['testfield2']['value']);
|
||||
|
||||
// A user with permissions in the category.
|
||||
$this->setUser($catcreator);
|
||||
|
||||
@@ -223,16 +223,28 @@ class provider implements
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . competency_framework::TABLE . "} cf
|
||||
ON cf.contextid = ctx.id
|
||||
LEFT JOIN {" . competency::TABLE . "} c
|
||||
ON c.competencyframeworkid = cf.id
|
||||
LEFT JOIN {" . related_competency::TABLE . "} cr
|
||||
ON cr.competencyid = c.id
|
||||
WHERE cf.usermodified = :userid1
|
||||
OR c.usermodified = :userid2
|
||||
OR cr.usermodified = :userid3";
|
||||
$params = ['userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid];
|
||||
JOIN {" . competency_framework::TABLE . "} cf ON cf.contextid = ctx.id
|
||||
WHERE cf.usermodified = :userid1";
|
||||
$params = ['userid1' => $userid];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . competency_framework::TABLE . "} cf ON cf.contextid = ctx.id
|
||||
JOIN {" . competency::TABLE . "} c ON c.competencyframeworkid = cf.id
|
||||
WHERE c.usermodified = :userid2";
|
||||
$params = ['userid2' => $userid];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . competency_framework::TABLE . "} cf ON cf.contextid = ctx.id
|
||||
JOIN {" . competency::TABLE . "} c ON c.competencyframeworkid = cf.id
|
||||
JOIN {" . related_competency::TABLE . "} cr ON cr.competencyid = c.id
|
||||
WHERE cr.usermodified = :userid3";
|
||||
$params = ['userid3' => $userid];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
// Find the contexts of the templates, and related data, modified by the user.
|
||||
@@ -241,16 +253,30 @@ class provider implements
|
||||
FROM {context} ctx
|
||||
JOIN {" . template::TABLE . "} tpl
|
||||
ON tpl.contextid = ctx.id
|
||||
LEFT JOIN {" . template_cohort::TABLE . "} tch
|
||||
WHERE tpl.usermodified = :userid1";
|
||||
$params = ['userid1' => $userid];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . template::TABLE . "} tpl
|
||||
ON tpl.contextid = ctx.id
|
||||
JOIN {" . template_cohort::TABLE . "} tch
|
||||
ON tch.templateid = tpl.id
|
||||
AND tch.usermodified = :userid2
|
||||
LEFT JOIN {" . template_competency::TABLE . "} tc
|
||||
AND tch.usermodified = :userid2";
|
||||
$params = ['userid2' => $userid];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . template::TABLE . "} tpl
|
||||
ON tpl.contextid = ctx.id
|
||||
JOIN {" . template_competency::TABLE . "} tc
|
||||
ON tc.templateid = tpl.id
|
||||
AND tc.usermodified = :userid3
|
||||
WHERE tpl.usermodified = :userid1
|
||||
OR tch.id IS NOT NULL
|
||||
OR tc.id IS NOT NULL";
|
||||
$params = ['userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid];
|
||||
AND tc.usermodified = :userid3";
|
||||
$params = ['userid3' => $userid];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
// Find the possible course contexts.
|
||||
@@ -302,21 +328,52 @@ class provider implements
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = p.userid
|
||||
AND ctx.contextlevel = :userlevel
|
||||
LEFT JOIN {" . plan_competency::TABLE . "} pc
|
||||
ON pc.planid = p.id
|
||||
AND pc.usermodified = :userid3
|
||||
LEFT JOIN {" . user_competency_plan::TABLE . "} upc
|
||||
ON upc.planid = p.id
|
||||
AND upc.usermodified = :userid4
|
||||
WHERE p.usermodified = :userid1
|
||||
OR p.reviewerid = :userid2
|
||||
OR pc.id IS NOT NULL
|
||||
OR upc.id IS NOT NULL";
|
||||
WHERE p.usermodified = :userid1";
|
||||
$params = [
|
||||
'userlevel' => CONTEXT_USER,
|
||||
'userid1' => $userid,
|
||||
];
|
||||
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {" . plan::TABLE . "} p
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = p.userid
|
||||
AND ctx.contextlevel = :userlevel
|
||||
WHERE p.reviewerid = :userid2";
|
||||
$params = [
|
||||
'userlevel' => CONTEXT_USER,
|
||||
'userid2' => $userid,
|
||||
];
|
||||
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {" . plan::TABLE . "} p
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = p.userid
|
||||
AND ctx.contextlevel = :userlevel
|
||||
JOIN {" . plan_competency::TABLE . "} pc
|
||||
ON pc.planid = p.id
|
||||
AND pc.usermodified = :userid3";
|
||||
$params = [
|
||||
'userlevel' => CONTEXT_USER,
|
||||
'userid3' => $userid,
|
||||
];
|
||||
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {" . plan::TABLE . "} p
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = p.userid
|
||||
AND ctx.contextlevel = :userlevel
|
||||
JOIN {" . user_competency_plan::TABLE . "} upc
|
||||
ON upc.planid = p.id
|
||||
AND upc.usermodified = :userid4";
|
||||
$params = [
|
||||
'userlevel' => CONTEXT_USER,
|
||||
'userid4' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
@@ -325,32 +382,78 @@ class provider implements
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
LEFT JOIN {" . user_competency::TABLE . "} uc
|
||||
JOIN {" . user_competency::TABLE . "} uc
|
||||
ON uc.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel1
|
||||
LEFT JOIN {" . evidence::TABLE . "} e
|
||||
ON e.usercompetencyid = uc.id
|
||||
AND (e.usermodified = :userid3 OR e.actionuserid = :userid4)
|
||||
LEFT JOIN {" . user_evidence::TABLE . "} ue
|
||||
ON ue.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel2
|
||||
AND ue.usermodified = :userid5
|
||||
LEFT JOIN {" . user_evidence_competency::TABLE . "} uec
|
||||
ON uec.userevidenceid = ue.id
|
||||
AND uec.usermodified = :userid6
|
||||
WHERE uc.usermodified = :userid1
|
||||
OR uc.reviewerid = :userid2
|
||||
OR e.id IS NOT NULL
|
||||
OR ue.id IS NOT NULL
|
||||
OR uec.id IS NOT NULL";
|
||||
WHERE uc.usermodified = :userid1";
|
||||
$params = [
|
||||
'userlevel1' => CONTEXT_USER,
|
||||
'userlevel2' => CONTEXT_USER,
|
||||
'userid1' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . user_competency::TABLE . "} uc
|
||||
ON uc.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel1
|
||||
WHERE uc.reviewerid = :userid2";
|
||||
$params = [
|
||||
'userlevel1' => CONTEXT_USER,
|
||||
'userid2' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . user_competency::TABLE . "} uc
|
||||
ON uc.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel1
|
||||
JOIN {" . evidence::TABLE . "} e
|
||||
ON e.usercompetencyid = uc.id
|
||||
AND e.usermodified = :userid3";
|
||||
$params = [
|
||||
'userlevel1' => CONTEXT_USER,
|
||||
'userid3' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . user_competency::TABLE . "} uc
|
||||
ON uc.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel1
|
||||
JOIN {" . evidence::TABLE . "} e
|
||||
ON e.usercompetencyid = uc.id
|
||||
AND e.actionuserid = :userid4";
|
||||
$params = [
|
||||
'userlevel1' => CONTEXT_USER,
|
||||
'userid4' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . user_evidence::TABLE . "} ue
|
||||
ON ue.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel2
|
||||
AND ue.usermodified = :userid5";
|
||||
$params = [
|
||||
'userlevel2' => CONTEXT_USER,
|
||||
'userid5' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . user_evidence::TABLE . "} ue
|
||||
ON ue.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel2
|
||||
JOIN {" . user_evidence_competency::TABLE . "} uec
|
||||
ON uec.userevidenceid = ue.id
|
||||
AND uec.usermodified = :userid6";
|
||||
$params = [
|
||||
'userlevel2' => CONTEXT_USER,
|
||||
'userid6' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
@@ -360,33 +463,48 @@ class provider implements
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
LEFT JOIN {" . plan::TABLE . "} p
|
||||
JOIN {" . plan::TABLE . "} p
|
||||
ON p.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel1
|
||||
LEFT JOIN {" . user_competency::TABLE . "} uc
|
||||
ON uc.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel2
|
||||
AND uc.userid = :userid2
|
||||
LEFT JOIN {" . user_evidence::TABLE . "} ue
|
||||
ON ue.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel3
|
||||
AND ue.userid = :userid3
|
||||
LEFT JOIN {" . user_competency_course::TABLE . "} ucc
|
||||
ON ucc.courseid = ctx.instanceid
|
||||
AND ctx.contextlevel = :courselevel
|
||||
AND ucc.userid = :userid4
|
||||
WHERE p.userid = :userid1
|
||||
OR uc.id IS NOT NULL
|
||||
OR ue.id IS NOT NULL
|
||||
OR ucc.id IS NOT NULL";
|
||||
WHERE p.userid = :userid1";
|
||||
$params = [
|
||||
'userlevel1' => CONTEXT_USER,
|
||||
'userlevel2' => CONTEXT_USER,
|
||||
'userlevel3' => CONTEXT_USER,
|
||||
'courselevel' => CONTEXT_COURSE,
|
||||
'userid1' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . user_competency::TABLE . "} uc
|
||||
ON uc.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel2
|
||||
AND uc.userid = :userid2";
|
||||
$params = [
|
||||
'userlevel2' => CONTEXT_USER,
|
||||
'userid2' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . user_evidence::TABLE . "} ue
|
||||
ON ue.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel3
|
||||
AND ue.userid = :userid3";
|
||||
$params = [
|
||||
'userlevel3' => CONTEXT_USER,
|
||||
'userid3' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . user_competency_course::TABLE . "} ucc
|
||||
ON ucc.courseid = ctx.instanceid
|
||||
AND ctx.contextlevel = :courselevel
|
||||
AND ucc.userid = :userid4";
|
||||
$params = [
|
||||
'courselevel' => CONTEXT_COURSE,
|
||||
'userid4' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
@@ -46,6 +46,7 @@ use core_competency\privacy\provider;
|
||||
* @copyright 2018 Frédéric Massart
|
||||
* @author Frédéric Massart <fred@branchup.tech>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \core_competency\privacy\provider
|
||||
*/
|
||||
class provider_test extends provider_testcase {
|
||||
|
||||
@@ -666,6 +667,7 @@ class provider_test extends provider_testcase {
|
||||
$u2 = $dg->create_user();
|
||||
$u3 = $dg->create_user();
|
||||
$u4 = $dg->create_user();
|
||||
$u5 = $dg->create_user();
|
||||
|
||||
$c1ctx = \context_course::instance($c1->id);
|
||||
$u1ctx = \context_user::instance($u1->id);
|
||||
@@ -681,30 +683,45 @@ class provider_test extends provider_testcase {
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u2->id), []);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u3->id), []);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u4->id), []);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u5->id), []);
|
||||
|
||||
$ccg->create_plan(['userid' => $u1->id]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u1->id), [$u1ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u2->id), []);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u3->id), []);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u4->id), []);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u5->id), []);
|
||||
|
||||
$ccg->create_user_competency(['userid' => $u2->id, 'competencyid' => $comp1->get('id')]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u1->id), [$u1ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u2->id), [$u2ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u3->id), []);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u4->id), []);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u5->id), []);
|
||||
|
||||
$ccg->create_user_competency_course(['userid' => $u3->id, 'competencyid' => $comp1->get('id'), 'courseid' => $c1->id]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u1->id), [$u1ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u2->id), [$u2ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u3->id), [$c1ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u4->id), []);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u5->id), []);
|
||||
|
||||
$ccg->create_user_evidence(['userid' => $u4->id]);
|
||||
$ue = $ccg->create_user_evidence(['userid' => $u4->id]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u1->id), [$u1ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u2->id), [$u2ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u3->id), [$c1ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u4->id), [$u4ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u5->id), []);
|
||||
|
||||
// A user editing a context relationship.
|
||||
$this->setUser($u5);
|
||||
$ccg->create_user_evidence_competency(['userevidenceid' => $ue->get('id'), 'competencyid' => $comp1->get('id')]);
|
||||
$this->setAdminUser();
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u1->id), [$u1ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u2->id), [$u2ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u3->id), [$c1ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u4->id), [$u4ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u5->id), [$u4ctx]);
|
||||
}
|
||||
|
||||
public function test_get_users_in_context_with_actual_data_and_actual_data_is_goooood() {
|
||||
@@ -2598,7 +2615,7 @@ class provider_test extends provider_testcase {
|
||||
$expectedids = array_unique(array_map(function($item) {
|
||||
return $item instanceof \context ? $item->id : $id;
|
||||
}, $expectedcontextsorids));
|
||||
$this->assert_array_match($contextids, $expectedids);
|
||||
$this->assert_array_match($expectedids, $contextids);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -179,8 +179,10 @@ class completion_completion extends data_object {
|
||||
// Notify user.
|
||||
$course = get_course($data->course);
|
||||
$messagesubject = get_string('coursecompleted', 'completion');
|
||||
$options = new stdClass();
|
||||
$options->context = context_course::instance($course->id);
|
||||
$a = [
|
||||
'coursename' => get_course_display_name_for_list($course),
|
||||
'coursename' => format_string(get_course_display_name_for_list($course), true, $options),
|
||||
'courselink' => (string) new moodle_url('/course/view.php', array('id' => $course->id)),
|
||||
];
|
||||
$messagebody = get_string('coursecompletedmessage', 'completion', $a);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -86,3 +86,10 @@ Feature: Allow teachers to bulk edit activity completion rules in a course.
|
||||
And I should see "With conditions" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment two']]" "xpath_element"
|
||||
And I should see "Student must receive a passing grade to complete this activity" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment two']]" "xpath_element"
|
||||
And I should not see "Completion expected on" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment two']]" "xpath_element"
|
||||
|
||||
@accessibility
|
||||
Scenario: Evaluate the accessibility of the bulk edit activity completion page
|
||||
Given I am on the "Course 1" course page logged in as "teacher1"
|
||||
When I navigate to "Course completion" in current page administration
|
||||
And I set the field "Course completion tertiary navigation" to "Bulk edit activity completion"
|
||||
And the page should meet "wcag21aa" accessibility standards
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -82,3 +82,19 @@ Feature: Allow teachers to edit the default activity completion rules in a cours
|
||||
And I click on "Save changes" "button"
|
||||
Then I should see "Changes saved"
|
||||
And I should see "Student must achieve at least one of the following statuses: Completed"
|
||||
|
||||
@javascript @accessibility
|
||||
Scenario: Evaluate the accessibility of the default activity completion page
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category | enablecompletion |
|
||||
| Course 1 | C1 | 0 | 1 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | First | teacher1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
When I navigate to "Course completion" in current page administration
|
||||
And I select "Default activity completion" from the "Course completion tertiary navigation" singleselect
|
||||
And the page should meet accessibility standards
|
||||
|
||||
@@ -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"
|
||||
|
||||
+5
-5
@@ -7,12 +7,12 @@
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "9.5.*",
|
||||
"mikey179/vfsstream": "1.6.*",
|
||||
"behat/mink": "^1.10.0",
|
||||
"friends-of-behat/mink-extension": "^2.7.2",
|
||||
"behat/mink": "^1.11.0",
|
||||
"friends-of-behat/mink-extension": "^2.7.5",
|
||||
"behat/mink-goutte-driver": "^2.0",
|
||||
"symfony/process": "^4.4 || ^5.0 || ^6.0",
|
||||
"behat/behat": "3.13.*",
|
||||
"oleg-andreyev/mink-phpwebdriver": "1.2.*"
|
||||
"symfony/process": "^4.4 || ^5.0 || ^6.0 || ^7.0",
|
||||
"behat/behat": "3.14.*",
|
||||
"oleg-andreyev/mink-phpwebdriver": "1.3.*"
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-0": {
|
||||
|
||||
Generated
+193
-200
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -298,10 +298,11 @@ $CFG->admin = 'admin';
|
||||
// '/dataroot/' => $CFG->dataroot,
|
||||
// '/cachedir/' => '/var/www/moodle/cache', // for custom $CFG->cachedir locations
|
||||
// '/localcachedir/' => '/var/local/cache', // for custom $CFG->localcachedir locations
|
||||
// '/localrequestdir/' => '/tmp', // for custom $CFG->localrequestdir locations
|
||||
// '/tempdir/' => '/var/www/moodle/temp', // for custom $CFG->tempdir locations
|
||||
// '/filedir' => '/var/www/moodle/filedir', // for custom $CFG->filedir locations
|
||||
// );
|
||||
// Please note: It is *not* possible to use X-Sendfile with the per-request directory.
|
||||
// The directory is highly likely to have been deleted by the time the web server sends the file.
|
||||
//
|
||||
// YUI caching may be sometimes improved by slasharguments:
|
||||
// $CFG->yuislasharguments = 1;
|
||||
|
||||
@@ -62,7 +62,7 @@ Feature: Content bank use editor feature
|
||||
And I switch to "h5p-editor-iframe" class iframe
|
||||
And I switch to the main frame
|
||||
And I click on "Cancel" "button"
|
||||
And I should see "filltheblanks.h5p" in the "h2" "css_element"
|
||||
And "filltheblanks.h5p" "heading" should exist
|
||||
|
||||
Scenario: Users can create new content if they have the required permission
|
||||
Given I navigate to "H5P > Manage H5P content types" in site administration
|
||||
@@ -116,7 +116,7 @@ Feature: Content bank use editor feature
|
||||
And I set the field "Title" to "New title"
|
||||
And I switch to the main frame
|
||||
When I click on "Save" "button"
|
||||
And I should see "filltheblanks.h5p" in the "h2" "css_element"
|
||||
And "filltheblanks.h5p" "heading" should exist
|
||||
And I click on "Edit" "link"
|
||||
And I switch to "h5p-editor-iframe" class iframe
|
||||
Then the field "Title" matches value "New title"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user