Compare commits

..

34 Commits

Author SHA1 Message Date
Andrew Nicols 2288e60615 Moodle release 3.5.10 2020-01-10 13:03:30 +08:00
Andrew Nicols f951911682 Merge branch 'install_35_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_35_STABLE 2020-01-10 13:03:27 +08:00
AMOS bot ad262b981f Automatically generated installer lang files 2020-01-10 00:12:37 +00:00
Sara Arjona da4ffac0c9 weekly release 3.5.9+ 2020-01-09 15:42:57 +01:00
Eloy Lafuente (stronk7) 702c4b4781 Merge branch 'MDL-67469-35' of git://github.com/junpataleta/moodle into MOODLE_35_STABLE 2020-01-07 23:00:29 +01:00
Jake Dallimore fedba1fcc4 weekly release 3.5.9+ 2020-01-03 11:25:21 +08:00
Jake Dallimore de96ddee74 Merge branch 'install_35_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_35_STABLE 2020-01-03 11:25:18 +08:00
AMOS bot 7b6a902ccd Automatically generated installer lang files 2020-01-03 00:12:31 +00:00
Adrian Greeve a0a03baad8 Merge branch 'MDL-67599-35' of https://github.com/snake/moodle into MOODLE_35_STABLE 2020-01-02 15:19:39 +08:00
Jake Dallimore e37073a24b MDL-67599 behat: fix behat tests assuming Jan 2020 is a future date 2020-01-02 13:56:16 +08:00
AMOS bot 9f59264afa Automatically generated installer lang files 2020-01-02 00:10:44 +00:00
Eloy Lafuente (stronk7) 7f4be41ed2 Merge branch 'MDL-67593-35' of https://github.com/snake/moodle into MOODLE_35_STABLE 2019-12-31 10:51:22 +01:00
Jake Dallimore 3dec6b78ce MDL-67593 core_iplookup: update lang string describing download process
Let users know they need to register on maxmind before granted download
access.
2019-12-31 15:59:41 +08:00
Jake Dallimore e3e41170fe MDL-67593 core_iplookup: use MaxMind test database in unit tests
Instead of downloading a live database (now not available publicly)
use a local test database.
2019-12-31 15:58:27 +08:00
Jun Pataleta 7c6be5b319 Merge branch 'MDL-63996-35' of git://github.com/andrewnicols/moodle into MOODLE_35_STABLE 2019-12-31 15:18:41 +08:00
Adrian Greeve 0613e56e69 Merge branch 'MDL-65809-35-onlyenv' of git://github.com/sarjona/moodle into MOODLE_35_STABLE 2019-12-24 14:58:52 +08:00
AMOS bot a3065e8220 Automatically generated installer lang files 2019-12-21 00:11:20 +00:00
Andrew Nicols f496b55189 MDL-63996 core/tags: Pending JS for all tag code 2019-12-18 12:02:54 +08:00
Andrew Nicols 135461b4a9 MDL-63996 behat: Fix multi-tag autocomplete usage 2019-12-18 10:25:57 +08:00
AMOS bot 8253f41405 Automatically generated installer lang files 2019-12-14 00:11:15 +00:00
AMOS bot cf0b79a80f Automatically generated installer lang files 2019-12-13 00:11:32 +00:00
AMOS bot 1a8dc02878 Automatically generated installer lang files 2019-12-11 13:50:31 +00:00
Jun Pataleta 5b3ec84495 MDL-67469 tool_dataprivacy: Put back missing test code
This patch simply brings back the missing test code for
\tool_dataprivacy_api_testcase::test_approve_data_request_non_dpo_user()
2019-12-06 16:11:38 +08:00
AMOS bot 0608b35a7b Automatically generated installer lang files 2019-12-06 00:10:04 +00:00
Sara Arjona abb5f229b1 MDL-65809 environment: mbstring extension required 2019-12-05 14:02:15 +01:00
Sara Arjona e2fd22fadd MDL-65809 environment: PHP 7.2 required 2019-12-05 14:02:15 +01:00
Sara Arjona 18c824af1f MDL-65809 environment: postgres requirement up to 9.5 2019-12-05 14:02:15 +01:00
Sara Arjona 37ce725987 MDL-65809 environment: mariadb requirement up to 10.2 2019-12-05 14:02:15 +01:00
Sara Arjona 8ac6793716 MDL-65809 environment: 3.5 (LTS) required 2019-12-05 14:02:15 +01:00
Sara Arjona 6022706b47 MDL-65809 environment: 3.9 base information
This is a direct copy of the 3.8 branch to have changes
under control easier in next commits.
2019-12-05 14:02:15 +01:00
Eloy Lafuente (stronk7) 250ea6a145 weekly release 3.5.9+ 2019-11-22 00:17:00 +01:00
Andrew Nicols 039096d1b1 Merge branch 'MDL-48024_35' of https://github.com/stronk7/moodle into MOODLE_35_STABLE 2019-11-21 19:45:47 +08:00
Eloy Lafuente (stronk7) 0ca3442925 MDL-48024 quiz: return void is only php >= 71
this branch supports php 70, so taking rid of it
2019-11-21 12:34:50 +01:00
Tim Hunt 1a4626d9fa MDL-48024 behat: allow plugins to have data generators
This extends the step
Given the following "users" exist:
to also support things like
Given the following "mod_quiz > user overrides" exist:

Instructions are on the behat_data_generators and
behat_generator_base classes.
2019-11-21 09:38:37 +08:00
32 changed files with 1856 additions and 1040 deletions
+181
View File
@@ -2982,4 +2982,185 @@
</CUSTOM_CHECK>
</CUSTOM_CHECKS>
</MOODLE>
<MOODLE version="3.9" requires="3.5">
<UNICODE level="required">
<FEEDBACK>
<ON_ERROR message="unicoderequired" />
</FEEDBACK>
</UNICODE>
<DATABASE level="required">
<VENDOR name="mariadb" version="10.2.29" />
<VENDOR name="mysql" version="5.6" />
<VENDOR name="postgres" version="9.5" />
<VENDOR name="mssql" version="11.0" />
<VENDOR name="oracle" version="11.2" />
</DATABASE>
<PHP version="7.2.0" level="required">
</PHP>
<PCREUNICODE level="optional">
<FEEDBACK>
<ON_CHECK message="pcreunicodewarning" />
</FEEDBACK>
</PCREUNICODE>
<PHP_EXTENSIONS>
<PHP_EXTENSION name="iconv" level="required">
<FEEDBACK>
<ON_ERROR message="iconvrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="mbstring" level="required">
<FEEDBACK>
<ON_ERROR message="mbstringrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="curl" level="required">
<FEEDBACK>
<ON_ERROR message="curlrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="openssl" level="required">
<FEEDBACK>
<ON_ERROR message="opensslrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="tokenizer" level="optional">
<FEEDBACK>
<ON_CHECK message="tokenizerrecommended" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="xmlrpc" level="optional">
<FEEDBACK>
<ON_CHECK message="xmlrpcrecommended" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="soap" level="optional">
<FEEDBACK>
<ON_CHECK message="soaprecommended" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="ctype" level="required">
<FEEDBACK>
<ON_ERROR message="ctyperequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="zip" level="required">
<FEEDBACK>
<ON_ERROR message="ziprequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="zlib" level="required">
</PHP_EXTENSION>
<PHP_EXTENSION name="gd" level="required">
<FEEDBACK>
<ON_ERROR message="gdrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="simplexml" level="required">
<FEEDBACK>
<ON_ERROR message="simplexmlrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="spl" level="required">
<FEEDBACK>
<ON_ERROR message="splrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="pcre" level="required">
</PHP_EXTENSION>
<PHP_EXTENSION name="dom" level="required">
</PHP_EXTENSION>
<PHP_EXTENSION name="xml" level="required">
</PHP_EXTENSION>
<PHP_EXTENSION name="xmlreader" level="required">
</PHP_EXTENSION>
<PHP_EXTENSION name="intl" level="required">
<FEEDBACK>
<ON_ERROR message="intlrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="json" level="required">
</PHP_EXTENSION>
<PHP_EXTENSION name="hash" level="required"/>
<PHP_EXTENSION name="fileinfo" level="required"/>
</PHP_EXTENSIONS>
<PHP_SETTINGS>
<PHP_SETTING name="memory_limit" value="96M" level="required">
<FEEDBACK>
<ON_ERROR message="settingmemorylimit" />
</FEEDBACK>
</PHP_SETTING>
<PHP_SETTING name="file_uploads" value="1" level="optional">
<FEEDBACK>
<ON_CHECK message="settingfileuploads" />
</FEEDBACK>
</PHP_SETTING>
<PHP_SETTING name="opcache.enable" value="1" level="optional">
<FEEDBACK>
<ON_CHECK message="opcacherecommended" />
</FEEDBACK>
</PHP_SETTING>
</PHP_SETTINGS>
<CUSTOM_CHECKS>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
<FEEDBACK>
<ON_ERROR message="unsupporteddbstorageengine" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
<FEEDBACK>
<ON_ERROR message="quizattemptsupgradedmessage" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
<FEEDBACK>
<ON_CHECK message="slashargumentswarning" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
<FEEDBACK>
<ON_CHECK message="unsupporteddbtablerowformat" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
<FEEDBACK>
<ON_CHECK message="unoconvwarning" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
<FEEDBACK>
<ON_CHECK message="libcurlwarning" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
<FEEDBACK>
<ON_ERROR message="unsupporteddbfileformat" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
<FEEDBACK>
<ON_ERROR message="unsupporteddbfilepertable" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
<FEEDBACK>
<ON_ERROR message="unsupporteddblargeprefix" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
<FEEDBACK>
<ON_CHECK message="ishttpswarning" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
<FEEDBACK>
<ON_CHECK message="incompleteunicodesupport" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="optional">
<FEEDBACK>
<ON_CHECK message="sixtyfourbitswarning" />
</FEEDBACK>
</CUSTOM_CHECK>
</CUSTOM_CHECKS>
</MOODLE>
</COMPATIBILITY_MATRIX>
@@ -337,6 +337,11 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
$datarequest = api::create_data_request($student->id, api::DATAREQUEST_TYPE_EXPORT);
$requestid = $datarequest->get('id');
// Login as a user without DPO role.
$this->setUser($teacher);
$this->expectException(required_capability_exception::class);
api::approve_data_request($requestid);
}
/**
+1 -1
View File
@@ -44,7 +44,7 @@ $string['dmlexceptiononinstall'] = '<p>Παρουσιάστηκε κάποιο
$string['downloadedfilecheckfailed'] = 'Αποτυχία ελέγχου αρχείου που έγινε λήψη';
$string['invalidmd5'] = 'Η μεταβλητή ελέγχου ήταν λανθασμένη - δοκιμάστε ξανά';
$string['missingrequiredfield'] = 'Κάποιο απαιτούμενο πεδίο λείπει';
$string['remotedownloaderror'] = '<p>Απέτυχε η λήψη του τμήματος στον εξυπηρετητή σας. Παρακαλούμε επιβεβαιώστε τις ρυθμίσεις του διακομιστή μεσολάβησης (proxy)· η επέκταση PHP cURL συνιστάται θερμά.</p> <p>Πρέπει να κατεβάσετε το <a href="{$a->url}">{$a->url}</a> χειροκίνητα, να το αντιγράψετε στο «{$a->dest}» στον εξυπηρετητή σας και να το αποσυμπιέσετε εκεί.</p>';
$string['remotedownloaderror'] = '<p>Η λήψη του στοιχείου λογισμικού στον εξυπηρετητή σας απέτυχε. Παρακαλούμε επαληθεύστε τις ρυθμίσεις του διακομιστή μεσολάβησης (proxy)· η επέκταση PHP cURL συνιστάται θερμά.</p><br /><p>Πρέπει να κατεβάσετε το αρχείο <a href="{$a->url}">{$a->url}</a> χειροκίνητα, να το αντιγράψετε στο «{$a->dest}» στον εξυπηρετητή σας και να το αποσυμπιέσετε εκεί.</p>';
$string['wrongdestpath'] = 'Λανθασμένη διαδρομή προορισμού (πλήρες όνομα).';
$string['wrongsourcebase'] = 'Λανθασμένη βάση πηγής URL.';
$string['wrongzipfilename'] = 'Λανθασμένo όνομα αρχείου ZIP.';
+1 -1
View File
@@ -76,7 +76,7 @@ $string['pathssubdataroot'] = '<p>Moodle-k erabiltzaileek igotako fitxategien ed
<p>Direktorio honetan web-zerbitzariaren erabiltzaileak irakurtzeko eta idazteko baimena izan beharko ditu (normalean \'www-data\', \'nobody\', edo \'apache\').</p>
<p>Ez litzateke web bidez eskuragarri egon beharko.</p>
<p>Direktorioa existitzen ez bada, instalazioan sortzeko saiakera egingo da.</p>';
$string['pathssubdirroot'] = '<p>Moodle-ko kodea daukan direktorioaren helbide osoa.</p>';
$string['pathssubdirroot'] = '<p>Moodle-ko kodea daukan direktorioaren bide osoa.</p>';
$string['pathssubwwwroot'] = '<p>Moodle eskuragarri egongo den helbide osoa, hau da, erabiltzaileek Moodle-n sartzeko nabigatzaileareko helbide barran idatziko duten helbidea</p>
<p>Moodle ezin da hainbat helbidetatik eskuragarri egon. Zure gunea hainbat helbidetatik eskuragarri badago errazena aukeratu eta bertara beste helbideetatik behin-betiko berbideraketak konfiguratu itzazu.</p>
<p>Zure gunea Internetetik eta barne-sare batetik eskuragarri badago (batzuetan Intranet deitutakoa), hemen helbide publikoa erabili ezazu.</p>
+34
View File
@@ -0,0 +1,34 @@
<?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/>.
/**
* Automatically generated strings for Moodle installer
*
* Do not edit this file manually! It contains just a subset of strings
* needed during the very first steps of installation. This file was
* generated automatically by export-installer.php (which is part of AMOS
* {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
* list of strings defined in /install/stringnames.txt.
*
* @package installer
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['parentlanguage'] = 'fr';
$string['thislanguage'] = 'Français (écriture inclusive)';
+1
View File
@@ -30,5 +30,6 @@
defined('MOODLE_INTERNAL') || die();
$string['clianswerno'] = 'ទេ';
$string['environmentrequireinstall'] = 'ត្រូវតែបានដំឡើង និងអនុញ្ញាត';
$string['environmentrequireversion'] = 'ត្រូវការកំណែ {$a->needed} ហើយអ្នកកំពុងរត់ {$a->current}';
+39
View File
@@ -0,0 +1,39 @@
<?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/>.
/**
* Automatically generated strings for Moodle installer
*
* Do not edit this file manually! It contains just a subset of strings
* needed during the very first steps of installation. This file was
* generated automatically by export-installer.php (which is part of AMOS
* {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
* list of strings defined in /install/stringnames.txt.
*
* @package installer
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['availablelangs'] = 'Giellapáhkat mat leat gávdnamis';
$string['chooselanguagehead'] = 'Vállje giela';
$string['chooselanguagesub'] = 'Vállje giela (dušše installašuvdnjii). Don sáhtát válljet neahttabáikki ja geavaheaddji giela šearbmagovas maŋŋelaš.';
$string['clialreadyconfigured'] = 'Fiila config.php gávdno jo. Geavat admin/cli/install_database.php jus áiggut installeret dán portála.';
$string['clialreadyinstalled'] = 'Fiila config.php gávdno jo. Geavat admin/cli/install_database.php jus háliidat ođasmahttit Moodle dán portálas.';
$string['langdownloaderror'] = 'Dađibahábut ii installerejuvvon giellapáhkka "{$a}". Installašuvdnaproseassa joatká eaŋgalsgillii.';
$string['phpextension'] = '{$a} PHP gilkor';
+34
View File
@@ -0,0 +1,34 @@
<?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/>.
/**
* Automatically generated strings for Moodle installer
*
* Do not edit this file manually! It contains just a subset of strings
* needed during the very first steps of installation. This file was
* generated automatically by export-installer.php (which is part of AMOS
* {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
* list of strings defined in /install/stringnames.txt.
*
* @package installer
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['language'] = 'Giella';
$string['next'] = 'Boahtte';
+34
View File
@@ -0,0 +1,34 @@
<?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/>.
/**
* Automatically generated strings for Moodle installer
*
* Do not edit this file manually! It contains just a subset of strings
* needed during the very first steps of installation. This file was
* generated automatically by export-installer.php (which is part of AMOS
* {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
* list of strings defined in /install/stringnames.txt.
*
* @package installer
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Tok Pisin';
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+10
View File
@@ -0,0 +1,10 @@
GeoIP2-City-Test.mmdb:
Copyright: maxmind (github.com/maxmind)
https://github.com/maxmind/MaxMind-DB/blob/master/test-data/GeoIP2-City-Test.mmdb
Licence: Creative Commons Attribution-ShareAlike 3.0 Unported License
http://creativecommons.org/licenses/by-sa/3.0/
No changes have been made to this file.
+3 -45
View File
@@ -36,56 +36,15 @@ require_once("{$CFG->dirroot}/iplookup/lib.php");
*/
class core_iplookup_geoip_testcase extends advanced_testcase {
public function setUp() {
if (!PHPUNIT_LONGTEST) {
// These tests are intensive and required downloads.
$this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
}
$this->resetAfterTest();
}
/**
* Setup the GeoIP2File system.
*/
public function setup_geoip2file() {
global $CFG;
// Store the file somewhere where it won't be wiped out..
$gzfile = "$CFG->dataroot/phpunit/geoip/GeoLite2-City.mmdb.gz";
check_dir_exists(dirname($gzfile));
if (file_exists($gzfile) and (filemtime($gzfile) < time() - 60*60*24*30)) {
// Delete file if older than 1 month.
unlink($gzfile);
}
if (!file_exists($gzfile)) {
download_file_content('http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz',
null, null, false, 300, 20, false, $gzfile);
}
$this->assertTrue(file_exists($gzfile));
$geoipfile = str_replace('.gz', '', $gzfile);
// Open our files (in binary mode).
$file = gzopen($gzfile, 'rb');
$geoipfilebuf = fopen($geoipfile, 'wb');
// Keep repeating until the end of the input file.
while (!gzeof($file)) {
// Read buffer-size bytes.
// Both fwrite and gzread and binary-safe.
fwrite($geoipfilebuf, gzread($file, 4096));
}
// Files are done, close files.
fclose($geoipfilebuf);
gzclose($file);
$this->assertTrue(file_exists($geoipfile));
$CFG->geoip2file = $geoipfile;
$CFG->geoip2file = "$CFG->dirroot/iplookup/tests/fixtures/GeoIP2-City-Test.mmdb";
}
/**
@@ -121,9 +80,8 @@ class core_iplookup_geoip_testcase extends advanced_testcase {
*/
public function ip_provider() {
return [
'IPv4: Sample suggested by maxmind themselves' => ['24.24.24.24'],
'IPv4: github.com' => ['192.30.255.112'],
'IPv6: UCLA' => ['2607:f010:3fe:fff1::ff:fe00:25'],
'IPv4: IPV4 test' => ['81.2.69.142'],
'IPv6: IPV6 test' => ['2001:252:1::1:1:1'],
];
}
}
+2 -1
View File
@@ -248,7 +248,7 @@ $string['configfrontpageloggedin'] = 'The items selected above will be displayed
$string['configfullnamedisplay'] = 'This defines how names are shown when they are displayed in full. The default value, "language", leaves it to the string "fullnamedisplay" in the current language pack to decide. Some languages have different name display conventions.
For most mono-lingual sites the most efficient setting is "firstname lastname", but you may choose to hide surnames altogether. Placeholders that can be used are: firstname, lastname, firstnamephonetic, lastnamephonetic, middlename, and alternatename.';
$string['configgeoipfile'] = 'Location of GeoLite2 City binary data file. This file is not part of Moodle distribution and must be obtained separately from <a href="http://www.maxmind.com/">MaxMind</a>. You can either buy a commercial version or use the free version. Simply download <a href="http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz" >http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz</a> and extract it into "{$a}" directory on your server.';
$string['configgeoipfile'] = 'Location of GeoLite2 City binary data file. This file is not part of Moodle distribution and must be obtained separately from <a href="https://www.maxmind.com/">MaxMind</a>. You can either buy a commercial version or use the free version. You\'ll need to register to download the City database file, which you can do at <a href="https://dev.maxmind.com/geoip/geoip2/geolite2/" >https://dev.maxmind.com/geoip/geoip2/geolite2/</a>. Once you\'ve registered and downloaded the file, extract it into "{$a}" directory on your server.';
$string['configgetremoteaddrconf'] = 'If your server is behind a reverse proxy, you can use this setting to specify which HTTP headers can be trusted to contain the remote IP address. The headers are read in order, using the first one that is available.';
$string['configgradebookroles'] = 'This setting allows you to control who appears on the gradebook. Users need to have at least one of these roles in a course to be shown in the gradebook for that course.';
$string['configgradeexport'] = 'Choose which gradebook export formats are your primary methods for exporting grades. Chosen plugins will then set and use a "last exported" field for every grade. For example, this might result in exported records being identified as being "new" or "updated". If you are not sure about this then leave everything unchecked.';
@@ -731,6 +731,7 @@ $string['maxeditingtime'] = 'Maximum time to edit posts';
$string['maxusersperpage'] = ' Maximum users per page';
$string['configmaxusersperpage'] = 'Maximum number of users displayed within user selector in course, group, cohort, webservice etc.';
$string['mbstringrecommended'] = 'Installing the optional MBSTRING library is highly recommended in order to improve site performance, particularly if your site is supporting non-Latin languages.';
$string['mbstringrequired'] = 'Installing the MBSTRING library is required in order to improve site performance, particularly if your site is supporting non-Latin languages.';
$string['mediapluginavi'] = 'Enable .avi filter';
$string['mediapluginflv'] = 'Enable .flv filter';
$string['mediapluginimg'] = 'Enable auto-embedding of linked images';
+1 -1
View File
@@ -1 +1 @@
define(["core/yui","jquery","core/log"],function(a,b,c){var d={types:{success:"core/notification_success",info:"core/notification_info",warning:"core/notification_warning",error:"core/notification_error"},fieldName:"user-notifications",fetchNotifications:function(){require(["core/ajax"],function(a){var b=a.call([{methodname:"core_fetch_notifications",args:{contextid:d.contextid}}]);b[0].done(d.addNotifications)})},addNotifications:function(a){a||(a=[]),b.each(a,function(a,b){d.renderNotification(b.template,b.variables)})},setupTargetRegion:function(){var a=b("#"+d.fieldName);if(a.length)return!1;var c=b("<span>").attr("id",d.fieldName);return a=b("#region-main"),a.length?a.prepend(c):(a=b('[role="main"]'),a.length?a.prepend(c):(a=b("body"),a.prepend(c)))},addNotification:function(a){var c=d.types.error;return a=b.extend({closebutton:!0,announce:!0,type:"error"},a),a.template?(c=a.template,delete a.template):a.type&&("undefined"!=typeof d.types[a.type]&&(c=d.types[a.type]),delete a.type),d.renderNotification(c,a)},renderNotification:function(a,e){return"undefined"!=typeof e.message&&e.message?void require(["core/templates"],function(c){c.render(a,e).done(function(a,e){b("#"+d.fieldName).prepend(a),c.runTemplateJS(e)}).fail(d.exception)}):void c.debug("Notification received without content. Skipping.")},alert:function(b,c,d){a.use("moodle-core-notification-alert",function(){var a=new M.core.alert({title:b,message:c,yesLabel:d});a.show()})},confirm:function(b,c,d,e,f,g){a.use("moodle-core-notification-confirm",function(){var a=new M.core.confirm({title:b,question:c,yesLabel:d,noLabel:e});a.on("complete-yes",function(){f()}),g&&a.on("complete-no",function(){g()}),a.show()})},exception:function(b){if("undefined"==typeof b.stack&&(b.stack=""),b.debuginfo&&(b.stack+=b.debuginfo+"\n"),!b.backtrace&&b.stacktrace&&(b.backtrace=b.stacktrace),b.backtrace){b.stack+=b.backtrace;var c=b.backtrace.match(/line ([^ ]*) of/),d=b.backtrace.match(/ of ([^:]*): /);c&&c[1]&&(b.lineNumber=c[1]),d&&d[1]&&(b.fileName=d[1],b.fileName.length>30&&(b.fileName="..."+b.fileName.substr(b.fileName.length-27)))}"undefined"==typeof b.name&&b.errorcode&&(b.name=b.errorcode),a.use("moodle-core-notification-exception",function(){var a=new M.core.exception(b);a.show()})}};return{init:function(a,b){d.contextid=a,d.setupTargetRegion(),d.addNotifications(b),d.fetchNotifications()},fetchNotifications:d.fetchNotifications,addNotification:d.addNotification,alert:d.alert,confirm:d.confirm,exception:d.exception}});
define(["core/yui","jquery","core/log","core/pending"],function(a,b,c,d){var e={types:{success:"core/notification_success",info:"core/notification_info",warning:"core/notification_warning",error:"core/notification_error"},fieldName:"user-notifications",fetchNotifications:function(){var a=new d("core/notification:fetchNotifications");require(["core/ajax"],function(b){var c=b.call([{methodname:"core_fetch_notifications",args:{contextid:e.contextid}}]);c[0].then(e.addNotifications).always(a.resolve)})},addNotifications:function(a){var c=new d("core/notification:addNotifications");a||(a=[]),b.each(a,function(a,b){e.renderNotification(b.template,b.variables)}),c.resolve()},setupTargetRegion:function(){var a=b("#"+e.fieldName);if(a.length)return!1;var c=b("<span>").attr("id",e.fieldName);return a=b("#region-main"),a.length?a.prepend(c):(a=b('[role="main"]'),a.length?a.prepend(c):(a=b("body"),a.prepend(c)))},addNotification:function(a){var c=new d("core/notification:addNotifications"),f=e.types.error;return a=b.extend({closebutton:!0,announce:!0,type:"error"},a),a.template?(f=a.template,delete a.template):a.type&&("undefined"!=typeof e.types[a.type]&&(f=e.types[a.type]),delete a.type),c.resolve(),e.renderNotification(f,a)},renderNotification:function(a,f){if("undefined"==typeof f.message||!f.message)return void c.debug("Notification received without content. Skipping.");var g=new d("core/notification:renderNotification");require(["core/templates"],function(c){c.render(a,f).then(function(a,d){b("#"+e.fieldName).prepend(a),c.runTemplateJS(d)}).then(g.resolve)["catch"](e.exception)})},alert:function(b,c,e){var f=new d("core/notification:alert");a.use("moodle-core-notification-alert",function(){var a=new M.core.alert({title:b,message:c,yesLabel:e});a.show(),f.resolve()})},confirm:function(b,c,e,f,g,h){var i=new d("core/notification:confirm");a.use("moodle-core-notification-confirm",function(){var a=new M.core.confirm({title:b,question:c,yesLabel:e,noLabel:f});a.on("complete-yes",function(){g()}),h&&a.on("complete-no",function(){h()}),a.show(),i.resolve()})},exception:function(b){var c=new d("core/notification:addNotifications");if("undefined"==typeof b.stack&&(b.stack=""),b.debuginfo&&(b.stack+=b.debuginfo+"\n"),!b.backtrace&&b.stacktrace&&(b.backtrace=b.stacktrace),b.backtrace){b.stack+=b.backtrace;var e=b.backtrace.match(/line ([^ ]*) of/),f=b.backtrace.match(/ of ([^:]*): /);e&&e[1]&&(b.lineNumber=e[1]),f&&f[1]&&(b.fileName=f[1],b.fileName.length>30&&(b.fileName="..."+b.fileName.substr(b.fileName.length-27)))}"undefined"==typeof b.name&&b.errorcode&&(b.name=b.errorcode),a.use("moodle-core-notification-exception",function(){var a=new M.core.exception(b);a.show(),c.resolve()})}};return{init:function(a,b){e.contextid=a,e.setupTargetRegion(),e.addNotifications(b),e.fetchNotifications()},fetchNotifications:e.fetchNotifications,addNotification:e.addNotification,alert:e.alert,confirm:e.confirm,exception:e.exception}});
+1 -1
View File
File diff suppressed because one or more lines are too long
+43 -14
View File
@@ -26,8 +26,8 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 2.9
*/
define(['core/yui', 'jquery', 'core/log'],
function(Y, $, log) {
define(['core/yui', 'jquery', 'core/log', 'core/pending'],
function(Y, $, log, Pending) {
var notificationModule = {
types: {
'success': 'core/notification_success',
@@ -39,6 +39,8 @@ function(Y, $, log) {
fieldName: 'user-notifications',
fetchNotifications: function() {
var pendingPromise = new Pending('core/notification:fetchNotifications');
require(['core/ajax'], function(ajax) {
var promises = ajax.call([{
methodname: 'core_fetch_notifications',
@@ -47,13 +49,17 @@ function(Y, $, log) {
}
}]);
// This currently fails when not logged in.
// eslint-disable-next-line promise/catch-or-return
promises[0]
.done(notificationModule.addNotifications)
;
.then(notificationModule.addNotifications)
.always(pendingPromise.resolve);
});
},
addNotifications: function(notifications) {
var pendingPromise = new Pending('core/notification:addNotifications');
if (!notifications) {
notifications = [];
}
@@ -61,6 +67,8 @@ function(Y, $, log) {
$.each(notifications, function(i, notification) {
notificationModule.renderNotification(notification.template, notification.variables);
});
pendingPromise.resolve();
},
setupTargetRegion: function() {
@@ -86,13 +94,15 @@ function(Y, $, log) {
},
addNotification: function(notification) {
var pendingPromise = new Pending('core/notification:addNotifications');
var template = notificationModule.types.error;
notification = $.extend({
closebutton: true,
announce: true,
type: 'error'
}, notification);
closebutton: true,
announce: true,
type: 'error'
}, notification);
if (notification.template) {
template = notification.template;
@@ -104,6 +114,8 @@ function(Y, $, log) {
delete notification.type;
}
pendingPromise.resolve();
return notificationModule.renderNotification(template, notification);
},
@@ -112,18 +124,25 @@ function(Y, $, log) {
log.debug('Notification received without content. Skipping.');
return;
}
var pendingPromise = new Pending('core/notification:renderNotification');
require(['core/templates'], function(templates) {
templates.render(template, variables)
.done(function(html, js) {
$('#' + notificationModule.fieldName).prepend(html);
templates.runTemplateJS(js);
})
.fail(notificationModule.exception)
;
.then(function(html, js) {
$('#' + notificationModule.fieldName).prepend(html);
templates.runTemplateJS(js);
return;
})
.then(pendingPromise.resolve)
.catch(notificationModule.exception);
});
},
alert: function(title, message, yesLabel) {
var pendingPromise = new Pending('core/notification:alert');
// Here we are wrapping YUI. This allows us to start transitioning, but
// wait for a good alternative without having inconsistent dialogues.
Y.use('moodle-core-notification-alert', function() {
@@ -134,10 +153,14 @@ function(Y, $, log) {
});
alert.show();
pendingPromise.resolve();
});
},
confirm: function(title, question, yesLabel, noLabel, yesCallback, noCallback) {
var pendingPromise = new Pending('core/notification:confirm');
// Here we are wrapping YUI. This allows us to start transitioning, but
// wait for a good alternative without having inconsistent dialogues.
Y.use('moodle-core-notification-confirm', function() {
@@ -157,10 +180,14 @@ function(Y, $, log) {
});
}
modal.show();
pendingPromise.resolve();
});
},
exception: function(ex) {
var pendingPromise = new Pending('core/notification:addNotifications');
// Fudge some parameters.
if (typeof ex.stack == 'undefined') {
ex.stack = '';
@@ -193,6 +220,8 @@ function(Y, $, log) {
var modal = new M.core.exception(ex);
modal.show();
pendingPromise.resolve();
});
}
};
+179 -121
View File
@@ -22,8 +22,8 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.0
*/
define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/yui'],
function($, ajax, templates, notification, str, Y) {
define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/yui', 'core/pending'],
function($, ajax, templates, notification, str, Y, Pending) {
return /** @alias module:core/tag */ {
/**
@@ -34,27 +34,31 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
initTagindexPage: function() {
// Click handler for changing tag type.
$('body').delegate('.tagarea[data-ta] a[data-quickload=1]', 'click', function(e) {
var pendingPromise = new Pending('core/tag:initTagindexPage');
e.preventDefault();
var target = $(this),
query = target[0].search.replace(/^\?/, ''),
tagarea = target.closest('.tagarea[data-ta]'),
args = query.split('&').reduce(function(s, c) {
var target = $(this);
var query = target[0].search.replace(/^\?/, '');
var tagarea = target.closest('.tagarea[data-ta]');
var args = query.split('&').reduce(function(s, c) {
var t = c.split('=');
s[t[0]] = decodeURIComponent(t[1]);
return s;
}, {});
var promises = ajax.call([{
ajax.call([{
methodname: 'core_tag_get_tagindex',
args: {tagindex: args}
}], true);
$.when.apply($, promises)
.done(function(data) {
templates.render('core_tag/index', data).done(function(html) {
tagarea.replaceWith(html);
});
});
}])[0]
.then(function(data) {
return templates.render('core_tag/index', data);
})
.then(function(html, js) {
templates.replaceNode(tagarea, html, js);
return;
})
.always(pendingPromise.resolve)
.catch(notification.exception);
});
},
@@ -64,17 +68,29 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
* @method initManagePage
*/
initManagePage: function() {
// Set cell 'time modified' to 'now' when any of the element is updated in this row.
$('body').on('updated', '[data-inplaceeditable]', function(e) {
str.get_string('selecttag', 'core_tag', e.ajaxreturn.value)
.then(function(s) {
return $('label[for="tagselect' + e.ajaxreturn.itemid + '"]').html(s);
})
.fail(notification.exception);
str.get_string('now').done(function(s) {
$(e.target).closest('tr').find('td.col-timemodified').html(s);
});
var pendingPromise = new Pending('core/tag:initManagePage');
str.get_strings([
{
key: 'selecttag',
component: 'core_tag',
},
{
key: 'now',
component: 'core',
},
])
.then(function(result) {
$('label[for="tagselect' + e.ajaxreturn.itemid + '"]').html(result[0]);
$(e.target).closest('tr').find('td.col-timemodified').html(result[1]);
return;
})
.always(pendingPromise.resolve)
.catch(notification.exception);
if (e.ajaxreturn.itemtype === 'tagflag') {
var row = $(e.target).closest('tr');
if (e.ajaxreturn.value === '0') {
@@ -87,67 +103,84 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
// Confirmation for single tag delete link.
$('.tag-management-table').delegate('a.tagdelete', 'click', function(e) {
var pendingPromise = new Pending('core/tag:tagdelete');
e.preventDefault();
var href = $(this).attr('href');
str.get_strings([
{key: 'delete'},
{key: 'confirmdeletetag', component: 'tag'},
{key: 'yes'},
{key: 'no'},
]).done(function(s) {
notification.confirm(s[0], s[1], s[2], s[3], function() {
window.location.href = href;
});
}
);
{key: 'delete', component: 'core'},
{key: 'confirmdeletetag', component: 'tag'},
{key: 'yes', component: 'core'},
{key: 'no', component: 'core'},
])
.then(function(s) {
return notification.confirm(s[0], s[1], s[2], s[3], function() {
window.location.href = href;
});
})
.always(pendingPromise.resolve)
.catch(notification.exception);
});
// Confirmation for bulk tag delete button.
$("#tag-management-delete").click(function(e) {
var form = $(this).closest('form').get(0),
cnt = $(form).find("input[type=checkbox]:checked").length;
var form = $(this).closest('form').get(0);
var cnt = $(form).find("input[type=checkbox]:checked").length;
if (!cnt) {
return;
}
var pendingPromise = new Pending('core/tag:tag-management-delete');
var tempElement = $("<input type='hidden'/>").attr('name', this.name);
e.preventDefault();
str.get_strings([
{key: 'delete'},
{key: 'confirmdeletetags', component: 'tag'},
{key: 'yes'},
{key: 'no'},
]).done(function(s) {
notification.confirm(s[0], s[1], s[2], s[3], function() {
tempElement.appendTo(form);
form.submit();
});
}
);
{key: 'delete', component: 'core'},
{key: 'confirmdeletetags', component: 'tag'},
{key: 'yes', component: 'core'},
{key: 'no', component: 'core'},
])
.then(function(s) {
return notification.confirm(s[0], s[1], s[2], s[3], function() {
tempElement.appendTo(form);
form.submit();
});
})
.always(pendingPromise.resolve)
.catch(notification.exception);
});
// Confirmation for bulk tag combine button.
$("#tag-management-combine").click(function(e) {
var pendingPromise = new Pending('core/tag:tag-management-combine');
e.preventDefault();
var form = $(this).closest('form').get(0),
tags = $(form).find("input[type=checkbox]:checked");
var form = $(this).closest('form').get(0);
var tags = $(form).find("input[type=checkbox]:checked");
if (tags.length <= 1) {
str.get_strings([
{key: 'combineselected', component: 'tag'},
{key: 'selectmultipletags', component: 'tag'},
{key: 'ok'},
]).done(function(s) {
notification.alert(s[0], s[1], s[2]);
}
);
])
.then(function(s) {
return notification.alert(s[0], s[1], s[2]);
})
.always(pendingPromise.resolve)
.catch(notification.exception);
return;
}
var tempElement = $("<input type='hidden'/>").attr('name', this.name);
str.get_strings([
{key: 'combineselected', component: 'tag'},
{key: 'selectmaintag', component: 'tag'},
{key: 'continue'},
{key: 'cancel'},
]).done(function(s) {
])
.then(function(s) {
var el = $('<div><form id="combinetags_form">' +
'<div class="description"></div><div class="form-group options"></div>' +
'<div class="form-group">' +
@@ -191,7 +224,11 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
return false;
});
});
});
return;
})
.always(pendingPromise.resolve)
.catch(notification.exception);
});
// When user changes tag name to some name that already exists suggest to combine the tags.
@@ -200,30 +237,38 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
var newvalue = e.newvalue; // The value that user tried to udpated the element to.
var tagid = $(e.target).attr('data-itemid');
if (exception.errorcode === 'namesalreadybeeingused') {
var pendingPromise = new Pending('core/tag:tag-management-combine-exists');
e.preventDefault(); // This will prevent default error dialogue.
str.get_strings([
{key: 'nameuseddocombine', component: 'tag'},
{key: 'yes'},
{key: 'cancel'},
]).done(function(s) {
notification.confirm(e.message, s[0], s[1], s[2], function() {
])
.then(function(s) {
return notification.confirm(e.message, s[0], s[1], s[2], function() {
window.location.href = window.location.href + "&newname=" + encodeURIComponent(newvalue) +
"&tagid=" + encodeURIComponent(tagid) +
'&action=renamecombine&sesskey=' + M.cfg.sesskey;
});
});
})
.always(pendingPromise.resolve)
.catch(notification.exception);
}
});
// Form for adding standard tags.
$('body').on('click', 'a[data-action=addstandardtag]', function(e) {
var pendingPromise = new Pending('core/tag:addstandardtag');
e.preventDefault();
str.get_strings([
{key: 'addotags', component: 'tag'},
{key: 'inputstandardtags', component: 'tag'},
{key: 'continue'},
{key: 'cancel'},
]).done(function(s) {
{key: 'continue', component: 'core'},
{key: 'cancel', component: 'core'},
])
.then(function(s) {
var el = $('<div><form id="addtags_form" method="POST">' +
'<input type="hidden" name="action" value="addstandardtag"/>' +
'<input type="hidden" name="sesskey" value="' + M.cfg.sesskey + '"/>' +
@@ -253,8 +298,10 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
$('#addtags_form #addtags_cancel').on('click', function() {
panel.destroy();
});
pendingPromise.resolve();
});
});
})
.catch(notification.exception);
});
},
@@ -265,6 +312,8 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
*/
initManageCollectionsPage: function() {
$('body').on('updated', '[data-inplaceeditable]', function(e) {
var pendingPromise = new Pending('core/tag:initManageCollectionsPage-updated');
var ajaxreturn = e.ajaxreturn,
areaid, collid, isenabled;
if (ajaxreturn.component === 'core_tag' && ajaxreturn.itemtype === 'tagareaenable') {
@@ -288,78 +337,87 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
$(".tag-collections-table ul[data-collectionid=" + collid + "] li[data-areaid=" + areaid + "]").show();
}
}
pendingPromise.resolve();
});
$('body').on('click', '.addtagcoll > a', function(e) {
var pendingPromise = new Pending('core/tag:initManageCollectionsPage-addtagcoll');
e.preventDefault();
var href = $(this).attr('data-url') + '&sesskey=' + M.cfg.sesskey;
str.get_strings([
{key: 'addtagcoll', component: 'tag'},
{key: 'name'},
{key: 'searchable', component: 'tag'},
{key: 'create'},
{key: 'cancel'},
]).done(function(s) {
var el = $('<div><form id="addtagcoll_form">' +
'<div class="form-group">' +
' <label for="addtagcoll_name"></label> ' +
' <input id="addtagcoll_name" type="text" class="form-control"/> ' +
'</div>' +
'<div class="form-group form-check">' +
' <input id="addtagcoll_searchable" type="checkbox" value="1" checked class="form-check-input"/>' +
' <label for="addtagcoll_searchable" class="form-check-label"></label>' +
'</div>' +
'<div class="form-group">' +
' <input type="submit" class="btn btn-primary" id="addtagcoll_submit"/>' +
' <input type="button" class="btn btn-secondary" id="addtagcoll_cancel"/>' +
'</div>' +
'</form></div>');
el.find('label[for="addtagcoll_name"]').html(s[1]);
el.find('label[for="addtagcoll_searchable"]').html(s[2]);
el.find('#addtagcoll_submit').attr('value', s[3]);
el.find('#addtagcoll_cancel').attr('value', s[4]);
// TODO: MDL-57778 Convert to core/modal.
Y.use('moodle-core-notification-dialogue', function() {
var panel = new M.core.dialogue({
draggable: true,
modal: true,
closeButton: true,
headerContent: s[0],
bodyContent: el.html()
});
panel.show();
$('#addtagcoll_form #addtagcoll_name').focus();
$('#addtagcoll_form #addtagcoll_cancel').on('click', function() {
panel.destroy();
});
$('#addtagcoll_form').on('submit', function() {
var name = $('#addtagcoll_form #addtagcoll_name').val();
var searchable = $('#addtagcoll_form #addtagcoll_searchable').prop('checked') ? 1 : 0;
if (String(name).length > 0) {
window.location.href = href + "&name=" + encodeURIComponent(name) + "&searchable=" + searchable;
}
return false;
});
{key: 'addtagcoll', component: 'tag'},
{key: 'name'},
{key: 'searchable', component: 'tag'},
{key: 'create'},
{key: 'cancel'},
])
.then(function(s) {
var el = $('<div><form id="addtagcoll_form">' +
'<div class="form-group">' +
' <label for="addtagcoll_name"></label> ' +
' <input id="addtagcoll_name" type="text" class="form-control"/> ' +
'</div>' +
'<div class="form-group form-check">' +
' <input id="addtagcoll_searchable" type="checkbox" value="1" checked class="form-check-input"/>' +
' <label for="addtagcoll_searchable" class="form-check-label"></label>' +
'</div>' +
'<div class="form-group">' +
' <input type="submit" class="btn btn-primary" id="addtagcoll_submit"/>' +
' <input type="button" class="btn btn-secondary" id="addtagcoll_cancel"/>' +
'</div>' +
'</form></div>');
el.find('label[for="addtagcoll_name"]').html(s[1]);
el.find('label[for="addtagcoll_searchable"]').html(s[2]);
el.find('#addtagcoll_submit').attr('value', s[3]);
el.find('#addtagcoll_cancel').attr('value', s[4]);
// TODO: MDL-57778 Convert to core/modal.
Y.use('moodle-core-notification-dialogue', function() {
var panel = new M.core.dialogue({
draggable: true,
modal: true,
closeButton: true,
headerContent: s[0],
bodyContent: el.html()
});
}
);
panel.show();
$('#addtagcoll_form #addtagcoll_name').focus();
$('#addtagcoll_form #addtagcoll_cancel').on('click', function() {
panel.destroy();
});
$('#addtagcoll_form').on('submit', function() {
var name = $('#addtagcoll_form #addtagcoll_name').val();
var searchable = $('#addtagcoll_form #addtagcoll_searchable').prop('checked') ? 1 : 0;
if (String(name).length > 0) {
window.location.href = href + "&name=" + encodeURIComponent(name) + "&searchable=" + searchable;
}
return false;
});
pendingPromise.resolve();
});
})
.catch(notification.exception);
});
$('body').on('click', '.tag-collections-table .action_delete', function(e) {
var pendingPromise = new Pending('core/tag:initManageCollectionsPage-action_delete');
e.preventDefault();
var href = $(this).attr('data-url') + '&sesskey=' + M.cfg.sesskey;
str.get_strings([
{key: 'delete'},
{key: 'suredeletecoll', component: 'tag', param: $(this).attr('data-collname')},
{key: 'yes'},
{key: 'no'},
]).done(function(s) {
notification.confirm(s[0], s[1], s[2], s[3], function() {
window.location.href = href;
});
}
);
{key: 'delete'},
{key: 'suredeletecoll', component: 'tag', param: $(this).attr('data-collname')},
{key: 'yes'},
{key: 'no'},
])
.then(function(s) {
return notification.confirm(s[0], s[1], s[2], s[3], function() {
window.location.href = href;
});
})
.always(pendingPromise.resolve)
.catch(notification.exception);
});
}
};
+538
View File
@@ -0,0 +1,538 @@
<?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/>.
/**
* Data generators for acceptance testing.
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
defined('MOODLE_INTERNAL') || die();
/**
* Behat data generator class for core entities.
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_core_generator extends behat_generator_base {
protected function get_creatable_entities(): array {
return [
'users' => [
'datagenerator' => 'user',
'required' => ['username'],
],
'categories' => [
'datagenerator' => 'category',
'required' => ['idnumber'],
'switchids' => ['category' => 'parent'],
],
'courses' => [
'datagenerator' => 'course',
'required' => ['shortname'],
'switchids' => ['category' => 'category'],
],
'groups' => [
'datagenerator' => 'group',
'required' => ['idnumber', 'course'],
'switchids' => ['course' => 'courseid'],
],
'groupings' => [
'datagenerator' => 'grouping',
'required' => ['idnumber', 'course'],
'switchids' => ['course' => 'courseid'],
],
'course enrolments' => [
'datagenerator' => 'enrol_user',
'required' => ['user', 'course', 'role'],
'switchids' => ['user' => 'userid', 'course' => 'courseid', 'role' => 'roleid'],
],
'permission overrides' => [
'datagenerator' => 'permission_override',
'required' => ['capability', 'permission', 'role', 'contextlevel', 'reference'],
'switchids' => ['role' => 'roleid'],
],
'system role assigns' => [
'datagenerator' => 'system_role_assign',
'required' => ['user', 'role'],
'switchids' => ['user' => 'userid', 'role' => 'roleid'],
],
'role assigns' => [
'datagenerator' => 'role_assign',
'required' => ['user', 'role', 'contextlevel', 'reference'],
'switchids' => ['user' => 'userid', 'role' => 'roleid'],
],
'activities' => [
'datagenerator' => 'activity',
'required' => ['activity', 'idnumber', 'course'],
'switchids' => ['course' => 'course', 'gradecategory' => 'gradecat', 'grouping' => 'groupingid'],
],
'blocks' => [
'datagenerator' => 'block_instance',
'required' => ['blockname', 'contextlevel', 'reference'],
],
'group members' => [
'datagenerator' => 'group_member',
'required' => ['user', 'group'],
'switchids' => ['user' => 'userid', 'group' => 'groupid'],
],
'grouping groups' => [
'datagenerator' => 'grouping_group',
'required' => ['grouping', 'group'],
'switchids' => ['grouping' => 'groupingid', 'group' => 'groupid'],
],
'cohorts' => [
'datagenerator' => 'cohort',
'required' => ['idnumber'],
],
'cohort members' => [
'datagenerator' => 'cohort_member',
'required' => ['user', 'cohort'],
'switchids' => ['user' => 'userid', 'cohort' => 'cohortid'],
],
'roles' => [
'datagenerator' => 'role',
'required' => ['shortname'],
],
'grade categories' => [
'datagenerator' => 'grade_category',
'required' => ['fullname', 'course'],
'switchids' => ['course' => 'courseid', 'gradecategory' => 'parent'],
],
'grade items' => [
'datagenerator' => 'grade_item',
'required' => ['course'],
'switchids' => [
'scale' => 'scaleid',
'outcome' => 'outcomeid',
'course' => 'courseid',
'gradecategory' => 'categoryid',
],
],
'grade outcomes' => [
'datagenerator' => 'grade_outcome',
'required' => ['shortname', 'scale'],
'switchids' => ['course' => 'courseid', 'gradecategory' => 'categoryid', 'scale' => 'scaleid'],
],
'scales' => [
'datagenerator' => 'scale',
'required' => ['name', 'scale'],
'switchids' => ['course' => 'courseid'],
],
'question categories' => [
'datagenerator' => 'question_category',
'required' => ['name', 'contextlevel', 'reference'],
'switchids' => ['questioncategory' => 'parent'],
],
'questions' => [
'datagenerator' => 'question',
'required' => ['qtype', 'questioncategory', 'name'],
'switchids' => ['questioncategory' => 'category', 'user' => 'createdby'],
],
'tags' => [
'datagenerator' => 'tag',
'required' => ['name'],
],
'events' => [
'datagenerator' => 'event',
'required' => ['name', 'eventtype'],
'switchids' => [
'user' => 'userid',
'course' => 'courseid',
'category' => 'categoryid',
],
],
];
}
/**
* If password is not set it uses the username.
*
* @param array $data
* @return array
*/
protected function preprocess_user($data) {
if (!isset($data['password'])) {
$data['password'] = $data['username'];
}
return $data;
}
/**
* If contextlevel and reference are specified for cohort, transform them to the contextid.
*
* @param array $data
* @return array
*/
protected function preprocess_cohort($data) {
if (isset($data['contextlevel'])) {
if (!isset($data['reference'])) {
throw new Exception('If field contextlevel is specified, field reference must also be present');
}
$context = $this->get_context($data['contextlevel'], $data['reference']);
unset($data['contextlevel']);
unset($data['reference']);
$data['contextid'] = $context->id;
}
return $data;
}
/**
* Preprocesses the creation of a grade item. Converts gradetype text to a number.
*
* @param array $data
* @return array
*/
protected function preprocess_grade_item($data) {
global $CFG;
require_once("$CFG->libdir/grade/constants.php");
if (isset($data['gradetype'])) {
$data['gradetype'] = constant("GRADE_TYPE_" . strtoupper($data['gradetype']));
}
if (!empty($data['category']) && !empty($data['courseid'])) {
$cat = grade_category::fetch(array('fullname' => $data['category'], 'courseid' => $data['courseid']));
if (!$cat) {
throw new Exception('Could not resolve category with name "' . $data['category'] . '"');
}
unset($data['category']);
$data['categoryid'] = $cat->id;
}
return $data;
}
/**
* Adapter to modules generator.
*
* @throws Exception Custom exception for test writers
* @param array $data
* @return void
*/
protected function process_activity($data) {
global $DB, $CFG;
// The the_following_exists() method checks that the field exists.
$activityname = $data['activity'];
unset($data['activity']);
// Convert scale name into scale id (negative number indicates using scale).
if (isset($data['grade']) && strlen($data['grade']) && !is_number($data['grade'])) {
$data['grade'] = - $this->get_scale_id($data['grade']);
require_once("$CFG->libdir/grade/constants.php");
if (!isset($data['gradetype'])) {
$data['gradetype'] = GRADE_TYPE_SCALE;
}
}
// We split $data in the activity $record and the course module $options.
$cmoptions = array();
$cmcolumns = $DB->get_columns('course_modules');
foreach ($cmcolumns as $key => $value) {
if (isset($data[$key])) {
$cmoptions[$key] = $data[$key];
}
}
// Custom exception.
try {
$this->datagenerator->create_module($activityname, $data, $cmoptions);
} catch (coding_exception $e) {
throw new Exception('\'' . $activityname . '\' activity can not be added using this step,' .
' use the step \'I add a "ACTIVITY_OR_RESOURCE_NAME_STRING" to section "SECTION_NUMBER"\' instead');
}
}
/**
* Add a block to a page.
*
* @param array $data should mostly match the fields of the block_instances table.
* The block type is specified by blockname.
* The parentcontextid is set from contextlevel and reference.
* Missing values are filled in by testing_block_generator::prepare_record.
* $data is passed to create_block as both $record and $options. Normally
* the keys are different, so this is a way to let people set values in either place.
*/
protected function process_block_instance($data) {
if (empty($data['blockname'])) {
throw new Exception('\'blocks\' requires the field \'block\' type to be specified');
}
if (empty($data['contextlevel'])) {
throw new Exception('\'blocks\' requires the field \'contextlevel\' to be specified');
}
if (!isset($data['reference'])) {
throw new Exception('\'blocks\' requires the field \'reference\' to be specified');
}
$context = $this->get_context($data['contextlevel'], $data['reference']);
$data['parentcontextid'] = $context->id;
// Pass $data as both $record and $options. I think that is unlikely to
// cause problems since the relevant key names are different.
// $options is not used in most blocks I have seen, but where it is, it is necessary.
$this->datagenerator->create_block($data['blockname'], $data, $data);
}
/**
* Adapter to enrol_user() data generator.
*
* @throws Exception
* @param array $data
* @return void
*/
protected function process_enrol_user($data) {
global $SITE;
if (empty($data['roleid'])) {
throw new Exception('\'course enrolments\' requires the field \'role\' to be specified');
}
if (!isset($data['userid'])) {
throw new Exception('\'course enrolments\' requires the field \'user\' to be specified');
}
if (!isset($data['courseid'])) {
throw new Exception('\'course enrolments\' requires the field \'course\' to be specified');
}
if (!isset($data['enrol'])) {
$data['enrol'] = 'manual';
}
if (!isset($data['timestart'])) {
$data['timestart'] = 0;
}
if (!isset($data['timeend'])) {
$data['timeend'] = 0;
}
if (!isset($data['status'])) {
$data['status'] = null;
}
// If the provided course shortname is the site shortname we consider it a system role assign.
if ($data['courseid'] == $SITE->id) {
// Frontpage course assign.
$context = context_course::instance($data['courseid']);
role_assign($data['roleid'], $data['userid'], $context->id);
} else {
// Course assign.
$this->datagenerator->enrol_user($data['userid'], $data['courseid'], $data['roleid'], $data['enrol'],
$data['timestart'], $data['timeend'], $data['status']);
}
}
/**
* Allows/denies a capability at the specified context
*
* @throws Exception
* @param array $data
* @return void
*/
protected function process_permission_override($data) {
// Will throw an exception if it does not exist.
$context = $this->get_context($data['contextlevel'], $data['reference']);
switch ($data['permission']) {
case get_string('allow', 'role'):
$permission = CAP_ALLOW;
break;
case get_string('prevent', 'role'):
$permission = CAP_PREVENT;
break;
case get_string('prohibit', 'role'):
$permission = CAP_PROHIBIT;
break;
default:
throw new Exception('The \'' . $data['permission'] . '\' permission does not exist');
break;
}
if (is_null(get_capability_info($data['capability']))) {
throw new Exception('The \'' . $data['capability'] . '\' capability does not exist');
}
role_change_permission($data['roleid'], $context, $data['capability'], $permission);
}
/**
* Assigns a role to a user at system context
*
* Used by "system role assigns" can be deleted when
* system role assign will be deprecated in favour of
* "role assigns"
*
* @throws Exception
* @param array $data
* @return void
*/
protected function process_system_role_assign($data) {
if (empty($data['roleid'])) {
throw new Exception('\'system role assigns\' requires the field \'role\' to be specified');
}
if (!isset($data['userid'])) {
throw new Exception('\'system role assigns\' requires the field \'user\' to be specified');
}
$context = context_system::instance();
$this->datagenerator->role_assign($data['roleid'], $data['userid'], $context->id);
}
/**
* Assigns a role to a user at the specified context
*
* @throws Exception
* @param array $data
* @return void
*/
protected function process_role_assign($data) {
if (empty($data['roleid'])) {
throw new Exception('\'role assigns\' requires the field \'role\' to be specified');
}
if (!isset($data['userid'])) {
throw new Exception('\'role assigns\' requires the field \'user\' to be specified');
}
if (empty($data['contextlevel'])) {
throw new Exception('\'role assigns\' requires the field \'contextlevel\' to be specified');
}
if (!isset($data['reference'])) {
throw new Exception('\'role assigns\' requires the field \'reference\' to be specified');
}
// Getting the context id.
$context = $this->get_context($data['contextlevel'], $data['reference']);
$this->datagenerator->role_assign($data['roleid'], $data['userid'], $context->id);
}
/**
* Creates a role.
*
* @param array $data
* @return void
*/
protected function process_role($data) {
// We require the user to fill the role shortname.
if (empty($data['shortname'])) {
throw new Exception('\'role\' requires the field \'shortname\' to be specified');
}
$this->datagenerator->create_role($data);
}
/**
* Adds members to cohorts
*
* @param array $data
* @return void
*/
protected function process_cohort_member($data) {
cohort_add_member($data['cohortid'], $data['userid']);
}
/**
* Create a question category.
*
* @param array $data the row of data from the behat script.
*/
protected function process_question_category($data) {
global $DB;
$context = $this->get_context($data['contextlevel'], $data['reference']);
// The way this class works, we have already looked up the given parent category
// name and found a matching category. However, it is possible, particularly
// for the 'top' category, for there to be several categories with the
// same name. So far one will have been picked at random, but we need
// the one from the right context. So, if we have the wrong category, try again.
// (Just fixing it here, rather than getting it right first time, is a bit
// of a bodge, but in general this class assumes that names are unique,
// and normally they are, so this was the easiest fix.)
if (!empty($data['parent'])) {
$foundparent = $DB->get_record('question_categories', ['id' => $data['parent']], '*', MUST_EXIST);
if ($foundparent->contextid != $context->id) {
$rightparentid = $DB->get_field('question_categories', 'id',
['contextid' => $context->id, 'name' => $foundparent->name]);
if (!$rightparentid) {
throw new Exception('The specified question category with name "' . $foundparent->name .
'" does not exist in context "' . $context->get_context_name() . '"."');
}
$data['parent'] = $rightparentid;
}
}
$data['contextid'] = $context->id;
$this->datagenerator->get_plugin_generator('core_question')->create_question_category($data);
}
/**
* Create a question.
*
* Creating questions relies on the question/type/.../tests/helper.php mechanism.
* We start with test_question_maker::get_question_form_data($data['qtype'], $data['template'])
* and then overlay the values from any other fields of $data that are set.
*
* @param array $data the row of data from the behat script.
*/
protected function process_question($data) {
if (array_key_exists('questiontext', $data)) {
$data['questiontext'] = array(
'text' => $data['questiontext'],
'format' => FORMAT_HTML,
);
}
if (array_key_exists('generalfeedback', $data)) {
$data['generalfeedback'] = array(
'text' => $data['generalfeedback'],
'format' => FORMAT_HTML,
);
}
$which = null;
if (!empty($data['template'])) {
$which = $data['template'];
}
$this->datagenerator->get_plugin_generator('core_question')->create_question($data['qtype'], $which, $data);
}
}
+527
View File
@@ -0,0 +1,527 @@
<?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/>.
/**
* Base class for data generators component support for acceptance testing.
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
use Behat\Gherkin\Node\TableNode as TableNode;
use Behat\Behat\Tester\Exception\PendingException as PendingException;
/**
* Class to quickly create Behat test data using component data generators.
*
* There is a subclass of class for each component that wants to be able to
* generate entities using the Behat step
* Given the following "entity types" exist:
* | test | data |
*
* For core entities, the entity type is like "courses" or "users" and
* generating those is handled by behat_core_generator. For other components
* the entity type is like "mod_quiz > User override" and that is handled by
* behat_mod_quiz_generator defined in mod/quiz/tests/generator/behat_mod_quiz_generator.php.
*
* The types of entities that can be generated are described by the array returned
* by the {@link get_generateable_entities()} method. The list in
* {@link behat_core_generator} is a good (if complex) example.
*
* How things work is best explained with a few examples. All this is implemented
* in the {@link generate_items()} method below, if you want to see every detail of
* how it works.
*
* Simple example from behat_core_generator:
* 'users' => [
* 'datagenerator' => 'user',
* 'required' => ['username'],
* ],
* The steps performed are:
*
* 1. 'datagenerator' => 'user' means that the word used in the method names below is 'user'.
*
* 2. Because 'required' is present, check the supplied data exists 'username' column is present
* in the supplied data table and if not display an error.
*
* 3. Then for each row in the table as an array $elementdata (array keys are column names)
* and process it as follows
*
* 4. (Not used in this example.)
*
* 5. If the method 'preprocess_user' exists, then call it to update $elementdata.
* (It does, in this case it sets the password to the username, if password was not given.)
*
* We then do one of 4 things:
*
* 6a. If there is a method 'process_user' we call it. (It doesn't for user,
* but there are other examples like process_enrol_user() in behat_core_generator.)
*
* 6b. (Not used in this example.)
*
* 6c. Else, if testing_data_generator::create_user exists, we call it with $elementdata. (it does.)
*
* 6d. If none of these three things work. an error is thrown.
*
* To understand the missing steps above, consider the example from behat_mod_quiz_generator:
* 'group override' => [
* 'datagenerator' => 'override',
* 'required' => ['quiz', 'group'],
* 'switchids' => ['quiz' => 'quiz', 'group' => 'groupid'],
* ],
* Processing is as above, except that:
*
* 1. Note 'datagenerator' is 'override' (not group_override). 'user override' maps to the
* same datagenerator. This works fine.
*
* 4. Because 'switchids' is present, human-readable data in the table gets converted to ids.
* They array key 'group' refers to a column which may be present in the table (it will be
* here because it is required, but it does not have to be in general). If that column
* is present and contains a value, then the method matching name like get_group_id() is
* called with the value from that column in the data table. You must implement this
* method. You can see several examples of this sort of method below.
*
* If that method returns a group id, then $elementdata['group'] is unset and
* $elementdata['groupid'] is set to the result of the get_group_id() call. 'groupid' here
* because of the definition is 'switchids' => [..., 'group' => 'groupid'].
* If get_group_id() cannot find the group, it should throw a helpful exception.
*
* Similarly, 'quiz' (the quiz name) is looked up with a call to get_quiz_id(). Here, the
* new array key set matches the old one removed. This is fine.
*
* 6b. We are in a plugin, so before checking whether testing_data_generator::create_override
* exists we first check whether mod_quiz_generator::create_override() exists. It does,
* and this is what gets called.
*
* This second example shows why the get_..._id methods for core entities are in this base
* class, not in behat_core_generator. Plugins may need to look up the ids of
* core entities.
*
* behat_core_generator is defined in lib/behat/classes/behat_core_generator.php
* and for components, behat_..._generator is defined in tests/generator/behat_..._generator.php
* inside the plugin. For example behat_mod_quiz_generator is defined in
* mod/quiz/tests/generator/behat_mod_quiz_generator.php.
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class behat_generator_base {
/**
* @var string the name of the component we belong to.
*
* This should probably only be used to make error messages clearer.
*/
protected $component;
/**
* @var testing_data_generator the core data generator
*/
protected $datagenerator;
/**
* @var testing_data_generator the data generator for this component.
*/
protected $componentdatagenerator;
/**
* Constructor.
*
* @param string $component component name, to make error messages more readable.
*/
public function __construct(string $component) {
$this->component = $component;
}
/**
* Get a list of the entities that can be created for this component.
*
* This function must be overridden in subclasses. See class comment
* above for a description of the data structure.
* See {@link behat_core_generator} for an example.
*
* @return array entity name => information about how to generate.
*/
protected abstract function get_creatable_entities(): array;
/**
* Do the work to generate an entity.
*
* This is called by {@link behat_data_generators::the_following_entities_exist()}.
*
* @param string $generatortype The name of the entity to create.
* @param TableNode $data from the step.
*/
public function generate_items(string $generatortype, TableNode $data) {
// Now that we need them require the data generators.
require_once(__DIR__ . '/../../testing/generator/lib.php');
$elements = $this->get_creatable_entities();
if (!isset($elements[$generatortype])) {
throw new PendingException($this->name_for_errors($generatortype) .
' is not a known type of entity that can be generated.');
}
$entityinfo = $elements[$generatortype];
$this->datagenerator = testing_util::get_data_generator();
if ($this->component === 'core') {
$this->componentdatagenerator = $this->datagenerator;
} else {
$this->componentdatagenerator = $this->datagenerator->get_plugin_generator($this->component);
}
$generatortype = $entityinfo['datagenerator'];
foreach ($data->getHash() as $elementdata) {
// Check if all the required fields are there.
foreach ($entityinfo['required'] as $requiredfield) {
if (!isset($elementdata[$requiredfield])) {
throw new Exception($this->name_for_errors($generatortype) .
' requires the field ' . $requiredfield . ' to be specified');
}
}
// Switch from human-friendly references to ids.
if (!empty($entityinfo['switchids'])) {
foreach ($entityinfo['switchids'] as $element => $field) {
$methodname = 'get_' . $element . '_id';
// Not all the switch fields are required, default vars will be assigned by data generators.
if (isset($elementdata[$element])) {
if (!method_exists($this, $methodname)) {
throw new coding_exception('The generator for ' .
$this->name_for_errors($generatortype) .
' entities specifies \'switchids\' => [..., \'' . $element .
'\' => \'' . $field . '\', ...] but the required method ' .
$methodname . '() has not been defined in ' .
get_class($this) . '.');
}
// Temp $id var to avoid problems when $element == $field.
$id = $this->{$methodname}($elementdata[$element]);
unset($elementdata[$element]);
$elementdata[$field] = $id;
}
}
}
// Preprocess the entities that requires a special treatment.
if (method_exists($this, 'preprocess_' . $generatortype)) {
$elementdata = $this->{'preprocess_' . $generatortype}($elementdata);
}
// Creates element.
if (method_exists($this, 'process_' . $generatortype)) {
// Use a method on this class to do the work.
$this->{'process_' . $generatortype}($elementdata);
} else if (method_exists($this->componentdatagenerator, 'create_' . $generatortype)) {
// Using the component't own data generator if it exists.
$this->componentdatagenerator->{'create_' . $generatortype}($elementdata);
} else if (method_exists($this->datagenerator, 'create_' . $generatortype)) {
// Use a method on the core data geneator, if there is one.
$this->datagenerator->{'create_' . $generatortype}($elementdata);
} else {
// Give up.
throw new PendingException($this->name_for_errors($generatortype) .
' data generator is not implemented');
}
}
}
/**
* Helper for formatting error messages.
*
* @param string $entitytype entity type without prefix, e.g. 'frog'.
* @return string either 'frog' for core entities, or 'mod_mymod > frog' for components.
*/
protected function name_for_errors(string $entitytype): string {
if ($this->component === 'core') {
return '"' . $entitytype . '"';
} else {
return '"' . $this->component . ' > ' . $entitytype . '"';
}
}
/**
* Gets the grade category id from the grade category fullname
*
* @param string $fullname the grade category name.
* @return int corresponding id.
*/
protected function get_gradecategory_id($fullname) {
global $DB;
if (!$id = $DB->get_field('grade_categories', 'id', array('fullname' => $fullname))) {
throw new Exception('The specified grade category with fullname "' . $fullname . '" does not exist');
}
return $id;
}
/**
* Gets the user id from it's username.
* @throws Exception
* @param string $username
* @return int
*/
protected function get_user_id($username) {
global $DB;
if (!$id = $DB->get_field('user', 'id', array('username' => $username))) {
throw new Exception('The specified user with username "' . $username . '" does not exist');
}
return $id;
}
/**
* Gets the role id from it's shortname.
* @throws Exception
* @param string $roleshortname
* @return int
*/
protected function get_role_id($roleshortname) {
global $DB;
if (!$id = $DB->get_field('role', 'id', array('shortname' => $roleshortname))) {
throw new Exception('The specified role with shortname "' . $roleshortname . '" does not exist');
}
return $id;
}
/**
* Gets the category id from it's idnumber.
* @throws Exception
* @param string $idnumber
* @return int
*/
protected function get_category_id($idnumber) {
global $DB;
// If no category was specified use the data generator one.
if ($idnumber == false) {
return null;
}
if (!$id = $DB->get_field('course_categories', 'id', array('idnumber' => $idnumber))) {
throw new Exception('The specified category with idnumber "' . $idnumber . '" does not exist');
}
return $id;
}
/**
* Gets the course id from it's shortname.
* @throws Exception
* @param string $shortname
* @return int
*/
protected function get_course_id($shortname) {
global $DB;
if (!$id = $DB->get_field('course', 'id', array('shortname' => $shortname))) {
throw new Exception('The specified course with shortname "' . $shortname . '" does not exist');
}
return $id;
}
/**
* Gets the group id from it's idnumber.
* @throws Exception
* @param string $idnumber
* @return int
*/
protected function get_group_id($idnumber) {
global $DB;
if (!$id = $DB->get_field('groups', 'id', array('idnumber' => $idnumber))) {
throw new Exception('The specified group with idnumber "' . $idnumber . '" does not exist');
}
return $id;
}
/**
* Gets the grouping id from it's idnumber.
* @throws Exception
* @param string $idnumber
* @return int
*/
protected function get_grouping_id($idnumber) {
global $DB;
// Do not fetch grouping ID for empty grouping idnumber.
if (empty($idnumber)) {
return null;
}
if (!$id = $DB->get_field('groupings', 'id', array('idnumber' => $idnumber))) {
throw new Exception('The specified grouping with idnumber "' . $idnumber . '" does not exist');
}
return $id;
}
/**
* Gets the cohort id from it's idnumber.
* @throws Exception
* @param string $idnumber
* @return int
*/
protected function get_cohort_id($idnumber) {
global $DB;
if (!$id = $DB->get_field('cohort', 'id', array('idnumber' => $idnumber))) {
throw new Exception('The specified cohort with idnumber "' . $idnumber . '" does not exist');
}
return $id;
}
/**
* Gets the outcome item id from its shortname.
* @throws Exception
* @param string $shortname
* @return int
*/
protected function get_outcome_id($shortname) {
global $DB;
if (!$id = $DB->get_field('grade_outcomes', 'id', array('shortname' => $shortname))) {
throw new Exception('The specified outcome with shortname "' . $shortname . '" does not exist');
}
return $id;
}
/**
* Get the id of a named scale.
* @param string $name the name of the scale.
* @return int the scale id.
*/
protected function get_scale_id($name) {
global $DB;
if (!$id = $DB->get_field('scale', 'id', array('name' => $name))) {
throw new Exception('The specified scale with name "' . $name . '" does not exist');
}
return $id;
}
/**
* Get the id of a named question category (must be globally unique).
* Note that 'Top' is a special value, used when setting the parent of another
* category, meaning top-level.
*
* @param string $name the question category name.
* @return int the question category id.
*/
protected function get_questioncategory_id($name) {
global $DB;
if ($name == 'Top') {
return 0;
}
if (!$id = $DB->get_field('question_categories', 'id', array('name' => $name))) {
throw new Exception('The specified question category with name "' . $name . '" does not exist');
}
return $id;
}
/**
* Gets the internal context id from the context reference.
*
* The context reference changes depending on the context
* level, it can be the system, a user, a category, a course or
* a module.
*
* @throws Exception
* @param string $levelname The context level string introduced by the test writer
* @param string $contextref The context reference introduced by the test writer
* @return context
*/
protected function get_context($levelname, $contextref) {
global $DB;
// Getting context levels and names (we will be using the English ones as it is the test site language).
$contextlevels = context_helper::get_all_levels();
$contextnames = array();
foreach ($contextlevels as $level => $classname) {
$contextnames[context_helper::get_level_name($level)] = $level;
}
if (empty($contextnames[$levelname])) {
throw new Exception('The specified "' . $levelname . '" context level does not exist');
}
$contextlevel = $contextnames[$levelname];
// Return it, we don't need to look for other internal ids.
if ($contextlevel == CONTEXT_SYSTEM) {
return context_system::instance();
}
switch ($contextlevel) {
case CONTEXT_USER:
$instanceid = $DB->get_field('user', 'id', array('username' => $contextref));
break;
case CONTEXT_COURSECAT:
$instanceid = $DB->get_field('course_categories', 'id', array('idnumber' => $contextref));
break;
case CONTEXT_COURSE:
$instanceid = $DB->get_field('course', 'id', array('shortname' => $contextref));
break;
case CONTEXT_MODULE:
$instanceid = $DB->get_field('course_modules', 'id', array('idnumber' => $contextref));
break;
default:
break;
}
$contextclass = $contextlevels[$contextlevel];
if (!$context = $contextclass::instance($instanceid, IGNORE_MISSING)) {
throw new Exception('The specified "' . $contextref . '" context reference does not exist');
}
return $context;
}
/**
* Gets the contact id from it's username.
* @throws Exception
* @param string $username
* @return int
*/
protected function get_contact_id($username) {
global $DB;
if (!$id = $DB->get_field('user', 'id', array('username' => $username))) {
throw new Exception('The specified user with username "' . $username . '" does not exist');
}
return $id;
}
}
@@ -86,6 +86,7 @@ class behat_form_autocomplete extends behat_form_text {
$this->wait_for_pending_js();
$this->key_press(27);
$this->wait_for_pending_js();
}
}
}
+1 -1
View File
@@ -27,4 +27,4 @@ unzip v1.1.3.zip
mv GeoIP2-php-2.6.0/src/ /path/to/moodle/lib/maxmind/GeoIp2/
mv MaxMind-DB-Reader-php-1.1.3/src/MaxMind/ /path/to/moodle/lib/maxmind/MaxMind/
4) Run unit tests on iplookup/tests/geoip_test.php with PHPUNIT_LONGTEST defined.
4) Run unit tests on iplookup/tests/geoip_test.php.
+80 -797
View File
@@ -33,16 +33,23 @@ use Behat\Behat\Tester\Exception\PendingException as PendingException;
/**
* Class to set up quickly a Given environment.
*
* Acceptance tests are block-boxed, so this steps definitions should only
* be used to set up the test environment as we are not replicating user steps.
* The entry point is the Behat steps:
* the following "entity types" exist:
* | test | data |
*
* All data generators should be in lib/testing/generator/*, shared between phpunit
* and behat and they should be called from here, if possible using the standard
* 'create_$elementname($options)' and if it's not possible (data generators arguments will not be
* always the same) or the element is not suitable to be a data generator, create a
* 'process_$elementname($options)' method and use the data generator from there if possible.
* Entity type will either look like "users" or "activities" for core entities, or
* "mod_forum > subscription" or "core_message > message" for entities belonging
* to components.
*
* Generally, you only need to specify properties relevant to your test,
* and everything else gets set to sensible defaults.
*
* The actual generation of entities is done by {@link behat_generator_base}.
* There is one subclass for each component, e.g. {@link behat_core_generator}
* or {@link behat_mod_quiz_generator}. To see the types of entity
* that can be created for each component, look at the arrays returned
* by the get_creatable_entities() method in each class.
*
* @todo If the available elements list grows too much this class must be split into smaller pieces
* @package core
* @category test
* @copyright 2012 David Monllaó
@@ -51,822 +58,98 @@ use Behat\Behat\Tester\Exception\PendingException as PendingException;
class behat_data_generators extends behat_base {
/**
* @var testing_data_generator
* Convert legacy entity names to the new component-specific form.
*
* In the past, there was no support for plugins, and everything that
* could be created was handled by the core generator. Now, we can
* support plugins, and so some thing should probably be moved.
*
* For example, in the future we should probably add
* 'message contacts' => 'core_message > contact'] to
* this array, and move generation of message contact
* from core to core_message.
*
* @var array old entity type => new entity type.
*/
protected $datagenerator;
/**
* Each element specifies:
* - The data generator sufix used.
* - The required fields.
* - The mapping between other elements references and database field names.
* @var array
*/
protected static $elements = array(
'users' => array(
'datagenerator' => 'user',
'required' => array('username')
),
'categories' => array(
'datagenerator' => 'category',
'required' => array('idnumber'),
'switchids' => array('category' => 'parent')
),
'courses' => array(
'datagenerator' => 'course',
'required' => array('shortname'),
'switchids' => array('category' => 'category')
),
'groups' => array(
'datagenerator' => 'group',
'required' => array('idnumber', 'course'),
'switchids' => array('course' => 'courseid')
),
'groupings' => array(
'datagenerator' => 'grouping',
'required' => array('idnumber', 'course'),
'switchids' => array('course' => 'courseid')
),
'course enrolments' => array(
'datagenerator' => 'enrol_user',
'required' => array('user', 'course', 'role'),
'switchids' => array('user' => 'userid', 'course' => 'courseid', 'role' => 'roleid')
),
'permission overrides' => array(
'datagenerator' => 'permission_override',
'required' => array('capability', 'permission', 'role', 'contextlevel', 'reference'),
'switchids' => array('role' => 'roleid')
),
'system role assigns' => array(
'datagenerator' => 'system_role_assign',
'required' => array('user', 'role'),
'switchids' => array('user' => 'userid', 'role' => 'roleid')
),
'role assigns' => array(
'datagenerator' => 'role_assign',
'required' => array('user', 'role', 'contextlevel', 'reference'),
'switchids' => array('user' => 'userid', 'role' => 'roleid')
),
'activities' => array(
'datagenerator' => 'activity',
'required' => array('activity', 'idnumber', 'course'),
'switchids' => array('course' => 'course', 'gradecategory' => 'gradecat', 'grouping' => 'groupingid')
),
'blocks' => array(
'datagenerator' => 'block_instance',
'required' => array('blockname', 'contextlevel', 'reference'),
),
'group members' => array(
'datagenerator' => 'group_member',
'required' => array('user', 'group'),
'switchids' => array('user' => 'userid', 'group' => 'groupid')
),
'grouping groups' => array(
'datagenerator' => 'grouping_group',
'required' => array('grouping', 'group'),
'switchids' => array('grouping' => 'groupingid', 'group' => 'groupid')
),
'cohorts' => array(
'datagenerator' => 'cohort',
'required' => array('idnumber')
),
'cohort members' => array(
'datagenerator' => 'cohort_member',
'required' => array('user', 'cohort'),
'switchids' => array('user' => 'userid', 'cohort' => 'cohortid')
),
'roles' => array(
'datagenerator' => 'role',
'required' => array('shortname')
),
'grade categories' => array(
'datagenerator' => 'grade_category',
'required' => array('fullname', 'course'),
'switchids' => array('course' => 'courseid', 'gradecategory' => 'parent')
),
'grade items' => array(
'datagenerator' => 'grade_item',
'required' => array('course'),
'switchids' => array('scale' => 'scaleid', 'outcome' => 'outcomeid', 'course' => 'courseid',
'gradecategory' => 'categoryid')
),
'grade outcomes' => array(
'datagenerator' => 'grade_outcome',
'required' => array('shortname', 'scale'),
'switchids' => array('course' => 'courseid', 'gradecategory' => 'categoryid', 'scale' => 'scaleid')
),
'scales' => array(
'datagenerator' => 'scale',
'required' => array('name', 'scale'),
'switchids' => array('course' => 'courseid')
),
'question categories' => array(
'datagenerator' => 'question_category',
'required' => array('name', 'contextlevel', 'reference'),
'switchids' => array('questioncategory' => 'parent')
),
'questions' => array(
'datagenerator' => 'question',
'required' => array('qtype', 'questioncategory', 'name'),
'switchids' => array('questioncategory' => 'category', 'user' => 'createdby')
),
'tags' => array(
'datagenerator' => 'tag',
'required' => array('name')
),
'events' => array(
'datagenerator' => 'event',
'required' => array('name', 'eventtype'),
'switchids' => array(
'user' => 'userid',
'course' => 'courseid',
'category' => 'categoryid',
)
),
);
protected $movedentitytypes = [
];
/**
* Creates the specified element.
*
* The most reliable list of what types of thing can be created is the
* $elements array defined above.
* See the class comment for an overview.
*
* @Given /^the following "(?P<element_string>(?:[^"]|\\")*)" exist:$/
*
* @throws Exception
* @throws PendingException
* @param string $elementname The name of the entity to add
* @param string $entitytype The name of the type entity to add
* @param TableNode $data
*/
public function the_following_exist($elementname, TableNode $data) {
// Now that we need them require the data generators.
require_once(__DIR__ . '/../../testing/generator/lib.php');
if (empty(self::$elements[$elementname])) {
throw new PendingException($elementname . ' data generator is not implemented');
public function the_following_entities_exist($entitytype, TableNode $data) {
if (isset($this->movedentitytypes[$entitytype])) {
$entitytype = $this->movedentitytypes[$entitytype];
}
$this->datagenerator = testing_util::get_data_generator();
$elementdatagenerator = self::$elements[$elementname]['datagenerator'];
$requiredfields = self::$elements[$elementname]['required'];
if (!empty(self::$elements[$elementname]['switchids'])) {
$switchids = self::$elements[$elementname]['switchids'];
}
foreach ($data->getHash() as $elementdata) {
// Check if all the required fields are there.
foreach ($requiredfields as $requiredfield) {
if (!isset($elementdata[$requiredfield])) {
throw new Exception($elementname . ' requires the field ' . $requiredfield . ' to be specified');
}
}
// Switch from human-friendly references to ids.
if (isset($switchids)) {
foreach ($switchids as $element => $field) {
$methodname = 'get_' . $element . '_id';
// Not all the switch fields are required, default vars will be assigned by data generators.
if (isset($elementdata[$element])) {
// Temp $id var to avoid problems when $element == $field.
$id = $this->{$methodname}($elementdata[$element]);
unset($elementdata[$element]);
$elementdata[$field] = $id;
}
}
}
// Preprocess the entities that requires a special treatment.
if (method_exists($this, 'preprocess_' . $elementdatagenerator)) {
$elementdata = $this->{'preprocess_' . $elementdatagenerator}($elementdata);
}
// Creates element.
$methodname = 'create_' . $elementdatagenerator;
if (method_exists($this->datagenerator, $methodname)) {
// Using data generators directly.
$this->datagenerator->{$methodname}($elementdata);
} else if (method_exists($this, 'process_' . $elementdatagenerator)) {
// Using an alternative to the direct data generator call.
$this->{'process_' . $elementdatagenerator}($elementdata);
} else {
throw new PendingException($elementname . ' data generator is not implemented');
}
}
list($component, $entity) = $this->parse_entity_type($entitytype);
$this->get_instance_for_component($component)->generate_items($entity, $data);
}
/**
* If password is not set it uses the username.
* @param array $data
* @return array
*/
protected function preprocess_user($data) {
if (!isset($data['password'])) {
$data['password'] = $data['username'];
}
return $data;
}
/**
* If contextlevel and reference are specified for cohort, transform them to the contextid.
* Parse a full entity type like 'users' or 'mod_forum > subscription'.
*
* @param array $data
* @return array
*/
protected function preprocess_cohort($data) {
if (isset($data['contextlevel'])) {
if (!isset($data['reference'])) {
throw new Exception('If field contextlevel is specified, field reference must also be present');
}
$context = $this->get_context($data['contextlevel'], $data['reference']);
unset($data['contextlevel']);
unset($data['reference']);
$data['contextid'] = $context->id;
}
return $data;
}
/**
* Preprocesses the creation of a grade item. Converts gradetype text to a number.
* @param array $data
* @return array
*/
protected function preprocess_grade_item($data) {
global $CFG;
require_once("$CFG->libdir/grade/constants.php");
if (isset($data['gradetype'])) {
$data['gradetype'] = constant("GRADE_TYPE_" . strtoupper($data['gradetype']));
}
if (!empty($data['category']) && !empty($data['courseid'])) {
$cat = grade_category::fetch(array('fullname' => $data['category'], 'courseid' => $data['courseid']));
if (!$cat) {
throw new Exception('Could not resolve category with name "' . $data['category'] . '"');
}
unset($data['category']);
$data['categoryid'] = $cat->id;
}
return $data;
}
/**
* Adapter to modules generator
* @throws Exception Custom exception for test writers
* @param array $data
* @return void
*/
protected function process_activity($data) {
global $DB, $CFG;
// The the_following_exists() method checks that the field exists.
$activityname = $data['activity'];
unset($data['activity']);
// Convert scale name into scale id (negative number indicates using scale).
if (isset($data['grade']) && strlen($data['grade']) && !is_number($data['grade'])) {
$data['grade'] = - $this->get_scale_id($data['grade']);
require_once("$CFG->libdir/grade/constants.php");
if (!isset($data['gradetype'])) {
$data['gradetype'] = GRADE_TYPE_SCALE;
}
}
// We split $data in the activity $record and the course module $options.
$cmoptions = array();
$cmcolumns = $DB->get_columns('course_modules');
foreach ($cmcolumns as $key => $value) {
if (isset($data[$key])) {
$cmoptions[$key] = $data[$key];
}
}
// Custom exception.
try {
$this->datagenerator->create_module($activityname, $data, $cmoptions);
} catch (coding_exception $e) {
throw new Exception('\'' . $activityname . '\' activity can not be added using this step,' .
' use the step \'I add a "ACTIVITY_OR_RESOURCE_NAME_STRING" to section "SECTION_NUMBER"\' instead');
}
}
/**
* Add a block to a page.
* E.g. parsing 'course' gives ['core', 'course'] and
* parsing 'core_message > message' gives ['core_message', 'message'].
*
* @param array $data should mostly match the fields of the block_instances table.
* The block type is specified by blockname.
* The parentcontextid is set from contextlevel and reference.
* Missing values are filled in by testing_block_generator::prepare_record.
* $data is passed to create_block as both $record and $options. Normally
* the keys are different, so this is a way to let people set values in either place.
* @param string $entitytype the entity type
* @return string[] with two elements, component and entity type.
*/
protected function process_block_instance($data) {
if (empty($data['blockname'])) {
throw new Exception('\'blocks\' requires the field \'block\' type to be specified');
}
if (empty($data['contextlevel'])) {
throw new Exception('\'blocks\' requires the field \'contextlevel\' to be specified');
}
if (!isset($data['reference'])) {
throw new Exception('\'blocks\' requires the field \'reference\' to be specified');
}
$context = $this->get_context($data['contextlevel'], $data['reference']);
$data['parentcontextid'] = $context->id;
// Pass $data as both $record and $options. I think that is unlikely to
// cause problems since the relevant key names are different.
// $options is not used in most blocks I have seen, but where it is, it is necessary.
$this->datagenerator->create_block($data['blockname'], $data, $data);
}
/**
* Adapter to enrol_user() data generator.
* @throws Exception
* @param array $data
* @return void
*/
protected function process_enrol_user($data) {
global $SITE;
if (empty($data['roleid'])) {
throw new Exception('\'course enrolments\' requires the field \'role\' to be specified');
}
if (!isset($data['userid'])) {
throw new Exception('\'course enrolments\' requires the field \'user\' to be specified');
}
if (!isset($data['courseid'])) {
throw new Exception('\'course enrolments\' requires the field \'course\' to be specified');
}
if (!isset($data['enrol'])) {
$data['enrol'] = 'manual';
}
if (!isset($data['timestart'])) {
$data['timestart'] = 0;
}
if (!isset($data['timeend'])) {
$data['timeend'] = 0;
}
if (!isset($data['status'])) {
$data['status'] = null;
}
// If the provided course shortname is the site shortname we consider it a system role assign.
if ($data['courseid'] == $SITE->id) {
// Frontpage course assign.
$context = context_course::instance($data['courseid']);
role_assign($data['roleid'], $data['userid'], $context->id);
protected function parse_entity_type(string $entitytype): array {
$dividercount = substr_count($entitytype, ' > ');
if ($dividercount === 0) {
return ['core', $entitytype];
} else if ($dividercount === 1) {
list($component, $type) = explode(' > ', $entitytype);
if ($component === 'core') {
throw new coding_exception('Do not specify the component "core > ..." for entity types.');
}
return [$component, $type];
} else {
// Course assign.
$this->datagenerator->enrol_user($data['userid'], $data['courseid'], $data['roleid'], $data['enrol'],
$data['timestart'], $data['timeend'], $data['status']);
throw new coding_exception('The entity type must be in the form ' .
'"{entity-type}" for core entities, or "{component} > {entity-type}" ' .
'for entities belonging to other components. ' .
'For example "users" or "mod_forum > subscriptions".');
}
}
/**
* Allows/denies a capability at the specified context
* Get an instance of the appropriate subclass of this class for a given component.
*
* @throws Exception
* @param array $data
* @return void
* @param string $component The name of the component to generate entities for.
* @return behat_generator_base the subclass of this class for the requested component.
*/
protected function process_permission_override($data) {
protected function get_instance_for_component(string $component): behat_generator_base {
global $CFG;
// Will throw an exception if it does not exist.
$context = $this->get_context($data['contextlevel'], $data['reference']);
switch ($data['permission']) {
case get_string('allow', 'role'):
$permission = CAP_ALLOW;
break;
case get_string('prevent', 'role'):
$permission = CAP_PREVENT;
break;
case get_string('prohibit', 'role'):
$permission = CAP_PROHIBIT;
break;
default:
throw new Exception('The \'' . $data['permission'] . '\' permission does not exist');
break;
}
if (is_null(get_capability_info($data['capability']))) {
throw new Exception('The \'' . $data['capability'] . '\' capability does not exist');
}
role_change_permission($data['roleid'], $context, $data['capability'], $permission);
}
/**
* Assigns a role to a user at system context
*
* Used by "system role assigns" can be deleted when
* system role assign will be deprecated in favour of
* "role assigns"
*
* @throws Exception
* @param array $data
* @return void
*/
protected function process_system_role_assign($data) {
if (empty($data['roleid'])) {
throw new Exception('\'system role assigns\' requires the field \'role\' to be specified');
}
if (!isset($data['userid'])) {
throw new Exception('\'system role assigns\' requires the field \'user\' to be specified');
}
$context = context_system::instance();
$this->datagenerator->role_assign($data['roleid'], $data['userid'], $context->id);
}
/**
* Assigns a role to a user at the specified context
*
* @throws Exception
* @param array $data
* @return void
*/
protected function process_role_assign($data) {
if (empty($data['roleid'])) {
throw new Exception('\'role assigns\' requires the field \'role\' to be specified');
}
if (!isset($data['userid'])) {
throw new Exception('\'role assigns\' requires the field \'user\' to be specified');
}
if (empty($data['contextlevel'])) {
throw new Exception('\'role assigns\' requires the field \'contextlevel\' to be specified');
}
if (!isset($data['reference'])) {
throw new Exception('\'role assigns\' requires the field \'reference\' to be specified');
}
// Getting the context id.
$context = $this->get_context($data['contextlevel'], $data['reference']);
$this->datagenerator->role_assign($data['roleid'], $data['userid'], $context->id);
}
/**
* Creates a role.
*
* @param array $data
* @return void
*/
protected function process_role($data) {
// We require the user to fill the role shortname.
if (empty($data['shortname'])) {
throw new Exception('\'role\' requires the field \'shortname\' to be specified');
}
$this->datagenerator->create_role($data);
}
/**
* Adds members to cohorts
*
* @param array $data
* @return void
*/
protected function process_cohort_member($data) {
cohort_add_member($data['cohortid'], $data['userid']);
}
/**
* Create a question category.
*
* @param array $data the row of data from the behat script.
*/
protected function process_question_category($data) {
global $DB;
$context = $this->get_context($data['contextlevel'], $data['reference']);
// The way this class works, we have already looked up the given parent category
// name and found a matching category. However, it is possible, particularly
// for the 'top' category, for there to be several categories with the
// same name. So far one will have been picked at random, but we need
// the one from the right context. So, if we have the wrong category, try again.
// (Just fixing it here, rather than getting it right first time, is a bit
// of a bodge, but in general this class assumes that names are unique,
// and normally they are, so this was the easiest fix.)
if (!empty($data['parent'])) {
$foundparent = $DB->get_record('question_categories', ['id' => $data['parent']], '*', MUST_EXIST);
if ($foundparent->contextid != $context->id) {
$rightparentid = $DB->get_field('question_categories', 'id',
['contextid' => $context->id, 'name' => $foundparent->name]);
if (!$rightparentid) {
throw new Exception('The specified question category with name "' . $foundparent->name .
'" does not exist in context "' . $context->get_context_name() . '"."');
}
$data['parent'] = $rightparentid;
// Ensure the generator class is loaded.
require_once($CFG->libdir . '/behat/classes/behat_generator_base.php');
if ($component === 'core') {
$lib = $CFG->libdir . '/behat/classes/behat_core_generator.php';
} else {
$dir = core_component::get_component_directory($component);
$lib = $dir . '/tests/generator/behat_' . $component . '_generator.php';
if (!$dir || !is_readable($lib)) {
throw new coding_exception("Component {$component} does not support " .
"behat generators yet. Missing {$lib}.");
}
}
require_once($lib);
$data['contextid'] = $context->id;
$this->datagenerator->get_plugin_generator('core_question')->create_question_category($data);
// Create an instance.
$componentclass = "behat_{$component}_generator";
if (!class_exists($componentclass)) {
throw new PendingException($component .
' does not yet support the Behat data generator mechanism. Class ' .
$componentclass . ' not found in file ' . $lib . '.');
}
$instance = new $componentclass($component);
return $instance;
}
/**
* Create a question.
*
* Creating questions relies on the question/type/.../tests/helper.php mechanism.
* We start with test_question_maker::get_question_form_data($data['qtype'], $data['template'])
* and then overlay the values from any other fields of $data that are set.
*
* @param array $data the row of data from the behat script.
*/
protected function process_question($data) {
if (array_key_exists('questiontext', $data)) {
$data['questiontext'] = array(
'text' => $data['questiontext'],
'format' => FORMAT_HTML,
);
}
if (array_key_exists('generalfeedback', $data)) {
$data['generalfeedback'] = array(
'text' => $data['generalfeedback'],
'format' => FORMAT_HTML,
);
}
$which = null;
if (!empty($data['template'])) {
$which = $data['template'];
}
$this->datagenerator->get_plugin_generator('core_question')->create_question($data['qtype'], $which, $data);
}
/**
* Gets the grade category id from the grade category fullname
* @throws Exception
* @param string $username
* @return int
*/
protected function get_gradecategory_id($fullname) {
global $DB;
if (!$id = $DB->get_field('grade_categories', 'id', array('fullname' => $fullname))) {
throw new Exception('The specified grade category with fullname "' . $fullname . '" does not exist');
}
return $id;
}
/**
* Gets the user id from it's username.
* @throws Exception
* @param string $username
* @return int
*/
protected function get_user_id($username) {
global $DB;
if (!$id = $DB->get_field('user', 'id', array('username' => $username))) {
throw new Exception('The specified user with username "' . $username . '" does not exist');
}
return $id;
}
/**
* Gets the role id from it's shortname.
* @throws Exception
* @param string $roleshortname
* @return int
*/
protected function get_role_id($roleshortname) {
global $DB;
if (!$id = $DB->get_field('role', 'id', array('shortname' => $roleshortname))) {
throw new Exception('The specified role with shortname "' . $roleshortname . '" does not exist');
}
return $id;
}
/**
* Gets the category id from it's idnumber.
* @throws Exception
* @param string $idnumber
* @return int
*/
protected function get_category_id($idnumber) {
global $DB;
// If no category was specified use the data generator one.
if ($idnumber == false) {
return null;
}
if (!$id = $DB->get_field('course_categories', 'id', array('idnumber' => $idnumber))) {
throw new Exception('The specified category with idnumber "' . $idnumber . '" does not exist');
}
return $id;
}
/**
* Gets the course id from it's shortname.
* @throws Exception
* @param string $shortname
* @return int
*/
protected function get_course_id($shortname) {
global $DB;
if (!$id = $DB->get_field('course', 'id', array('shortname' => $shortname))) {
throw new Exception('The specified course with shortname "' . $shortname . '" does not exist');
}
return $id;
}
/**
* Gets the group id from it's idnumber.
* @throws Exception
* @param string $idnumber
* @return int
*/
protected function get_group_id($idnumber) {
global $DB;
if (!$id = $DB->get_field('groups', 'id', array('idnumber' => $idnumber))) {
throw new Exception('The specified group with idnumber "' . $idnumber . '" does not exist');
}
return $id;
}
/**
* Gets the grouping id from it's idnumber.
* @throws Exception
* @param string $idnumber
* @return int
*/
protected function get_grouping_id($idnumber) {
global $DB;
// Do not fetch grouping ID for empty grouping idnumber.
if (empty($idnumber)) {
return null;
}
if (!$id = $DB->get_field('groupings', 'id', array('idnumber' => $idnumber))) {
throw new Exception('The specified grouping with idnumber "' . $idnumber . '" does not exist');
}
return $id;
}
/**
* Gets the cohort id from it's idnumber.
* @throws Exception
* @param string $idnumber
* @return int
*/
protected function get_cohort_id($idnumber) {
global $DB;
if (!$id = $DB->get_field('cohort', 'id', array('idnumber' => $idnumber))) {
throw new Exception('The specified cohort with idnumber "' . $idnumber . '" does not exist');
}
return $id;
}
/**
* Gets the outcome item id from its shortname.
* @throws Exception
* @param string $shortname
* @return int
*/
protected function get_outcome_id($shortname) {
global $DB;
if (!$id = $DB->get_field('grade_outcomes', 'id', array('shortname' => $shortname))) {
throw new Exception('The specified outcome with shortname "' . $shortname . '" does not exist');
}
return $id;
}
/**
* Get the id of a named scale.
* @param string $name the name of the scale.
* @return int the scale id.
*/
protected function get_scale_id($name) {
global $DB;
if (!$id = $DB->get_field('scale', 'id', array('name' => $name))) {
throw new Exception('The specified scale with name "' . $name . '" does not exist');
}
return $id;
}
/**
* Get the id of a named question category (must be globally unique).
* Note that 'Top' is a special value, used when setting the parent of another
* category, meaning top-level.
*
* @param string $name the question category name.
* @return int the question category id.
*/
protected function get_questioncategory_id($name) {
global $DB;
if ($name == 'Top') {
return 0;
}
if (!$id = $DB->get_field('question_categories', 'id', array('name' => $name))) {
throw new Exception('The specified question category with name "' . $name . '" does not exist');
}
return $id;
}
/**
* Gets the internal context id from the context reference.
*
* The context reference changes depending on the context
* level, it can be the system, a user, a category, a course or
* a module.
*
* @throws Exception
* @param string $levelname The context level string introduced by the test writer
* @param string $contextref The context reference introduced by the test writer
* @return context
*/
protected function get_context($levelname, $contextref) {
global $DB;
// Getting context levels and names (we will be using the English ones as it is the test site language).
$contextlevels = context_helper::get_all_levels();
$contextnames = array();
foreach ($contextlevels as $level => $classname) {
$contextnames[context_helper::get_level_name($level)] = $level;
}
if (empty($contextnames[$levelname])) {
throw new Exception('The specified "' . $levelname . '" context level does not exist');
}
$contextlevel = $contextnames[$levelname];
// Return it, we don't need to look for other internal ids.
if ($contextlevel == CONTEXT_SYSTEM) {
return context_system::instance();
}
switch ($contextlevel) {
case CONTEXT_USER:
$instanceid = $DB->get_field('user', 'id', array('username' => $contextref));
break;
case CONTEXT_COURSECAT:
$instanceid = $DB->get_field('course_categories', 'id', array('idnumber' => $contextref));
break;
case CONTEXT_COURSE:
$instanceid = $DB->get_field('course', 'id', array('shortname' => $contextref));
break;
case CONTEXT_MODULE:
$instanceid = $DB->get_field('course_modules', 'id', array('idnumber' => $contextref));
break;
default:
break;
}
$contextclass = $contextlevels[$contextlevel];
if (!$context = $contextclass::instance($instanceid, IGNORE_MISSING)) {
throw new Exception('The specified "' . $contextref . '" context reference does not exist');
}
return $context;
}
}
@@ -142,11 +142,11 @@ Feature: Assign group override
| id_cutoffdate_enabled | 1 |
| cutoffdate[day] | 1 |
| cutoffdate[month] | January |
| cutoffdate[year] | 2020 |
| cutoffdate[year] | 2030 |
| cutoffdate[hour] | 08 |
| cutoffdate[minute] | 00 |
And I press "Save"
And I should see "Wednesday, 1 January 2020, 8:00"
And I should see "Tuesday, 1 January 2030, 8:00"
And I log out
And I log in as "student2"
And I am on "Course 1" course homepage
@@ -169,7 +169,7 @@ Feature: Assign group override
| id_cutoffdate_enabled | 0 |
| allowsubmissionsfromdate[day] | 1 |
| allowsubmissionsfromdate[month] | January |
| allowsubmissionsfromdate[year] | 2020 |
| allowsubmissionsfromdate[year] | 2030 |
| allowsubmissionsfromdate[hour] | 08 |
| allowsubmissionsfromdate[minute] | 00 |
And I press "Save and display"
@@ -189,13 +189,13 @@ Feature: Assign group override
And I log in as "student2"
And I am on "Course 1" course homepage
And I follow "Test assignment name"
Then I should see "This assignment will accept submissions from Wednesday, 1 January 2020, 8:00"
Then I should see "This assignment will accept submissions from Tuesday, 1 January 2030, 8:00"
And I should not see "Add submission"
And I log out
And I log in as "student1"
And I am on "Course 1" course homepage
And I follow "Test assignment name"
And I should not see "This assignment will accept submissions from Wednesday, 1 January 2020, 8:00"
And I should not see "This assignment will accept submissions from Tuesday, 1 January 2030, 8:00"
@javascript
Scenario: Add both a user and group override and verify that both are applied correctly
@@ -209,7 +209,7 @@ Feature: Assign group override
| id_cutoffdate_enabled | 0 |
| allowsubmissionsfromdate[day] | 1 |
| allowsubmissionsfromdate[month] | January |
| allowsubmissionsfromdate[year] | 2030 |
| allowsubmissionsfromdate[year] | 2040 |
| allowsubmissionsfromdate[hour] | 08 |
| allowsubmissionsfromdate[minute] | 00 |
And I press "Save and display"
@@ -220,11 +220,11 @@ Feature: Assign group override
| id_allowsubmissionsfromdate_enabled | 1 |
| allowsubmissionsfromdate[day] | 1 |
| allowsubmissionsfromdate[month] | January |
| allowsubmissionsfromdate[year] | 2020 |
| allowsubmissionsfromdate[year] | 2030 |
| allowsubmissionsfromdate[hour] | 08 |
| allowsubmissionsfromdate[minute] | 00 |
And I press "Save"
And I should see "Wednesday, 1 January 2020, 8:00"
And I should see "Tuesday, 1 January 2030, 8:00"
And I follow "Test assignment name"
And I navigate to "User overrides" in current page administration
And I press "Add user override"
@@ -233,26 +233,26 @@ Feature: Assign group override
| id_allowsubmissionsfromdate_enabled | 1 |
| allowsubmissionsfromdate[day] | 1 |
| allowsubmissionsfromdate[month] | January |
| allowsubmissionsfromdate[year] | 2021 |
| allowsubmissionsfromdate[year] | 2031 |
| allowsubmissionsfromdate[hour] | 08 |
| allowsubmissionsfromdate[minute] | 00 |
And I press "Save"
And I should see "Friday, 1 January 2021, 8:00"
And I should see "Wednesday, 1 January 2031, 8:00"
And I log out
Then I log in as "student1"
And I am on "Course 1" course homepage
And I follow "Test assignment name"
And I should see "This assignment will accept submissions from Friday, 1 January 2021, 8:00"
And I should see "This assignment will accept submissions from Wednesday, 1 January 2031, 8:00"
And I log out
And I log in as "student2"
And I am on "Course 1" course homepage
And I follow "Test assignment name"
And I should see "This assignment will accept submissions from Tuesday, 1 January 2030, 8:00"
And I should see "This assignment will accept submissions from Sunday, 1 January 2040, 8:00"
And I log out
And I log in as "student3"
And I am on "Course 1" course homepage
And I follow "Test assignment name"
And I should see "This assignment will accept submissions from Wednesday, 1 January 2020, 8:00"
And I should see "This assignment will accept submissions from Tuesday, 1 January 2030, 8:00"
Scenario: Override a group when teacher is in no group, and does not have accessallgroups permission, and the activity's group mode is "separate groups"
Given the following "permission overrides" exist:
@@ -135,11 +135,11 @@ Feature: Assign user override
| id_cutoffdate_enabled | 1 |
| cutoffdate[day] | 1 |
| cutoffdate[month] | January |
| cutoffdate[year] | 2020 |
| cutoffdate[year] | 2030 |
| cutoffdate[hour] | 08 |
| cutoffdate[minute] | 00 |
And I press "Save"
And I should see "Wednesday, 1 January 2020, 8:00"
And I should see "Tuesday, 1 January 2030, 8:00"
And I log out
And I log in as "student2"
And I am on "Course 1" course homepage
@@ -163,7 +163,7 @@ Feature: Assign user override
| id_cutoffdate_enabled | 0 |
| allowsubmissionsfromdate[day] | 1 |
| allowsubmissionsfromdate[month] | January |
| allowsubmissionsfromdate[year] | 2020 |
| allowsubmissionsfromdate[year] | 2030 |
| allowsubmissionsfromdate[hour] | 08 |
| allowsubmissionsfromdate[minute] | 00 |
And I press "Save and display"
@@ -183,12 +183,12 @@ Feature: Assign user override
And I log in as "student2"
And I am on "Course 1" course homepage
And I follow "Test assignment name"
Then I should see "This assignment will accept submissions from Wednesday, 1 January 2020, 8:00"
Then I should see "This assignment will accept submissions from Tuesday, 1 January 2030, 8:00"
And I log out
And I log in as "student1"
And I am on "Course 1" course homepage
And I follow "Test assignment name"
And I should not see "This assignment will accept submissions from Wednesday, 1 January 2020, 8:00"
And I should not see "This assignment will accept submissions from Tuesday, 1 January 2030, 8:00"
Scenario: Override a user when teacher is in no group, and does not have accessallgroups permission, and the activity's group mode is "separate groups"
Given the following "permission overrides" exist:
@@ -123,11 +123,11 @@ Feature: Assignments correctly add feedback to the grade report when workflow an
| id_duedate_enabled | 1 |
| duedate[day] | 1 |
| duedate[month] | January |
| duedate[year] | 2020 |
| duedate[year] | 2030 |
| duedate[hour] | 08 |
| duedate[minute] | 00 |
And I press "Save"
And I should see "Wednesday, 1 January 2020, 8:00"
And I should see "Tuesday, 1 January 2030, 8:00"
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
And I should see "In review" in the "I'm the student's first submission" "table_row"
@@ -28,7 +28,7 @@ Feature: A teacher can set available from and deadline dates to access a lesson
| Description | Test lesson description |
| available[day] | 1 |
| available[month] | January |
| available[year] | 2020 |
| available[year] | 2030 |
| available[hour] | 08 |
| available[minute] | 00 |
And I press "Save and display"
@@ -43,7 +43,7 @@ Feature: A teacher can set available from and deadline dates to access a lesson
And I log in as "student1"
And I am on "Course 1" course homepage
When I follow "Test lesson"
Then I should see "This lesson will be open on Wednesday, 1 January 2020, 8:00"
Then I should see "This lesson will be open on Tuesday, 1 January 2030, 8:00"
And I should not see "First page contents"
Scenario: Forbidding lesson accesses until a specified date
@@ -207,7 +207,7 @@ Feature: Lesson group override
| id_deadline_enabled | 1 |
| deadline[day] | 1 |
| deadline[month] | January |
| deadline[year] | 2020 |
| deadline[year] | 2030 |
| deadline[hour] | 08 |
| deadline[minute] | 00 |
And I press "Save"
@@ -233,7 +233,7 @@ Feature: Lesson group override
| id_available_enabled | 1 |
| available[day] | 1 |
| available[month] | January |
| available[year] | 2020 |
| available[year] | 2030 |
| available[hour] | 08 |
| available[minute] | 00 |
And I press "Save and display"
@@ -253,7 +253,7 @@ Feature: Lesson group override
And I log in as "student2"
And I am on "Course 1" course homepage
And I follow "Test lesson"
Then I should see "This lesson will be open on Wednesday, 1 January 2020, 8:00"
Then I should see "This lesson will be open on Tuesday, 1 January 2030, 8:00"
And I should not see "Cat is an amphibian"
And I log out
And I log in as "student1"
@@ -312,7 +312,7 @@ Feature: Lesson group override
| id_available_enabled | 1 |
| available[day] | 1 |
| available[month] | January |
| available[year] | 2030 |
| available[year] | 2040 |
| available[hour] | 08 |
| available[minute] | 00 |
And I press "Save and display"
@@ -324,11 +324,11 @@ Feature: Lesson group override
| id_available_enabled | 1 |
| available[day] | 1 |
| available[month] | January |
| available[year] | 2020 |
| available[year] | 2030 |
| available[hour] | 08 |
| available[minute] | 00 |
And I press "Save"
And I should see "Wednesday, 1 January 2020, 8:00"
And I should see "Tuesday, 1 January 2030, 8:00"
And I follow "Test lesson name"
And I navigate to "User overrides" in current page administration
And I press "Add user override"
@@ -337,26 +337,26 @@ Feature: Lesson group override
| id_available_enabled | 1 |
| available[day] | 1 |
| available[month] | January |
| available[year] | 2021 |
| available[year] | 2031 |
| available[hour] | 08 |
| available[minute] | 00 |
And I press "Save"
And I should see "Friday, 1 January 2021, 8:00"
And I should see "Wednesday, 1 January 2031, 8:00"
And I log out
Then I log in as "student1"
And I am on "Course 1" course homepage
And I follow "Test lesson"
And I should see "This lesson will be open on Friday, 1 January 2021, 8:00"
And I should see "This lesson will be open on Wednesday, 1 January 2031, 8:00"
And I log out
And I log in as "student2"
And I am on "Course 1" course homepage
And I follow "Test lesson"
And I should see "This lesson will be open on Tuesday, 1 January 2030, 8:00"
And I should see "This lesson will be open on Sunday, 1 January 2040, 8:00"
And I log out
And I log in as "student3"
And I am on "Course 1" course homepage
And I follow "Test lesson"
And I should see "This lesson will be open on Wednesday, 1 January 2020, 8:00"
And I should see "This lesson will be open on Tuesday, 1 January 2030, 8:00"
Scenario: Override a group when teacher is in no group, and does not have accessallgroups permission, and the activity's group mode is 'separate groups'
Given the following "permission overrides" exist:
@@ -201,7 +201,7 @@ Feature: Lesson user override
| id_deadline_enabled | 1 |
| deadline[day] | 1 |
| deadline[month] | January |
| deadline[year] | 2020 |
| deadline[year] | 2030 |
| deadline[hour] | 08 |
| deadline[minute] | 00 |
And I press "Save"
@@ -228,7 +228,7 @@ Feature: Lesson user override
| id_available_enabled | 1 |
| available[day] | 1 |
| available[month] | January |
| available[year] | 2020 |
| available[year] | 2030 |
| available[hour] | 08 |
| available[minute] | 00 |
And I press "Save and display"
@@ -248,7 +248,7 @@ Feature: Lesson user override
And I log in as "student2"
And I am on "Course 1" course homepage
And I follow "Test lesson"
Then I should see "This lesson will be open on Wednesday, 1 January 2020, 8:00"
Then I should see "This lesson will be open on Tuesday, 1 January 2030, 8:00"
And I should not see "Cat is an amphibian"
And I log out
And I log in as "student1"
+13 -19
View File
@@ -52,32 +52,26 @@ Feature: Quiz reset
And I am on the "Test quiz name" "mod_quiz > Grades report" page
Then I should see "Attempts: 0"
@javascript
Scenario: Use course reset to remove user overrides.
When I am on the "Test quiz name" "mod_quiz > User overrides" page logged in as "teacher1"
And I press "Add user override"
And I set the following fields to these values:
| Override user | Student1 |
| Attempts allowed | 2 |
And I press "Save"
And I should see "Sam1 Student1"
And I navigate to "Reset" node in "Course administration"
And I set the following fields to these values:
| Delete all user overrides | 1 |
Given the following "mod_quiz > user overrides" exist:
| quiz | user | attempts |
| Test quiz name | student1 | 2 |
When I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Reset" in current page administration
And I set the field "Delete all user overrides" to "1"
And I press "Reset course"
And I press "Continue"
And I am on the "Test quiz name" "mod_quiz > User overrides" page
Then I should not see "Sam1 Student1"
Scenario: Use course reset to remove group overrides.
When I am on the "Test quiz name" "mod_quiz > Group overrides" page logged in as "teacher1"
And I press "Add group override"
And I set the following fields to these values:
| Override group | Group 1 |
| Attempts allowed | 2 |
And I press "Save"
And I should see "Group 1"
And I navigate to "Reset" node in "Course administration"
Given the following "mod_quiz > group overrides" exist:
| quiz | group | attempts |
| Test quiz name | G1 | 2 |
When I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Reset" in current page administration
And I set the following fields to these values:
| Delete all group overrides | 1 |
And I press "Reset course"
@@ -0,0 +1,66 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Behat data generator for mod_quiz.
*
* @package mod_quiz
* @category test
* @copyright 2019 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Behat data generator for mod_quiz.
*
* @copyright 2019 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_mod_quiz_generator extends behat_generator_base {
protected function get_creatable_entities(): array {
return [
'group overrides' => [
'datagenerator' => 'override',
'required' => ['quiz', 'group'],
'switchids' => ['quiz' => 'quiz', 'group' => 'groupid'],
],
'user overrides' => [
'datagenerator' => 'override',
'required' => ['quiz', 'user'],
'switchids' => ['quiz' => 'quiz', 'user' => 'userid'],
],
];
}
/**
* Look up the id of a quiz from its name.
*
* @param string $quizname the quiz name, for example 'Test quiz'.
* @return int corresponding id.
*/
protected function get_quiz_id(string $quizname): int {
global $DB;
if (!$id = $DB->get_field('quiz', 'id', ['name' => $quizname])) {
throw new Exception('There is no quiz with name "' . $quizname . '" does not exist');
}
return $id;
}
}
+23
View File
@@ -95,4 +95,27 @@ class mod_quiz_generator extends testing_module_generator {
return parent::create_instance($record, (array)$options);
}
/**
* Create a quiz override (either user or group).
*
* @param array $data must specify quizid, and one of userid or groupid.
*/
public function create_override(array $data) {
global $DB;
if (!isset($data['quiz'])) {
throw new coding_exception('Must specify quiz (id) when creating a quiz override.');
}
if (!isset($data['userid']) && !isset($data['groupid'])) {
throw new coding_exception('Must specify one of userid or groupid when creating a quiz override.');
}
if (isset($data['userid']) && isset($data['groupid'])) {
throw new coding_exception('Cannot specify both userid and groupid when creating a quiz override.');
}
$DB->insert_record('quiz_overrides', (object) $data);
}
}
+2 -2
View File
@@ -29,11 +29,11 @@
defined('MOODLE_INTERNAL') || die();
$version = 2018051709.00; // 20180517 = branching date YYYYMMDD - do not modify!
$version = 2018051710.00; // 20180517 = branching date YYYYMMDD - do not modify!
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '3.5.9 (Build: 20191111)'; // Human-friendly version name
$release = '3.5.10 (Build: 20200113)'; // Human-friendly version name
$branch = '35'; // This version's branch.
$maturity = MATURITY_STABLE; // This version's maturity level.