Compare commits
136 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 | |||
| 1dfd13841a | |||
| 5d15e96a98 |
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
@@ -3557,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>
|
||||
@@ -3781,11 +3783,6 @@
|
||||
<ON_CHECK message="tokenizerrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xmlrpc" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="xmlrpcrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="soap" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="soaprecommended" />
|
||||
|
||||
+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>
|
||||
@@ -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 |"
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+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,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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
+7
-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';
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
+2
-1
@@ -1063,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,3 +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.
|
||||
|
||||
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#
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -331,14 +334,8 @@ class Mustache_Compiler
|
||||
|
||||
if (%s) {
|
||||
$source = %s;
|
||||
$result = (string) call_user_func($value, $source, %s);
|
||||
if (strpos($result, \'{{\') === false) {
|
||||
$buffer .= $result;
|
||||
} else {
|
||||
$buffer .= $this->mustache
|
||||
->loadLambda($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) {
|
||||
@@ -352,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.
|
||||
*
|
||||
@@ -383,7 +410,7 @@ 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);
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
class Mustache_Engine
|
||||
{
|
||||
const VERSION = '2.14.0';
|
||||
const VERSION = '2.14.1';
|
||||
const SPEC_VERSION = '1.2.2';
|
||||
|
||||
const PRAGMA_FILTERS = 'FILTERS';
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {
|
||||
/**
|
||||
|
||||
@@ -1307,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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2099,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'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' => [
|
||||
|
||||
@@ -684,6 +684,108 @@ class core_datalib_testcase extends advanced_testcase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the get_users_listing function.
|
||||
*/
|
||||
public function test_get_users_listing(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = $this->getDataGenerator();
|
||||
|
||||
// Set up profile field.
|
||||
$generator->create_custom_profile_field(['datatype' => 'text',
|
||||
'shortname' => 'specialid', 'name' => 'Special user id']);
|
||||
|
||||
// Set up the show user identity option.
|
||||
set_config('showuseridentity', 'department');
|
||||
|
||||
// Get all the existing user ids (we're going to remove these from test results).
|
||||
$existingids = array_fill_keys($DB->get_fieldset_select('user', 'id', '1 = 1'), true);
|
||||
|
||||
// Create some test user accounts.
|
||||
$userids = [];
|
||||
foreach (['a', 'b', 'c', 'd'] as $key) {
|
||||
$record = [
|
||||
'username' => 'user_' . $key,
|
||||
'firstname' => $key . '_first',
|
||||
'lastname' => 'last_' . $key,
|
||||
'department' => 'department_' . $key,
|
||||
'lastaccess' => ord($key)
|
||||
];
|
||||
$user = $generator->create_user($record);
|
||||
$userids[] = $user->id;
|
||||
}
|
||||
|
||||
// Check default result with no parameters.
|
||||
$results = get_users_listing();
|
||||
$results = array_diff_key($results, $existingids);
|
||||
|
||||
// It should return all the results in order.
|
||||
$this->assertEquals($userids, array_keys($results));
|
||||
|
||||
// Results should have some general fields and name fields, check some samples.
|
||||
$this->assertEquals('user_a', $results[$userids[0]]->username);
|
||||
$this->assertEquals('user_a@example.com', $results[$userids[0]]->email);
|
||||
$this->assertEquals(1, $results[$userids[0]]->confirmed);
|
||||
$this->assertEquals('a_first', $results[$userids[0]]->firstname);
|
||||
$this->assertObjectHasAttribute('firstnamephonetic', $results[$userids[0]]);
|
||||
|
||||
// Should not have department because no context specified.
|
||||
$this->assertObjectNotHasAttribute('department', $results[$userids[0]]);
|
||||
|
||||
// Check sorting.
|
||||
$results = get_users_listing('username', 'DESC');
|
||||
$results = array_diff_key($results, $existingids);
|
||||
$this->assertEquals([$userids[3], $userids[2], $userids[1], $userids[0]], array_keys($results));
|
||||
|
||||
// Check default fallback sort field works as expected.
|
||||
$results = get_users_listing('blah2', 'ASC');
|
||||
$results = array_diff_key($results, $existingids);
|
||||
$this->assertEquals([$userids[0], $userids[1], $userids[2], $userids[3]], array_keys($results));
|
||||
|
||||
// Check default fallback sort direction works as expected.
|
||||
$results = get_users_listing('lastaccess', 'blah2');
|
||||
$results = array_diff_key($results, $existingids);
|
||||
$this->assertEquals([$userids[0], $userids[1], $userids[2], $userids[3]], array_keys($results));
|
||||
|
||||
// Add the options to showuseridentity and check it returns those fields but only if you
|
||||
// specify a context AND have permissions.
|
||||
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', '', null,
|
||||
\context_system::instance());
|
||||
$this->assertObjectNotHasAttribute('department', $results[$userids[0]]);
|
||||
$this->setAdminUser();
|
||||
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', '', null,
|
||||
\context_system::instance());
|
||||
$this->assertEquals('department_a', $results[$userids[0]]->department);
|
||||
|
||||
// Check search (full name, email, username).
|
||||
$results = get_users_listing('lastaccess', 'asc', 0, 0, 'b_first last_b');
|
||||
$this->assertEquals([$userids[1]], array_keys($results));
|
||||
$results = get_users_listing('lastaccess', 'asc', 0, 0, 'c@example');
|
||||
$this->assertEquals([$userids[2]], array_keys($results));
|
||||
$results = get_users_listing('lastaccess', 'asc', 0, 0, 'user_d');
|
||||
$this->assertEquals([$userids[3]], array_keys($results));
|
||||
|
||||
// Check first and last initial restriction (all the test ones have same last initial).
|
||||
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', 'C');
|
||||
$this->assertEquals([$userids[2]], array_keys($results));
|
||||
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', 'L');
|
||||
$results = array_diff_key($results, $existingids);
|
||||
$this->assertEquals($userids, array_keys($results));
|
||||
|
||||
// Check the extra where clause, either with the 'u.' prefix or not.
|
||||
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', 'id IN (:x,:y)',
|
||||
['x' => $userids[1], 'y' => $userids[3]]);
|
||||
$results = array_diff_key($results, $existingids);
|
||||
$this->assertEquals([$userids[1], $userids[3]], array_keys($results));
|
||||
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', 'id IN (:x,:y)',
|
||||
['x' => $userids[1], 'y' => $userids[3]]);
|
||||
$results = array_diff_key($results, $existingids);
|
||||
$this->assertEquals([$userids[1], $userids[3]], array_keys($results));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_get_safe_orderby().
|
||||
*
|
||||
|
||||
@@ -1679,6 +1679,7 @@ EOF;
|
||||
}
|
||||
|
||||
// Burst up to the capacity and make sure that the bucket allows it.
|
||||
$burststart = microtime();
|
||||
for ($i = 0; $i < $capacity; $i++) {
|
||||
if ($i) {
|
||||
sleep(1); // A little delay so we have different timemodified value for files.
|
||||
@@ -1693,9 +1694,16 @@ EOF;
|
||||
// The bucket should be full after bursting.
|
||||
$this->assertTrue(file_is_draft_areas_limit_reached($user->id));
|
||||
|
||||
// Calculate the time taken to burst up the bucket capacity.
|
||||
$timetaken = microtime_diff($burststart, microtime());
|
||||
|
||||
// The bucket leaks so it shouldn't be full after a certain time.
|
||||
// Reiterating that this test could have been faster if MDL-37327 was implemented.
|
||||
sleep(ceil(1 / $leak) - ($capacity - 1));
|
||||
// Items are added into the bucket at the rate of 1 item per second.
|
||||
// One item leaks from the bucket every 1/$leak seconds.
|
||||
// So it takes 1/$leak - ($capacity-1) seconds for the bucket to leak one item and not be full anymore.
|
||||
$milliseconds = ceil(1000000 * ((1 / $leak) - ($capacity - 1)) - ($timetaken * 1000));
|
||||
usleep($milliseconds);
|
||||
|
||||
$this->assertFalse(file_is_draft_areas_limit_reached($user->id));
|
||||
|
||||
// Only one item was leaked from the bucket. So the bucket should become full again if we add a single item to it.
|
||||
|
||||
@@ -30,7 +30,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @package core
|
||||
* @copyright 2017 Damyon Wiese
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
|
||||
* @coversDefaultClass \core\oauth2\api
|
||||
* @covers \core\oauth2\api
|
||||
*/
|
||||
class core_oauth2_testcase extends advanced_testcase {
|
||||
|
||||
|
||||
+2
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -3479,6 +3479,7 @@
|
||||
_proto.createEl = function createEl$1(tagName, properties, attributes) {
|
||||
return createEl(tagName, properties, attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Localize a string given the string in english.
|
||||
*
|
||||
@@ -17820,11 +17821,19 @@
|
||||
_proto.createEl = function createEl(type, props, attrs) {
|
||||
// The control is textual, not just an icon
|
||||
this.nonIconControl = true;
|
||||
return _ClickableComponent.prototype.createEl.call(this, 'li', assign({
|
||||
var el = _ClickableComponent.prototype.createEl.call(this, 'li', assign({
|
||||
className: 'vjs-menu-item',
|
||||
innerHTML: "<span class=\"vjs-menu-item-text\">" + this.localize(this.options_.label) + "</span>",
|
||||
tabIndex: -1
|
||||
}, props), attrs);
|
||||
|
||||
var iconplaceholder = el.querySelector('.vjs-icon-placeholder');
|
||||
if (typeof iconplaceholder === 'object' && iconplaceholder !== null) {
|
||||
el.replaceChild(_ClickableComponent.prototype.createEl.call(this, 'span', {
|
||||
className: 'vjs-menu-item-text',
|
||||
textContent: this.localize(this.options_.label)
|
||||
}), iconplaceholder);
|
||||
}
|
||||
return el;
|
||||
}
|
||||
/**
|
||||
* Ignore keys which are used by the menu, but pass any other ones up. See
|
||||
@@ -18888,18 +18897,18 @@
|
||||
var _proto = SubsCapsMenuItem.prototype;
|
||||
|
||||
_proto.createEl = function createEl(type, props, attrs) {
|
||||
var innerHTML = "<span class=\"vjs-menu-item-text\">" + this.localize(this.options_.label);
|
||||
|
||||
if (this.options_.track.kind === 'captions') {
|
||||
innerHTML += "\n <span aria-hidden=\"true\" class=\"vjs-icon-placeholder\"></span>\n <span class=\"vjs-control-text\"> " + this.localize('Captions') + "</span>\n ";
|
||||
}
|
||||
|
||||
innerHTML += '</span>';
|
||||
|
||||
var el = _TextTrackMenuItem.prototype.createEl.call(this, type, assign({
|
||||
innerHTML: innerHTML
|
||||
var el = _TextTrackMenuItem.prototype.createEl.call(this, 'li', assign({
|
||||
className: 'vjs-menu-item',
|
||||
tabIndex: -1
|
||||
}, props), attrs);
|
||||
|
||||
const menuitem = el.querySelector('.vjs-menu-item-text');
|
||||
if (this.options_.track.kind === 'captions' && menuitem) {
|
||||
const icon = document.createElement("span");
|
||||
icon.className = 'vjs-icon-placeholder';
|
||||
menuitem.appendChild(icon);
|
||||
el.appendChild(menuitem);
|
||||
}
|
||||
return el;
|
||||
};
|
||||
|
||||
@@ -19049,18 +19058,19 @@
|
||||
var _proto = AudioTrackMenuItem.prototype;
|
||||
|
||||
_proto.createEl = function createEl(type, props, attrs) {
|
||||
var innerHTML = "<span class=\"vjs-menu-item-text\">" + this.localize(this.options_.label);
|
||||
|
||||
if (this.options_.track.kind === 'main-desc') {
|
||||
innerHTML += "\n <span aria-hidden=\"true\" class=\"vjs-icon-placeholder\"></span>\n <span class=\"vjs-control-text\"> " + this.localize('Descriptions') + "</span>\n ";
|
||||
}
|
||||
|
||||
innerHTML += '</span>';
|
||||
|
||||
var el = _MenuItem.prototype.createEl.call(this, type, assign({
|
||||
innerHTML: innerHTML
|
||||
var el = _MenuItem.prototype.createEl.call(this, 'li', assign({
|
||||
className: 'vjs-menu-item',
|
||||
tabIndex: -1
|
||||
}, props), attrs);
|
||||
|
||||
const menuitem = el.querySelector('.vjs-menu-item-text');
|
||||
if (this.options_.track.kind === 'main' && menuitem) {
|
||||
const icon = document.createElement("span");
|
||||
icon.className = 'vjs-icon-placeholder';
|
||||
menuitem.appendChild(icon);
|
||||
el.appendChild(menuitem);
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
/**
|
||||
@@ -32546,7 +32556,22 @@
|
||||
return buf.push(' ', node.name, '="', node.value.replace(/[<&"]/g, _xmlEncoder), '"');
|
||||
|
||||
case TEXT_NODE:
|
||||
return buf.push(node.data.replace(/[<&]/g, _xmlEncoder));
|
||||
/**
|
||||
* The ampersand character (&) and the left angle bracket (<) must not appear in their literal form,
|
||||
* except when used as markup delimiters, or within a comment, a processing instruction, or a CDATA section.
|
||||
* If they are needed elsewhere, they must be escaped using either numeric character references or the strings
|
||||
* `&` and `<` respectively.
|
||||
* The right angle bracket (>) may be represented using the string " > ", and must, for compatibility,
|
||||
* be escaped using either `>` or a character reference when it appears in the string `]]>` in content,
|
||||
* when that string is not marking the end of a CDATA section.
|
||||
*
|
||||
* In the content of elements, character data is any string of characters
|
||||
* which does not contain the start-delimiter of any markup
|
||||
* and does not include the CDATA-section-close delimiter, `]]>`.
|
||||
*
|
||||
* @see https://www.w3.org/TR/xml/#NT-CharData
|
||||
*/
|
||||
return buf.push(node.data.replace(/[<&]/g, _xmlEncoder).replace(/]]>/g, ']]>'));
|
||||
|
||||
case CDATA_SECTION_NODE:
|
||||
return buf.push('<![CDATA[', node.data, ']]>');
|
||||
|
||||
@@ -36,4 +36,12 @@ Import plugins:
|
||||
define(['media_videojs/video-lazy']
|
||||
|
||||
3. Download https://github.com/videojs/video-js-swf/blob/master/dist/video-js.swf
|
||||
and place it into 'videojs/video-js.swf'
|
||||
and place it into 'videojs/video-js.swf'
|
||||
|
||||
Changes:
|
||||
In order to improve the validation, a couple of minor changes have been added to the video-lazy.js:
|
||||
1. Partial upgrade from VideoJS 7.17.0 for 'Subtitles' label check in 17824 - 17833
|
||||
2. Partial upgrade from VideoJS 7.17.0 for 'Captions' label check from 18897 - 18911
|
||||
3. Partial upgrade from VideoJS 7.17.0 for 'Description' label check from 19062 - 19075
|
||||
4. Fixed the video controls not being displayed when playing a video
|
||||
5. Fixed the menu item not being displayed as expected
|
||||
@@ -262,8 +262,10 @@ Feature: Delete messages from conversations
|
||||
And I click on "//button[@data-action='confirm-delete-selected-messages']" "xpath_element"
|
||||
And I go back in "view-conversation" message drawer
|
||||
Then I should not see "Student 2" in the "//*[@data-region='message-drawer']//div[@data-region='view-overview-favourites']" "xpath_element"
|
||||
And I send "Hi!" message to "Student 2" user
|
||||
And I go back in "view-conversation" message drawer
|
||||
And I go back in "view-search" message drawer
|
||||
And the following "private messages" exist:
|
||||
| user | contact | message |
|
||||
| student1 | student2 | Hi! |
|
||||
And I open messaging
|
||||
And I open the "Starred" conversations list
|
||||
And I should see "Student 2" in the "//*[@data-region='message-drawer']//div[@data-region='view-overview-favourites']" "xpath_element"
|
||||
And I select "Student 2" conversation in the "favourites" conversations list
|
||||
And I should see "Hi!" in the "Student 2" "core_message > Message conversation"
|
||||
|
||||
@@ -118,7 +118,8 @@ Feature: Message delete conversations
|
||||
And I should not see "Hi!" in the "Student 2" "core_message > Message conversation"
|
||||
And I go back in "view-conversation" message drawer
|
||||
And I should not see "Student 2" in the "favourites" "core_message > Message list area"
|
||||
And I send "Hi!" message to "Student 2" user
|
||||
And I go back in "view-conversation" message drawer
|
||||
And I go back in "view-search" message drawer
|
||||
And the following "private messages" exist:
|
||||
| user | contact | message |
|
||||
| student1 | student2 | Hi! |
|
||||
And I open messaging
|
||||
And I should see "Student 2" in the "favourites" "core_message > Message list area"
|
||||
|
||||
@@ -4,7 +4,8 @@ Feature: In an assignment, teachers can edit feedback for a students previous su
|
||||
As a teacher
|
||||
I need to be able to edit the feedback for a students previous submission attempt.
|
||||
|
||||
@javascript
|
||||
# This scenario has Atto-specific steps. See MDL-75913 for further details.
|
||||
@javascript @editor_atto
|
||||
Scenario: Edit feedback for a students previous attempt.
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category | groupmode |
|
||||
|
||||
@@ -183,8 +183,10 @@ if (($mode === 'csstemplate') || ($mode === 'jstemplate')) {
|
||||
$usehtmleditor = data_get_config($data, "editor_{$mode}", true);
|
||||
}
|
||||
|
||||
$datafieldtype = '';
|
||||
if ($usehtmleditor) {
|
||||
$format = FORMAT_HTML;
|
||||
$datafieldtype = ' data-fieldtype="editor" ';
|
||||
} else {
|
||||
$format = FORMAT_PLAIN;
|
||||
}
|
||||
@@ -226,7 +228,7 @@ if ($mode == 'listtemplate'){
|
||||
$field = 'listtemplateheader';
|
||||
$editor->set_text($data->listtemplateheader);
|
||||
$editor->use_editor($field, $options);
|
||||
echo '<div><textarea id="'.$field.'" name="'.$field.'" class="form-control" rows="15" cols="80">' .
|
||||
echo "<div><textarea id='{$field}' {$datafieldtype} name='{$field}' class='form-control' rows='15' cols='80'>" .
|
||||
s($data->listtemplateheader) . '</textarea></div>';
|
||||
|
||||
echo '</td>';
|
||||
@@ -344,7 +346,7 @@ $field = 'template';
|
||||
$editor->set_text($data->{$mode});
|
||||
$editor->use_editor($field, $options);
|
||||
echo '<div>';
|
||||
echo '<textarea class="form-control" id="' . $field . '" ' .
|
||||
echo '<textarea class="form-control" id="' . $field . '" ' . $datafieldtype .
|
||||
'name="' . $field . '" rows="15" cols="80">' . s($data->{$mode}) . '</textarea>';
|
||||
echo '</div>';
|
||||
echo '</td>';
|
||||
@@ -360,7 +362,7 @@ if ($mode == 'listtemplate'){
|
||||
$editor->set_text($data->listtemplatefooter);
|
||||
$editor->use_editor($field, $options);
|
||||
echo '<div>';
|
||||
echo '<textarea id="' . $field . '" class="form-control" ' .
|
||||
echo '<textarea id="' . $field . '" class="form-control" ' . $datafieldtype .
|
||||
'name="' . $field . '" rows="15" cols="80">' . s($data->listtemplatefooter) . '</textarea>';
|
||||
echo '</div>';
|
||||
echo '</td>';
|
||||
@@ -377,7 +379,7 @@ if ($mode == 'listtemplate'){
|
||||
$editor->set_text($data->rsstitletemplate);
|
||||
$editor->use_editor($field, $options);
|
||||
echo '<div>';
|
||||
echo '<textarea id="' . $field . '" name="' . $field . '" ' .
|
||||
echo '<textarea id="' . $field . '" name="' . $field . '" ' . $datafieldtype .
|
||||
'class="form-control" rows="15" cols="80">' . s($data->rsstitletemplate) . '</textarea>';
|
||||
echo '</div>';
|
||||
echo '</td>';
|
||||
|
||||
@@ -14,18 +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/>.
|
||||
|
||||
/**
|
||||
* PHPUnit data generator tests.
|
||||
*
|
||||
* @package mod_data
|
||||
* @category phpunit
|
||||
* @copyright 2012 Petr Skoda {@link http://skodak.org}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* PHPUnit data generator testcase.
|
||||
*
|
||||
@@ -33,8 +21,12 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @category phpunit
|
||||
* @copyright 2012 Petr Skoda {@link http://skodak.org}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @coversDefaultClass \mod_data_generator
|
||||
*/
|
||||
class mod_data_generator_testcase extends advanced_testcase {
|
||||
/**
|
||||
* @covers ::create_instance
|
||||
*/
|
||||
public function test_generator() {
|
||||
global $DB;
|
||||
|
||||
@@ -72,6 +64,9 @@ class mod_data_generator_testcase extends advanced_testcase {
|
||||
$this->assertEquals(GRADE_TYPE_VALUE, $gitem->gradetype);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::create_field
|
||||
*/
|
||||
public function test_create_field() {
|
||||
global $DB;
|
||||
|
||||
@@ -104,7 +99,6 @@ class mod_data_generator_testcase extends advanced_testcase {
|
||||
|
||||
// Creating test Fields with default parameter values.
|
||||
foreach ($fieldtypes as $fieldtype) {
|
||||
|
||||
// Creating variables dynamically.
|
||||
$fieldname = 'field-' . $count;
|
||||
$record = new StdClass();
|
||||
@@ -124,10 +118,15 @@ class mod_data_generator_testcase extends advanced_testcase {
|
||||
|
||||
for ($i = 1; $i < $count; $i++) {
|
||||
$fieldname = 'field-' . $i;
|
||||
$this->assertTrue(strpos($addtemplate, '[[' . $fieldname . ']]') >= 0);
|
||||
$position = strpos($addtemplate, '[[' . $fieldname . ']]');
|
||||
$this->assertIsNumeric($position);
|
||||
$this->assertGreaterThanOrEqual(0, $position);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::create_entry
|
||||
*/
|
||||
public function test_create_entry() {
|
||||
global $DB;
|
||||
|
||||
@@ -166,7 +165,6 @@ class mod_data_generator_testcase extends advanced_testcase {
|
||||
|
||||
// Creating test Fields with default parameter values.
|
||||
foreach ($fieldtypes as $fieldtype) {
|
||||
|
||||
// Creating variables dynamically.
|
||||
$fieldname = 'field-' . $count;
|
||||
$record = new StdClass();
|
||||
@@ -174,13 +172,10 @@ class mod_data_generator_testcase extends advanced_testcase {
|
||||
$record->type = $fieldtype;
|
||||
$record->required = 1;
|
||||
|
||||
${$fieldname} = $this->getDataGenerator()->get_plugin_generator('mod_data')->create_field($record, $data);
|
||||
$this->assertInstanceOf('data_field_' . $fieldtype, ${$fieldname});
|
||||
$this->getDataGenerator()->get_plugin_generator('mod_data')->create_field($record, $data);
|
||||
$count++;
|
||||
}
|
||||
|
||||
$this->assertEquals(count($fieldtypes), $DB->count_records('data_fields', array('dataid' => $data->id)));
|
||||
|
||||
$fields = $DB->get_records('data_fields', array('dataid' => $data->id), 'id');
|
||||
|
||||
$contents = array();
|
||||
|
||||
@@ -649,7 +649,7 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
|
||||
];
|
||||
$this->queue_tasks_and_assert($expect);
|
||||
|
||||
$tasks = $DB->get_records('task_adhoc');
|
||||
$tasks = $DB->get_records('task_adhoc', ['component' => 'mod_forum']);
|
||||
$task = reset($tasks);
|
||||
$this->assertGreaterThanOrEqual(time(), $task->nextruntime);
|
||||
}
|
||||
@@ -689,7 +689,7 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
|
||||
];
|
||||
$this->queue_tasks_and_assert($expect);
|
||||
|
||||
$tasks = $DB->get_records('task_adhoc');
|
||||
$tasks = $DB->get_records('task_adhoc', ['component' => 'mod_forum']);
|
||||
$task = reset($tasks);
|
||||
$digesttime = usergetmidnight(time(), \core_date::get_server_timezone()) + ($CFG->digestmailtime * 3600);
|
||||
$this->assertLessThanOrEqual($digesttime, $task->nextruntime);
|
||||
|
||||
@@ -332,15 +332,24 @@ class manager {
|
||||
*
|
||||
* @since Moodle 3.9.7
|
||||
* @param bool $allpotentialusers if true, the join will return all active users, not only the ones with attempts.
|
||||
* @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
|
||||
* @return sql_join the active users attempts join
|
||||
*/
|
||||
public function get_active_users_join(bool $allpotentialusers = false): sql_join {
|
||||
public function get_active_users_join(bool $allpotentialusers = false, $currentgroup = false): sql_join {
|
||||
|
||||
// Only valid users counts. By default, all users with submit capability are considered potential ones.
|
||||
$context = $this->get_context();
|
||||
$coursemodule = $this->get_coursemodule();
|
||||
|
||||
// Ensure user can view users from all groups.
|
||||
if ($currentgroup === 0 && $coursemodule->effectivegroupmode == SEPARATEGROUPS
|
||||
&& !has_capability('moodle/site:accessallgroups', $context)) {
|
||||
|
||||
return new sql_join('', '1=2', [], true);
|
||||
}
|
||||
|
||||
// We want to present all potential users.
|
||||
$capjoin = get_enrolled_with_capabilities_join($context, '', 'mod/h5pactivity:view');
|
||||
$capjoin = get_enrolled_with_capabilities_join($context, '', 'mod/h5pactivity:view', $currentgroup);
|
||||
|
||||
if ($capjoin->cannotmatchanyrows) {
|
||||
return $capjoin;
|
||||
@@ -438,10 +447,13 @@ class manager {
|
||||
*
|
||||
* @param int $userid an opional userid to show
|
||||
* @param int $attemptid an optional $attemptid to show
|
||||
* @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
|
||||
* @return report|null available report (or null if no report available)
|
||||
*/
|
||||
public function get_report(int $userid = null, int $attemptid = null): ?report {
|
||||
global $USER;
|
||||
public function get_report(int $userid = null, int $attemptid = null, $currentgroup = false): ?report {
|
||||
global $USER, $CFG;
|
||||
|
||||
require_once("{$CFG->dirroot}/user/lib.php");
|
||||
|
||||
// If tracking is disabled, no reports are available.
|
||||
if (!$this->instance->enabletracking) {
|
||||
@@ -460,6 +472,14 @@ class manager {
|
||||
|
||||
if ($this->can_view_all_attempts()) {
|
||||
$user = core_user::get_user($userid);
|
||||
|
||||
// Ensure user can view the attempt of specific userid, respecting access checks.
|
||||
if ($user && $user->id != $USER->id) {
|
||||
$course = get_course($this->coursemodule->course);
|
||||
if ($this->coursemodule->effectivegroupmode == SEPARATEGROUPS && !user_can_view_profile($user, $course)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else if ($this->can_view_own_attempts()) {
|
||||
$user = core_user::get_user($USER->id);
|
||||
if ($userid && $user->id != $userid) {
|
||||
@@ -480,7 +500,7 @@ class manager {
|
||||
} else if ($user) {
|
||||
return new attempts($this, $user);
|
||||
}
|
||||
return new participants($this);
|
||||
return new participants($this, $currentgroup);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -61,8 +61,9 @@ class participants extends table_sql implements report {
|
||||
* Create a new participants report.
|
||||
*
|
||||
* @param manager $manager h5pactivitymanager object
|
||||
* @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
|
||||
*/
|
||||
public function __construct(manager $manager) {
|
||||
public function __construct(manager $manager, $currentgroup = false) {
|
||||
parent::__construct('mod_h5pactivity-participants');
|
||||
$this->manager = $manager;
|
||||
$this->scores = $manager->get_users_scaled_score();
|
||||
@@ -83,7 +84,7 @@ class participants extends table_sql implements report {
|
||||
$this->no_sorting('attempts');
|
||||
$this->pageable(true);
|
||||
|
||||
$capjoin = $this->manager->get_active_users_join(true);
|
||||
$capjoin = $this->manager->get_active_users_join(true, $currentgroup);
|
||||
|
||||
// Final SQL.
|
||||
$this->set_sql(
|
||||
|
||||
@@ -43,9 +43,11 @@ if (empty($id)) {
|
||||
|
||||
require_login($course, true, $cm);
|
||||
|
||||
$currentgroup = groups_get_activity_group($cm, true);
|
||||
|
||||
$manager = manager::create_from_coursemodule($cm);
|
||||
|
||||
$report = $manager->get_report($userid, $attemptid);
|
||||
$report = $manager->get_report($userid, $attemptid, $currentgroup);
|
||||
if (!$report) {
|
||||
print_error('permissiondenied');
|
||||
}
|
||||
@@ -125,6 +127,8 @@ $PAGE->set_context($context);
|
||||
|
||||
echo $OUTPUT->header();
|
||||
|
||||
groups_print_activity_menu($cm, $PAGE->url);
|
||||
|
||||
echo $report->print();
|
||||
|
||||
echo $OUTPUT->footer();
|
||||
|
||||
@@ -22,13 +22,13 @@ Feature: Add H5P activity
|
||||
|
||||
@javascript
|
||||
Scenario: Add a h5pactivity activity to a course
|
||||
When I add a "H5P" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Awesome H5P package |
|
||||
| Description | H5P activity Description |
|
||||
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
And I wait until the page is ready
|
||||
Given the following "activity" exists:
|
||||
| activity | h5pactivity |
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| intro | H5P activity Description |
|
||||
| packagefilepath | h5p/tests/fixtures/ipsums.h5p |
|
||||
When I am on the "Awesome H5P package" "h5pactivity activity" page
|
||||
Then I should see "H5P activity Description"
|
||||
And I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
@@ -40,13 +40,13 @@ Feature: Add H5P activity
|
||||
|
||||
@javascript
|
||||
Scenario: Add a h5pactivity activity with download
|
||||
When I add a "H5P" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Awesome H5P package |
|
||||
| Description | Description |
|
||||
| Allow download | 1 |
|
||||
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
Given the following "activity" exists:
|
||||
| activity | h5pactivity |
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| displayoptions | 12 |
|
||||
| packagefilepath | h5p/tests/fixtures/ipsums.h5p |
|
||||
When I am on the "Awesome H5P package" "h5pactivity activity" page
|
||||
And I wait until the page is ready
|
||||
Then I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
@@ -57,13 +57,13 @@ Feature: Add H5P activity
|
||||
|
||||
@javascript
|
||||
Scenario: Add a h5pactivity activity with embed
|
||||
When I add a "H5P" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Awesome H5P package |
|
||||
| Description | Description |
|
||||
| Embed button | 1 |
|
||||
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
Given the following "activity" exists:
|
||||
| activity | h5pactivity |
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| displayoptions | 10 |
|
||||
| packagefilepath | h5p/tests/fixtures/ipsums.h5p |
|
||||
When I am on the "Awesome H5P package" "h5pactivity activity" page
|
||||
And I wait until the page is ready
|
||||
Then I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
@@ -74,13 +74,13 @@ Feature: Add H5P activity
|
||||
|
||||
@javascript
|
||||
Scenario: Add a h5pactivity activity with copyright
|
||||
When I add a "H5P" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Awesome H5P package |
|
||||
| Description | Description |
|
||||
| Copyright button | 1 |
|
||||
And I upload "h5p/tests/fixtures/guess-the-answer.h5p" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
Given the following "activity" exists:
|
||||
| activity | h5pactivity |
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| displayoptions | 6 |
|
||||
| packagefilepath | h5p/tests/fixtures/guess-the-answer.h5p |
|
||||
When I am on the "Awesome H5P package" "h5pactivity activity" page
|
||||
And I wait until the page is ready
|
||||
Then I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
@@ -91,13 +91,13 @@ Feature: Add H5P activity
|
||||
|
||||
@javascript
|
||||
Scenario: Add a h5pactivity activity with copyright in a content without copyright
|
||||
When I add a "H5P" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Awesome H5P package |
|
||||
| Description | Description |
|
||||
| Copyright button | 1 |
|
||||
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
Given the following "activity" exists:
|
||||
| activity | h5pactivity |
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| displayoptions | 6 |
|
||||
| packagefilepath | h5p/tests/fixtures/ipsums.h5p |
|
||||
When I am on the "Awesome H5P package" "h5pactivity activity" page
|
||||
And I wait until the page is ready
|
||||
Then I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
@@ -108,15 +108,13 @@ Feature: Add H5P activity
|
||||
|
||||
@javascript
|
||||
Scenario: Add a h5pactivity activity to a course with all display options enabled
|
||||
When I add a "H5P" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Awesome H5P package |
|
||||
| Description | Description |
|
||||
| Allow download | 1 |
|
||||
| Embed button | 1 |
|
||||
| Copyright button | 1 |
|
||||
And I upload "h5p/tests/fixtures/guess-the-answer.h5p" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
Given the following "activity" exists:
|
||||
| activity | h5pactivity |
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| displayoptions | 0 |
|
||||
| packagefilepath | h5p/tests/fixtures/guess-the-answer.h5p |
|
||||
When I am on the "Awesome H5P package" "h5pactivity activity" page
|
||||
And I wait until the page is ready
|
||||
Then I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
|
||||
@@ -22,6 +22,7 @@ Feature: Content bank link in the activity settings form
|
||||
And I add a "H5P" to section "1"
|
||||
Then I should see "Use the content bank (opens in new window) to manage your H5P files"
|
||||
And I click on "content bank (opens in new window)" "link" in the "General" "fieldset"
|
||||
And I switch to a second window
|
||||
And I should see "C1" in the "page-navbar" "region"
|
||||
And I close all opened windows
|
||||
|
||||
|
||||
@@ -19,36 +19,23 @@ Feature: Change grading options in an H5P activity
|
||||
And the following "permission overrides" exist:
|
||||
| capability | permission | role | contextlevel | reference |
|
||||
| moodle/h5p:updatelibraries | Allow | editingteacher | System | |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "H5P" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Awesome H5P package |
|
||||
| Description | Description |
|
||||
And I upload "h5p/tests/fixtures/multiple-choice-2-6.h5p" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
And I log out
|
||||
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
|
||||
And I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element"
|
||||
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
|
||||
And I click on "Retry" "button" in the ".h5p-question-buttons" "css_element"
|
||||
And I click on "Correct one" "text" in the ".h5p-question-content" "css_element"
|
||||
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
|
||||
And I switch to the main frame
|
||||
# H5P does not allow to Retry if the user checks the correct answer, we need to refresh the page.
|
||||
And I reload the page
|
||||
And I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element"
|
||||
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
|
||||
And I switch to the main frame
|
||||
And I log out
|
||||
And the following "activity" exists:
|
||||
| activity | h5pactivity |
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| packagefilepath | h5p/tests/fixtures/multiple-choice-2-6.h5p |
|
||||
And the following "mod_h5pactivity > attempts" exist:
|
||||
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
|
||||
| student1 | Awesome H5P package | 1 | choice | 0 | 1 | 4 | 1 | 0 |
|
||||
| student1 | Awesome H5P package | 2 | choice | 1 | 1 | 4 | 1 | 1 |
|
||||
| student1 | Awesome H5P package | 3 | choice | 0 | 1 | 4 | 1 | 0 |
|
||||
|
||||
@javascript
|
||||
Scenario: Default grading is max attempt grade
|
||||
Given I am on the "Awesome H5P package" "h5pactivity activity" page logged in as teacher1
|
||||
Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1
|
||||
And I expand all fieldsets
|
||||
And the field "Grading method" matches value "Highest grade"
|
||||
And I click on "Save and return to course" "button"
|
||||
When I navigate to "View > User report" in the course gradebook
|
||||
And I set the field "Select all or one user" to "Student 1"
|
||||
Then the following should exist in the "user-grade" table:
|
||||
@@ -68,7 +55,7 @@ Feature: Change grading options in an H5P activity
|
||||
| Awesome H5P package | 0.00 | 0.00 % |
|
||||
|
||||
@javascript
|
||||
Scenario: Change setting to first attempt
|
||||
Scenario: Change setting to last attempt
|
||||
Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1
|
||||
When I set the following fields to these values:
|
||||
| Grading method | Last attempt |
|
||||
|
||||
@@ -17,16 +17,12 @@ Feature: View fill the blanks attempt report
|
||||
And the following config values are set as admin:
|
||||
# No HTML should appear even with formatstringstriptags disabled.
|
||||
| formatstringstriptags | 0 |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "H5P" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Awesome H5P package |
|
||||
| Description | Description |
|
||||
| Grading method | Average grade |
|
||||
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
And I log out
|
||||
And the following "activity" exists:
|
||||
| activity | h5pactivity |
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| grademethod | 2 |
|
||||
| packagefilepath | h5p/tests/fixtures/filltheblanks.h5p |
|
||||
|
||||
Scenario: View attempt in a fill the blanks content
|
||||
Given I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
|
||||
|
||||
@@ -17,16 +17,12 @@ Feature: View essay attempt report
|
||||
And the following config values are set as admin:
|
||||
# No HTML should appear even with formatstringstriptags disabled.
|
||||
| formatstringstriptags | 0 |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "H5P" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Awesome H5P package |
|
||||
| Description | Description |
|
||||
| Grading method | Average grade |
|
||||
And I upload "h5p/tests/fixtures/basic_essay.h5p" file to "Package file" filemanager
|
||||
And I click on "Save and display" "button"
|
||||
And I log out
|
||||
And the following "activity" exists:
|
||||
| activity | h5pactivity |
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| grademethod | 2 |
|
||||
| packagefilepath | h5p/tests/fixtures/basic_essay.h5p |
|
||||
|
||||
Scenario: View attempt essay content
|
||||
# Do an attempt.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Feature: Do a H5P attempt
|
||||
In order to let students do a H5P attempt
|
||||
As a teacher
|
||||
I need to list students attempts on the log report
|
||||
I need to list students attempts on various reports
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
@@ -19,25 +19,21 @@ Feature: Do a H5P attempt
|
||||
And the following "permission overrides" exist:
|
||||
| capability | permission | role | contextlevel | reference |
|
||||
| moodle/h5p:updatelibraries | Allow | editingteacher | System | |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "H5P" to section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | Awesome H5P package |
|
||||
| Description | Description |
|
||||
And I upload "h5p/tests/fixtures/multiple-choice-2-6.h5p" file to "Package file" filemanager
|
||||
And the following "activity" exists:
|
||||
| activity | h5pactivity |
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| packagefilepath | h5p/tests/fixtures/multiple-choice-2-6.h5p |
|
||||
| grademethod | 2 |
|
||||
|
||||
@javascript
|
||||
Scenario: View an H5P as a teacher
|
||||
When I click on "Save and display" "button"
|
||||
When I am on the "Awesome H5P package" "h5pactivity activity" page logged in as teacher1
|
||||
And I wait until the page is ready
|
||||
Then I should see "This content is displayed in preview mode"
|
||||
|
||||
@javascript
|
||||
Scenario: To an attempts and check on course log report
|
||||
When I click on "Save and return to course" "button"
|
||||
And I log out
|
||||
Given I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
|
||||
Scenario: Do an attempt and check on course log report
|
||||
When I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
|
||||
And I wait until the page is ready
|
||||
And I should not see "This content is displayed in preview mode"
|
||||
And I switch to "h5p-player" class iframe
|
||||
@@ -51,3 +47,45 @@ Feature: Do a H5P attempt
|
||||
And I follow "Student 1"
|
||||
Then I follow "Today's logs"
|
||||
And I should see "xAPI statement received"
|
||||
|
||||
@javascript
|
||||
Scenario: Do various attempts and check them with the attempts and user grades reports
|
||||
Given I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
|
||||
And I wait until the page is ready
|
||||
And I should not see "This content is displayed in preview mode"
|
||||
And I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element"
|
||||
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
|
||||
And I click on "Retry" "button" in the ".h5p-question-buttons" "css_element"
|
||||
# We need to wait 1 second here because, in very quick environments, the 2nd
|
||||
# attempts happen too close to the 1st one and it's not sent properly. See MDL-76010.
|
||||
And I wait "1" seconds
|
||||
And I click on "Correct one" "text" in the ".h5p-question-content" "css_element"
|
||||
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
|
||||
# H5P does not allow to Retry if the user checks the correct answer, we need to refresh the page.
|
||||
And I switch to the main frame
|
||||
And I reload the page
|
||||
And I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
# Because of the steps above, the 2nd and 3rd attempts are enough "separated" and we don't
|
||||
# need to add any wait here.
|
||||
And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element"
|
||||
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
|
||||
And I click on "Retry" "button" in the ".h5p-question-buttons" "css_element"
|
||||
# Again, the wait between 3rd and 4th attempt, to separate them a little bit.
|
||||
And I wait "1" seconds
|
||||
And I click on "Correct one" "text" in the ".h5p-question-content" "css_element"
|
||||
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
|
||||
And I switch to the main frame
|
||||
When I follow "View my attempts"
|
||||
And "1" row "Score" column of "table" table should contain "0"
|
||||
And "2" row "Score" column of "table" table should contain "1"
|
||||
And "3" row "Score" column of "table" table should contain "0"
|
||||
And "4" row "Score" column of "table" table should contain "1"
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I navigate to "View > User report" in the course gradebook
|
||||
And I set the field "Select all or one user" to "Student 1"
|
||||
Then the following should exist in the "user-grade" table:
|
||||
| Grade item | Grade | Percentage |
|
||||
| Awesome H5P package | 50.00 | 50.00 % |
|
||||
|
||||
@@ -55,6 +55,8 @@ class mod_h5pactivity_generator extends testing_module_generator {
|
||||
// Fill in optional values if not specified.
|
||||
if (!isset($record->packagefilepath)) {
|
||||
$record->packagefilepath = $CFG->dirroot.'/h5p/tests/fixtures/h5ptest.zip';
|
||||
} else if (strpos($record->packagefilepath, $CFG->dirroot) !== 0) {
|
||||
$record->packagefilepath = "{$CFG->dirroot}/{$record->packagefilepath}";
|
||||
}
|
||||
if (!isset($record->grade)) {
|
||||
$record->grade = 100;
|
||||
@@ -189,6 +191,7 @@ class mod_h5pactivity_generator extends testing_module_generator {
|
||||
* @param array $data the attempts data array
|
||||
*/
|
||||
public function create_attempt(array $data): void {
|
||||
global $DB;
|
||||
|
||||
if (!isset($data['h5pactivityid'])) {
|
||||
throw new coding_exception('Must specify h5pactivityid when creating a H5P attempt.');
|
||||
@@ -216,6 +219,12 @@ class mod_h5pactivity_generator extends testing_module_generator {
|
||||
}
|
||||
|
||||
$this->insert_statement($data, $this->$method($data));
|
||||
|
||||
// If the activity has tracking enabled, try to recalculate grades.
|
||||
$activity = $DB->get_record('h5pactivity', ['id' => $data['h5pactivityid']]);
|
||||
if ($activity->enabletracking) {
|
||||
h5pactivity_update_grades($activity, $data['userid']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,14 +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/>.
|
||||
|
||||
/**
|
||||
* mod_h5pactivity manager tests
|
||||
*
|
||||
* @package mod_h5pactivity
|
||||
* @category test
|
||||
* @copyright 2020 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace mod_h5pactivity\local;
|
||||
use context_module;
|
||||
@@ -31,6 +23,7 @@ use stdClass;
|
||||
* Manager tests class for mod_h5pactivity.
|
||||
*
|
||||
* @package mod_h5pactivity
|
||||
* @covers \mod_h5pactivity\local\manager
|
||||
* @category test
|
||||
* @copyright 2020 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
@@ -627,7 +620,7 @@ class manager_testcase extends \advanced_testcase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Test static count_attempts of all active participants.
|
||||
* Test static test_get_active_users_join of all active participants.
|
||||
*
|
||||
* Most method scenarios are tested in test_count_attempts_all so we only
|
||||
* need to test the with $allpotentialusers true and false.
|
||||
@@ -692,6 +685,58 @@ class manager_testcase extends \advanced_testcase {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test active users joins returns appropriate results for groups
|
||||
*/
|
||||
public function test_get_active_users_join_groupmode(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course(['groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1]);
|
||||
|
||||
// Teacher/user one in group one.
|
||||
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
|
||||
$userone = $this->getDataGenerator()->create_and_enrol($course, 'student');
|
||||
|
||||
$groupone = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
|
||||
$this->getDataGenerator()->create_group_member(['groupid' => $groupone->id, 'userid' => $teacher->id]);
|
||||
$this->getDataGenerator()->create_group_member(['groupid' => $groupone->id, 'userid' => $userone->id]);
|
||||
|
||||
// User two in group two.
|
||||
$usertwo = $this->getDataGenerator()->create_and_enrol($course, 'student');
|
||||
|
||||
$grouptwo = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
|
||||
$this->getDataGenerator()->create_group_member(['groupid' => $grouptwo->id, 'userid' => $usertwo->id]);
|
||||
|
||||
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
|
||||
$manager = manager::create_from_instance($activity);
|
||||
|
||||
// Admin user can view all participants.
|
||||
$usersjoin = $manager->get_active_users_join(true, 0);
|
||||
$users = $DB->get_fieldset_sql("SELECT u.username FROM {user} u {$usersjoin->joins} WHERE {$usersjoin->wheres}",
|
||||
$usersjoin->params);
|
||||
|
||||
$this->assertEqualsCanonicalizing([$teacher->username, $userone->username, $usertwo->username], $users);
|
||||
|
||||
// Switch to teacher, who cannot view all participants.
|
||||
$this->setUser($teacher);
|
||||
|
||||
$usersjoin = $manager->get_active_users_join(true, 0);
|
||||
$users = $DB->get_fieldset_sql("SELECT u.username FROM {user} u {$usersjoin->joins} WHERE {$usersjoin->wheres}",
|
||||
$usersjoin->params);
|
||||
|
||||
$this->assertEmpty($users);
|
||||
|
||||
// Teacher can view participants inside group.
|
||||
$usersjoin = $manager->get_active_users_join(true, $groupone->id);
|
||||
$users = $DB->get_fieldset_sql("SELECT u.username FROM {user} u {$usersjoin->joins} WHERE {$usersjoin->wheres}",
|
||||
$usersjoin->params);
|
||||
|
||||
$this->assertEqualsCanonicalizing([$teacher->username, $userone->username], $users);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test static count_attempts.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
This files describes API changes in /mod/h5pactivity/*,
|
||||
information provided here is intended especially for developers.
|
||||
|
||||
=== 3.9.17 ===
|
||||
|
||||
* The following methods now accept an optional `$currentgroup` parameter in order to better support groups:
|
||||
- `\mod_h5pactivity\local\manager::get_active_users_join`
|
||||
- `\mod_h5pactivity\local\manager::get_report`
|
||||
- `\mod_h5pactivity\local\report\participants` constructor
|
||||
|
||||
=== 3.9.7 ===
|
||||
|
||||
* Added mod_h5pactivity\local\manager::get_active_users_join method to query all active
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@mod @mod_lesson
|
||||
# This feature has Atto-specific steps. See MDL-75913 for further details.
|
||||
@mod @mod_lesson @editor_atto
|
||||
Feature: In a lesson activity, a teacher can duplicate a lesson page
|
||||
In order to duplicate a lesson page
|
||||
As a teacher
|
||||
|
||||
@@ -3,7 +3,8 @@ Feature: In a lesson activity, teacher can add embedded images in questions answ
|
||||
As a teacher
|
||||
I need to add questions with images in answers and responses
|
||||
|
||||
@javascript @_file_upload
|
||||
# This scenario has Atto-specific steps. See MDL-75913 for further details.
|
||||
@javascript @_file_upload @editor_atto
|
||||
Scenario: questions with images in answers and responses
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
|
||||
@@ -55,7 +55,8 @@ Feature: Teachers can override the grade for any question
|
||||
# This time is same as time the window is open. So wait for it to close before proceeding.
|
||||
And I wait "2" seconds
|
||||
|
||||
@javascript @_switch_window @_file_upload @_bug_phantomjs
|
||||
# This scenario has Atto-specific steps. See MDL-75913 for further details.
|
||||
@javascript @_switch_window @_file_upload @_bug_phantomjs @editor_atto
|
||||
Scenario: Comment on a response to an essay question attempt.
|
||||
When I log in as "teacher1"
|
||||
And I follow "Manage private files"
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
@mod @mod_quiz
|
||||
Feature: Several attempts in a quiz
|
||||
As a student
|
||||
In order to demonstrate what I know
|
||||
I need to be able to attempt quizzes and sometimes take multiple attempts
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student1 | Student | One | student1@example.com |
|
||||
| student2 | Student | One | student2@example.com |
|
||||
| teacher | Teacher | One | teacher@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
| student2 | C1 | student |
|
||||
| teacher | C1 | teacher |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Test questions | truefalse | TF1 | First question |
|
||||
| Test questions | truefalse | TF2 | Second question |
|
||||
And the following "activities" exist:
|
||||
| activity | name | intro | course | idnumber | preferredbehaviour | navmethod |
|
||||
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback | free |
|
||||
And quiz "Quiz 1" contains the following questions:
|
||||
| question | page | requireprevious |
|
||||
| TF1 | 1 | 1 |
|
||||
| TF2 | 2 | 1 |
|
||||
# Add some attempts
|
||||
And user "student1" has attempted "Quiz 1" with responses:
|
||||
| slot | response |
|
||||
| 1 | True |
|
||||
| 2 | False |
|
||||
And user "student2" has attempted "Quiz 1" with responses:
|
||||
| slot | response |
|
||||
| 1 | True |
|
||||
| 2 | True |
|
||||
# Add a second attempt by student1
|
||||
And user "student1" has attempted "Quiz 1" with responses:
|
||||
| slot | response |
|
||||
| 1 | False |
|
||||
| 2 | False |
|
||||
|
||||
@javascript
|
||||
Scenario: The redo question buttons are visible after 2 attempts are preset for student1.
|
||||
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "student1"
|
||||
Then "Re-attempt quiz" "button" should exist
|
||||
And "1" row "Marks / 2.00" column of "quizattemptsummary" table should contain "1.00"
|
||||
And "2" row "Marks / 2.00" column of "quizattemptsummary" table should contain "0.00"
|
||||
@@ -99,9 +99,6 @@ class mod_quiz_generator extends testing_module_generator {
|
||||
/**
|
||||
* Create a quiz attempt for a particular user at a particular course.
|
||||
*
|
||||
* Currently this method can only create a first attempt for each
|
||||
* user at each quiz. TODO remove this limitation.
|
||||
*
|
||||
* @param int $quizid the quiz id (from the mdl_quit table, not cmid).
|
||||
* @param int $userid the user id.
|
||||
* @param array $forcedrandomquestions slot => questionid. Optional,
|
||||
@@ -116,12 +113,18 @@ class mod_quiz_generator extends testing_module_generator {
|
||||
// Build quiz object and load questions.
|
||||
$quizobj = quiz::create($quizid, $userid);
|
||||
|
||||
if (quiz_get_user_attempts($quizid, $userid, 'all', true)) {
|
||||
throw new coding_exception('mod_quiz_generator is currently limited to only ' .
|
||||
'be able to create one attempt for each user. (This should be fixed.)');
|
||||
$attemptnumber = 1;
|
||||
$attempt = null;
|
||||
|
||||
if ($attempts = quiz_get_user_attempts($quizid, $userid, 'all', true)) {
|
||||
// There is/are already an attempt/some attempts.
|
||||
// Take the last attempt.
|
||||
$attempt = end($attempts);
|
||||
// Take the attempt number of the last attempt and increase it.
|
||||
$attemptnumber = $attempt->attempt + 1;
|
||||
}
|
||||
|
||||
return quiz_prepare_and_start_new_attempt($quizobj, 1, null, false,
|
||||
return quiz_prepare_and_start_new_attempt($quizobj, $attemptnumber, $attempt, false,
|
||||
$forcedrandomquestions, $forcedvariants);
|
||||
}
|
||||
|
||||
@@ -179,6 +182,7 @@ class mod_quiz_generator extends testing_module_generator {
|
||||
public function create_override(array $data): void {
|
||||
global $DB;
|
||||
|
||||
// Validate.
|
||||
if (!isset($data['quiz'])) {
|
||||
throw new coding_exception('Must specify quiz (id) when creating a quiz override.');
|
||||
}
|
||||
@@ -191,6 +195,10 @@ class mod_quiz_generator extends testing_module_generator {
|
||||
throw new coding_exception('Cannot specify both userid and groupid when creating a quiz override.');
|
||||
}
|
||||
|
||||
// Create the override.
|
||||
$DB->insert_record('quiz_overrides', (object) $data);
|
||||
|
||||
// Update any associated calendar events, if necessary.
|
||||
quiz_update_events($DB->get_record('quiz', ['id' => $data['quiz']], '*', MUST_EXIST));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @category phpunit
|
||||
* @copyright 2012 Matt Petro
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \mod_quiz_generator
|
||||
*/
|
||||
class mod_quiz_generator_testcase extends advanced_testcase {
|
||||
public function test_generator() {
|
||||
@@ -42,7 +43,7 @@ class mod_quiz_generator_testcase extends advanced_testcase {
|
||||
|
||||
$this->assertEquals(0, $DB->count_records('quiz'));
|
||||
|
||||
/** @var mod_quiz_generator $generator */
|
||||
/** @var \mod_quiz_generator $generator */
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
|
||||
$this->assertInstanceOf('mod_quiz_generator', $generator);
|
||||
$this->assertEquals('quiz', $generator->get_modulename());
|
||||
@@ -64,4 +65,65 @@ class mod_quiz_generator_testcase extends advanced_testcase {
|
||||
$this->assertEqualsWithDelta($createtime,
|
||||
$DB->get_field('quiz', 'timecreated', ['id' => $cm->instance]), 2);
|
||||
}
|
||||
|
||||
public function test_generating_a_user_override() {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course();
|
||||
$user = $generator->create_user();
|
||||
$quiz = $generator->create_module('quiz', ['course' => $course->id]);
|
||||
$generator->enrol_user($user->id, $course->id, 'student');
|
||||
|
||||
/** @var \mod_quiz_generator $quizgenerator */
|
||||
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
|
||||
$quizgenerator->create_override([
|
||||
'quiz' => $quiz->id,
|
||||
'userid' => $user->id,
|
||||
'timeclose' => strtotime('2022-10-20'),
|
||||
]);
|
||||
|
||||
// Check the corresponding calendar event now exists.
|
||||
$events = calendar_get_events(strtotime('2022-01-01'),
|
||||
strtotime('2022-12-31'), $user->id, false, $course->id);
|
||||
$this->assertCount(1, $events);
|
||||
$event = reset($events);
|
||||
$this->assertEquals($user->id, $event->userid);
|
||||
$this->assertEquals(0, $event->groupid);
|
||||
$this->assertEquals(0, $event->courseid);
|
||||
$this->assertEquals('quiz', $event->modulename);
|
||||
$this->assertEquals($quiz->id, $event->instance);
|
||||
$this->assertEquals('close', $event->eventtype);
|
||||
$this->assertEquals(strtotime('2022-10-20'), $event->timestart);
|
||||
}
|
||||
|
||||
public function test_generating_a_group_override() {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course();
|
||||
$quiz = $generator->create_module('quiz', ['course' => $course->id]);
|
||||
$group = $generator->create_group(['courseid' => $course->id]);
|
||||
|
||||
/** @var \mod_quiz_generator $quizgenerator */
|
||||
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
|
||||
$quizgenerator->create_override([
|
||||
'quiz' => $quiz->id,
|
||||
'groupid' => $group->id,
|
||||
'timeclose' => strtotime('2022-10-20'),
|
||||
]);
|
||||
|
||||
// Check the corresponding calendar event now exists.
|
||||
$events = calendar_get_events(strtotime('2022-01-01'),
|
||||
strtotime('2022-12-31'), false, $group->id, $course->id);
|
||||
$this->assertCount(1, $events);
|
||||
$event = reset($events);
|
||||
$this->assertEquals(0, $event->userid);
|
||||
$this->assertEquals($group->id, $event->groupid);
|
||||
$this->assertEquals($course->id, $event->courseid);
|
||||
$this->assertEquals('quiz', $event->modulename);
|
||||
$this->assertEquals($quiz->id, $event->instance);
|
||||
$this->assertEquals('close', $event->eventtype);
|
||||
$this->assertEquals(strtotime('2022-10-20'), $event->timestart);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,31 +20,20 @@ Feature: A teacher can use activity completion to track a student progress
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
|
||||
Scenario: Require survey view
|
||||
Given I add a "Survey" to section "1" and I fill the form with:
|
||||
| Name | Test survey name |
|
||||
| Survey type | Critical incidents |
|
||||
| Description | Test survey description |
|
||||
| Completion tracking | Show activity as complete when conditions are met |
|
||||
| id_completionview | 1 |
|
||||
| id_completionsubmit | 0 |
|
||||
And I log out
|
||||
When I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
Given the following "activities" exist:
|
||||
| activity | course | name | template | completion | completionview | completionsubmit |
|
||||
| survey | C1 | Test survey name | 5 | 2 | 1 | 0 |
|
||||
When I am on the "Course 1" course page logged in as student1
|
||||
And the "Test survey name" "survey" activity with "auto" completion should be marked as not complete
|
||||
And I follow "Test survey name"
|
||||
And I am on "Course 1" course homepage
|
||||
Then the "Test survey name" "survey" activity with "auto" completion should be marked as complete
|
||||
|
||||
Scenario: Require survey submission
|
||||
Given I add a "Survey" to section "1" and I fill the form with:
|
||||
| Name | Test survey name |
|
||||
| Survey type | Critical incidents |
|
||||
| Description | Test survey description |
|
||||
| Completion tracking | Show activity as complete when conditions are met |
|
||||
| id_completionsubmit | 1 |
|
||||
And I log out
|
||||
When I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
Given the following "activities" exist:
|
||||
| activity | course | name | template | completion | completionsubmit |
|
||||
| survey | C1 | Test survey name | 5 | 2 | 1 |
|
||||
When I am on the "Course 1" course page logged in as student1
|
||||
And the "Test survey name" "survey" activity with "auto" completion should be marked as not complete
|
||||
And I follow "Test survey name"
|
||||
And I press "Click here to continue"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user