Compare commits
213 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc01a52823 | |||
| d84283e8cc | |||
| 9f1768c098 | |||
| 4babd8cdb7 | |||
| 312318201b | |||
| 3d9ac7137e | |||
| 838e230d54 | |||
| c26ac2e85d | |||
| ee3504f673 | |||
| 31256fae74 | |||
| 0c91f5a454 | |||
| 1ab21607a5 | |||
| 05fc7360c9 | |||
| 4c3211871d | |||
| 95f7d16801 | |||
| 400b2086ff | |||
| 7ef99eb8cf | |||
| ab4b2404c7 | |||
| d859c2875c | |||
| d14ff96dfa | |||
| bc53f961e7 | |||
| 91b2170fdb | |||
| fadadf8a64 | |||
| 65be624c43 | |||
| 3e5d4ba208 | |||
| dfea1a4f0d | |||
| 2409994d32 | |||
| 05dd1161a4 | |||
| 7ed61f310b | |||
| 15b8b5cdf0 | |||
| e6eb619c08 | |||
| 8826294d5d | |||
| cf2780d5c2 | |||
| 154bf3d9f5 | |||
| 768d683beb | |||
| 7f8aca035d | |||
| 8db1cdf462 | |||
| 48dc7ef01e | |||
| d782e76640 | |||
| e693c811f0 | |||
| d2f37aab56 | |||
| 8d08094ac3 | |||
| 8483f7adef | |||
| 4cffe42b56 | |||
| 84935b9585 | |||
| bf8f9164c0 | |||
| 64b5af4b58 | |||
| 959721b58a | |||
| 746fa817c1 | |||
| cfce223e9b | |||
| 375d8ffb56 | |||
| f89a47eff8 | |||
| 6a16596f78 | |||
| 7e22b75c28 | |||
| 7d547f726f | |||
| 75c9e608ce | |||
| fa8591ea3e | |||
| ae8c5e16e9 | |||
| 4fe95b0e61 | |||
| 5425233b37 | |||
| dcc561762b | |||
| 6307f8dd89 | |||
| 1a3e679835 | |||
| 6cfdd1bbae | |||
| 56a368facf | |||
| 4842ae967c | |||
| 6f62e05a96 | |||
| 6f23e028dd | |||
| ff013438d6 | |||
| 3e0941fb5b | |||
| e667c191f8 | |||
| 4566a44f14 | |||
| 8b2c0d969d | |||
| 32d337a934 | |||
| 4e1d36c8bc | |||
| 87c2aae85c | |||
| 8c6831f0fe | |||
| 5dbc672825 | |||
| b97d313c11 | |||
| ee0b0b4fc8 | |||
| 5b707aa422 | |||
| 519ba9e103 | |||
| c3a318e16a | |||
| faf63d076d | |||
| b1fe51cf0d | |||
| 651a97efc0 | |||
| cd8af2d23a | |||
| 8b75bb1772 | |||
| b672263e04 | |||
| 1b506c9639 | |||
| c463d72821 | |||
| 162053448e | |||
| 2034d440af | |||
| 70f7efe0fe | |||
| f1447db751 | |||
| 7fc01a94e2 | |||
| 568e72e7dc | |||
| 5d4db3dce3 | |||
| e5c7b50d85 | |||
| 9fda41582c | |||
| bddacfd5d7 | |||
| f3d9759375 | |||
| 1d776476b6 | |||
| fca9778023 | |||
| 482de4e370 | |||
| 91f43dd47f | |||
| fa9ba48533 | |||
| 141657a483 | |||
| c8be5a9ab3 | |||
| 507f737266 | |||
| 6d75761b94 | |||
| 52a9113c6d | |||
| 94f703e1d1 | |||
| d9ceef45de | |||
| 1ad332bbde | |||
| d23efa1ff2 | |||
| bf1ebf3b44 | |||
| 33e9b8c394 | |||
| 5bb0f00c0e | |||
| 670b636c47 | |||
| e48460c8cc | |||
| ad5bc5b5ca | |||
| 013cb2fb64 | |||
| c4df60bb8d | |||
| 786a4956dd | |||
| 323e272c85 | |||
| 6e769797fd | |||
| 0a48ddad8f | |||
| c939cd4b6d | |||
| fd27acfcb1 | |||
| 9ae0e4a58b | |||
| 874472a85d | |||
| 5f893f890f | |||
| 69e03eacba | |||
| d369112db4 | |||
| 62d603a054 | |||
| 4cb39164fa | |||
| d5e8561011 | |||
| 944455bcce | |||
| ee888615ea | |||
| 3f58d35337 | |||
| 6983a17fc0 | |||
| ce2d88a951 | |||
| 5a4f73d144 | |||
| 1c8396e2b5 | |||
| 5668055015 | |||
| 1dfd13841a | |||
| 405cd4fb35 | |||
| 2f4b6bd2d7 | |||
| acf70635c6 | |||
| 5d15e96a98 | |||
| e3b1e103bd | |||
| 8d1867cdcc | |||
| 2cfa9817b1 | |||
| 641b8c31ee | |||
| 6ce1d39f4e | |||
| 9d4fb0a3ad | |||
| 4110054b51 | |||
| dd4d110c9b | |||
| 017f8ff6bf | |||
| 1ff7beef8f | |||
| 35783784b3 | |||
| 194ca4970a | |||
| d4f3852dd2 | |||
| 3fbbc7a86b | |||
| 7cff4e8bc1 | |||
| 827803ea3d | |||
| ccb639002f | |||
| df62f09a4c | |||
| 7c0a982958 | |||
| 940e4da0fa | |||
| ae531ae852 | |||
| 84d3ae1b08 | |||
| d51f307ffa | |||
| a4018fbcdd | |||
| 2dd34c5ae4 | |||
| 71f83af13c | |||
| d587d8b242 | |||
| f1b1570b02 | |||
| f9ca67a78d | |||
| c8f9140e70 | |||
| 47c211ba0e | |||
| 9f270d956e | |||
| 32ee8e3d34 | |||
| 77a1413fa0 | |||
| 14760d9787 | |||
| 03b6b93589 | |||
| 47402416fd | |||
| 0087cede97 | |||
| 264c405192 | |||
| 1742e0c27c | |||
| 7371cb4838 | |||
| c1512e8d89 | |||
| e5aaeed609 | |||
| a950ed04a7 | |||
| 964c98c8d9 | |||
| 635389b235 | |||
| 1385e3bc09 | |||
| 2dc581f717 | |||
| 4f8369c3f1 | |||
| 730b405d68 | |||
| 87c04f1e0b | |||
| f4df13fb6c | |||
| 4a8abf77ac | |||
| 7e48ec1820 | |||
| 05087afd40 | |||
| 56afed9678 | |||
| af8c168f78 | |||
| 4c52746b41 | |||
| 7768a35366 | |||
| 05847affc1 | |||
| f652a2b613 | |||
| 04d4cbde78 |
@@ -13,14 +13,14 @@ env:
|
||||
|
||||
jobs:
|
||||
Grunt:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checking out code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Configuring node & npm
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
@@ -54,10 +54,10 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-18.04
|
||||
- os: ubuntu-22.04
|
||||
php: 7.2
|
||||
db: mysqli
|
||||
- os: ubuntu-18.04
|
||||
- os: ubuntu-22.04
|
||||
php: 7.4
|
||||
db: pgsql
|
||||
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
postgresql password: test
|
||||
|
||||
- name: Configuring git vars
|
||||
uses: rlespinasse/github-slug-action@v3.x
|
||||
uses: rlespinasse/github-slug-action@v4
|
||||
|
||||
- name: Setting up PHP ${{ matrix.php }}
|
||||
uses: shivammathur/setup-php@v2
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
coverage: none
|
||||
|
||||
- name: Checking out code from ${{ env.GITHUB_REF_SLUG }}
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setting up PHPUnit
|
||||
env:
|
||||
|
||||
+2
-2
@@ -36,7 +36,7 @@ jobs:
|
||||
- php: 7.2
|
||||
env: DB=none TASK=CITEST
|
||||
- php: 7.2
|
||||
env: DB=none TASK=GRUNT NVM_VERSION='lts/carbon'
|
||||
env: DB=none TASK=GRUNT NVM_VERSION='lts/gallium'
|
||||
|
||||
- if: env(MOODLE_DATABASE) = "pgsql" OR env(MOODLE_DATABASE) = "all" OR env(MOODLE_DATABASE) IS NOT present
|
||||
php: 7.2
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
env: DB=none TASK=CITEST
|
||||
- if: env(MOODLE_PHP) = "all"
|
||||
php: 7.4
|
||||
env: DB=none TASK=GRUNT NVM_VERSION='lts/carbon'
|
||||
env: DB=none TASK=GRUNT NVM_VERSION='lts/gallium'
|
||||
|
||||
- if: env(MOODLE_PHP) = "all" AND (env(MOODLE_DATABASE) = "pgsql" OR env(MOODLE_DATABASE) = "all" OR env(MOODLE_DATABASE) IS NOT present)
|
||||
php: 7.4
|
||||
|
||||
@@ -3371,6 +3371,7 @@
|
||||
<VENDOR name="oracle" version="11.2" />
|
||||
</DATABASE>
|
||||
<PHP version="7.3.0" level="required">
|
||||
<RESTRICT function="restrict_php_version_81" message="unsupportedphpversion81" />
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
@@ -3539,6 +3540,8 @@
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_max_input_vars" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_xmlrpc_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="4.0" requires="3.6">
|
||||
@@ -3555,6 +3558,7 @@
|
||||
<VENDOR name="oracle" version="11.2" />
|
||||
</DATABASE>
|
||||
<PHP version="7.3.0" level="required">
|
||||
<RESTRICT function="restrict_php_version_81" message="unsupportedphpversion81" />
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
@@ -3729,6 +3733,195 @@
|
||||
<ON_CHECK message="iscustomadminwarnings" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_xmlrpc_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="4.1" requires="3.9">
|
||||
<UNICODE level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unicoderequired" />
|
||||
</FEEDBACK>
|
||||
</UNICODE>
|
||||
<DATABASE level="required">
|
||||
<VENDOR name="mariadb" version="10.4" />
|
||||
<VENDOR name="mysql" version="5.7" />
|
||||
<VENDOR name="postgres" version="12" />
|
||||
<VENDOR name="mssql" version="14.0" />
|
||||
<VENDOR name="oracle" version="19" />
|
||||
</DATABASE>
|
||||
<PHP version="7.4.0" level="required">
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="pcreunicodewarning" />
|
||||
</FEEDBACK>
|
||||
</PCREUNICODE>
|
||||
<PHP_EXTENSIONS>
|
||||
<PHP_EXTENSION name="iconv" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="iconvrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="mbstring" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="mbstringrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="curl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="curlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="openssl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="opensslrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="tokenizer" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="tokenizerrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="soap" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="soaprecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="ctype" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ctyperequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zip" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ziprequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zlib" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="gd" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="gdrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="simplexml" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="simplexmlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="spl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="splrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="pcre" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="dom" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xml" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xmlreader" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="intl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="intlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="json" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="hash" level="required"/>
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="optional"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="settingmemorylimit" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="file_uploads" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="settingfileuploads" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="opcache.enable" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="opcacherecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
</PHP_SETTINGS>
|
||||
<CUSTOM_CHECKS>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbstorageengine" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="quizattemptsupgradedmessage" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="slashargumentswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unsupporteddbtablerowformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unoconvwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="libcurlwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfileformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfilepertable" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddblargeprefix" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="ishttpswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="incompleteunicodesupport" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="sixtyfourbitswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_max_input_vars" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_admin_dir_usage" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="iscustomadminwarnings" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_xmlrpc_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
</COMPATIBILITY_MATRIX>
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
define("tool_behat/steps",["exports","core/ajax","core/templates","core/pending"],(function(_exports,_ajax,_templates,_pending){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function asyncGeneratorStep(gen,resolve,reject,_next,_throw,key,arg){try{var info=gen[key](arg),value=info.value}catch(error){return void reject(error)}info.done?resolve(value):Promise.resolve(value).then(_next,_throw)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_templates=_interopRequireDefault(_templates),_pending=_interopRequireDefault(_pending);
|
||||
/**
|
||||
* Enhancements for the step definitions page.
|
||||
*
|
||||
* @module tool_behat/steps
|
||||
* @copyright 2022 Catalyst IT EU
|
||||
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
var getRequiredFieldsContent=function(entityData){var _entityData$required;return null!==(_entityData$required=entityData.required)&&void 0!==_entityData$required&&_entityData$required.length?_templates.default.renderForPromise("tool_behat/steprequiredfields",{fields:entityData.required}):Promise.resolve({html:"",js:""})};_exports.init=function(){var fn,_ref;document.addEventListener("change",(fn=regeneratorRuntime.mark((function _callee(e){var entityElement,stepElement,pendingPromise,entityData,_yield$getRequiredFie,html,js,stepRequiredFields;return regeneratorRuntime.wrap((function(_context){for(;;)switch(_context.prev=_context.next){case 0:if(entityElement=e.target.closest(".entities"),stepElement=e.target.closest(".stepcontent"),entityElement&&stepElement){_context.next=4;break}return _context.abrupt("return");case 4:return pendingPromise=new _pending.default("tool_behat/steps:change"),_context.next=7,entityType=e.target.value,_ajax.default.call([{methodname:"tool_behat_get_entity_generator",args:{entitytype:entityType}}])[0];case 7:return entityData=_context.sent,_context.next=10,getRequiredFieldsContent(entityData);case 10:if(_yield$getRequiredFie=_context.sent,html=_yield$getRequiredFie.html,js=_yield$getRequiredFie.js,!(stepRequiredFields=stepElement.querySelector(".steprequiredfields"))){_context.next=19;break}return _context.next=17,_templates.default.replaceNode(stepRequiredFields,html,js);case 17:_context.next=21;break;case 19:return _context.next=21,_templates.default.appendNodeContents(stepElement,html,js);case 21:pendingPromise.resolve();case 22:case"end":return _context.stop()}var entityType}),_callee)})),_ref=function(){var self=this,args=arguments;return new Promise((function(resolve,reject){var gen=fn.apply(self,args);function _next(value){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"next",value)}function _throw(err){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"throw",err)}_next(void 0)}))},function(_x){return _ref.apply(this,arguments)}))}}));
|
||||
|
||||
//# sourceMappingURL=steps.min.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"steps.min.js","sources":["../src/steps.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\nimport Ajax from 'core/ajax';\nimport Templates from 'core/templates';\nimport PendingJS from 'core/pending';\n\n/**\n * Enhancements for the step definitions page.\n *\n * @module tool_behat/steps\n * @copyright 2022 Catalyst IT EU\n * @author Mark Johnson <mark.johnson@catalyst-eu.net>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * Call the get_entity_generator web service function\n *\n * Takes the name of an entity generator and returns an object containing a list of the required fields.\n *\n * @param {String} entityType\n * @returns {Promise}\n */\nconst getGeneratorEntities = (entityType) => Ajax.call([{\n methodname: 'tool_behat_get_entity_generator',\n args: {entitytype: entityType}\n}])[0];\n\n/**\n * Render HTML for required fields\n *\n * Takes the entity data returned from getGeneratorEntities and renders the HTML to display the required fields.\n *\n * @param {String} entityData\n * @return {Promise}\n */\nconst getRequiredFieldsContent = (entityData) => {\n if (!entityData.required?.length) {\n return Promise.resolve({\n html: '',\n js: ''\n });\n }\n return Templates.renderForPromise('tool_behat/steprequiredfields', {fields: entityData.required});\n};\n\nexport const init = () => {\n // When an entity is selected in the \"the following exist\" step, fetch and display the required fields.\n document.addEventListener('change', async(e) => {\n const entityElement = e.target.closest('.entities');\n const stepElement = e.target.closest('.stepcontent');\n if (!entityElement || !stepElement) {\n return;\n }\n\n const pendingPromise = new PendingJS('tool_behat/steps:change');\n\n const entityData = await getGeneratorEntities(e.target.value);\n const {html, js} = await getRequiredFieldsContent(entityData);\n\n const stepRequiredFields = stepElement.querySelector('.steprequiredfields');\n if (stepRequiredFields) {\n await Templates.replaceNode(stepRequiredFields, html, js);\n } else {\n await Templates.appendNodeContents(stepElement, html, js);\n }\n pendingPromise.resolve();\n });\n};\n"],"names":["getRequiredFieldsContent","entityData","required","_entityData$required","length","Templates","renderForPromise","fields","Promise","resolve","html","js","document","addEventListener","e","entityElement","target","closest","stepElement","pendingPromise","PendingJS","entityType","value","Ajax","call","methodname","args","entitytype","stepRequiredFields","querySelector","replaceNode","appendNodeContents"],"mappings":";;;;;;;;;IAiDMA,yBAA2B,SAACC,yEACzBA,WAAWC,0CAAXC,qBAAqBC,OAMnBC,mBAAUC,iBAAiB,gCAAiC,CAACC,OAAQN,WAAWC,WAL5EM,QAAQC,QAAQ,CACnBC,KAAM,GACNC,GAAI,oBAMI,uBAEhBC,SAASC,iBAAiB,sCAAU,iBAAMC,oNAChCC,cAAgBD,EAAEE,OAAOC,QAAQ,aACjCC,YAAcJ,EAAEE,OAAOC,QAAQ,gBAChCF,eAAkBG,kFAIjBC,eAAiB,IAAIC,iBAAU,2CAhCfC,WAkCwBP,EAAEE,OAAOM,MAlClBC,cAAKC,KAAK,CAAC,CACpDC,WAAY,kCACZC,KAAM,CAACC,WAAYN,eACnB,iBA+BUpB,0CACmBD,yBAAyBC,2DAA3CS,2BAAAA,KAAMC,yBAAAA,KAEPiB,mBAAqBV,YAAYW,cAAc,wEAE3CxB,mBAAUyB,YAAYF,mBAAoBlB,KAAMC,mEAEhDN,mBAAU0B,mBAAmBb,YAAaR,KAAMC,YAE1DQ,eAAeV,mDA3CM,IAACY"}
|
||||
@@ -0,0 +1,82 @@
|
||||
// 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/>.
|
||||
|
||||
import Ajax from 'core/ajax';
|
||||
import Templates from 'core/templates';
|
||||
import PendingJS from 'core/pending';
|
||||
|
||||
/**
|
||||
* Enhancements for the step definitions page.
|
||||
*
|
||||
* @module tool_behat/steps
|
||||
* @copyright 2022 Catalyst IT EU
|
||||
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* Call the get_entity_generator web service function
|
||||
*
|
||||
* Takes the name of an entity generator and returns an object containing a list of the required fields.
|
||||
*
|
||||
* @param {String} entityType
|
||||
* @returns {Promise}
|
||||
*/
|
||||
const getGeneratorEntities = (entityType) => Ajax.call([{
|
||||
methodname: 'tool_behat_get_entity_generator',
|
||||
args: {entitytype: entityType}
|
||||
}])[0];
|
||||
|
||||
/**
|
||||
* Render HTML for required fields
|
||||
*
|
||||
* Takes the entity data returned from getGeneratorEntities and renders the HTML to display the required fields.
|
||||
*
|
||||
* @param {String} entityData
|
||||
* @return {Promise}
|
||||
*/
|
||||
const getRequiredFieldsContent = (entityData) => {
|
||||
if (!entityData.required?.length) {
|
||||
return Promise.resolve({
|
||||
html: '',
|
||||
js: ''
|
||||
});
|
||||
}
|
||||
return Templates.renderForPromise('tool_behat/steprequiredfields', {fields: entityData.required});
|
||||
};
|
||||
|
||||
export const init = () => {
|
||||
// When an entity is selected in the "the following exist" step, fetch and display the required fields.
|
||||
document.addEventListener('change', async(e) => {
|
||||
const entityElement = e.target.closest('.entities');
|
||||
const stepElement = e.target.closest('.stepcontent');
|
||||
if (!entityElement || !stepElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingPromise = new PendingJS('tool_behat/steps:change');
|
||||
|
||||
const entityData = await getGeneratorEntities(e.target.value);
|
||||
const {html, js} = await getRequiredFieldsContent(entityData);
|
||||
|
||||
const stepRequiredFields = stepElement.querySelector('.steprequiredfields');
|
||||
if (stepRequiredFields) {
|
||||
await Templates.replaceNode(stepRequiredFields, html, js);
|
||||
} else {
|
||||
await Templates.appendNodeContents(stepElement, html, js);
|
||||
}
|
||||
pendingPromise.resolve();
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Return data about an entity generator.
|
||||
*
|
||||
* @package tool_behat
|
||||
* @copyright 2022 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace tool_behat\external;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->libdir . '/externallib.php');
|
||||
|
||||
/**
|
||||
* External function for getting properties of entity generators.
|
||||
*/
|
||||
class get_entity_generator extends \external_api {
|
||||
|
||||
/**
|
||||
* Define parameters for external function.
|
||||
*
|
||||
* The parameter is either in the format 'entity' or 'component_name > entity'. There is no appropriate param type for a
|
||||
* string like this containing angle brackets, so we will do PARAM_RAW. The value will be parsed by
|
||||
* behat_data_generators::parse_entity_type, which validates the format of the parameter and throws an exception if it is not
|
||||
* correct.
|
||||
*
|
||||
* @return \external_function_parameters
|
||||
*/
|
||||
public static function execute_parameters(): \external_function_parameters {
|
||||
return new \external_function_parameters([
|
||||
'entitytype' => new \external_value(PARAM_RAW, 'Entity type that can be created by a generator.'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of the required fields for a given entity type.
|
||||
*
|
||||
* @param string $entitytype
|
||||
* @return array
|
||||
*/
|
||||
public static function execute(string $entitytype): array {
|
||||
global $CFG;
|
||||
|
||||
// Ensure we can load Behat and Facebook namespaces in behat libraries.
|
||||
require_once("{$CFG->dirroot}/vendor/autoload.php");
|
||||
require_once("{$CFG->libdir}/tests/behat/behat_data_generators.php");
|
||||
|
||||
$params = self::validate_parameters(self::execute_parameters(), ['entitytype' => $entitytype]);
|
||||
$context = \context_system::instance();
|
||||
self::validate_context($context);
|
||||
require_capability('moodle/site:config', $context);
|
||||
|
||||
$generators = new \behat_data_generators();
|
||||
$entity = $generators->get_entity($params['entitytype']);
|
||||
return ['required' => $entity['required']];
|
||||
}
|
||||
|
||||
/**
|
||||
* Define return values.
|
||||
*
|
||||
* Return required fields
|
||||
*
|
||||
* @return \external_single_structure
|
||||
*/
|
||||
public static function execute_returns(): \external_single_structure {
|
||||
return new \external_single_structure([
|
||||
'required' => new \external_multiple_structure(
|
||||
new \external_value(PARAM_TEXT, 'Required field'),
|
||||
'Required fields',
|
||||
VALUE_OPTIONAL
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Define web service functions for tool_behat
|
||||
*
|
||||
* @package tool_behat
|
||||
* @copyright 2022 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$functions = [
|
||||
'tool_behat_get_entity_generator' => [
|
||||
'classname' => 'tool_behat\\external\\get_entity_generator',
|
||||
'methodname' => 'execute',
|
||||
'description' => 'Get the generator details for an entity',
|
||||
'type' => 'read',
|
||||
'ajax' => true,
|
||||
'capabilities' => 'moodle/site:config'
|
||||
]
|
||||
];
|
||||
@@ -56,6 +56,7 @@ if ($components) {
|
||||
$form = new steps_definitions_form(null, array('components' => $componentswithsteps));
|
||||
|
||||
// Output contents.
|
||||
$PAGE->requires->js_call_amd('tool_behat/steps', 'init');
|
||||
$renderer = $PAGE->get_renderer('tool_behat');
|
||||
echo $renderer->render_stepsdefinitions($steps, $form);
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->libdir . '/behat/classes/behat_generator_base.php');
|
||||
|
||||
/**
|
||||
* Renderer for behat tool web features
|
||||
*
|
||||
@@ -106,6 +108,32 @@ class tool_behat_renderer extends plugin_renderer_base {
|
||||
},
|
||||
$stepsdefinitions
|
||||
);
|
||||
|
||||
$elementstrings = [];
|
||||
$count = 1;
|
||||
$stepsdefinitions = preg_replace_callback('/(the following ")ELEMENT\d?_STRING(" exist:)/',
|
||||
function($matches) use (&$elementstrings, &$count) {
|
||||
// Replace element type arguments with a user-friendly select.
|
||||
if (empty($elementstrings)) {
|
||||
$behatgenerators = new behat_data_generators();
|
||||
$componententities = $behatgenerators->get_all_entities();
|
||||
ksort($componententities);
|
||||
$elementstrings = [];
|
||||
foreach ($componententities as $component => $entities) {
|
||||
asort($entities);
|
||||
foreach ($entities as $entity) {
|
||||
$string = ($component === 'core') ? $entity : $component . ' > ' . $entity;
|
||||
$elementstrings[$string] = $string;
|
||||
}
|
||||
}
|
||||
}
|
||||
$select = html_writer::select($elementstrings, 'entities' . $count, '', ['' => 'choosedots'],
|
||||
['class' => 'entities']);
|
||||
$count++;
|
||||
return $matches[1] . $select . $matches[2];
|
||||
},
|
||||
$stepsdefinitions
|
||||
);
|
||||
}
|
||||
|
||||
// Steps definitions.
|
||||
|
||||
@@ -29,3 +29,9 @@
|
||||
#page-admin-tool-behat-index .steps-definitions .stepregex {
|
||||
color: #060;
|
||||
}
|
||||
|
||||
#page-admin-tool-behat-index .steprequiredfields {
|
||||
font-weight: bold;
|
||||
font-size: 1em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{{!
|
||||
This file is part of Moodle - https://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template tool_behat/steprequiredfields
|
||||
|
||||
Display a Gherkin-style table row showing required columns for a step.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"fields": [
|
||||
"user",
|
||||
"role",
|
||||
"context"
|
||||
]
|
||||
}
|
||||
}}
|
||||
<pre class="steprequiredfields">
|
||||
|{{#fields}} {{{.}}} |{{/fields}}
|
||||
</pre>
|
||||
@@ -7,8 +7,8 @@ Feature: Behat steps for interacting with form work
|
||||
@javascript
|
||||
Scenario: Test fields in containers
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
| fullname | shortname | summary | summaryformat |
|
||||
| Course 1 | C1 | Red | 1 |
|
||||
When I log in as "admin"
|
||||
And I am on "Course 1" course homepage
|
||||
# Just get to any form.
|
||||
@@ -17,11 +17,15 @@ Feature: Behat steps for interacting with form work
|
||||
And I set the following fields in the "Appearance" "fieldset" to these values:
|
||||
| Show activity reports | Yes |
|
||||
| Number of announcements | 1 |
|
||||
And I set the following fields in the "Description" "fieldset" to these values:
|
||||
| Course summary | Green |
|
||||
Then the field "Show activity reports" in the "Appearance" "fieldset" matches value "Yes"
|
||||
And the field "Show activity reports" in the "Appearance" "fieldset" does not match value "No"
|
||||
And the following fields in the "region-main" "region" match these values:
|
||||
| Course full name | Frog |
|
||||
| Number of announcements | 1 |
|
||||
| Course summary | Green |
|
||||
And the following fields in the "region-main" "region" do not match these values:
|
||||
| Course full name | Course 1 |
|
||||
| Number of announcements | 5 |
|
||||
| Course summary | Red |
|
||||
|
||||
@@ -13,6 +13,10 @@ Feature: List the system steps definitions
|
||||
Scenario: Accessing the list
|
||||
Then I should see "Step definitions"
|
||||
And I should not see "There aren't steps definitions matching this filter"
|
||||
And I should not see "the following \"ELEMENT_STRING\" exist:"
|
||||
And "entities1" "select" should exist
|
||||
And the "entities1" select box should contain "users"
|
||||
And the "entities1" select box should contain "mod_assign > submissions"
|
||||
|
||||
@javascript
|
||||
Scenario: Filtering by type
|
||||
@@ -34,3 +38,11 @@ Feature: List the system steps definitions
|
||||
Then I should not see "There aren't steps definitions matching this filter"
|
||||
And I should see "Checks the provided element and selector type exists in the current page."
|
||||
And I should see "Checks that an element and selector type exists in another element and selector type on the current page."
|
||||
|
||||
@javascript
|
||||
Scenario: Get required fields
|
||||
Given I set the field "entities1" to "users"
|
||||
And I should see "| username |"
|
||||
When I set the field "entities1" to "mod_quiz > user overrides"
|
||||
Then I should not see "| username |"
|
||||
And I should see "| quiz | user |"
|
||||
|
||||
@@ -20,9 +20,10 @@ Feature: Forms manipulation
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Quiz" to section "1"
|
||||
And the following "activities" exist:
|
||||
| activity | course | section | name |
|
||||
| quiz | C1 | 1 | Quiz 1 |
|
||||
And I am on the "Quiz 1" "quiz activity editing" page logged in as admin
|
||||
When I expand all fieldsets
|
||||
Then I should see "Close the quiz"
|
||||
And I should see "Group mode"
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Unit tests for get_entity_generator web service
|
||||
*
|
||||
* @package tool_behat
|
||||
* @copyright 2022 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace tool_behat\external;
|
||||
|
||||
/**
|
||||
* Tests for get_entity_generator web service
|
||||
*
|
||||
* @covers \tool_behat\external\get_entity_generator
|
||||
*/
|
||||
class get_entity_generator_test extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* Log in as admin
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setUp(): void {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the generator for a core entity.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_execute_core_entity(): void {
|
||||
$generator = get_entity_generator::execute('users');
|
||||
$this->assertEquals(['required' => ['username']], $generator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the generator for the plugin entity.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_execute_plugin_entity(): void {
|
||||
$generator = get_entity_generator::execute('mod_book > chapters');
|
||||
$this->assertEquals(['required' => ['book', 'title', 'content']], $generator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the generator for an entity with no required fields.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_execute_no_requried(): void {
|
||||
$generator = get_entity_generator::execute('mod_forum > posts');
|
||||
$this->assertEquals(['required' => []], $generator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to get the generator for a core entity that does not exist.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_execute_invalid_entity(): void {
|
||||
$this->expectException('coding_exception');
|
||||
get_entity_generator::execute('foo');
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to get a generator form a plugin that does not exist.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_execute_invalid_plugin(): void {
|
||||
$this->expectException('coding_exception');
|
||||
get_entity_generator::execute('foo > bar');
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to get a generator for an entity that does not exist, from a plugin that does.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_execute_invalid_plugin_entity(): void {
|
||||
$this->expectException('coding_exception');
|
||||
get_entity_generator::execute('mod_book > bar');
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2020061500; // The current plugin version (Date: YYYYMMDDXX)
|
||||
$plugin->version = 2020061501; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2020060900; // Requires this Moodle version
|
||||
$plugin->component = 'tool_behat'; // Full name of the plugin (used for diagnostics)
|
||||
|
||||
@@ -79,6 +79,19 @@ class locale {
|
||||
* @return string|false Returns the new current locale, or FALSE on error.
|
||||
*/
|
||||
protected function set_locale(int $category = LC_ALL, string $locale = '0') {
|
||||
return setlocale($category, $locale);
|
||||
if (strlen($locale) <= 255 || PHP_OS_FAMILY === 'BSD' || PHP_OS_FAMILY === 'Darwin') {
|
||||
// We can set the whole locale all together.
|
||||
return setlocale($category, $locale);
|
||||
}
|
||||
|
||||
// Too long locale with linux or windows, let's split it into known and supported categories.
|
||||
$split = explode(';', $locale);
|
||||
foreach ($split as $element) {
|
||||
[$category, $value] = explode('=', $element);
|
||||
if (defined($category)) { // Only if the category exists, there are OS differences.
|
||||
setlocale(constant($category), $value);
|
||||
}
|
||||
}
|
||||
return setlocale(LC_ALL, 0); // Finally, return the complete configured locale.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
/**
|
||||
* Tests for \tool_langimport\locale class.
|
||||
*
|
||||
* @coversDefaultClass \tool_langimport\locale
|
||||
* @copyright 2018 Université Rennes 2 {@link https://www.univ-rennes2.fr}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
@@ -34,6 +35,7 @@ class locale_testcase extends \advanced_testcase {
|
||||
/**
|
||||
* Test that \tool_langimport\locale::check_locale_availability() works as expected.
|
||||
*
|
||||
* @covers ::check_locale_availability
|
||||
* @return void
|
||||
*/
|
||||
public function test_check_locale_availability() {
|
||||
@@ -68,4 +70,100 @@ class locale_testcase extends \advanced_testcase {
|
||||
$this->expectException(coding_exception::class);
|
||||
$locale->check_locale_availability('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test \tool_langimport\locale::set_locale() own logic.
|
||||
*
|
||||
* We have to explicitly test set_locale() own logic and results,
|
||||
* that effectively sets the current locale, so we need to restore
|
||||
* the original locale after every test (ugly, from a purist unit test
|
||||
* point of view, but needed).
|
||||
*
|
||||
* @dataProvider set_locale_provider
|
||||
* @covers ::set_locale
|
||||
*
|
||||
* @param string $set locale string to be set.
|
||||
* @param string $ret expected results returned after setting the locale.
|
||||
*/
|
||||
public function test_set_locale(string $set, string $ret) {
|
||||
// Make set_locale() public.
|
||||
$loc = new \tool_langimport\locale();
|
||||
$rc = new \ReflectionClass(\tool_langimport\locale::class);
|
||||
$rm = $rc->getMethod('set_locale');
|
||||
$rm->setAccessible(true);
|
||||
|
||||
// Capture current locale for later restore (funnily, using the set_locale() method itself.
|
||||
$originallocale = $rm->invokeArgs($loc, [LC_ALL, 0]);
|
||||
|
||||
// Assert we get the locale defined as expected.
|
||||
$this->assertEquals($ret, $rm->invokeArgs($loc, [LC_ALL, $set]));
|
||||
|
||||
// We have finished, restore the original locale, so this doesn't affect other tests at distance.
|
||||
// (again, funnily, using the very same set_locale() method).
|
||||
$rm->invokeArgs($loc, [LC_ALL, $originallocale]);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_set_locale().
|
||||
*
|
||||
* Provides a locale to be set (as 'set') and a expected return value (as 'ret'). Note that
|
||||
* some of the locales are OS dependent, so only the ones matching the OS will be provided.
|
||||
*
|
||||
* We make extensive use of the en_AU.UTF-8/English_Australia.1252 locale that is mandatory to
|
||||
* be installed in any system running PHPUnit tests.
|
||||
*/
|
||||
public function set_locale_provider(): array {
|
||||
// Let's list the allowed categories by OS.
|
||||
$bsdallowed = ['LC_COLLATE', 'LC_CTYPE', 'LC_MESSAGES', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME'];
|
||||
$winallowed = ['LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME'];
|
||||
$linuxallowed = [
|
||||
'LC_COLLATE', 'LC_CTYPE', 'LC_MESSAGES', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME',
|
||||
'LC_PAPER', 'LC_NAME', 'LC_ADDRESS', 'LC_TELEPHONE', 'LC_MEASUREMENT', 'LC_IDENTIFICATION'
|
||||
];
|
||||
|
||||
// The base locale name is also OS dependent.
|
||||
$baselocale = get_string('locale', 'langconfig');
|
||||
if (PHP_OS_FAMILY === 'Windows') {
|
||||
$baselocale = get_string('localewin', 'langconfig');
|
||||
}
|
||||
|
||||
// Here we'll go accumulating cases to be provided.
|
||||
$cases = [];
|
||||
|
||||
// First, the simplest case, just pass a locale name, without categories.
|
||||
$cases['rawlocale'] = [
|
||||
'set' => $baselocale,
|
||||
'ret' => $baselocale,
|
||||
];
|
||||
|
||||
// Now, let's fill ALL LC categories, we should get back the locale name if all them are set with same value.
|
||||
// Note that this case is the one that, under Linux only, covers the changes performed to the set_locale() method.
|
||||
// Pick the correct categories depending on the OS.
|
||||
$oscategories = $bsdallowed; // Default to BSD/Dawrwin ones because they are the standard 6 supported by PHP.
|
||||
if (PHP_OS_FAMILY === 'Windows') {
|
||||
$oscategories = $winallowed;
|
||||
} else if (PHP_OS_FAMILY === 'Linux') {
|
||||
$oscategories = $linuxallowed;
|
||||
}
|
||||
|
||||
$localestr = '';
|
||||
foreach ($oscategories as $category) {
|
||||
// Format is different by OS too, so let build the string conditionally.
|
||||
if (PHP_OS_FAMILY === 'BSD' || PHP_OS_FAMILY === 'Darwin') {
|
||||
// BSD uses slashes (/) separated list of the 6 values in exact order.
|
||||
$localestr .= '/' . $baselocale;
|
||||
} else {
|
||||
// Linux/Windows use semicolon (;) separated list of category=value pairs.
|
||||
$localestr .= ';' . $category . '=' . $baselocale;
|
||||
}
|
||||
}
|
||||
$cases['allcategories'] = [
|
||||
'set' => trim($localestr, ';/'),
|
||||
'ret' => $baselocale,
|
||||
];
|
||||
|
||||
// Return all the built cases.
|
||||
return $cases;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,9 +77,10 @@
|
||||
ucw.registerEvents('[data-region="user-competency-status"]');
|
||||
ucw.on('status-changed', infoReloader.reload.bind(infoReloader));
|
||||
ucw.on('error-occured', infoReloader.reload.bind(infoReloader));
|
||||
|
||||
var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '{{plan.id}}', '', '{{#str}}chooserating, tool_lp{{/str}}');
|
||||
inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader));
|
||||
{{#cangrade}}
|
||||
var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '{{plan.id}}', '', '{{#str}}chooserating, tool_lp{{/str}}');
|
||||
inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader));
|
||||
{{/cangrade}}
|
||||
});
|
||||
{{/js}}
|
||||
{{/usercompetency}}
|
||||
|
||||
@@ -95,13 +95,14 @@
|
||||
{{#js}}
|
||||
require(['jquery', 'tool_lp/grade_user_competency_inline', 'tool_lp/user_competency_info'], function($, mod, info) {
|
||||
|
||||
var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '', '{{course.id}}', '{{#str}}chooserating, tool_lp{{/str}}');
|
||||
var competencyElement = $('[data-region-id="{{uniqid}}"]');
|
||||
|
||||
var displayuser = ('{{displayuser}}' == 'true') ? true : false;
|
||||
var infoReloader = new info(competencyElement, '{{competency.competency.id}}', '{{user.id}}', '', '{{course.id}}', displayuser);
|
||||
|
||||
inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader));
|
||||
{{#cangrade}}
|
||||
var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '', '{{course.id}}', '{{#str}}chooserating, tool_lp{{/str}}');
|
||||
inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader));
|
||||
{{/cangrade}}
|
||||
});
|
||||
|
||||
{{/js}}
|
||||
|
||||
@@ -63,4 +63,67 @@ class behat_tool_lp extends behat_base {
|
||||
$this->execute('behat_general::i_click_on', [$xpathtarget, 'xpath_element']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'.
|
||||
*
|
||||
* Recognised page names are:
|
||||
* | pagetype | name meaning | description |
|
||||
* | Course competencies | Course name | The course competencies page |
|
||||
*
|
||||
* @param string $page identifies which type of page this is, e.g. 'Course competencies'.
|
||||
* @param string $identifier identifies the particular page, e.g. 'C1'.
|
||||
* @return moodle_url the corresponding URL.
|
||||
* @throws Exception with a meaningful error message if the specified page cannot be found.
|
||||
*/
|
||||
protected function resolve_page_instance_url(string $page, string $identifier): moodle_url {
|
||||
switch (strtolower($page)) {
|
||||
case 'course competencies':
|
||||
$courseid = $this->get_course_id($identifier);
|
||||
return new moodle_url('/admin/tool/lp/coursecompetencies.php', [
|
||||
'courseid' => $courseid,
|
||||
]);
|
||||
default:
|
||||
throw new Exception("Unrecognised page type '{$page}'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of the exact named selectors for the component.
|
||||
*
|
||||
* @return behat_component_named_selector[]
|
||||
*/
|
||||
public static function get_exact_named_selectors(): array {
|
||||
return [
|
||||
new behat_component_named_selector('competency', [
|
||||
"//*[@data-region='coursecompetencies']//table[contains(@class,'managecompetencies')]".
|
||||
"//tr[contains(., //a[@title='View details'][contains(., %locator%)])]",
|
||||
]),
|
||||
new behat_component_named_selector('learning plan', [
|
||||
"//*[@data-region='plan-competencies']//table[contains(@class,'managecompetencies')]".
|
||||
"//tr[@data-node='user-competency'][contains(., //a[@data-usercompetency='true'][contains(., %locator%)])]",
|
||||
]),
|
||||
new behat_component_named_selector('competency description', [
|
||||
"//td/p[contains(., %locator%)]",
|
||||
]),
|
||||
new behat_component_named_selector('competency grade', [
|
||||
"//span[contains(concat(' ', normalize-space(@class), ' '), ' badge ')][contains(., %locator%)]",
|
||||
]),
|
||||
new behat_component_named_selector('learning plan rating', [
|
||||
"//td[position()=2][contains(., %locator%)]",
|
||||
]),
|
||||
new behat_component_named_selector('learning plan proficiency', [
|
||||
"//td[position()=3][contains(., %locator%)]",
|
||||
]),
|
||||
new behat_component_named_selector('competency page proficiency', [
|
||||
"//dt[contains(., 'Proficient')]/following-sibling::dd[1][contains(., %locator%)]",
|
||||
]),
|
||||
new behat_component_named_selector('competency page rating', [
|
||||
"//dt[contains(., 'Rating')]/following-sibling::dd[1][contains(., %locator%)]",
|
||||
]),
|
||||
new behat_component_named_selector('competency page related competency', [
|
||||
"//*[@data-region='relatedcompetencies']//a[contains(., %locator%)]",
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
@tool @tool_lp @javascript
|
||||
Feature: View competencies
|
||||
In order to access competency information
|
||||
As a user
|
||||
I need to view user competencies
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname |
|
||||
| student1 | Student | first |
|
||||
| teacher1 | Teacher | first |
|
||||
And the following "system role assigns" exist:
|
||||
| user | role | contextlevel |
|
||||
| teacher1 | editingteacher | System |
|
||||
And the following "permission overrides" exist:
|
||||
| capability | permission | role | contextlevel | reference |
|
||||
| moodle/competency:planview | Allow | editingteacher | System | |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "scales" exist:
|
||||
| name | scale |
|
||||
| Test Scale | Bad, Good, Great |
|
||||
And the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber | scale |
|
||||
| Cookery | cookery | Test Scale |
|
||||
| Literacy | literacy | Test Scale |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | idnumber | description | competencyframework |
|
||||
| Salads | salads | Salads are important | cookery |
|
||||
| Desserts | desserts | Desserts are important | cookery |
|
||||
| Cakes | cakes | Cakes are important | cookery |
|
||||
| Reading | reading | Reading is important | literacy |
|
||||
| Writing | writing | Writing is important | literacy |
|
||||
And the following "core_competency > related_competencies" exist:
|
||||
| competency | relatedcompetency |
|
||||
| desserts | cakes |
|
||||
And the following "core_competency > plans" exist:
|
||||
| name | description | competencies | user |
|
||||
| Cookery | Cookery is important | salads, desserts, cakes | student1 |
|
||||
| Literacy | Literacy is important | reading, writing | student1 |
|
||||
And the following "core_competency > course_competencies" exist:
|
||||
| course | competency |
|
||||
| C1 | salads |
|
||||
| C1 | desserts |
|
||||
| C1 | cakes |
|
||||
| C1 | reading |
|
||||
| C1 | writing |
|
||||
And the following "core_competency > user_competency" exist:
|
||||
| competency | user | grade |
|
||||
| salads | student1 | Good |
|
||||
| desserts | student1 | Great |
|
||||
| cakes | student1 | Great |
|
||||
And the following "core_competency > user_competency_courses" exist:
|
||||
| course | competency | user | grade |
|
||||
| C1 | salads | student1 | Good |
|
||||
| C1 | desserts | student1 | Great |
|
||||
| C1 | cakes | student1 | Great |
|
||||
|
||||
Scenario: Student view
|
||||
# Course competencies
|
||||
Given I am on the "C1" "tool_lp > course competencies" page logged in as "student1"
|
||||
Then I should see "You are proficient in 3 out of 5 competencies in this course"
|
||||
|
||||
And "Salads are important" "tool_lp > competency description" should exist in the "Salads" "tool_lp > competency"
|
||||
And "Good" "tool_lp > competency grade" should exist in the "Salads" "tool_lp > competency"
|
||||
|
||||
And "Desserts are important" "tool_lp > competency description" should exist in the "Desserts" "tool_lp > competency"
|
||||
And "Great" "tool_lp > competency grade" should exist in the "Desserts" "tool_lp > competency"
|
||||
|
||||
And "Cakes are important" "tool_lp > competency description" should exist in the "Cakes" "tool_lp > competency"
|
||||
And "Great" "tool_lp > competency grade" should exist in the "Cakes" "tool_lp > competency"
|
||||
|
||||
And "Reading is important" "tool_lp > competency description" should exist in the "Reading" "tool_lp > competency"
|
||||
And "Good" "tool_lp > competency grade" should not exist in the "Reading" "tool_lp > competency"
|
||||
And "Great" "tool_lp > competency grade" should not exist in the "Reading" "tool_lp > competency"
|
||||
And "Bad" "tool_lp > competency grade" should not exist in the "Reading" "tool_lp > competency"
|
||||
|
||||
And "Writing is important" "tool_lp > competency description" should exist in the "Writing" "tool_lp > competency"
|
||||
And "Good" "tool_lp > competency grade" should not exist in the "Writing" "tool_lp > competency"
|
||||
And "Great" "tool_lp > competency grade" should not exist in the "Writing" "tool_lp > competency"
|
||||
And "Bad" "tool_lp > competency grade" should not exist in the "Writing" "tool_lp > competency"
|
||||
|
||||
# Course competencies details
|
||||
And I click on "Desserts" "link" in the "Desserts" "tool_lp > competency"
|
||||
And I should see "Desserts are important"
|
||||
And "Yes" "tool_lp > competency page proficiency" should exist
|
||||
And "Great" "tool_lp > competency page rating" should exist
|
||||
|
||||
# Course competencies summary
|
||||
And I click on "Cakes" "link"
|
||||
And I should see "Cakes are important"
|
||||
|
||||
# Learning plans
|
||||
And I click on "Close" "button" in the "Cakes" "dialogue"
|
||||
And I follow "Profile" in the user menu
|
||||
And I click on "Learning plans" "link"
|
||||
And I should see "Cookery"
|
||||
And I should see "Literacy"
|
||||
|
||||
# Learning plans details
|
||||
And I click on "Cookery" "link"
|
||||
And I should see "Cookery is important"
|
||||
And I should see "3 out of 3 competencies are proficient"
|
||||
|
||||
And "Good" "tool_lp > learning plan rating" should exist in the "Salads" "tool_lp > learning plan"
|
||||
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Salads" "tool_lp > learning plan"
|
||||
|
||||
And "Great" "tool_lp > learning plan rating" should exist in the "Desserts" "tool_lp > learning plan"
|
||||
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Desserts" "tool_lp > learning plan"
|
||||
|
||||
And "Great" "tool_lp > learning plan rating" should exist in the "Cakes" "tool_lp > learning plan"
|
||||
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Cakes" "tool_lp > learning plan"
|
||||
|
||||
And I should not see "Literacy"
|
||||
And I should not see "Reading"
|
||||
And I should not see "Writing"
|
||||
|
||||
# Learning plans competency details
|
||||
And I click on "Desserts" "link" in the "Desserts" "tool_lp > learning plan"
|
||||
And I should see "Desserts are important"
|
||||
And "Yes" "tool_lp > competency page proficiency" should exist
|
||||
And "Great" "tool_lp > competency page rating" should exist
|
||||
|
||||
# Learning plans competency summary
|
||||
And I click on "Cakes cakes" "link"
|
||||
And I should see "Cakes are important"
|
||||
|
||||
Scenario: Teacher view
|
||||
# Participant competencies
|
||||
Given I am on the "C1" "report_competency > breakdown" page logged in as "teacher1"
|
||||
Then I should see "Student first"
|
||||
And "Good" "report_competency > breakdown rating" should exist in the "Salads" "report_competency > breakdown"
|
||||
And "Great" "report_competency > breakdown rating" should exist in the "Desserts" "report_competency > breakdown"
|
||||
And "Great" "report_competency > breakdown rating" should exist in the "Cakes" "report_competency > breakdown"
|
||||
And "Not rated" "report_competency > breakdown rating" should exist in the "Reading" "report_competency > breakdown"
|
||||
And "Not rated" "report_competency > breakdown rating" should exist in the "Writing" "report_competency > breakdown"
|
||||
|
||||
# Participant competencies details
|
||||
And I click on "Great" "report_competency > breakdown rating" in the "Desserts" "report_competency > breakdown"
|
||||
And "Yes" "tool_lp > competency page proficiency" should exist
|
||||
And "Great" "tool_lp > competency page rating" should exist
|
||||
|
||||
# Participant competencies summary
|
||||
And I click on "Cakes" "tool_lp > competency page related competency"
|
||||
And I should see "Cakes are important"
|
||||
|
||||
# Participant learning plans
|
||||
And I click on "Close" "button" in the "Cakes" "dialogue"
|
||||
And I click on "Close" "button" in the "User competency summary" "dialogue"
|
||||
And I navigate to course participants
|
||||
And I click on "Student first" "link"
|
||||
And I click on "Learning plans" "link"
|
||||
And I should see "Cookery"
|
||||
And I should see "Literacy"
|
||||
|
||||
# Participant learning plans details
|
||||
And I click on "Cookery" "link"
|
||||
And I should see "Cookery is important"
|
||||
And I should see "3 out of 3 competencies are proficient"
|
||||
|
||||
And "Good" "tool_lp > learning plan rating" should exist in the "Salads" "tool_lp > learning plan"
|
||||
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Salads" "tool_lp > learning plan"
|
||||
|
||||
And "Great" "tool_lp > learning plan rating" should exist in the "Desserts" "tool_lp > learning plan"
|
||||
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Desserts" "tool_lp > learning plan"
|
||||
|
||||
And "Great" "tool_lp > learning plan rating" should exist in the "Cakes" "tool_lp > learning plan"
|
||||
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Cakes" "tool_lp > learning plan"
|
||||
|
||||
And I should not see "Literacy"
|
||||
And I should not see "Reading"
|
||||
And I should not see "Writing"
|
||||
|
||||
# Learning plans competency details
|
||||
And I click on "Desserts" "link"
|
||||
And I should see "Desserts are important"
|
||||
And "Yes" "tool_lp > competency page proficiency" should exist
|
||||
And "Great" "tool_lp > competency page rating" should exist
|
||||
|
||||
# Learning plans competency summary
|
||||
And I click on "Cakes" "tool_lp > competency page related competency"
|
||||
And I should see "Cakes are important"
|
||||
@@ -27,7 +27,8 @@ require_once($CFG->libdir . '/externallib.php');
|
||||
|
||||
$userid = required_param('userid', PARAM_INT); // The user id the key belongs to (for double-checking).
|
||||
$key = required_param('key', PARAM_ALPHANUMEXT); // The key generated by the tool_mobile_external::get_autologin_key() external function.
|
||||
$urltogo = optional_param('urltogo', $CFG->wwwroot, PARAM_URL); // URL to redirect.
|
||||
$urltogo = optional_param('urltogo', $CFG->wwwroot, PARAM_LOCALURL); // URL to redirect.
|
||||
$urltogo = $urltogo ?: $CFG->wwwroot;
|
||||
|
||||
$context = context_system::instance();
|
||||
$PAGE->set_context($context);
|
||||
|
||||
@@ -19,14 +19,13 @@ Feature: Backup user data
|
||||
And the following config values are set as admin:
|
||||
| coursebinenable | 1 | tool_recyclebin |
|
||||
| autohide | 0 | tool_recyclebin |
|
||||
And the following "activities" exist:
|
||||
| activity | course | section | name | intro |
|
||||
| quiz | C1 | 1 | Quiz 1 | Test quiz description |
|
||||
|
||||
@javascript
|
||||
Scenario: Delete and restore a quiz with user data
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Quiz" to section "1" and I fill the form with:
|
||||
| Name | Quiz 1 |
|
||||
| Description | Test quiz description |
|
||||
Given I am on the "Quiz 1" "quiz activity" page logged in as teacher1
|
||||
And I add a "True/False" question to the "Quiz 1" quiz with:
|
||||
| Question name | TF1 |
|
||||
| Question text | First question |
|
||||
@@ -41,6 +40,8 @@ Feature: Backup user data
|
||||
| Correct answer | False |
|
||||
| Feedback for the response 'True'. | So you think it is true |
|
||||
| Feedback for the response 'False'. | So you think it is false |
|
||||
And I set the field "maxgrade" to "10.0"
|
||||
And I press "savechanges"
|
||||
And I log out
|
||||
When I am on the "Quiz 1" "quiz activity" page logged in as student1
|
||||
And I press "Attempt quiz now"
|
||||
|
||||
@@ -48,7 +48,7 @@ class backup_files_edit_form extends moodleform {
|
||||
$mform->setType('component', PARAM_COMPONENT);
|
||||
|
||||
$mform->addElement('hidden', 'returnurl', $this->_customdata['returnurl']);
|
||||
$mform->setType('returnurl', PARAM_URL);
|
||||
$mform->setType('returnurl', PARAM_LOCALURL);
|
||||
|
||||
$this->add_action_buttons(true, get_string('savechanges'));
|
||||
$this->set_data($this->_customdata['data']);
|
||||
|
||||
@@ -77,6 +77,7 @@ abstract class backup_cron_automated_helper {
|
||||
* First backup courses that do not have an entry in backup_courses first,
|
||||
* as they are likely new and never been backed up. Do the oldest modified courses first.
|
||||
* Then backup courses that have previously been backed up starting with the oldest next start time.
|
||||
* Finally, all else being equal, defer to the sortorder of the courses.
|
||||
*
|
||||
* @param null|int $now timestamp to use in course selection.
|
||||
* @return moodle_recordset The recordset of matching courses.
|
||||
@@ -93,7 +94,8 @@ abstract class backup_cron_automated_helper {
|
||||
LEFT JOIN {backup_courses} bc ON bc.courseid = c.id
|
||||
WHERE bc.nextstarttime IS NULL OR bc.nextstarttime < ?
|
||||
ORDER BY nextstarttime ASC,
|
||||
c.timemodified DESC';
|
||||
c.timemodified DESC,
|
||||
c.sortorder';
|
||||
|
||||
$params = array(
|
||||
$now, // Only get courses where the backup start time is in the past.
|
||||
|
||||
@@ -383,7 +383,7 @@ class backup_cron_helper_testcase extends advanced_testcase {
|
||||
$backupcourse3 = new stdClass;
|
||||
$backupcourse3->courseid = $course3->id;
|
||||
$backupcourse3->laststatus = testable_backup_cron_automated_helper::BACKUP_STATUS_OK;
|
||||
$backupcourse3->nextstarttime = 1554858160;
|
||||
$backupcourse3->nextstarttime = 1554822160;
|
||||
$DB->insert_record('backup_courses', $backupcourse3);
|
||||
|
||||
$backupcourse4 = new stdClass;
|
||||
|
||||
@@ -643,7 +643,7 @@ class backup_ui_stage_complete extends backup_ui_stage_final {
|
||||
}
|
||||
$output .= $renderer->get_samesite_notification();
|
||||
$output .= $renderer->notification(get_string('executionsuccess', 'backup'), 'notifysuccess');
|
||||
$output .= $renderer->continue_button($restorerul);
|
||||
$output .= $renderer->continue_button($restorerul, 'get');
|
||||
$output .= $renderer->box_end();
|
||||
|
||||
return $output;
|
||||
|
||||
@@ -553,7 +553,8 @@ class core_backup_renderer extends plugin_renderer_base {
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a continue button
|
||||
* Displays a continue button, overriding core renderer method of the same in order
|
||||
* to override submission method of the button form
|
||||
*
|
||||
* @param string|moodle_url $url
|
||||
* @param string $method
|
||||
@@ -566,7 +567,6 @@ class core_backup_renderer extends plugin_renderer_base {
|
||||
if ($method != 'post') {
|
||||
$method = 'get';
|
||||
}
|
||||
$url->param('sesskey', sesskey());
|
||||
$button = new single_button($url, get_string('continue'), $method, true);
|
||||
$button->class = 'continuebutton';
|
||||
return $this->render($button);
|
||||
|
||||
@@ -1161,8 +1161,10 @@ class restore_ui_stage_complete extends restore_ui_stage_process {
|
||||
$html .= $renderer->notification(get_string('restorefileweremissing', 'backup'), 'notifyproblem');
|
||||
}
|
||||
$html .= $renderer->notification(get_string('restoreexecutionsuccess', 'backup'), 'notifysuccess');
|
||||
$html .= $renderer->continue_button(new moodle_url('/course/view.php', array(
|
||||
'id' => $this->get_ui()->get_controller()->get_courseid())), 'get');
|
||||
|
||||
$courseurl = course_get_url($this->get_ui()->get_controller()->get_courseid());
|
||||
$html .= $renderer->continue_button($courseurl, 'get');
|
||||
|
||||
$html .= $renderer->box_end();
|
||||
|
||||
return $html;
|
||||
|
||||
@@ -12,9 +12,10 @@ Feature: Restore Moodle 2 course backups
|
||||
| Course 3 | C3 | 0 | topics | 2 | 0 |
|
||||
| Course 4 | C4 | 0 | topics | 20 | 0 |
|
||||
And the following "activities" exist:
|
||||
| activity | course | idnumber | name | intro | section |
|
||||
| assign | C3 | assign1 | Test assign name | Assign description | 1 |
|
||||
| data | C3 | data1 | Test database name | Database description | 2 |
|
||||
| activity | course | idnumber | name | intro | section | externalurl |
|
||||
| assign | C3 | assign1 | Test assign name | Assign description | 1 | |
|
||||
| data | C3 | data1 | Test database name | Database description | 2 | |
|
||||
| url | C1 | url1 | Test URL name | Test URL description | 3 | http://www.moodle.org |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Forum" to section "1" and I fill the form with:
|
||||
@@ -108,11 +109,7 @@ Feature: Restore Moodle 2 course backups
|
||||
|
||||
@javascript
|
||||
Scenario: Restore a backup in an existing course retaining the backup course settings
|
||||
Given I add a "URL" to section "3" and I fill the form with:
|
||||
| Name | Test URL name |
|
||||
| Description | Test URL description |
|
||||
| id_externalurl | http://www.moodle.org |
|
||||
And I hide section "3"
|
||||
Given I hide section "3"
|
||||
And I hide section "7"
|
||||
When I backup "Course 1" course using this options:
|
||||
| Confirmation | Filename | test_backup.mbz |
|
||||
@@ -134,11 +131,7 @@ Feature: Restore Moodle 2 course backups
|
||||
|
||||
@javascript
|
||||
Scenario: Restore a backup in an existing course keeping the target course settings
|
||||
Given I add a "URL" to section "3" and I fill the form with:
|
||||
| Name | Test URL name |
|
||||
| Description | Test URL description |
|
||||
| id_externalurl | http://www.moodle.org |
|
||||
And I hide section "3"
|
||||
Given I hide section "3"
|
||||
And I hide section "7"
|
||||
When I backup "Course 1" course using this options:
|
||||
| Confirmation | Filename | test_backup.mbz |
|
||||
@@ -160,11 +153,7 @@ Feature: Restore Moodle 2 course backups
|
||||
|
||||
@javascript
|
||||
Scenario: Restore a backup in an existing course deleting contents and retaining the backup course settings
|
||||
Given I add a "URL" to section "3" and I fill the form with:
|
||||
| Name | Test URL name |
|
||||
| Description | Test URL description |
|
||||
| id_externalurl | http://www.moodle.org |
|
||||
And I hide section "3"
|
||||
Given I hide section "3"
|
||||
And I hide section "7"
|
||||
When I backup "Course 1" course using this options:
|
||||
| Initial | Include enrolled users | 0 |
|
||||
@@ -189,11 +178,7 @@ Feature: Restore Moodle 2 course backups
|
||||
|
||||
@javascript
|
||||
Scenario: Restore a backup in an existing course deleting contents and keeping the current course settings
|
||||
Given I add a "URL" to section "3" and I fill the form with:
|
||||
| Name | Test URL name |
|
||||
| Description | Test URL description |
|
||||
| id_externalurl | http://www.moodle.org |
|
||||
And I hide section "3"
|
||||
Given I hide section "3"
|
||||
And I hide section "7"
|
||||
When I backup "Course 1" course using this options:
|
||||
| Initial | Include enrolled users | 0 |
|
||||
@@ -218,11 +203,7 @@ Feature: Restore Moodle 2 course backups
|
||||
|
||||
@javascript
|
||||
Scenario: Restore a backup in an existing course deleting contents decreasing the number of sections
|
||||
Given I add a "URL" to section "3" and I fill the form with:
|
||||
| Name | Test URL name |
|
||||
| Description | Test URL description |
|
||||
| id_externalurl | http://www.moodle.org |
|
||||
And I hide section "3"
|
||||
Given I hide section "3"
|
||||
And I hide section "7"
|
||||
When I backup "Course 1" course using this options:
|
||||
| Initial | Include enrolled users | 0 |
|
||||
|
||||
@@ -30,9 +30,7 @@ Feature: Award badges with separate groups
|
||||
| teacher1 | CB |
|
||||
| student2 | CA |
|
||||
| teacher2 | CA |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the "Course 1" "course editing" page logged in as "teacher1"
|
||||
And I expand all fieldsets
|
||||
And I set the field "Group mode" to "Separate groups"
|
||||
And I press "Save and display"
|
||||
|
||||
@@ -36,7 +36,7 @@ class moodle1_block_html_handler extends moodle1_block_handler {
|
||||
$contextid = $this->converter->get_contextid(CONTEXT_BLOCK, $olddata['id']);
|
||||
$decodeddata = base64_decode($olddata['configdata']);
|
||||
list($updated, $configdata) = upgrade_fix_serialized_objects($decodeddata);
|
||||
$configdata = unserialize($configdata);
|
||||
$configdata = unserialize_object($configdata);
|
||||
|
||||
// get a fresh new file manager for this instance
|
||||
$this->fileman = $this->converter->get_file_manager($contextid, 'block_html');
|
||||
@@ -44,7 +44,7 @@ class moodle1_block_html_handler extends moodle1_block_handler {
|
||||
// convert course files embedded in the block content
|
||||
$this->fileman->filearea = 'content';
|
||||
$this->fileman->itemid = 0;
|
||||
$configdata->text = moodle1_converter::migrate_referenced_files($configdata->text, $this->fileman);
|
||||
$configdata->text = moodle1_converter::migrate_referenced_files($configdata->text ?? '', $this->fileman);
|
||||
$configdata->format = FORMAT_HTML;
|
||||
|
||||
return base64_encode(serialize($configdata));
|
||||
|
||||
@@ -10,10 +10,18 @@ Feature: Add URL to main menu block
|
||||
And I am on site homepage
|
||||
And I navigate to "Turn editing on" in current page administration
|
||||
And I add the "Main menu" block
|
||||
When I add a "URL" to section "0" and I fill the form with:
|
||||
| Name | google |
|
||||
| Description | gooooooooogle |
|
||||
| External URL | http://www.google.com |
|
||||
And the following "activity" exists:
|
||||
| activity | url |
|
||||
| course | Acceptance test site |
|
||||
| name | google |
|
||||
| intro | gooooooooogle |
|
||||
| externalurl | http://www.google.com |
|
||||
| section | 0 |
|
||||
When I am on the "google" "url activity editing" page
|
||||
And I expand all fieldsets
|
||||
And I set the following fields to these values:
|
||||
| id_display | In pop-up |
|
||||
And I press "Save and return to course"
|
||||
Then "google" "link" should exist in the "Main menu" "block"
|
||||
And "Add an activity" "button" should exist in the "Main menu" "block"
|
||||
And "Add an activity or resource" "button" should exist in the "Main menu" "block"
|
||||
|
||||
@@ -14,12 +14,12 @@ Feature: Block appearances
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "activities" exist:
|
||||
| activity | course | name | element |
|
||||
| survey | C1 | Test survey name | 4 |
|
||||
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Survey" to section "1" and I fill the form with:
|
||||
| Name | Test survey name |
|
||||
| Survey type | ATTLS (20 item version) |
|
||||
| Description | Test survey description |
|
||||
And I add a "Book" to section "1" and I fill the form with:
|
||||
| Name | Test book name |
|
||||
| Description | Test book description |
|
||||
@@ -28,9 +28,8 @@ Feature: Block appearances
|
||||
| Chapter title | Book title |
|
||||
| Content | Book content test test |
|
||||
And I press "Save changes"
|
||||
And I log out
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I turn editing mode on
|
||||
And I add the "Comments" block
|
||||
And I configure the "Comments" block
|
||||
And I set the following fields to these values:
|
||||
|
||||
@@ -14,12 +14,11 @@ Feature: Block region moving
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | template |
|
||||
| survey | Test survey name | C1 | 4 |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Survey" to section "1" and I fill the form with:
|
||||
| Name | Test survey name |
|
||||
| Survey type | ATTLS (20 item version) |
|
||||
| Description | Test survey description |
|
||||
And I add a "Book" to section "1" and I fill the form with:
|
||||
| Name | Test book name |
|
||||
| Description | Test book description |
|
||||
|
||||
@@ -64,7 +64,7 @@ class blog_edit_external_form extends moodleform {
|
||||
$mform->setDefault('id', 0);
|
||||
|
||||
$mform->addElement('hidden', 'returnurl');
|
||||
$mform->setType('returnurl', PARAM_URL);
|
||||
$mform->setType('returnurl', PARAM_LOCALURL);
|
||||
$mform->setDefault('returnurl', 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ Feature: Comment on a blog entry
|
||||
| username | firstname | lastname | email |
|
||||
| testuser | Test | User | moodle@example.com |
|
||||
| testuser2 | Test2 | User2 | moodle2@example.com |
|
||||
And the following "core_blog > entries" exist:
|
||||
| subject | body | user |
|
||||
| Blog post from user 1 | User 1 blog post content | testuser |
|
||||
And I log in as "admin"
|
||||
And I am on site homepage
|
||||
And I turn editing mode on
|
||||
@@ -19,15 +22,6 @@ Feature: Comment on a blog entry
|
||||
| Page contexts | Display throughout the entire site |
|
||||
And I press "Save changes"
|
||||
And I log out
|
||||
And I log in as "testuser"
|
||||
And I click on "Site pages" "list_item" in the "Navigation" "block"
|
||||
And I click on "Site blogs" "link" in the "Navigation" "block"
|
||||
And I follow "Add a new entry"
|
||||
And I set the following fields to these values:
|
||||
| Entry title | Blog post from user 1 |
|
||||
| Blog entry body | User 1 blog post content |
|
||||
And I press "Save changes"
|
||||
And I log out
|
||||
|
||||
@javascript
|
||||
Scenario: Commenting on my own blog entry
|
||||
|
||||
@@ -8,6 +8,10 @@ Feature: Delete a blog entry
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| testuser | Test | User | moodle@example.com |
|
||||
And the following "core_blog > entries" exist:
|
||||
| subject | body | user |
|
||||
| Blog post one | User 1 blog post content | testuser |
|
||||
| Blog post two | User 1 blog post content | testuser |
|
||||
And I log in as "admin"
|
||||
And I am on site homepage
|
||||
And I turn editing mode on
|
||||
@@ -19,17 +23,6 @@ Feature: Delete a blog entry
|
||||
And I press "Save changes"
|
||||
And I log out
|
||||
And I log in as "testuser"
|
||||
And I click on "Site blogs" "link" in the "Navigation" "block"
|
||||
And I follow "Add a new entry"
|
||||
And I set the following fields to these values:
|
||||
| Entry title | Blog post one |
|
||||
| Blog entry body | User 1 blog post content |
|
||||
And I press "Save changes"
|
||||
And I follow "Add a new entry"
|
||||
And I set the following fields to these values:
|
||||
| Entry title | Blog post two |
|
||||
| Blog entry body | User 1 blog post content |
|
||||
And I press "Save changes"
|
||||
And I am on site homepage
|
||||
And I click on "Site blogs" "link" in the "Navigation" "block"
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Behat data generator for core_blog.
|
||||
*
|
||||
* @package core_blog
|
||||
* @category test
|
||||
* @copyright 2022 Noel De Martin
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* Behat data generator for core_blog.
|
||||
*
|
||||
* @package core_blog
|
||||
* @category test
|
||||
* @copyright 2022 Noel De Martin
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class behat_core_blog_generator extends behat_generator_base {
|
||||
|
||||
/**
|
||||
* Get a list of the entities that can be created.
|
||||
*
|
||||
* @return array entity name => information about how to generate.
|
||||
*/
|
||||
protected function get_creatable_entities(): array {
|
||||
return [
|
||||
'entries' => [
|
||||
'singular' => 'entry',
|
||||
'datagenerator' => 'entry',
|
||||
'required' => ['subject', 'body'],
|
||||
'switchids' => ['user' => 'userid'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Generator for blog area.
|
||||
*
|
||||
* @package core_blog
|
||||
* @category test
|
||||
* @copyright 2022 Noel De Martin
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/blog/locallib.php');
|
||||
|
||||
/**
|
||||
* Blog module test data generator class
|
||||
*
|
||||
* @package core_blog
|
||||
* @category test
|
||||
* @copyright 2022 Noel De Martin
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class core_blog_generator extends component_generator_base {
|
||||
|
||||
/**
|
||||
* Create a blog entry
|
||||
*
|
||||
* @param array $data Entry data.
|
||||
* @return blog_entry Entry instance.
|
||||
*/
|
||||
public function create_entry(array $data = []): blog_entry {
|
||||
$data['publishstate'] = $data['publishstate'] ?? 'site';
|
||||
$data['summary'] = $data['summary'] ?? $data['body'];
|
||||
|
||||
$entry = new blog_entry(null, $data);
|
||||
$entry->add();
|
||||
|
||||
return $entry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
<?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/>.
|
||||
|
||||
use core_competency\competency;
|
||||
use core_competency\competency_framework;
|
||||
use core_competency\plan;
|
||||
|
||||
/**
|
||||
* Behat data generator for core_competency.
|
||||
*
|
||||
* @package core_competency
|
||||
* @category test
|
||||
* @copyright 2022 Noel De Martin
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class behat_core_competency_generator extends behat_generator_base {
|
||||
|
||||
/**
|
||||
* Get a list of the entities that Behat can create using the generator step.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_creatable_entities(): array {
|
||||
return [
|
||||
'competencies' => [
|
||||
'singular' => 'competency',
|
||||
'datagenerator' => 'competency',
|
||||
'required' => ['shortname', 'competencyframework'],
|
||||
'switchids' => ['competencyframework' => 'competencyframeworkid'],
|
||||
],
|
||||
'course_competencies' => [
|
||||
'singular' => 'course_competency',
|
||||
'datagenerator' => 'course_competency',
|
||||
'required' => ['course', 'competency'],
|
||||
'switchids' => ['course' => 'courseid', 'competency' => 'competencyid'],
|
||||
],
|
||||
'frameworks' => [
|
||||
'singular' => 'framework',
|
||||
'datagenerator' => 'framework',
|
||||
'required' => ['shortname'],
|
||||
'switchids' => ['scale' => 'scaleid'],
|
||||
],
|
||||
'plans' => [
|
||||
'singular' => 'plan',
|
||||
'datagenerator' => 'plan',
|
||||
'required' => ['name'],
|
||||
'switchids' => ['user' => 'userid'],
|
||||
],
|
||||
'related_competencies' => [
|
||||
'singular' => 'related_competency',
|
||||
'datagenerator' => 'related_competency',
|
||||
'required' => ['competency', 'relatedcompetency'],
|
||||
'switchids' => ['competency' => 'competencyid', 'relatedcompetency' => 'relatedcompetencyid'],
|
||||
],
|
||||
'user_competency' => [
|
||||
'singular' => 'user_competency',
|
||||
'datagenerator' => 'user_competency',
|
||||
'required' => ['competency', 'user'],
|
||||
'switchids' => ['competency' => 'competencyid', 'user' => 'userid'],
|
||||
],
|
||||
'user_competency_courses' => [
|
||||
'singular' => 'user_competency_course',
|
||||
'datagenerator' => 'user_competency_course',
|
||||
'required' => ['course', 'competency', 'user'],
|
||||
'switchids' => ['course' => 'courseid', 'competency' => 'competencyid', 'user' => 'userid'],
|
||||
],
|
||||
'user_competency_plans' => [
|
||||
'singular' => 'user_competency_plan',
|
||||
'datagenerator' => 'user_competency_plan',
|
||||
'required' => ['plan', 'competency', 'user'],
|
||||
'switchids' => ['plan' => 'planid', 'competency' => 'competencyid', 'user' => 'userid'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the competecy framework id using an idnumber.
|
||||
*
|
||||
* @param string $idnumber
|
||||
* @return int The competecy framework id
|
||||
*/
|
||||
protected function get_competencyframework_id(string $idnumber): int {
|
||||
global $DB;
|
||||
|
||||
if (!$id = $DB->get_field('competency_framework', 'id', ['idnumber' => $idnumber])) {
|
||||
throw new Exception('The specified competency framework with idnumber "' . $idnumber . '" could not be found.');
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the competecy id using an idnumber.
|
||||
*
|
||||
* @param string $idnumber
|
||||
* @return int The competecy id
|
||||
*/
|
||||
protected function get_competency_id(string $idnumber): int {
|
||||
global $DB;
|
||||
|
||||
if (!$id = $DB->get_field('competency', 'id', ['idnumber' => $idnumber])) {
|
||||
throw new Exception('The specified competency with idnumber "' . $idnumber . '" could not be found.');
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the learning plan id using a name.
|
||||
*
|
||||
* @param string $name
|
||||
* @return int The learning plan id
|
||||
*/
|
||||
protected function get_plan_id(string $name): int {
|
||||
global $DB;
|
||||
|
||||
if (!$id = $DB->get_field('competency_plan', 'id', ['name' => $name])) {
|
||||
throw new Exception('The specified learning plan with name "' . $name . '" could not be found.');
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the related competecy id using an idnumber.
|
||||
*
|
||||
* @param string $idnumber
|
||||
* @return int The related competecy id
|
||||
*/
|
||||
protected function get_relatedcompetency_id(string $idnumber): int {
|
||||
return $this->get_competency_id($idnumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a plan.
|
||||
*
|
||||
* @param array $data Plan data.
|
||||
*/
|
||||
public function process_plan(array $data): void {
|
||||
$generator = $this->get_data_generator();
|
||||
$competencyids = $data['competencyids'] ?? [];
|
||||
|
||||
unset($data['competencyids']);
|
||||
|
||||
$plan = $generator->create_plan($data);
|
||||
|
||||
foreach ($competencyids as $competencyid) {
|
||||
$generator->create_plan_competency([
|
||||
'planid' => $plan->get('id'),
|
||||
'competencyid' => $competencyid,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocess user competency data.
|
||||
*
|
||||
* @param array $data Raw data.
|
||||
* @return array Processed data.
|
||||
*/
|
||||
protected function preprocess_user_competency(array $data): array {
|
||||
$this->prepare_grading($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocess user course competency data.
|
||||
*
|
||||
* @param array $data Raw data.
|
||||
* @return array Processed data.
|
||||
*/
|
||||
protected function preprocess_user_competency_course(array $data): array {
|
||||
$this->prepare_grading($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocess user learning plan competency data.
|
||||
*
|
||||
* @param array $data Raw data.
|
||||
* @return array Processed data.
|
||||
*/
|
||||
protected function preprocess_user_competency_plan(array $data): array {
|
||||
$this->prepare_grading($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocess plan data.
|
||||
*
|
||||
* @param array $data Raw data.
|
||||
* @return array Processed data.
|
||||
*/
|
||||
protected function preprocess_plan(array $data): array {
|
||||
if (isset($data['competencies'])) {
|
||||
$competencies = array_map('trim', str_getcsv($data['competencies']));
|
||||
$data['competencyids'] = array_map([$this, 'get_competency_id'], $competencies);
|
||||
|
||||
unset($data['competencies']);
|
||||
}
|
||||
|
||||
global $USER;
|
||||
|
||||
return $data + [
|
||||
'userid' => $USER->id,
|
||||
'status' => plan::STATUS_ACTIVE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare grading attributes for record data.
|
||||
*
|
||||
* @param array $data Record data.
|
||||
*/
|
||||
protected function prepare_grading(array &$data): void {
|
||||
if (!isset($data['grade'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $DB;
|
||||
|
||||
$competency = competency::get_record(['id' => $data['competencyid']]);
|
||||
$competencyframework = competency_framework::get_record(['id' => $competency->get('competencyframeworkid')]);
|
||||
$scale = $DB->get_field('scale', 'scale', ['id' => $competencyframework->get('scaleid')]);
|
||||
$grades = array_map('trim', explode(',', $scale));
|
||||
$grade = array_search($data['grade'], $grades);
|
||||
|
||||
if ($grade === false) {
|
||||
throw new Exception('The grade "'.$data['grade'].'" was not found in the "'.
|
||||
$competencyframework->get('shortname').'" competency framework.');
|
||||
}
|
||||
|
||||
$data['proficiency'] = true;
|
||||
$data['grade'] = $grade + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the module data generator.
|
||||
*
|
||||
* @return core_competency_generator Competency data generator.
|
||||
*/
|
||||
protected function get_data_generator(): core_competency_generator {
|
||||
return $this->componentdatagenerator;
|
||||
}
|
||||
}
|
||||
@@ -136,7 +136,11 @@ class core_competency_generator extends component_generator_base {
|
||||
* @return competency_framework
|
||||
*/
|
||||
public function create_framework($record = null) {
|
||||
$generator = phpunit_util::get_data_generator();
|
||||
if (defined('BEHAT_TEST') && BEHAT_TEST) {
|
||||
$generator = behat_util::get_data_generator();
|
||||
} else {
|
||||
$generator = phpunit_util::get_data_generator();
|
||||
}
|
||||
$this->frameworkcount++;
|
||||
$i = $this->frameworkcount;
|
||||
$record = (object) $record;
|
||||
@@ -584,4 +588,3 @@ class core_competency_generator extends component_generator_base {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -34,9 +34,9 @@ Feature: Restrict sections availability through completion or grade conditions
|
||||
And I set the following fields to these values:
|
||||
| Enable completion tracking | Yes |
|
||||
And I press "Save and display"
|
||||
And I add a "Label" to section "1" and I fill the form with:
|
||||
| Label text | Test label |
|
||||
| Completion tracking | Students can manually mark the activity as completed |
|
||||
And the following "activities" exist:
|
||||
| activity | course | section | intro | completion | idnumber |
|
||||
| label | C1 | 1 | Test label | 1 | 1 |
|
||||
And I add a "Page" to section "2" and I fill the form with:
|
||||
| Name | Test page name |
|
||||
| Description | Test page description |
|
||||
|
||||
+5
-5
@@ -7,13 +7,13 @@
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "7.5.*",
|
||||
"phpunit/dbunit": "4.0.*",
|
||||
"mikey179/vfsstream": "^1.6",
|
||||
"behat/mink": "~1.8",
|
||||
"mikey179/vfsstream": "1.6.*",
|
||||
"behat/mink": "^1.10.0",
|
||||
"behat/mink-extension": "~2.3",
|
||||
"behat/mink-goutte-driver": "~1.2",
|
||||
"behat/mink-goutte-driver": "^1.3",
|
||||
"symfony/process": "^4.0 || ^5.0",
|
||||
"behat/behat": "3.10.*",
|
||||
"oleg-andreyev/mink-phpwebdriver": "^1.0"
|
||||
"behat/behat": "3.11.*",
|
||||
"oleg-andreyev/mink-phpwebdriver": "^1.2.1"
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-0": {
|
||||
|
||||
Generated
+385
-338
File diff suppressed because it is too large
Load Diff
@@ -34,7 +34,7 @@ require_once($CFG->dirroot.'/course/lib.php');
|
||||
|
||||
$id = required_param('id', PARAM_INT);
|
||||
$switchrole = optional_param('switchrole', -1, PARAM_INT);
|
||||
$returnurl = optional_param('returnurl', '', PARAM_RAW);
|
||||
$returnurl = optional_param('returnurl', '', PARAM_LOCALURL);
|
||||
|
||||
if (strpos($returnurl, '?') === false) {
|
||||
// Looks like somebody did not set proper page url, better go to course page.
|
||||
@@ -43,7 +43,7 @@ if (strpos($returnurl, '?') === false) {
|
||||
if (strpos($returnurl, $CFG->wwwroot) !== 0) {
|
||||
$returnurl = $CFG->wwwroot.$returnurl;
|
||||
}
|
||||
$returnurl = clean_param($returnurl, PARAM_URL);
|
||||
$returnurl = clean_param($returnurl, PARAM_LOCALURL);
|
||||
}
|
||||
|
||||
$PAGE->set_url('/course/switchrole.php', array('id'=>$id, 'switchrole'=>$switchrole));
|
||||
|
||||
@@ -14,11 +14,11 @@ Feature: Force group mode in a course
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "activities" exist:
|
||||
| activity | course | name |
|
||||
| chat | C1 | Chat room |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Chat" to section "1" and I fill the form with:
|
||||
| Name of this chat room | Chat room |
|
||||
| Description | Chat description |
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
|
||||
@javascript
|
||||
|
||||
@@ -43,15 +43,15 @@ Feature: Course paged mode
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category | format | coursedisplay | numsections | startdate |
|
||||
| Course 1 | C1 | 0 | <courseformat> | 1 | 3 | 0 |
|
||||
And I log in as "admin"
|
||||
And the following "activities" exist:
|
||||
| activity | course | name |
|
||||
| chat | C1 | Chat room |
|
||||
When I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I click on <section1> "link" in the <section1> "section"
|
||||
And I should see <section1> in the "div.single-section" "css_element"
|
||||
And I should see <section2> in the ".single-section span.mdl-right" "css_element"
|
||||
And I should not see <prevunexistingsection> in the ".single-section" "css_element"
|
||||
When I add a "Chat" to section "1" and I fill the form with:
|
||||
| Name of this chat room | Chat room |
|
||||
| Description | Chat description |
|
||||
Then I should see <section1> in the "div.single-section" "css_element"
|
||||
And I should see <section2> in the ".single-section span.mdl-right" "css_element"
|
||||
And I should not see <prevunexistingsection> in the ".single-section" "css_element"
|
||||
|
||||
@@ -14,6 +14,9 @@ Feature: Restrict activities availability
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "activities" exist:
|
||||
| activity | course | name |
|
||||
| chat | C1 | Test chat name |
|
||||
|
||||
@javascript
|
||||
Scenario: Activities can be added with the default permissions
|
||||
@@ -22,9 +25,6 @@ Feature: Restrict activities availability
|
||||
When I add a "Glossary" to section "1" and I fill the form with:
|
||||
| Name | Test glossary name |
|
||||
| Description | Test glossary description |
|
||||
And I add a "Chat" to section "1" and I fill the form with:
|
||||
| Name of this chat room | Test chat name |
|
||||
| Description | Test chat description |
|
||||
Then I should see "Test glossary name"
|
||||
And I should see "Test chat name"
|
||||
|
||||
|
||||
@@ -14,13 +14,10 @@ Feature: View subfolders in a course in-line
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Folder" to section "3" and I fill the form with:
|
||||
| Name | Test folder |
|
||||
| Display folder contents | On a separate page |
|
||||
| Show subfolders expanded | |
|
||||
And I am on the "Test folder" "folder activity" page
|
||||
And the following "activities" exist:
|
||||
| activity | course | name | display | showexpanded |
|
||||
| folder | C1 | Test folder | 0 | 0 |
|
||||
And I am on the "Test folder" "folder activity" page logged in as "teacher1"
|
||||
And I press "Edit"
|
||||
And I press "Create folder"
|
||||
And I set the field "New folder name" to "Test subfolder 1"
|
||||
|
||||
@@ -33,7 +33,6 @@ use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\writer;
|
||||
use core_privacy\manager;
|
||||
use Horde\Socket\Client\Exception;
|
||||
|
||||
/**
|
||||
* Class provider
|
||||
@@ -196,7 +195,7 @@ class provider implements
|
||||
$field = array_key_exists($record->fieldid, $fields) ? $fields[$record->fieldid] : null;
|
||||
$data = data_controller::create(0, $record, $field);
|
||||
self::export_customfield_data($data, array_merge($subcontext, [$record->id]));
|
||||
} catch (Exception $e) {
|
||||
} catch (\Exception $e) {
|
||||
// We store some data that we can not initialise controller for. We still need to export it.
|
||||
self::export_customfield_data_unknown($record, $field, array_merge($subcontext, [$record->id]));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@customfield @customfield_textarea @javascript @_file_upload
|
||||
# This feature has Atto-specific steps. See MDL-75913 for further details.
|
||||
@customfield @customfield_textarea @javascript @_file_upload @editor_atto
|
||||
Feature: Default value for the textarea custom field can contain images
|
||||
In order to see images on custom fields
|
||||
As a manager
|
||||
|
||||
@@ -28,9 +28,8 @@ Feature: Users can be added to multiple groups at once
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Participants"
|
||||
And I click on "Edit groups for \"Student 1\"" "link" in the "student1" "table_row"
|
||||
And I click on ".form-autocomplete-downarrow" "css_element" in the "student1" "table_row"
|
||||
And I open the autocomplete suggestions list in the "student1" "table_row"
|
||||
And I click on "Group 1" item in the autocomplete list
|
||||
And I click on ".form-autocomplete-downarrow" "css_element" in the "student1" "table_row"
|
||||
And I click on "Group 2" item in the autocomplete list
|
||||
And I press the escape key
|
||||
And I click on "Save changes" "link" in the "student1" "table_row"
|
||||
|
||||
@@ -49,7 +49,8 @@ Feature: Render H5P content using filters
|
||||
And I wait until the page is ready
|
||||
Then ".h5p-iframe" "css_element" should not exist
|
||||
|
||||
@javascript
|
||||
# This scenario has Atto-specific steps. See MDL-75913 for further details.
|
||||
@javascript @editor_atto
|
||||
Scenario: Render a local H5P file as admin
|
||||
Given I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
@@ -83,7 +84,8 @@ Feature: Render H5P content using filters
|
||||
And I should not see "you don't have access"
|
||||
And I should see "Lorum ipsum"
|
||||
|
||||
@javascript
|
||||
# This scenario has Atto-specific steps. See MDL-75913 for further details.
|
||||
@javascript @editor_atto
|
||||
Scenario: Render a local H5P file as teacher
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
@@ -107,7 +109,8 @@ Feature: Render H5P content using filters
|
||||
Then I should see "Note that the libraries may exist in the file you uploaded, but you're not allowed to upload new libraries."
|
||||
And I should see "missing-required-library"
|
||||
|
||||
@javascript
|
||||
# This scenario has Atto-specific steps. See MDL-75913 for further details.
|
||||
@javascript @editor_atto
|
||||
Scenario: Render a local H5P file with existing libraries
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
|
||||
@@ -12,7 +12,7 @@ Feature: We can change what we are viewing on the grader report
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
| student2 | Student | 1 | student2@example.com |
|
||||
| student2 | Student | 2 | student2@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
@@ -26,19 +26,15 @@ Feature: We can change what we are viewing on the grader report
|
||||
| assign | user | onlinetext |
|
||||
| Test assignment name 1 | student1 | This is a submission for assignment 1 |
|
||||
| Test assignment name 2 | student1 | This is a submission for assignment 2 |
|
||||
And I am on the "Test assignment name 1" "assign activity" page logged in as student1
|
||||
Then I should see "Submitted for grading"
|
||||
And I am on the "Test assignment name 2" "assign activity" page
|
||||
Then I should see "Submitted for grading"
|
||||
And I log out
|
||||
And the following "grade items" exist:
|
||||
| itemname | grademin | grademax | course |
|
||||
| Manual grade | 20 | 40 | C1 |
|
||||
And the following "grade grades" exist:
|
||||
| gradeitem | user | grade |
|
||||
| Test assignment name 1 | student1 | 80 |
|
||||
| Test assignment name 2 | student1 | 90 |
|
||||
| Manual grade | student1 | 30 |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "View > Grader report" in the course gradebook
|
||||
And I turn editing mode on
|
||||
And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment name 1"
|
||||
And I give the grade "90.00" to the user "Student 1" for the grade item "Test assignment name 2"
|
||||
And I press "Save changes"
|
||||
And I turn editing mode off
|
||||
|
||||
@javascript
|
||||
Scenario: View and minimise the grader report containing hidden activities
|
||||
@@ -49,13 +45,15 @@ Feature: We can change what we are viewing on the grader report
|
||||
And I navigate to "View > Grader report" in the course gradebook
|
||||
And I should see "Test assignment name 1"
|
||||
And I should see "Test assignment name 2"
|
||||
And I should see "Manual grade"
|
||||
And I should see "Course total"
|
||||
And the following should exist in the "user-grades" table:
|
||||
| -1- | -4- | -5- | -6- |
|
||||
| Student 1 | 80 | 90 | 170 |
|
||||
| -1- | -4- | -5- | -6- | -7- |
|
||||
| Student 1 | 80 | 90 | 30 | 170 |
|
||||
And I click on "Change to aggregates only" "link"
|
||||
And I should not see "Test assignment name 1"
|
||||
And I should not see "Test assignment name 2"
|
||||
And I should not see "Manual grade"
|
||||
And I should see "Course total"
|
||||
And the following should exist in the "user-grades" table:
|
||||
| -1- | -4- |
|
||||
@@ -63,10 +61,11 @@ Feature: We can change what we are viewing on the grader report
|
||||
And I click on "Change to grades only" "link"
|
||||
And I should see "Test assignment name 1"
|
||||
And I should see "Test assignment name 2"
|
||||
And I should see "Manual grade"
|
||||
And I should not see "Course total"
|
||||
And the following should exist in the "user-grades" table:
|
||||
| -1- | -4- | -5- |
|
||||
| Student 1 | 80 | 90 |
|
||||
| -1- | -4- | -5- | -6- |
|
||||
| Student 1 | 80 | 90 | 30 |
|
||||
|
||||
@javascript @skip_chrome_zerosize
|
||||
Scenario: View and minimise the grader report containing hidden activities without the 'moodle/grade:viewhidden' capability
|
||||
@@ -84,21 +83,24 @@ Feature: We can change what we are viewing on the grader report
|
||||
And I navigate to "View > Grader report" in the course gradebook
|
||||
And I should see "Test assignment name 1"
|
||||
And I should see "Test assignment name 2"
|
||||
And I should see "Manual grade"
|
||||
And I should see "Course total"
|
||||
And the following should exist in the "user-grades" table:
|
||||
| -1- | -4- | -5- | -6- |
|
||||
| Student 1 | 80 | - | 80 |
|
||||
| -1- | -4- | -5- | -6- | -7- |
|
||||
| Student 1 | 80 | - | 30 | 105.71 |
|
||||
And I click on "Change to aggregates only" "link"
|
||||
And I should not see "Test assignment name 1"
|
||||
And I should not see "Test assignment name 2"
|
||||
And I should not see "Manual grade"
|
||||
And I should see "Course total"
|
||||
And the following should exist in the "user-grades" table:
|
||||
| -1- | -4- |
|
||||
| Student 1 | 80 |
|
||||
| Student 1 | 105.71 |
|
||||
And I click on "Change to grades only" "link"
|
||||
And I should see "Test assignment name 1"
|
||||
And I should see "Test assignment name 2"
|
||||
And I should see "Manual grade"
|
||||
And I should not see "Course total"
|
||||
And the following should exist in the "user-grades" table:
|
||||
| -1- | -4- | -5- |
|
||||
| Student 1 | 80 | - |
|
||||
| -1- | -4- | -5- | -6- |
|
||||
| Student 1 | 80 | - | 30 |
|
||||
|
||||
@@ -26,8 +26,7 @@ Feature: We can set the grade to pass value
|
||||
| name | Test Assignment 1 |
|
||||
| intro | Submit your online text |
|
||||
| assignsubmission_onlinetext_enabled | 1 |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
|
||||
@javascript
|
||||
Scenario: Validate that switching the type of grading used correctly disables grade to pass
|
||||
@@ -115,11 +114,10 @@ Feature: We can set the grade to pass value
|
||||
|
||||
Scenario: Set a valid grade to pass for workshop activity
|
||||
Given the following "activities" exist:
|
||||
| activity | name | intro | course | section | idnumber |
|
||||
| workshop | Test Workshop 1 | Test workshop | C1 | 1 | workshop1 |
|
||||
| activity | name | course | idnumber |
|
||||
| workshop | Test Workshop 1 | C1 | workshop1 |
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I follow "Test Workshop 1"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the "Test Workshop 1" "workshop activity editing" page
|
||||
And I set the following fields to these values:
|
||||
| grade | 80 |
|
||||
| Submission grade to pass | 40 |
|
||||
@@ -138,19 +136,16 @@ Feature: We can set the grade to pass value
|
||||
And the field "Grade to pass" matches value "10"
|
||||
And I set the field "Grade to pass" to "15"
|
||||
And I press "Save changes"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Test Workshop 1"
|
||||
And I follow "Edit settings"
|
||||
And I am on the "Test Workshop 1" "workshop activity editing" page
|
||||
And the field "Submission grade to pass" matches value "45"
|
||||
And the field "Assessment grade to pass" matches value "15"
|
||||
|
||||
Scenario: Set an invalid grade to pass for workshop activity
|
||||
Given the following "activities" exist:
|
||||
| activity | name | intro | course | section | idnumber |
|
||||
| workshop | Test Workshop 1 | Test workshop | C1 | 1 | workshop1 |
|
||||
| activity | name | course | idnumber |
|
||||
| workshop | Test Workshop 1 | C1 | workshop1 |
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I follow "Test Workshop 1"
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the "Test Workshop 1" "workshop activity editing" page
|
||||
And I set the following fields to these values:
|
||||
| grade | 80 |
|
||||
| Submission grade to pass | 90 |
|
||||
|
||||
+10
-6
@@ -1346,12 +1346,13 @@ $string['unsupporteddbfilepertable'] = 'For full support of UTF-8 both MySQL and
|
||||
$string['unsupporteddblargeprefix'] = 'For full support of UTF-8 both MySQL and MariaDB require you to change your MySQL setting \'innodb_large_prefix\' to \'ON\'. See the documentation for further details.';
|
||||
$string['unsupporteddbstorageengine'] = 'The database storage engine being used is no longer supported.';
|
||||
$string['unsupporteddbtablerowformat'] = 'Your database has tables using Antelope as the file format. You are recommended to convert the tables to the Barracuda file format. See the documentation <a href="https://docs.moodle.org/en/cli">Administration via command line</a> for details of a tool for converting InnoDB tables to Barracuda.';
|
||||
$string['unsupportedphpversion7'] = 'PHP version 7 is not supported.';
|
||||
$string['unsupportedphpversion71'] = 'PHP version 7.1 is not supported.';
|
||||
$string['unsupportedphpversion72'] = 'PHP version 7.2 is not supported.';
|
||||
$string['unsupportedphpversion73'] = 'PHP version 7.3 is not supported.';
|
||||
$string['unsupportedphpversion74'] = 'PHP version 7.4 is not supported.';
|
||||
$string['unsupportedphpversion80'] = 'PHP version 8.0 is not supported.';
|
||||
$string['unsupportedphpversion7'] = 'PHP version 7 and higher are not supported.';
|
||||
$string['unsupportedphpversion71'] = 'PHP version 7.1 and higher are not supported.';
|
||||
$string['unsupportedphpversion72'] = 'PHP version 7.2 and higher are not supported.';
|
||||
$string['unsupportedphpversion73'] = 'PHP version 7.3 and higher are not supported.';
|
||||
$string['unsupportedphpversion74'] = 'PHP version 7.4 and higher are not supported.';
|
||||
$string['unsupportedphpversion80'] = 'PHP version 8.0 and higher are not supported.';
|
||||
$string['unsupportedphpversion81'] = 'PHP version 8.1 and higher are not supported.';
|
||||
$string['unsuspenduser'] = 'Activate user account';
|
||||
$string['updateaccounts'] = 'Update existing accounts';
|
||||
$string['updatecomponent'] = 'Update component';
|
||||
@@ -1453,6 +1454,9 @@ $string['warningiconvbuggy'] = 'Your version of the iconv library does not suppo
|
||||
$string['webproxy'] = 'Web proxy';
|
||||
$string['webproxyinfo'] = 'Fill in the following options if your Moodle server cannot access the internet directly. Internet access is required for the download of environment data, language packs, RSS feeds, timezones, etc.<br /><em>The PHP cURL extension is highly recommended.</em>';
|
||||
$string['xmlrpcrecommended'] = 'The XMLRPC extension is useful for web services and Moodle networking.';
|
||||
$string['xmlrpcmaharaenabled'] = 'It has been detected that the Mahara ePortfolio is enabled on your site. This feature relies on the PHP XML-RPC extension which is no longer maintained by PHP.';
|
||||
$string['xmlrpcmnetenabled'] = 'It has been detected that the Moodle Networking is enabled on your site. This feature relies on the PHP XML-RPC extension which is no longer maintained by PHP.';
|
||||
$string['xmlrpcwebserviceenabled'] = 'It has been detected that the XML-RPC Web Service protocol is enabled on your site. This feature relies on the PHP XML-RPC extension which is no longer maintained by PHP.';
|
||||
$string['yuicomboloading'] = 'YUI combo loading';
|
||||
$string['ziprequired'] = 'The Zip PHP extension is now required by Moodle, info-ZIP binaries or PclZip library are not used anymore.';
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ $string['america/cayenne'] = 'America/Cayenne';
|
||||
$string['america/cayman'] = 'America/Cayman';
|
||||
$string['america/chicago'] = 'America/Chicago';
|
||||
$string['america/chihuahua'] = 'America/Chihuahua';
|
||||
$string['america/ciudad_juarez'] = 'America/Ciudad_Juarez';
|
||||
$string['america/costa_rica'] = 'America/Costa_Rica';
|
||||
$string['america/creston'] = 'America/Creston';
|
||||
$string['america/cuiaba'] = 'America/Cuiaba';
|
||||
@@ -363,6 +364,7 @@ $string['europe/jersey'] = 'Europe/Jersey';
|
||||
$string['europe/kaliningrad'] = 'Europe/Kaliningrad';
|
||||
$string['europe/kiev'] = 'Europe/Kiev';
|
||||
$string['europe/kirov'] = 'Europe/Kirov';
|
||||
$string['europe/kyiv'] = 'Europe/Kyiv';
|
||||
$string['europe/lisbon'] = 'Europe/Lisbon';
|
||||
$string['europe/ljubljana'] = 'Europe/Ljubljana';
|
||||
$string['europe/london'] = 'Europe/London';
|
||||
|
||||
@@ -333,6 +333,34 @@ class behat_config_util {
|
||||
return $this->get_component_contexts($component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the list of components contexts.
|
||||
*
|
||||
* This ensures that contexts are sorted consistently.
|
||||
* Core hooks defined in the behat_hooks class _must_ be defined first.
|
||||
*
|
||||
* @param array $contexts
|
||||
* @return array The sorted context list
|
||||
*/
|
||||
protected function sort_component_contexts(array $contexts): array {
|
||||
// Ensure that the lib_tests are first as they include the root of all tests, hooks, and more.
|
||||
uksort($contexts, function($a, $b): int {
|
||||
if ($a === 'behat_hooks') {
|
||||
return -1;
|
||||
}
|
||||
if ($b === 'behat_hooks') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($a == $b) {
|
||||
return 0;
|
||||
}
|
||||
return ($a < $b) ? -1 : 1;
|
||||
});
|
||||
|
||||
return $contexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Behat config file specifing the main context class,
|
||||
* the required Behat extensions and Moodle test wwwroot.
|
||||
@@ -1537,8 +1565,9 @@ class behat_config_util {
|
||||
}
|
||||
|
||||
// Sort the list of contexts.
|
||||
ksort($contexts);
|
||||
$contexts = $this->sort_component_contexts($contexts);
|
||||
|
||||
// Cache it for subsequent fetches.
|
||||
$this->themecontexts[$theme] = $contexts;
|
||||
|
||||
return $contexts;
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
|
||||
|
||||
use Behat\Testwork\Environment\Environment;
|
||||
use Behat\Mink\Exception\DriverException;
|
||||
|
||||
/**
|
||||
* Helper to get behat contexts.
|
||||
@@ -115,6 +116,34 @@ class behat_context_helper {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all Behat contexts which match the specified context class name prefix.
|
||||
*
|
||||
* Moodle uses a consistent class naming scheme for all Behat contexts, whereby the context name is in the format:
|
||||
*
|
||||
* behat_{component}
|
||||
*
|
||||
* This method will return all contexts which match the specified prefix.
|
||||
*
|
||||
* For example, to find all editors, you would pass in 'behat_editor', and this might return:
|
||||
* - behat_editor_atto
|
||||
* - behat_editor_textarea
|
||||
*
|
||||
* @param string $prefix The prefix to search for
|
||||
* @return \Behat\Behat\Context\Context[]
|
||||
*/
|
||||
public static function get_prefixed_contexts(string $prefix): array {
|
||||
if (!is_a(self::$environment, \Behat\Behat\Context\Environment\InitializedContextEnvironment::class)) {
|
||||
throw new DriverException(
|
||||
'Cannot get prefixed contexts - the environment is not an InitializedContextEnvironment'
|
||||
);
|
||||
}
|
||||
|
||||
return array_filter(self::$environment->getContexts(), function($context) use ($prefix): bool {
|
||||
return (strpos(get_class($context), $prefix) === 0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for any theme override of the specified class name.
|
||||
*
|
||||
|
||||
@@ -150,6 +150,12 @@ class behat_core_generator extends behat_generator_base {
|
||||
'required' => ['fullname', 'course'],
|
||||
'switchids' => ['course' => 'courseid', 'gradecategory' => 'parent'],
|
||||
],
|
||||
'grade grades' => [
|
||||
'singular' => 'grade grade',
|
||||
'datagenerator' => 'grade_grade',
|
||||
'required' => ['gradeitem'],
|
||||
'switchids' => ['user' => 'userid', 'gradeitem' => 'itemid'],
|
||||
],
|
||||
'grade items' => [
|
||||
'singular' => 'grade item',
|
||||
'datagenerator' => 'grade_item',
|
||||
@@ -275,11 +281,33 @@ class behat_core_generator extends behat_generator_base {
|
||||
'required' => ['user', 'course', 'lastaccess'],
|
||||
'switchids' => ['user' => 'userid', 'course' => 'courseid'],
|
||||
],
|
||||
'notifications' => [
|
||||
'singular' => 'notification',
|
||||
'datagenerator' => 'notification',
|
||||
'required' => ['subject', 'userfrom', 'userto'],
|
||||
'switchids' => ['userfrom' => 'userfromid', 'userto' => 'usertoid'],
|
||||
],
|
||||
];
|
||||
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the grade item id using a name.
|
||||
*
|
||||
* @param string $name
|
||||
* @return int The grade item id
|
||||
*/
|
||||
protected function get_gradeitem_id(string $name): int {
|
||||
global $DB;
|
||||
|
||||
if (!$id = $DB->get_field('grade_items', 'id', ['itemname' => $name])) {
|
||||
throw new Exception('The specified grade item with name "' . $name . '" could not be found.');
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any empty custom fields, to avoid errors when creating the course.
|
||||
*
|
||||
@@ -1009,6 +1037,39 @@ class behat_core_generator extends behat_generator_base {
|
||||
$DB->insert_record('badge_backpack', $backpack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates notifications to specific user.
|
||||
*
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
protected function process_notification(array $data) {
|
||||
global $DB;
|
||||
|
||||
$notification = new stdClass();
|
||||
$notification->useridfrom = $data['userfromid'];
|
||||
$notification->useridto = $data['usertoid'];
|
||||
$notification->subject = $data['subject'];
|
||||
$notification->fullmessage = $data['subject'] . ' description';
|
||||
$notification->smallmessage = $data['subject'] . ' description';
|
||||
$notification->fullmessagehtml = $data['subject'] . ' description';
|
||||
|
||||
if ($data['timecreated'] !== 'null') {
|
||||
$notification->timecreated = $data['timecreated'];
|
||||
}
|
||||
|
||||
if ($data['timeread'] !== 'null') {
|
||||
$notification->timeread = $data['timeread'];
|
||||
}
|
||||
|
||||
if (!empty($data)) {
|
||||
$popupnotification = new stdClass();
|
||||
$popupnotification->notificationid = $DB->insert_record('notifications', $notification);
|
||||
$DB->insert_record('message_popup_notifications', $popupnotification);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates user last access data within given courses.
|
||||
*
|
||||
|
||||
@@ -165,6 +165,15 @@ abstract class behat_generator_base {
|
||||
*/
|
||||
protected abstract function get_creatable_entities(): array;
|
||||
|
||||
/**
|
||||
* Get the list of available generators for this class.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
final public function get_available_generators(): array {
|
||||
return $this->get_creatable_entities();
|
||||
}
|
||||
|
||||
/**
|
||||
* Do the work to generate an entity.
|
||||
*
|
||||
@@ -316,6 +325,36 @@ abstract class behat_generator_base {
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user id from it's username.
|
||||
* @throws Exception
|
||||
* @param string $username
|
||||
* @return int
|
||||
*/
|
||||
protected function get_userfrom_id(string $username) {
|
||||
global $DB;
|
||||
|
||||
if (!$id = $DB->get_field('user', 'id', ['username' => $username])) {
|
||||
throw new Exception('The specified user with username "' . $username . '" does not exist');
|
||||
}
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user id from it's username.
|
||||
* @throws Exception
|
||||
* @param string $username
|
||||
* @return int
|
||||
*/
|
||||
protected function get_userto_id(string $username) {
|
||||
global $DB;
|
||||
|
||||
if (!$id = $DB->get_field('user', 'id', ['username' => $username])) {
|
||||
throw new Exception('The specified user with username "' . $username . '" does not exist');
|
||||
}
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the role id from it's shortname.
|
||||
* @throws Exception
|
||||
|
||||
@@ -30,6 +30,7 @@ use Behat\Mink\Exception\ExpectationException;
|
||||
use Behat\Mink\Exception\ElementNotFoundException;
|
||||
use Behat\Mink\Exception\NoSuchWindowException;
|
||||
use Behat\Mink\Session;
|
||||
use Behat\Testwork\Hook\Scope\HookScope;
|
||||
use Facebook\WebDriver\Exception\ScriptTimeoutException;
|
||||
use Facebook\WebDriver\WebDriverBy;
|
||||
use Facebook\WebDriver\WebDriverElement;
|
||||
@@ -223,6 +224,22 @@ trait behat_session_trait {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a description of the selector and locator to use in an exception message.
|
||||
*
|
||||
* @param string $selector The type of locator
|
||||
* @param mixed $locator The locator text
|
||||
* @return string
|
||||
*/
|
||||
protected function get_selector_description(string $selector, $locator): string {
|
||||
if ($selector === 'NodeElement') {
|
||||
$description = $locator->getText();
|
||||
return "'{$description}' {$selector}";
|
||||
}
|
||||
|
||||
return "'{$locator}' {$selector}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Send key presses straight to the currently active element.
|
||||
*
|
||||
@@ -423,13 +440,12 @@ trait behat_session_trait {
|
||||
if ($containerselectortype === 'NodeElement' && is_a($containerelement, NodeElement::class)) {
|
||||
// Support a NodeElement being passed in for use in step chaining.
|
||||
$containernode = $containerelement;
|
||||
$locatorexceptionmsg = $element;
|
||||
} else {
|
||||
// Gets the container, it will always be text based.
|
||||
$containernode = $this->get_text_selector_node($containerselectortype, $containerelement);
|
||||
$locatorexceptionmsg = $element . '" in the "' . $containerelement. '" "' . $containerselectortype. '"';
|
||||
}
|
||||
|
||||
$locatorexceptionmsg = $element . '" in the "' . $this->get_selector_description($containerselectortype, $containerelement);
|
||||
$exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $locatorexceptionmsg);
|
||||
|
||||
return $this->find($selectortype, $element, $exception, $containernode);
|
||||
@@ -1029,6 +1045,29 @@ EOF;
|
||||
$this->look_for_exceptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a function in a specific behat context.
|
||||
*
|
||||
* For example, to call the 'set_editor_value' function for all editors, you would call:
|
||||
*
|
||||
* behat_base::execute_in_matching_contexts('editor', 'set_editor_value', ['Some value']);
|
||||
*
|
||||
* This would find all behat contexts whose class name starts with 'behat_editor_' and
|
||||
* call the 'set_editor_value' function on that context.
|
||||
*
|
||||
* @param string $prefix
|
||||
* @param string $method
|
||||
* @param array $params
|
||||
*/
|
||||
public static function execute_in_matching_contexts(string $prefix, string $method, array $params): void {
|
||||
$contexts = behat_context_helper::get_prefixed_contexts("behat_{$prefix}_");
|
||||
foreach ($contexts as $context) {
|
||||
if (method_exists($context, $method) && is_callable([$context, $method])) {
|
||||
call_user_func_array([$context, $method], $params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actual user in the behat session (note $USER does not correspond to the behat session's user).
|
||||
* @return mixed
|
||||
@@ -1578,4 +1617,63 @@ EOF;
|
||||
|
||||
return get_fast_modinfo($course)->get_cm($result->cmid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether any of the tags availble to the current scope match using the given callable.
|
||||
*
|
||||
* This function is typically called from within a Behat Hook, such as BeforeFeature, BeforeScenario, AfterStep, etc.
|
||||
*
|
||||
* The callable is used as the second argument to `array_filter()`, and is passed a single string argument for each of the
|
||||
* tags available in the scope.
|
||||
*
|
||||
* The tags passed will include:
|
||||
* - For a FeatureScope, the Feature tags only
|
||||
* - For a ScenarioScope, the Feature and Scenario tags
|
||||
* - For a StepScope, the Feature, Scenario, and Step tags
|
||||
*
|
||||
* An example usage may be:
|
||||
*
|
||||
* // Note: phpDoc beforeStep attribution not shown.
|
||||
* public function before_step(StepScope $scope) {
|
||||
* $callback = function (string $tag): bool {
|
||||
* return $tag === 'editor_atto' || substr($tag, 0, 5) === 'atto_';
|
||||
* };
|
||||
*
|
||||
* if (!self::scope_tags_match($scope, $callback)) {
|
||||
* return;
|
||||
* }
|
||||
*
|
||||
* // Do something here.
|
||||
* }
|
||||
*
|
||||
* @param HookScope $scope The scope to check
|
||||
* @param callable $callback The callable to use to check the scope
|
||||
* @return boolean Whether any of the scope tags match
|
||||
*/
|
||||
public static function scope_tags_match(HookScope $scope, callable $callback): bool {
|
||||
$tags = [];
|
||||
|
||||
if (is_subclass_of($scope, \Behat\Behat\Hook\Scope\FeatureScope::class)) {
|
||||
$tags = $scope->getFeature()->getTags();
|
||||
}
|
||||
|
||||
if (is_subclass_of($scope, \Behat\Behat\Hook\Scope\ScenarioScope::class)) {
|
||||
$tags = array_merge(
|
||||
$scope->getFeature()->getTags(),
|
||||
$scope->getScenario()->getTags()
|
||||
);
|
||||
}
|
||||
|
||||
if (is_subclass_of($scope, \Behat\Behat\Hook\Scope\StepScope::class)) {
|
||||
$tags = array_merge(
|
||||
$scope->getFeature()->getTags(),
|
||||
$scope->getScenario()->getTags(),
|
||||
$scope->getStep()->getTags()
|
||||
);
|
||||
}
|
||||
|
||||
$matches = array_filter($tags, $callback);
|
||||
|
||||
return !empty($matches);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
|
||||
*/
|
||||
protected static $allowedselectors = array(
|
||||
'activity' => 'activity',
|
||||
'actionmenu' => 'actionmenu',
|
||||
'block' => 'block',
|
||||
'button' => 'button',
|
||||
'checkbox' => 'checkbox',
|
||||
@@ -136,6 +137,17 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
|
||||
protected static $moodleselectors = array(
|
||||
'activity' => <<<XPATH
|
||||
.//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][descendant::*[contains(normalize-space(.), %locator%)]]
|
||||
XPATH
|
||||
, 'actionmenu' => <<<XPATH
|
||||
.//*[
|
||||
contains(concat(' ', normalize-space(@class), ' '), ' action-menu ')
|
||||
and
|
||||
descendant::*[
|
||||
contains(concat(' ', normalize-space(@class), ' '), ' dropdown-toggle ')
|
||||
and
|
||||
(contains(normalize-space(.), %locator%) or descendant::*[%titleMatch%])
|
||||
]
|
||||
]
|
||||
XPATH
|
||||
, 'block' => <<<XPATH
|
||||
.//*[@data-block][contains(concat(' ', normalize-space(@class), ' '), concat(' ', %locator%, ' ')) or
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Editor interface for setting editor values.
|
||||
*
|
||||
* @package behat
|
||||
* @category test
|
||||
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core_behat;
|
||||
|
||||
interface settable_editor {
|
||||
|
||||
/**
|
||||
* Set the editor value.
|
||||
*
|
||||
* @param string $editorid The id of the editor within the page
|
||||
* @param string $value The intended content of the editor
|
||||
*/
|
||||
public function set_editor_value(string $editorid, string $value): void;
|
||||
}
|
||||
@@ -32,7 +32,6 @@ require_once(__DIR__ . '/behat_form_textarea.php');
|
||||
/**
|
||||
* Moodle editor field.
|
||||
*
|
||||
* @todo Support for multiple editors
|
||||
* @package core_form
|
||||
* @category test
|
||||
* @copyright 2012 David Monllaó
|
||||
@@ -44,31 +43,40 @@ class behat_form_editor extends behat_form_textarea {
|
||||
* Sets the value to a field.
|
||||
*
|
||||
* @param string $value
|
||||
* @return void
|
||||
*/
|
||||
public function set_value($value) {
|
||||
|
||||
public function set_value($value): void {
|
||||
$editorid = $this->field->getAttribute('id');
|
||||
if ($this->running_javascript()) {
|
||||
$value = addslashes($value);
|
||||
// This will be transported in JSON, which doesn't allow newlines in strings, so we must escape them.
|
||||
$value = str_replace("\n", "\\n", $value);
|
||||
$js = '
|
||||
(function() {
|
||||
var editor = Y.one(document.getElementById("'.$editorid.'editable"));
|
||||
if (editor) {
|
||||
editor.setHTML("' . $value . '");
|
||||
}
|
||||
editor = Y.one(document.getElementById("'.$editorid.'"));
|
||||
editor.set("value", "' . $value . '");
|
||||
})();
|
||||
';
|
||||
behat_base::execute_script_in_session($this->session, $js);
|
||||
behat_base::execute_in_matching_contexts('editor', 'set_editor_value', [
|
||||
$editorid,
|
||||
$value,
|
||||
]);
|
||||
|
||||
} else {
|
||||
parent::set_value($value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current value of the select element.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_value(): string {
|
||||
if ($this->running_javascript()) {
|
||||
// Give any listening editors a chance to persist the value to the textarea.
|
||||
// Some editors only do this on form submission or similar events.
|
||||
behat_base::execute_in_matching_contexts('editor', 'store_current_value', [
|
||||
$this->field->getAttribute('id'),
|
||||
]);
|
||||
}
|
||||
|
||||
return parent::get_value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select all the text in the form field.
|
||||
*
|
||||
@@ -102,8 +110,60 @@ class behat_form_editor extends behat_form_textarea {
|
||||
* @return bool The provided value matches the field value?
|
||||
*/
|
||||
public function matches($expectedvalue) {
|
||||
// A text editor may silently wrap the content in p tags (or not). Neither is an error.
|
||||
return $this->text_matches($expectedvalue) || $this->text_matches('<p>' . $expectedvalue . '</p>');
|
||||
// Fetch the actual value to save fetching it multiple times.
|
||||
$actualvalue = $this->get_value();
|
||||
|
||||
if ($this->text_matches($expectedvalue, $actualvalue)) {
|
||||
// The text is an exact match already.
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->text_matches("<p>{$expectedvalue}</p>", $actualvalue)) {
|
||||
// A text editor may silently wrap the content in p tags.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Standardise both the expected value and the actual field value.
|
||||
// We are likely dealing with HTML content, given this is an editor.
|
||||
$expectedvalue = $this->standardise_html($expectedvalue);
|
||||
$actualvalue = $this->standardise_html($actualvalue);
|
||||
|
||||
// Note: We don't need to worry about the floats here that we care about in text_matches.
|
||||
// That condition isn't relevant to the content of an editor.
|
||||
if ($expectedvalue === $actualvalue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standardises the HTML content for comparison.
|
||||
*
|
||||
* @param string $html The HTML content to standardise
|
||||
* @return string The standardised HTML content
|
||||
*/
|
||||
protected function standardise_html(string $html): string {
|
||||
$document = new DOMDocument();
|
||||
$errorstate = libxml_use_internal_errors(true);
|
||||
|
||||
// Format the whitespace nicely.
|
||||
$document->preserveWhiteSpace = false;
|
||||
$document->formatOutput = true;
|
||||
|
||||
// Wrap the content in a DIV element so that it is not parsed weirdly.
|
||||
// Note: We must remove newlines too because DOMDocument does not do so, despite preserveWhiteSpace being false.
|
||||
// Unfortunately this is slightly limited in that it will also remove newlines from <pre> content and similar.
|
||||
$document->loadHTML(str_replace("\n", "", "<div>{$html}</div>"), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
$document->normalizeDocument();
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($errorstate);
|
||||
|
||||
// Save the content of the 'div' element, removing the <div> and </div> tags at the start and end.
|
||||
return trim(substr(
|
||||
$document->saveHTML($document->getElementsByTagName('div')->item(0)),
|
||||
5,
|
||||
-6
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -244,10 +244,14 @@ class behat_form_field implements behat_session_interface {
|
||||
* Checks if the provided text matches the field value.
|
||||
*
|
||||
* @param string $expectedvalue
|
||||
* @param string|null $actualvalue The actual value. If not specified, this will be fetched from $this->get_value().
|
||||
* @return bool
|
||||
*/
|
||||
protected function text_matches($expectedvalue) {
|
||||
if (trim($expectedvalue) != trim($this->get_value())) {
|
||||
protected function text_matches($expectedvalue, ?string $actualvalue = null): bool {
|
||||
$actualvalue = $actualvalue ?? $this->get_value();
|
||||
|
||||
// Non strict string comparison.
|
||||
if (trim($expectedvalue) != trim($actualvalue)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -111,7 +111,7 @@ abstract class scheduled_task extends task_base {
|
||||
*/
|
||||
public function set_minute($minute, $expandr = true) {
|
||||
if ($minute === 'R' && $expandr) {
|
||||
$minute = mt_rand(self::HOURMIN, self::HOURMAX);
|
||||
$minute = mt_rand(self::MINUTEMIN, self::MINUTEMAX);
|
||||
}
|
||||
$this->minute = $minute;
|
||||
}
|
||||
|
||||
@@ -991,8 +991,9 @@ class core_user {
|
||||
$preferences['user_home_page_preference'] = array('type' => PARAM_INT, 'null' => NULL_ALLOWED, 'default' => HOMEPAGE_MY,
|
||||
'choices' => array(HOMEPAGE_SITE, HOMEPAGE_MY),
|
||||
'permissioncallback' => function ($user, $preferencename) {
|
||||
global $CFG;
|
||||
return (!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_USER));
|
||||
global $CFG, $USER;
|
||||
return $user->id == $USER->id &&
|
||||
(!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_USER));
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
+13
-7
@@ -506,21 +506,27 @@ function get_users_listing($sort='lastaccess', $dir='ASC', $page=0, $recordsperp
|
||||
$params = $params + (array)$extraparams;
|
||||
}
|
||||
|
||||
if ($sort) {
|
||||
$sort = " ORDER BY $sort $dir";
|
||||
}
|
||||
|
||||
// If a context is specified, get extra user fields that the current user
|
||||
// is supposed to see.
|
||||
$extrafields = '';
|
||||
$includedfields = ['id', 'username', 'email', 'firstname', 'lastname', 'city', 'country',
|
||||
'lastaccess', 'confirmed', 'mnethostid', 'suspended'];
|
||||
if ($extracontext) {
|
||||
$extrafields = get_extra_user_fields_sql($extracontext, '', '',
|
||||
array('id', 'username', 'email', 'firstname', 'lastname', 'city', 'country',
|
||||
'lastaccess', 'confirmed', 'mnethostid'));
|
||||
$extrafields = get_extra_user_fields_sql($extracontext, '', '', $includedfields);
|
||||
}
|
||||
$namefields = get_all_user_name_fields(true);
|
||||
$extrafields = "$extrafields, $namefields";
|
||||
|
||||
if ($sort) {
|
||||
$orderbymap = trim($extrafields, ', ');
|
||||
$orderbymap = array_merge(explode(',', $orderbymap), $includedfields);
|
||||
$neworderbymap = ['default' => 'lastaccess'];
|
||||
foreach($orderbymap as $value) {
|
||||
$neworderbymap[$value] = $value;
|
||||
}
|
||||
$sort = get_safe_orderby($neworderbymap, $sort, $dir);
|
||||
}
|
||||
|
||||
// warning: will return UNCONFIRMED USERS
|
||||
return $DB->get_records_sql("SELECT id, username, email, city, country, lastaccess, confirmed, mnethostid, suspended $extrafields
|
||||
FROM {user}
|
||||
|
||||
+2
-2
@@ -242,8 +242,8 @@ $tasks = array(
|
||||
array(
|
||||
'classname' => 'core\task\check_for_updates_task',
|
||||
'blocking' => 0,
|
||||
'minute' => '0',
|
||||
'hour' => '*/2',
|
||||
'minute' => 'R',
|
||||
'hour' => 'R',
|
||||
'day' => '*',
|
||||
'dayofweek' => '*',
|
||||
'month' => '*'
|
||||
|
||||
@@ -23,9 +23,12 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
|
||||
|
||||
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
|
||||
|
||||
require_once(__DIR__ . '/../../../../behat/behat_base.php');
|
||||
require_once(__DIR__ . '/../../../../behat/classes/settable_editor.php');
|
||||
|
||||
/**
|
||||
* Steps definitions to deal with the atto text editor
|
||||
@@ -35,7 +38,30 @@ require_once(__DIR__ . '/../../../../behat/behat_base.php');
|
||||
* @copyright 2014 Damyon Wiese
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class behat_editor_atto extends behat_base {
|
||||
class behat_editor_atto extends behat_base implements \core_behat\settable_editor {
|
||||
|
||||
/**
|
||||
* Set the value for the editor.
|
||||
*
|
||||
* @param string $editorid
|
||||
* @param string $value
|
||||
*/
|
||||
public function set_editor_value(string $editorid, string $value): void {
|
||||
$js = <<<EOF
|
||||
(function() {
|
||||
const editableEditor = document.getElementById("${editorid}editable");
|
||||
if (editableEditor && editableEditor.classList.contains('editor_atto_content')) {
|
||||
editableEditor.innerHTML = "${value}";
|
||||
}
|
||||
const editor = document.getElementById("${editorid}");
|
||||
if (editor) {
|
||||
editor.value = "${value}";
|
||||
}
|
||||
})();
|
||||
EOF;
|
||||
|
||||
$this->execute_script($js);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the text in an Atto field.
|
||||
@@ -59,6 +85,24 @@ class behat_editor_atto extends behat_base {
|
||||
$field->select_text();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the Atto editor is used for all tests using the editor_atto, or atto_* tags.
|
||||
*
|
||||
* This ensures, whatever the default editor, that the Atto editor is used for these tests.
|
||||
*
|
||||
* @BeforeScenario
|
||||
* @param BeforeScenarioScope $scope The Behat Scope
|
||||
*/
|
||||
public function set_default_editor_flag(BeforeScenarioScope $scope): void {
|
||||
// This only applies to a scenario which matches the editor_atto, or an atto subplugin.
|
||||
$callback = function (string $tag): bool {
|
||||
return $tag === 'editor_atto' || substr($tag, 0, 5) === 'atto_';
|
||||
};
|
||||
|
||||
if (!self::scope_tags_match($scope, $callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->execute('behat_general::the_default_editor_is_set_to', ['atto']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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/>.
|
||||
|
||||
/**
|
||||
* Moodle textarea editor field.
|
||||
*
|
||||
* @package core_form
|
||||
* @category test
|
||||
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../../../behat/behat_base.php');
|
||||
require_once(__DIR__ . '/../../../../behat/classes/settable_editor.php');
|
||||
|
||||
class behat_editor_textarea extends behat_base implements \core_behat\settable_editor {
|
||||
|
||||
/**
|
||||
* Set the value for the editor.
|
||||
*
|
||||
* @param string $editorid
|
||||
* @param string $value
|
||||
*/
|
||||
public function set_editor_value(string $editorid, string $value): void {
|
||||
$js = <<<EOF
|
||||
(function() {
|
||||
const editor = document.getElementById("${editorid}");
|
||||
if (editor && editor.tagName.toLowerCase() === 'textarea') {
|
||||
editor.value = "${value}";
|
||||
}
|
||||
})();
|
||||
EOF;
|
||||
$this->execute_script($js);
|
||||
}
|
||||
}
|
||||
@@ -1657,3 +1657,14 @@ function restrict_php_version_74(&$result) {
|
||||
function restrict_php_version_80($result) {
|
||||
return restrict_php_version($result, '8.0');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current PHP version is greater than or equal to
|
||||
* PHP version 8.1
|
||||
*
|
||||
* @param object $result an environment_results instance
|
||||
* @return bool result of version check
|
||||
*/
|
||||
function restrict_php_version_81($result) {
|
||||
return restrict_php_version($result, '8.1');
|
||||
}
|
||||
|
||||
+5
-1
@@ -3265,7 +3265,11 @@ class curl {
|
||||
throw new coding_exception('Curl options should be defined using strings, not constant values.');
|
||||
}
|
||||
if (stripos($name, 'CURLOPT_') === false) {
|
||||
$name = strtoupper('CURLOPT_'.$name);
|
||||
// Only prefix with CURLOPT_ if the option doesn't contain CURLINFO_,
|
||||
// which is a valid prefix for at least one option CURLINFO_HEADER_OUT.
|
||||
if (stripos($name, 'CURLINFO_') === false) {
|
||||
$name = strtoupper('CURLOPT_'.$name);
|
||||
}
|
||||
} else {
|
||||
$name = strtoupper($name);
|
||||
}
|
||||
|
||||
@@ -64,6 +64,9 @@ class MoodleQuickForm_editor extends HTML_QuickForm_element implements templatab
|
||||
/** @var array values for editor */
|
||||
protected $_values = array('text'=>null, 'format'=>null, 'itemid'=>null);
|
||||
|
||||
/** @var bool if true label will be hidden */
|
||||
protected $_hiddenLabel = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
@@ -496,4 +499,14 @@ class MoodleQuickForm_editor extends HTML_QuickForm_element implements templatab
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets label to be hidden.
|
||||
*
|
||||
* @param bool $hiddenLabel Whether the label should be hidden or not.
|
||||
* @return void
|
||||
*/
|
||||
function setHiddenLabel($hiddenLabel) {
|
||||
$this->_hiddenLabel = $hiddenLabel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{{< core_form/element-template-inline }}
|
||||
{{$element}}
|
||||
{{{element.html}}}
|
||||
{{/element}}
|
||||
{{/ core_form/element-template-inline }}
|
||||
@@ -16,6 +16,8 @@ These changes can be reverted once the following pull requests have been integra
|
||||
This local changes can be reverted once it's checked that they are present upstream (note the
|
||||
LTI-Tool-Provider-Library-PHP repo has been archived so it doesn't accept pull requests anymore):
|
||||
* MDL-67034 php74 compliance fixes
|
||||
* MDL-71920: Migrated from curl_exec and friends to use our Moodle curl wrapper,
|
||||
so we can better handle site security settings
|
||||
|
||||
It is recommended by upstream to install depdencies via composer - but the composer installation is bundled
|
||||
with an autoloader so it's better to do it manually.
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
namespace IMSGlobal\LTI;
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/filelib.php');
|
||||
|
||||
/**
|
||||
* Class to represent an HTTP message
|
||||
*
|
||||
@@ -110,31 +113,33 @@ class HTTPMessage
|
||||
{
|
||||
|
||||
$this->ok = false;
|
||||
// Try using curl if available
|
||||
// Try using curl if available
|
||||
if (function_exists('curl_init')) {
|
||||
$resp = '';
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $this->url);
|
||||
$chResp = '';
|
||||
|
||||
$curl = new \curl();
|
||||
$options = [
|
||||
'CURLOPT_HEADER' => true,
|
||||
'CURLINFO_HEADER_OUT' => true,
|
||||
];
|
||||
if (!empty($this->requestHeaders)) {
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->requestHeaders);
|
||||
$options['CURLOPT_HTTPHEADER'] = $this->requestHeaders;
|
||||
} else {
|
||||
curl_setopt($ch, CURLOPT_HEADER, 0);
|
||||
$options['CURLOPT_HEADER'] = 0;
|
||||
}
|
||||
if ($this->method === 'POST') {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->request);
|
||||
$chResp = $curl->post($this->url, $this->request, $options);
|
||||
} else if ($this->method !== 'GET') {
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->method);
|
||||
if (!is_null($this->request)) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->request);
|
||||
$chResp = $curl->post($this->url, $this->request, $options);
|
||||
}
|
||||
} else {
|
||||
$chResp = $curl->get($this->url, null, $options);
|
||||
}
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
|
||||
curl_setopt($ch, CURLOPT_HEADER, true);
|
||||
$chResp = curl_exec($ch);
|
||||
$this->ok = $chResp !== false;
|
||||
if ($this->ok) {
|
||||
$info = $curl->get_info();
|
||||
|
||||
if (!$curl->get_errno() && !$curl->error) {
|
||||
$chResp = str_replace("\r\n", "\n", $chResp);
|
||||
$chRespSplit = explode("\n\n", $chResp, 2);
|
||||
if ((count($chRespSplit) > 1) && (substr($chRespSplit[1], 0, 5) === 'HTTP/')) {
|
||||
@@ -142,17 +147,20 @@ class HTTPMessage
|
||||
}
|
||||
$this->responseHeaders = $chRespSplit[0];
|
||||
$resp = $chRespSplit[1];
|
||||
$this->status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$this->status = $info['http_code'];
|
||||
$this->ok = $this->status < 400;
|
||||
if (!$this->ok) {
|
||||
$this->error = curl_error($ch);
|
||||
$this->error = $curl->error;
|
||||
}
|
||||
} else {
|
||||
$this->error = $curl->error;
|
||||
$resp = $chResp;
|
||||
}
|
||||
$this->requestHeaders = str_replace("\r\n", "\n", curl_getinfo($ch, CURLINFO_HEADER_OUT));
|
||||
curl_close($ch);
|
||||
|
||||
$this->response = $resp;
|
||||
$this->requestHeaders = !empty($info['request_header']) ? str_replace("\r\n", "\n", $info['request_header']) : '';
|
||||
} else {
|
||||
// Try using fopen if curl was not available
|
||||
// Try using fopen if curl was not available
|
||||
$opts = array('method' => $this->method,
|
||||
'content' => $this->request
|
||||
);
|
||||
|
||||
@@ -38,7 +38,7 @@ class processor implements \core_analytics\classifier, \core_analytics\regresso
|
||||
/**
|
||||
* The required version of the python package that performs all calculations.
|
||||
*/
|
||||
const REQUIRED_PIP_PACKAGE_VERSION = '2.6.5';
|
||||
const REQUIRED_PIP_PACKAGE_VERSION = '2.6.6';
|
||||
|
||||
/**
|
||||
* The python package is installed in a server.
|
||||
|
||||
+6
-2
@@ -206,7 +206,10 @@ define('PARAM_RAW_TRIMMED', 'raw_trimmed');
|
||||
define('PARAM_SAFEDIR', 'safedir');
|
||||
|
||||
/**
|
||||
* PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths, etc.
|
||||
* PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths
|
||||
* and other references to Moodle code files.
|
||||
*
|
||||
* This is NOT intended to be used for absolute paths or any user uploaded files.
|
||||
*/
|
||||
define('PARAM_SAFEPATH', 'safepath');
|
||||
|
||||
@@ -1060,8 +1063,9 @@ function clean_param($param, $type) {
|
||||
} else if (preg_match('/^' . preg_quote($CFG->wwwroot . '/', '/') . '/i', $param)) {
|
||||
// Absolute, and matches our wwwroot.
|
||||
} else {
|
||||
|
||||
// Relative - let's make sure there are no tricks.
|
||||
if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?')) {
|
||||
if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?') && !preg_match('/javascript:/i', $param)) {
|
||||
// Looks ok.
|
||||
} else {
|
||||
$param = '';
|
||||
|
||||
@@ -18,5 +18,5 @@ Note: All this changes need to be reviewed on every upgrade and, if they have
|
||||
been already applied upstream for the release being used, can be removed
|
||||
from the list. If still not available upstream, they will need to be re-applied.
|
||||
|
||||
- MDL-67114: PHP 7.4 compatibility. Array operations on scalar value.
|
||||
This corresponds to upstream https://github.com/bobthecow/mustache.php/pull/352
|
||||
1) If the relevant pull request has not been accepted yet, apply the following commit, so we are able to disable unnecessary rendering:
|
||||
https://github.com/bobthecow/mustache.php/pull/402/commits/db771014c7e346438f68077813ebdda3fdae12df#
|
||||
|
||||
@@ -33,4 +33,11 @@ interface Mustache_Cache
|
||||
* @param string $value
|
||||
*/
|
||||
public function cache($key, $value);
|
||||
|
||||
/**
|
||||
* Set a logger instance.
|
||||
*
|
||||
* @param Mustache_Logger|Psr\Log\LoggerInterface $logger
|
||||
*/
|
||||
public function setLogger($logger = null);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ abstract class Mustache_Cache_AbstractCache implements Mustache_Cache
|
||||
/**
|
||||
* Add a log record if logging is enabled.
|
||||
*
|
||||
* @param int $level The logging level
|
||||
* @param string $level The logging level
|
||||
* @param string $message The log message
|
||||
* @param array $context The log context
|
||||
*/
|
||||
|
||||
@@ -26,31 +26,34 @@ class Mustache_Compiler
|
||||
private $entityFlags;
|
||||
private $charset;
|
||||
private $strictCallables;
|
||||
private $disableLambdaRendering;
|
||||
|
||||
/**
|
||||
* Compile a Mustache token parse tree into PHP source code.
|
||||
*
|
||||
* @param string $source Mustache Template source code
|
||||
* @param string $tree Parse tree of Mustache tokens
|
||||
* @param string $name Mustache Template class name
|
||||
* @param bool $customEscape (default: false)
|
||||
* @param string $charset (default: 'UTF-8')
|
||||
* @param bool $strictCallables (default: false)
|
||||
* @param int $entityFlags (default: ENT_COMPAT)
|
||||
* @param string $source Mustache Template source code
|
||||
* @param array $tree Parse tree of Mustache tokens
|
||||
* @param string $name Mustache Template class name
|
||||
* @param bool $customEscape (default: false)
|
||||
* @param string $charset (default: 'UTF-8')
|
||||
* @param bool $strictCallables (default: false)
|
||||
* @param int $entityFlags (default: ENT_COMPAT)
|
||||
* @param bool $disableLambdaRendering (default: false)
|
||||
*
|
||||
* @return string Generated PHP source code
|
||||
*/
|
||||
public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT)
|
||||
public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT, $disableLambdaRendering = false)
|
||||
{
|
||||
$this->pragmas = $this->defaultPragmas;
|
||||
$this->sections = array();
|
||||
$this->blocks = array();
|
||||
$this->source = $source;
|
||||
$this->indentNextLine = true;
|
||||
$this->customEscape = $customEscape;
|
||||
$this->entityFlags = $entityFlags;
|
||||
$this->charset = $charset;
|
||||
$this->strictCallables = $strictCallables;
|
||||
$this->pragmas = $this->defaultPragmas;
|
||||
$this->sections = array();
|
||||
$this->blocks = array();
|
||||
$this->source = $source;
|
||||
$this->indentNextLine = true;
|
||||
$this->customEscape = $customEscape;
|
||||
$this->entityFlags = $entityFlags;
|
||||
$this->charset = $charset;
|
||||
$this->strictCallables = $strictCallables;
|
||||
$this->disableLambdaRendering = $disableLambdaRendering;
|
||||
|
||||
return $this->writeCode($tree, $name);
|
||||
}
|
||||
@@ -286,7 +289,6 @@ class Mustache_Compiler
|
||||
private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level)
|
||||
{
|
||||
$key = $this->block($nodes);
|
||||
$keystr = var_export($key, true);
|
||||
$id = var_export($id, true);
|
||||
|
||||
return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key);
|
||||
@@ -321,7 +323,6 @@ class Mustache_Compiler
|
||||
}
|
||||
|
||||
const SECTION_CALL = '
|
||||
// %s section
|
||||
$value = $context->%s(%s);%s
|
||||
$buffer .= $this->section%s($context, $indent, $value);
|
||||
';
|
||||
@@ -333,14 +334,8 @@ class Mustache_Compiler
|
||||
|
||||
if (%s) {
|
||||
$source = %s;
|
||||
$result = call_user_func($value, $source, %s);
|
||||
if (strpos($result, \'{{\') === false) {
|
||||
$buffer .= $result;
|
||||
} else {
|
||||
$buffer .= $this->mustache
|
||||
->loadLambda((string) $result%s)
|
||||
->renderInternal($context);
|
||||
}
|
||||
$result = (string) call_user_func($value, $source, %s);%s
|
||||
$buffer .= $result;
|
||||
} elseif (!empty($value)) {
|
||||
$values = $this->isIterable($value) ? $value : array($value);
|
||||
foreach ($values as $value) {
|
||||
@@ -354,6 +349,36 @@ class Mustache_Compiler
|
||||
}
|
||||
';
|
||||
|
||||
const SECTION_RENDER_LAMBDA = '
|
||||
if (strpos($result, \'{{\') !== false) {
|
||||
$result = $this->mustache
|
||||
->loadLambda($result%s)
|
||||
->renderInternal($context);
|
||||
}
|
||||
';
|
||||
|
||||
/**
|
||||
* Helper function to compile section with and without lambda rendering.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $callable
|
||||
* @param string $source
|
||||
* @param string $helper
|
||||
* @param string $delims
|
||||
* @param string $content
|
||||
*
|
||||
* @return string section code
|
||||
*/
|
||||
private function getSection($key, $callable, $source, $helper, $delims, $content)
|
||||
{
|
||||
$render = '';
|
||||
if (!$this->disableLambdaRendering) {
|
||||
$render = sprintf($this->prepare(self::SECTION_RENDER_LAMBDA, 2), $delims);
|
||||
}
|
||||
|
||||
return sprintf($this->prepare(self::SECTION), $key, $callable, $source, $helper, $render, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Mustache Template section PHP source.
|
||||
*
|
||||
@@ -385,18 +410,17 @@ class Mustache_Compiler
|
||||
$key = ucfirst(md5($delims . "\n" . $source));
|
||||
|
||||
if (!isset($this->sections[$key])) {
|
||||
$this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $helper, $delims, $this->walk($nodes, 2));
|
||||
$this->sections[$key] = $this->getSection($key, $callable, $source, $helper, $delims, $this->walk($nodes, 2));
|
||||
}
|
||||
|
||||
$method = $this->getFindMethod($id);
|
||||
$id = var_export($id, true);
|
||||
$filters = $this->getFilters($filters, $level);
|
||||
|
||||
return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $method, $id, $filters, $key);
|
||||
return sprintf($this->prepare(self::SECTION_CALL, $level), $method, $id, $filters, $key);
|
||||
}
|
||||
|
||||
const INVERTED_SECTION = '
|
||||
// %s inverted section
|
||||
$value = $context->%s(%s);%s
|
||||
if (empty($value)) {
|
||||
%s
|
||||
@@ -419,7 +443,7 @@ class Mustache_Compiler
|
||||
$id = var_export($id, true);
|
||||
$filters = $this->getFilters($filters, $level);
|
||||
|
||||
return sprintf($this->prepare(self::INVERTED_SECTION, $level), $id, $method, $id, $filters, $this->walk($nodes, $level));
|
||||
return sprintf($this->prepare(self::INVERTED_SECTION, $level), $method, $id, $filters, $this->walk($nodes, $level));
|
||||
}
|
||||
|
||||
const PARTIAL_INDENT = ', $indent . %s';
|
||||
@@ -507,7 +531,7 @@ class Mustache_Compiler
|
||||
|
||||
const VARIABLE = '
|
||||
$value = $this->resolveValue($context->%s(%s), $context);%s
|
||||
$buffer .= %s%s;
|
||||
$buffer .= %s($value === null ? \'\' : %s);
|
||||
';
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
*/
|
||||
class Mustache_Engine
|
||||
{
|
||||
const VERSION = '2.13.0';
|
||||
const SPEC_VERSION = '1.1.2';
|
||||
const VERSION = '2.14.1';
|
||||
const SPEC_VERSION = '1.2.2';
|
||||
|
||||
const PRAGMA_FILTERS = 'FILTERS';
|
||||
const PRAGMA_BLOCKS = 'BLOCKS';
|
||||
@@ -53,6 +53,7 @@ class Mustache_Engine
|
||||
private $charset = 'UTF-8';
|
||||
private $logger;
|
||||
private $strictCallables = false;
|
||||
private $disableLambdaRendering = false;
|
||||
private $pragmas = array();
|
||||
private $delimiters;
|
||||
|
||||
@@ -130,6 +131,11 @@ class Mustache_Engine
|
||||
* // This currently defaults to false, but will default to true in v3.0.
|
||||
* 'strict_callables' => true,
|
||||
*
|
||||
* // Do not render the output of lambdas. Use this to prevent repeated rendering if the lambda already
|
||||
* // takes care of rendering its content. This helps protect against mustache code injection when user
|
||||
* // input is passed directly into the template. Defaults to false.
|
||||
* 'disable_lambda_rendering' => true,
|
||||
*
|
||||
* // Enable pragmas across all templates, regardless of the presence of pragma tags in the individual
|
||||
* // templates.
|
||||
* 'pragmas' => [Mustache_Engine::PRAGMA_FILTERS],
|
||||
@@ -204,6 +210,10 @@ class Mustache_Engine
|
||||
$this->strictCallables = $options['strict_callables'];
|
||||
}
|
||||
|
||||
if (isset($options['disable_lambda_rendering'])) {
|
||||
$this->disableLambdaRendering = $options['disable_lambda_rendering'];
|
||||
}
|
||||
|
||||
if (isset($options['delimiters'])) {
|
||||
$this->delimiters = $options['delimiters'];
|
||||
}
|
||||
@@ -624,14 +634,15 @@ class Mustache_Engine
|
||||
//
|
||||
// Keep this list in alphabetical order :)
|
||||
$chunks = array(
|
||||
'charset' => $this->charset,
|
||||
'delimiters' => $this->delimiters ? $this->delimiters : '{{ }}',
|
||||
'entityFlags' => $this->entityFlags,
|
||||
'escape' => isset($this->escape) ? 'custom' : 'default',
|
||||
'key' => ($source instanceof Mustache_Source) ? $source->getKey() : 'source',
|
||||
'pragmas' => $this->getPragmas(),
|
||||
'strictCallables' => $this->strictCallables,
|
||||
'version' => self::VERSION,
|
||||
'charset' => $this->charset,
|
||||
'delimiters' => $this->delimiters ? $this->delimiters : '{{ }}',
|
||||
'entityFlags' => $this->entityFlags,
|
||||
'escape' => isset($this->escape) ? 'custom' : 'default',
|
||||
'key' => ($source instanceof Mustache_Source) ? $source->getKey() : 'source',
|
||||
'pragmas' => $this->getPragmas(),
|
||||
'strictCallables' => $this->strictCallables,
|
||||
'disableLambdaRendering' => $this->disableLambdaRendering,
|
||||
'version' => self::VERSION,
|
||||
);
|
||||
|
||||
$key = json_encode($chunks);
|
||||
@@ -810,7 +821,7 @@ class Mustache_Engine
|
||||
$compiler = $this->getCompiler();
|
||||
$compiler->setPragmas($this->getPragmas());
|
||||
|
||||
return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags);
|
||||
return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags, $this->disableLambdaRendering);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,7 +38,7 @@ class Mustache_Source_FilesystemSource implements Mustache_Source
|
||||
/**
|
||||
* Get the Source key (used to generate the compiled class name).
|
||||
*
|
||||
* @throws RuntimeException when a source file cannot be read
|
||||
* @throws Mustache_Exception_RuntimeException when a source file cannot be read
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
@@ -54,7 +54,7 @@ class Mustache_Source_FilesystemSource implements Mustache_Source
|
||||
}
|
||||
|
||||
if ($this->stat === false) {
|
||||
throw new RuntimeException(sprintf('Failed to read source file "%s".', $this->fileName));
|
||||
throw new Mustache_Exception_RuntimeException(sprintf('Failed to read source file "%s".', $this->fileName));
|
||||
}
|
||||
|
||||
foreach ($this->statProps as $prop) {
|
||||
|
||||
@@ -88,11 +88,11 @@ class Mustache_Tokenizer
|
||||
* @throws Mustache_Exception_InvalidArgumentException when $delimiters string is invalid
|
||||
*
|
||||
* @param string $text Mustache template source to tokenize
|
||||
* @param string $delimiters Optionally, pass initial opening and closing delimiters (default: null)
|
||||
* @param string $delimiters Optionally, pass initial opening and closing delimiters (default: empty string)
|
||||
*
|
||||
* @return array Set of Mustache tokens
|
||||
*/
|
||||
public function scan($text, $delimiters = null)
|
||||
public function scan($text, $delimiters = '')
|
||||
{
|
||||
// Setting mbstring.func_overload makes things *really* slow.
|
||||
// Let's do everyone a favor and scan this string as ASCII instead.
|
||||
@@ -107,7 +107,7 @@ class Mustache_Tokenizer
|
||||
|
||||
$this->reset();
|
||||
|
||||
if ($delimiters = trim($delimiters)) {
|
||||
if (is_string($delimiters) && $delimiters = trim($delimiters)) {
|
||||
$this->setDelimiters($delimiters);
|
||||
}
|
||||
|
||||
|
||||
@@ -129,7 +129,10 @@ class renderer_base {
|
||||
// Don't allow the JavaScript helper to be executed from within another
|
||||
// helper. If it's allowed it can be used by users to inject malicious
|
||||
// JS into the page.
|
||||
'blacklistednestedhelpers' => ['js']));
|
||||
'blacklistednestedhelpers' => ['js'],
|
||||
// Disable lambda rendering - content in helpers is already rendered, no need to render it again.
|
||||
'disable_lambda_rendering' => true,
|
||||
));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -14,15 +14,6 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Unit tests for core_table\local\filter\filter.
|
||||
*
|
||||
* @package core_table
|
||||
* @category test
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace core_table\local\filter;
|
||||
@@ -33,11 +24,11 @@ use InvalidArgumentException;
|
||||
/**
|
||||
* Unit tests for core_table\local\filter\filter.
|
||||
*
|
||||
* @coversDefaultClass \core_table\local\filter\filter
|
||||
* @package core_table
|
||||
* @category test
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \core_table\local\filter\filter
|
||||
*/
|
||||
class filter_test extends advanced_testcase {
|
||||
/**
|
||||
|
||||
@@ -1002,6 +1002,57 @@ EOD;
|
||||
return $gradecategory->get_record_data();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a grade_grade.
|
||||
*
|
||||
* @param array $record
|
||||
* @return grade_grade the grade record
|
||||
*/
|
||||
public function create_grade_grade(?array $record = null): grade_grade {
|
||||
global $DB, $USER;
|
||||
|
||||
$item = $DB->get_record('grade_items', ['id' => $record['itemid']]);
|
||||
$userid = $record['userid'] ?? $USER->id;
|
||||
|
||||
unset($record['itemid']);
|
||||
unset($record['userid']);
|
||||
|
||||
if ($item->itemtype === 'mod') {
|
||||
$cm = get_coursemodule_from_instance($item->itemmodule, $item->iteminstance);
|
||||
$module = new $item->itemmodule(context_module::instance($cm->id), $cm, false);
|
||||
$record['attemptnumber'] = $record['attemptnumber'] ?? 0;
|
||||
|
||||
$module->save_grade($userid, (object) $record);
|
||||
|
||||
$grade = grade_grade::fetch(['userid' => $userid, 'itemid' => $item->id]);
|
||||
} else {
|
||||
$grade = grade_grade::fetch(['userid' => $userid, 'itemid' => $item->id]);
|
||||
$record['rawgrade'] = $record['rawgrade'] ?? $record['grade'] ?? null;
|
||||
$record['finalgrade'] = $record['finalgrade'] ?? $record['grade'] ?? null;
|
||||
|
||||
unset($record['grade']);
|
||||
|
||||
if ($grade) {
|
||||
$fields = $grade->required_fields + array_keys($grade->optional_fields);
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$grade->{$field} = $record[$field] ?? $grade->{$field};
|
||||
}
|
||||
|
||||
$grade->update();
|
||||
} else {
|
||||
$record['userid'] = $userid;
|
||||
$record['itemid'] = $item->id;
|
||||
|
||||
$grade = new grade_grade($record, false);
|
||||
|
||||
$grade->insert();
|
||||
}
|
||||
}
|
||||
|
||||
return $grade;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a grade_item.
|
||||
*
|
||||
@@ -1256,4 +1307,120 @@ EOD;
|
||||
"data generators yet. Class {$classname} not found.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new category for custom profile fields.
|
||||
*
|
||||
* @param array $data Array with 'name' and optionally 'sortorder'
|
||||
* @return \stdClass New category object
|
||||
*/
|
||||
public function create_custom_profile_field_category(array $data): \stdClass {
|
||||
global $DB;
|
||||
|
||||
// Pick next sortorder if not defined.
|
||||
if (!array_key_exists('sortorder', $data)) {
|
||||
$data['sortorder'] = (int)$DB->get_field_sql('SELECT MAX(sortorder) FROM {user_info_category}') + 1;
|
||||
}
|
||||
|
||||
$category = (object)[
|
||||
'name' => $data['name'],
|
||||
'sortorder' => $data['sortorder']
|
||||
];
|
||||
$category->id = $DB->insert_record('user_info_category', $category);
|
||||
|
||||
return $category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new custom profile field.
|
||||
*
|
||||
* Optional fields are:
|
||||
*
|
||||
* categoryid (or use 'category' to specify by name). If you don't specify
|
||||
* either, it will add the field to a 'Testing' category, which will be created for you if
|
||||
* necessary.
|
||||
*
|
||||
* sortorder (if you don't specify this, it will pick the next one in the category).
|
||||
*
|
||||
* all the other database fields (if you don't specify this, it will pick sensible defaults
|
||||
* based on the data type).
|
||||
*
|
||||
* @param array $data Array with 'datatype', 'shortname', and 'name'
|
||||
* @return \stdClass Database object from the user_info_field table
|
||||
*/
|
||||
public function create_custom_profile_field(array $data): \stdClass {
|
||||
global $DB, $CFG;
|
||||
require_once($CFG->dirroot . '/user/profile/lib.php');
|
||||
|
||||
// Set up category if necessary.
|
||||
if (!array_key_exists('categoryid', $data)) {
|
||||
if (array_key_exists('category', $data)) {
|
||||
$data['categoryid'] = $DB->get_field('user_info_category', 'id',
|
||||
['name' => $data['category']], MUST_EXIST);
|
||||
} else {
|
||||
// Make up a 'Testing' category or use existing.
|
||||
$data['categoryid'] = $DB->get_field('user_info_category', 'id', ['name' => 'Testing']);
|
||||
if (!$data['categoryid']) {
|
||||
$created = $this->create_custom_profile_field_category(['name' => 'Testing']);
|
||||
$data['categoryid'] = $created->id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pick sort order if necessary.
|
||||
if (!array_key_exists('sortorder', $data)) {
|
||||
$data['sortorder'] = (int)$DB->get_field_sql(
|
||||
'SELECT MAX(sortorder) FROM {user_info_field} WHERE categoryid = ?',
|
||||
[$data['categoryid']]) + 1;
|
||||
}
|
||||
|
||||
// Defaults for other values.
|
||||
$defaults = [
|
||||
'description' => '',
|
||||
'descriptionformat' => 0,
|
||||
'required' => 0,
|
||||
'locked' => 0,
|
||||
'visible' => PROFILE_VISIBLE_ALL,
|
||||
'forceunique' => 0,
|
||||
'signup' => 0,
|
||||
'defaultdata' => '',
|
||||
'defaultdataformat' => 0,
|
||||
'param1' => '',
|
||||
'param2' => '',
|
||||
'param3' => '',
|
||||
'param4' => '',
|
||||
'param5' => ''
|
||||
];
|
||||
|
||||
// Type-specific defaults for other values.
|
||||
$typedefaults = [
|
||||
'text' => [
|
||||
'param1' => 30,
|
||||
'param2' => 2048
|
||||
],
|
||||
'menu' => [
|
||||
'param1' => "Yes\nNo",
|
||||
'defaultdata' => 'No'
|
||||
],
|
||||
'datetime' => [
|
||||
'param1' => '2010',
|
||||
'param2' => '2015',
|
||||
'param3' => 1
|
||||
],
|
||||
'checkbox' => [
|
||||
'defaultdata' => 0
|
||||
]
|
||||
];
|
||||
foreach ($typedefaults[$data['datatype']] ?? [] as $field => $value) {
|
||||
$defaults[$field] = $value;
|
||||
}
|
||||
|
||||
foreach ($defaults as $field => $value) {
|
||||
if (!array_key_exists($field, $data)) {
|
||||
$data[$field] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$data['id'] = $DB->insert_record('user_info_field', $data);
|
||||
return (object)$data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
|
||||
require_once(__DIR__ . '/../../behat/behat_base.php');
|
||||
|
||||
use Behat\Mink\Exception\ExpectationException as ExpectationException;
|
||||
use Behat\Mink\Exception\DriverException as DriverException;
|
||||
use Behat\Mink\Element\NodeElement;
|
||||
use Behat\Mink\Exception\DriverException;
|
||||
|
||||
/**
|
||||
* Steps definitions to open and close action menus.
|
||||
@@ -78,4 +78,122 @@ class behat_action_menu extends behat_base {
|
||||
$this->ensure_node_is_visible($node);
|
||||
$node->click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a specific item in an action menu.
|
||||
*
|
||||
* @When /^I choose the "(?P<item_string>(?:[^"]|\\")*)" item in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu$/
|
||||
* @param string $item The item to choose
|
||||
* @param string $actionmenu The text used in the description of the action menu
|
||||
*/
|
||||
public function i_choose_in_the_named_menu(string $item, string $actionmenu): void {
|
||||
$menu = $this->find('actionmenu', $actionmenu);
|
||||
$this->select_item_in_action_menu($item, $menu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a specific item in an action menu within a container.
|
||||
*
|
||||
* @When /^I choose the "(?P<item_string>(?:[^"]|\\")*)" item in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu of the "(?P<locator_string>(?:[^"]|\\")*)" "(?P<type_string>(?:[^"]|\\")*)"$/
|
||||
* @param string $item The item to choose
|
||||
* @param string $actionmenu The text used in the description of the action menu
|
||||
* @param string|NodeElement $locator The identifer used for the container
|
||||
* @param string $selector The type of container to locate
|
||||
*/
|
||||
public function i_choose_in_the_named_menu_in_container(string $item, string $actionmenu, $locator, $selector): void {
|
||||
$container = $this->find($selector, $locator);
|
||||
$menu = $this->find('actionmenu', $actionmenu, false, $container);
|
||||
$this->select_item_in_action_menu($item, $menu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select an item in the specified menu.
|
||||
*
|
||||
* Note: This step does work both with, and without, JavaScript.
|
||||
*
|
||||
* @param string $item Item string value
|
||||
* @param NodeElement $menu The menu NodeElement to select from
|
||||
*/
|
||||
protected function select_item_in_action_menu(string $item, NodeElement $menu): void {
|
||||
if ($this->running_javascript()) {
|
||||
// Open the menu by clicking on the trigger.
|
||||
$this->execute(
|
||||
'behat_general::i_click_on',
|
||||
[$menu, "NodeElement"]
|
||||
);
|
||||
}
|
||||
|
||||
// Select the menu item.
|
||||
$this->execute(
|
||||
'behat_general::i_click_on_in_the',
|
||||
[$item, "link", $menu, "NodeElement"]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The action menu item should not exist.
|
||||
*
|
||||
* @Then /^the "(?P<item_string>(?:[^"]|\\")*)" item should not exist in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu$/
|
||||
* @param string $item The item to check
|
||||
* @param string $actionmenu The text used in the description of the action menu
|
||||
*/
|
||||
public function item_should_not_exist(string $item, string $actionmenu): void {
|
||||
$menu = $this->find('actionmenu', $actionmenu);
|
||||
$this->execute('behat_general::should_not_exist_in_the', [
|
||||
$item, 'link',
|
||||
$menu, 'NodeElement'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The action menu item should not exist within a container.
|
||||
*
|
||||
* @Then /^the "(?P<item_string>(?:[^"]|\\")*)" item should not exist in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu of the "(?P<locator_string>(?:[^"]|\\")*)" "(?P<type_string>(?:[^"]|\\")*)"$/
|
||||
* @param string $item The item to check
|
||||
* @param string $actionmenu The text used in the description of the action menu
|
||||
* @param string|NodeElement $locator The identifer used for the container
|
||||
* @param string $selector The type of container to locate
|
||||
*/
|
||||
public function item_should_not_exist_in_the(string $item, string $actionmenu, $locator, $selector): void {
|
||||
$container = $this->find($selector, $locator);
|
||||
$menu = $this->find('actionmenu', $actionmenu, false, $container);
|
||||
$this->execute('behat_general::should_not_exist_in_the', [
|
||||
$item, 'link',
|
||||
$menu, 'NodeElement'
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The action menu item should exist.
|
||||
*
|
||||
* @Then /^the "(?P<item_string>(?:[^"]|\\")*)" item should exist in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu$/
|
||||
* @param string $item The item to check
|
||||
* @param string $actionmenu The text used in the description of the action menu
|
||||
*/
|
||||
public function item_should_exist(string $item, string $actionmenu): void {
|
||||
$menu = $this->find('actionmenu', $actionmenu);
|
||||
$this->execute('behat_general::should_exist_in_the', [
|
||||
$item, 'link',
|
||||
$menu, 'NodeElement'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The action menu item should exist within a container.
|
||||
*
|
||||
* @Then /^the "(?P<item_string>(?:[^"]|\\")*)" item should exist in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu of the "(?P<locator_string>(?:[^"]|\\")*)" "(?P<type_string>(?:[^"]|\\")*)"$/
|
||||
* @param string $item The item to check
|
||||
* @param string $actionmenu The text used in the description of the action menu
|
||||
* @param string|NodeElement $locator The identifer used for the container
|
||||
* @param string $selector The type of container to locate
|
||||
*/
|
||||
public function item_should_exist_in_the(string $item, string $actionmenu, $locator, $selector): void {
|
||||
$container = $this->find($selector, $locator);
|
||||
$menu = $this->find('actionmenu', $actionmenu, false, $container);
|
||||
$this->execute('behat_general::should_exist_in_the', [
|
||||
$item, 'link',
|
||||
$menu, 'NodeElement'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,4 +170,51 @@ class behat_data_generators extends behat_base {
|
||||
$instance = new $componentclass($component);
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all entities that can be created in all components using the_following_entities_exist()
|
||||
*
|
||||
* @return array
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public function get_all_entities(): array {
|
||||
global $CFG;
|
||||
// Ensure the generator class is loaded.
|
||||
require_once($CFG->libdir . '/behat/classes/behat_generator_base.php');
|
||||
$componenttypes = core_component::get_component_list();
|
||||
$coregenerator = $this->get_instance_for_component('core');
|
||||
$pluginswithentities = ['core' => array_keys($coregenerator->get_available_generators())];
|
||||
foreach ($componenttypes as $components) {
|
||||
foreach ($components as $component => $componentdir) {
|
||||
try {
|
||||
$plugingenerator = $this->get_instance_for_component($component);
|
||||
$entities = array_keys($plugingenerator->get_available_generators());
|
||||
if (!empty($entities)) {
|
||||
$pluginswithentities[$component] = $entities;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// The component has no generator, skip it.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $pluginswithentities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required fields for a specific creatable entity.
|
||||
*
|
||||
* @param string $entitytype
|
||||
* @return mixed
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public function get_entity(string $entitytype): array {
|
||||
[$component, $entity] = $this->parse_entity_type($entitytype);
|
||||
$generator = $this->get_instance_for_component($component);
|
||||
$entities = $generator->get_available_generators();
|
||||
if (!array_key_exists($entity, $entities)) {
|
||||
throw new coding_exception('No generator for ' . $entity . ' in component ' . $component);
|
||||
}
|
||||
return $entities[$entity];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -651,7 +651,6 @@ class behat_forms extends behat_base {
|
||||
* @param string $value
|
||||
*/
|
||||
public function set_field_node_value(NodeElement $fieldnode, string $value): void {
|
||||
$this->ensure_node_is_visible($fieldnode);
|
||||
$field = behat_field_manager::get_form_field($fieldnode, $this->getSession());
|
||||
$field->set_value($value);
|
||||
}
|
||||
@@ -719,7 +718,7 @@ class behat_forms extends behat_base {
|
||||
public function i_open_the_autocomplete_suggestions_list($container = null, $containertype = null) {
|
||||
$csstarget = ".form-autocomplete-downarrow";
|
||||
if ($container && $containertype) {
|
||||
$this->execute('behat_general::i_click_on', [$csstarget, 'css_element', $container, $containertype]);
|
||||
$this->execute('behat_general::i_click_on_in_the', [$csstarget, 'css_element', $container, $containertype]);
|
||||
} else {
|
||||
$this->execute('behat_general::i_click_on', [$csstarget, 'css_element']);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
require_once(__DIR__ . '/../../behat/behat_base.php');
|
||||
|
||||
use Behat\Gherkin\Node\TableNode;
|
||||
use Behat\Mink\Element\NodeElement;
|
||||
use Behat\Mink\Exception\DriverException;
|
||||
use Behat\Mink\Exception\ElementNotFoundException;
|
||||
use Behat\Mink\Exception\ExpectationException;
|
||||
@@ -187,6 +188,22 @@ class behat_general extends behat_base {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to a second window.
|
||||
*
|
||||
* @Given /^I switch to a second window$/
|
||||
* @throws DriverException If there aren't exactly 2 windows open.
|
||||
*/
|
||||
public function switch_to_second_window() {
|
||||
$names = $this->getSession()->getWindowNames();
|
||||
|
||||
if (count($names) !== 2) {
|
||||
throw new DriverException('Expected to see 2 windows open, found ' . count($names));
|
||||
}
|
||||
|
||||
$this->getSession()->switchToWindow($names[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to the main Moodle frame.
|
||||
*
|
||||
@@ -1146,19 +1163,12 @@ EOF;
|
||||
* @throws ElementNotFoundException Thrown by behat_base::find
|
||||
* @param string $element The locator of the specified selector
|
||||
* @param string $selectortype The selector type
|
||||
* @param string $containerelement The container selector type
|
||||
* @param string $containerselectortype The container locator
|
||||
* @param NodeElement|string $containerelement The locator of the container selector
|
||||
* @param string $containerselectortype The container selector type
|
||||
*/
|
||||
public function should_exist_in_the($element, $selectortype, $containerelement, $containerselectortype) {
|
||||
// Get the container node.
|
||||
$containernode = $this->find($containerselectortype, $containerelement);
|
||||
|
||||
// Specific exception giving info about where can't we find the element.
|
||||
$locatorexceptionmsg = "{$element} in the {$containerelement} {$containerselectortype}";
|
||||
$exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $locatorexceptionmsg);
|
||||
|
||||
// Looks for the requested node inside the container node.
|
||||
$this->find($selectortype, $element, $exception, $containernode);
|
||||
// Will throw an ElementNotFoundException if it does not exist.
|
||||
$this->get_node_in_container($selectortype, $element, $containerselectortype, $containerelement);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1170,8 +1180,8 @@ EOF;
|
||||
* @throws ExpectationException
|
||||
* @param string $element The locator of the specified selector
|
||||
* @param string $selectortype The selector type
|
||||
* @param string $containerelement The container selector type
|
||||
* @param string $containerselectortype The container locator
|
||||
* @param NodeElement|string $containerelement The locator of the container selector
|
||||
* @param string $containerselectortype The container selector type
|
||||
*/
|
||||
public function should_not_exist_in_the($element, $selectortype, $containerelement, $containerselectortype) {
|
||||
// Get the container node.
|
||||
@@ -1188,8 +1198,10 @@ EOF;
|
||||
}
|
||||
|
||||
// The element was found and should not have been. Throw an exception.
|
||||
$elementdescription = $this->get_selector_description($selectortype, $element);
|
||||
$containerdescription = $this->get_selector_description($containerselectortype, $containerelement);
|
||||
throw new ExpectationException(
|
||||
"The '{$element}' '{$selectortype}' exists in the '{$containerelement}' '{$containerselectortype}'",
|
||||
"The {$elementdescription} exists in the {$containerdescription}",
|
||||
$this->getSession()
|
||||
);
|
||||
}
|
||||
@@ -1713,7 +1725,17 @@ EOF;
|
||||
* @param string $keys The key, or list of keys, to type
|
||||
*/
|
||||
public function i_type(string $keys): void {
|
||||
behat_base::type_keys($this->getSession(), str_split($keys));
|
||||
// Certain keys, such as the newline character, must be converted to the appropriate character code.
|
||||
// Without this, keys will behave differently depending on the browser.
|
||||
$keylist = array_map(function($key): string {
|
||||
switch ($key) {
|
||||
case "\n":
|
||||
return behat_keys::ENTER;
|
||||
default:
|
||||
return $key;
|
||||
}
|
||||
}, str_split($keys));
|
||||
behat_base::type_keys($this->getSession(), $keylist);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1922,6 +1944,21 @@ EOF;
|
||||
throw new \Moodle\BehatExtension\Exception\SkippedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if given plugin is installed, and skips the current scenario if not.
|
||||
*
|
||||
* @Given the :plugin plugin is installed
|
||||
* @param string $plugin frankenstyle plugin name, e.g. 'filter_embedquestion'.
|
||||
* @throws \Moodle\BehatExtension\Exception\SkippedException
|
||||
*/
|
||||
public function plugin_is_installed(string $plugin): void {
|
||||
$path = core_component::get_component_directory($plugin);
|
||||
if (!is_readable($path . '/version.php')) {
|
||||
throw new \Moodle\BehatExtension\Exception\SkippedException(
|
||||
'Skipping this scenario because the ' . $plugin . ' is not installed.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks focus is with the given element.
|
||||
*
|
||||
@@ -2084,4 +2121,71 @@ EOF;
|
||||
public function i_mark_this_test_as_long_running(int $factor = 2): void {
|
||||
$this->set_test_timeout_factor($factor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default text editor to the named text editor.
|
||||
*
|
||||
* @Given the default editor is set to :editor
|
||||
* @param string $editor
|
||||
* @throws ExpectationException If the specified editor is not available.
|
||||
*/
|
||||
public function the_default_editor_is_set_to(string $editor): void {
|
||||
global $CFG;
|
||||
|
||||
// Check if the provided editor is available.
|
||||
if (!array_key_exists($editor, editors_get_available())) {
|
||||
throw new ExpectationException(
|
||||
"Unable to set the editor to {$editor} as it is not installed. The available editors are: " .
|
||||
implode(', ', array_keys(editors_get_available())),
|
||||
$this->getSession()
|
||||
);
|
||||
}
|
||||
|
||||
// Make the provided editor the default one in $CFG->texteditors by
|
||||
// moving it to the first [editor],atto,tiny,tinymce,textarea on the list.
|
||||
$list = explode(',', $CFG->texteditors);
|
||||
array_unshift($list, $editor);
|
||||
$list = array_unique($list);
|
||||
|
||||
// Set the list new list of editors.
|
||||
set_config('texteditors', implode(',', $list));
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow to check for minimal Moodle version.
|
||||
*
|
||||
* @Given the site is running Moodle version :minversion or higher
|
||||
* @param string $minversion The minimum version of Moodle required (inclusive).
|
||||
*/
|
||||
public function the_site_is_running_moodle_version_or_higher(string $minversion): void {
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/environmentlib.php');
|
||||
|
||||
$currentversion = normalize_version(get_config('', 'release'));
|
||||
|
||||
if (version_compare($currentversion, $minversion, '<')) {
|
||||
throw new Moodle\BehatExtension\Exception\SkippedException(
|
||||
'Site must be running Moodle version ' . $minversion . ' or higher'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow to check for maximum Moodle version.
|
||||
*
|
||||
* @Given the site is running Moodle version :maxversion or lower
|
||||
* @param string $maxversion The maximum version of Moodle required (inclusive).
|
||||
*/
|
||||
public function the_site_is_running_moodle_version_or_lower(string $maxversion): void {
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/environmentlib.php');
|
||||
|
||||
$currentversion = normalize_version(get_config('', 'release'));
|
||||
|
||||
if (version_compare($currentversion, $maxversion, '>')) {
|
||||
throw new Moodle\BehatExtension\Exception\SkippedException(
|
||||
'Site must be running Moodle version ' . $maxversion . ' or lower'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,6 +718,7 @@ class behat_navigation extends behat_base {
|
||||
* | Page type | Identifier meaning | description |
|
||||
* | Category | category idnumber | List of courses in that category. |
|
||||
* | Course | course shortname | Main course home pag |
|
||||
* | Course editing | course shortname | Edit settings page for the course |
|
||||
* | Activity | activity idnumber | Start page for that activity |
|
||||
* | Activity editing | activity idnumber | Edit settings page for that activity |
|
||||
* | [modname] Activity | activity name or idnumber | Start page for that activity |
|
||||
@@ -745,6 +746,14 @@ class behat_navigation extends behat_base {
|
||||
}
|
||||
return new moodle_url('/course/index.php', ['categoryid' => $categoryid]);
|
||||
|
||||
case 'course editing':
|
||||
$courseid = $this->get_course_id($identifier);
|
||||
if (!$courseid) {
|
||||
throw new Exception('The specified course with shortname, fullname, or idnumber "' .
|
||||
$identifier . '" does not exist');
|
||||
}
|
||||
return new moodle_url('/course/edit.php', ['id' => $courseid]);
|
||||
|
||||
case 'course':
|
||||
$courseid = $this->get_course_id($identifier);
|
||||
if (!$courseid) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
@core
|
||||
Feature: Check for minimum or maximimum version of Moodle
|
||||
In order adapt acceptance tests for different versions of Moodle
|
||||
As a developer
|
||||
I should be able to skip tests according to the Moodle version present on a site
|
||||
|
||||
Scenario: Minimum version too low
|
||||
Given the site is running Moodle version 99.0 or higher
|
||||
# The following steps should not be executed. If they are, the test will fail.
|
||||
When I log in as "admin"
|
||||
Then I should not see "Moodle"
|
||||
|
||||
Scenario: Maximum version too high
|
||||
Given the site is running Moodle version 3.0 or lower
|
||||
# The following steps should not be executed. If they are, the test will fail.
|
||||
When I log in as "admin"
|
||||
Then I should not see "Moodle"
|
||||
|
||||
Scenario: Minimum version OK
|
||||
Given the site is running Moodle version 3.0 or higher
|
||||
When I log in as "admin"
|
||||
Then I should see "Moodle"
|
||||
|
||||
Scenario: Maximum version OK
|
||||
Given the site is running Moodle version 99.0 or lower
|
||||
When I log in as "admin"
|
||||
Then I should see "Moodle"
|
||||
@@ -90,7 +90,7 @@ class core_renderer_template_exploit_testcase extends advanced_testcase {
|
||||
'test1' => $norender,
|
||||
],
|
||||
'js' => 'some nasty JS',
|
||||
'expected' => 'core, move, some text',
|
||||
'expected' => 'core, move, some text {{}}',
|
||||
'include' => false
|
||||
],
|
||||
'js helper not nested' => [
|
||||
@@ -133,7 +133,7 @@ class core_renderer_template_exploit_testcase extends advanced_testcase {
|
||||
'testpix' => $singlerender
|
||||
],
|
||||
'js' => 'some nasty JS',
|
||||
'expected' => 'core, move,',
|
||||
'expected' => 'core, move, {{}}',
|
||||
'include' => false
|
||||
],
|
||||
'js in context double depth with single render' => [
|
||||
@@ -149,7 +149,7 @@ class core_renderer_template_exploit_testcase extends advanced_testcase {
|
||||
'testpix' => $singlerender
|
||||
],
|
||||
'js' => 'some nasty JS',
|
||||
'expected' => 'core, move, {{#js}} some nasty JS {{/js}}',
|
||||
'expected' => 'core, move, {{second}}',
|
||||
'include' => false
|
||||
],
|
||||
'js in context double depth with recursive render' => [
|
||||
@@ -269,24 +269,7 @@ class core_renderer_template_exploit_testcase extends advanced_testcase {
|
||||
'testpix' => $singlerender
|
||||
],
|
||||
'js' => 'some nasty JS',
|
||||
'expected' => 'core, move,',
|
||||
'include' => false
|
||||
],
|
||||
'partial double nested with js from context single render' => [
|
||||
'templates' => [
|
||||
'test' => '{{#testpix}} core, move, {{foo}}{{/testpix}}',
|
||||
'test2' => '{{#js}} some nasty JS {{/js}}',
|
||||
],
|
||||
'torender' => 'test',
|
||||
'context' => [
|
||||
'foo' => '{{{bar}}}',
|
||||
'bar' => '{{> test2}}'
|
||||
],
|
||||
'helpers' => [
|
||||
'testpix' => $singlerender
|
||||
],
|
||||
'js' => 'some nasty JS',
|
||||
'expected' => 'core, move, {{> test2}}',
|
||||
'expected' => 'core, move, {{> test2}}',
|
||||
'include' => false
|
||||
],
|
||||
'partial double nested with js from context recursive render' => [
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user