Compare commits
157 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d3f01844da | |||
| 7f2a97e632 | |||
| 2a512be9f6 | |||
| 3b643e9cad | |||
| cc446ad1ec | |||
| 7251f154ef | |||
| e6e07e0960 | |||
| 51d52d273c | |||
| 67c44ebd2e | |||
| eeaf8e0e62 | |||
| 651a6bf5fa | |||
| 7135dc455a | |||
| c6b033233f | |||
| cd7201ad49 | |||
| 15a4027603 | |||
| d6eb0134af | |||
| 7505c8ce76 | |||
| 90e5c2b889 | |||
| 84a9a24b96 | |||
| 78255acbf9 | |||
| 34af3ee683 | |||
| ae422c191c | |||
| 8e77498176 | |||
| 89fb92a551 | |||
| a624a36856 | |||
| 56cfcc65cb | |||
| efb7b7d668 | |||
| 2f5e4bc739 | |||
| f21f69f0b8 | |||
| fda8d5d13d | |||
| ea12d48932 | |||
| 383af5dab9 | |||
| f1cf68fedc | |||
| 7fdc181bfe | |||
| 0422d333a8 | |||
| f857f28d7a | |||
| c777c77434 | |||
| e2d5d5457f | |||
| 4802739d19 | |||
| 01087d1e45 | |||
| f30469004a | |||
| e34df5a1c4 | |||
| dd3e9a0c25 | |||
| f246b1c05c | |||
| 2288e60615 | |||
| f951911682 | |||
| ad262b981f | |||
| da4ffac0c9 | |||
| 702c4b4781 | |||
| fedba1fcc4 | |||
| de96ddee74 | |||
| 7b6a902ccd | |||
| a0a03baad8 | |||
| e37073a24b | |||
| 9f59264afa | |||
| 7f4be41ed2 | |||
| 3dec6b78ce | |||
| e3e41170fe | |||
| 7c6be5b319 | |||
| 0613e56e69 | |||
| a3065e8220 | |||
| f496b55189 | |||
| 135461b4a9 | |||
| 8253f41405 | |||
| cf0b79a80f | |||
| 1a8dc02878 | |||
| 5b3ec84495 | |||
| 0608b35a7b | |||
| abb5f229b1 | |||
| e2fd22fadd | |||
| 18c824af1f | |||
| 37ce725987 | |||
| 8ac6793716 | |||
| 6022706b47 | |||
| 250ea6a145 | |||
| 039096d1b1 | |||
| 0ca3442925 | |||
| 1a4626d9fa | |||
| fa0b5768cc | |||
| 7b33c5e9ec | |||
| d394a22ee6 | |||
| 83fe3c44c0 | |||
| 282c4224da | |||
| 1312197a82 | |||
| 5dc6fd3b23 | |||
| b49b76f397 | |||
| a4f4656110 | |||
| 83bdb872dc | |||
| 39a83aa250 | |||
| e70853386d | |||
| 451d514d84 | |||
| 72f5a6e119 | |||
| 7f624852e4 | |||
| 8abe8980d6 | |||
| 431c7ba150 | |||
| 9b632e17d8 | |||
| b731ea9798 | |||
| 472b1b095e | |||
| 2cddf32f7e | |||
| 9b898bf8b4 | |||
| a87d3a64bb | |||
| 55cd43d397 | |||
| 9c1462ecfe | |||
| e22aba78ca | |||
| d952f227d0 | |||
| d80cec9c2b | |||
| 77fe56108a | |||
| 03550b7b8f | |||
| 274ccc948b | |||
| bc8d5da483 | |||
| 94bf3e9c78 | |||
| e92bd648db | |||
| e4bb1cc603 | |||
| c2f12aa742 | |||
| 454af7cf52 | |||
| f73e64f166 | |||
| 18a738f88f | |||
| 493a30cdc2 | |||
| 65edc59a51 | |||
| 28b9209af5 | |||
| 8b2c91e627 | |||
| 8256d18453 | |||
| ab11164395 | |||
| 29933a945f | |||
| c7b384ce66 | |||
| 665e256961 | |||
| 879532b41f | |||
| 4e83a79cd5 | |||
| 3ecba892e3 | |||
| b92f4ba9f1 | |||
| 6a3a83af93 | |||
| 6e3a070030 | |||
| 5d701a706c | |||
| 612e4d9d02 | |||
| df6f010a86 | |||
| bf4904c32b | |||
| 2821f78e6c | |||
| f904215fa7 | |||
| 635b605eab | |||
| 08dd41b304 | |||
| 04cd0cb2ca | |||
| 5cacda33bb | |||
| 864ef22534 | |||
| f008a03c60 | |||
| 8831cc14c7 | |||
| ebb87a1e63 | |||
| 93d9b19607 | |||
| 92d04a7913 | |||
| a5cdde3c79 | |||
| d073f5daf0 | |||
| 3972ef0a32 | |||
| 01b63287ef | |||
| b3259b51bb | |||
| b683a48f69 | |||
| 944888a509 | |||
| 21ac04e12e | |||
| ab9b65b6a7 |
@@ -2451,6 +2451,7 @@
|
||||
<VENDOR name="oracle" version="11.2" />
|
||||
</DATABASE>
|
||||
<PHP version="7.0.0" level="required">
|
||||
<RESTRICT function="restrict_php_version_74" message="unsupportedphpversion74" />
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
@@ -2632,6 +2633,7 @@
|
||||
<VENDOR name="oracle" version="11.2" />
|
||||
</DATABASE>
|
||||
<PHP version="7.1.0" level="required">
|
||||
<RESTRICT function="restrict_php_version_74" message="unsupportedphpversion74" />
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
@@ -2980,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>
|
||||
|
||||
@@ -98,7 +98,10 @@ $options = array(
|
||||
GETREMOTEADDR_SKIP_HTTP_CLIENT_IP => 'HTTP_X_FORWARDED_FOR, REMOTE_ADDR',
|
||||
GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR => 'HTTP_CLIENT, REMOTE_ADDR',
|
||||
GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP => 'REMOTE_ADDR');
|
||||
$temp->add(new admin_setting_configselect('getremoteaddrconf', new lang_string('getremoteaddrconf', 'admin'), new lang_string('configgetremoteaddrconf', 'admin'), 0, $options));
|
||||
$temp->add(new admin_setting_configselect('getremoteaddrconf', new lang_string('getremoteaddrconf', 'admin'),
|
||||
new lang_string('configgetremoteaddrconf', 'admin'),
|
||||
GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP, $options));
|
||||
$temp->add(new admin_setting_configtext('reverseproxyignore', new lang_string('reverseproxyignore', 'admin'), new lang_string('configreverseproxyignore', 'admin'), ''));
|
||||
|
||||
$temp->add(new admin_setting_heading('webproxy', new lang_string('webproxy', 'admin'), new lang_string('webproxyinfo', 'admin')));
|
||||
$temp->add(new admin_setting_configtext('proxyhost', new lang_string('proxyhost', 'admin'), new lang_string('configproxyhost', 'admin'), '', PARAM_HOST));
|
||||
|
||||
@@ -48,7 +48,6 @@ class behat_admin extends behat_base {
|
||||
* @param TableNode $table
|
||||
*/
|
||||
public function i_set_the_following_administration_settings_values(TableNode $table) {
|
||||
|
||||
if (!$data = $table->getRowsHash()) {
|
||||
return;
|
||||
}
|
||||
@@ -57,17 +56,12 @@ class behat_admin extends behat_base {
|
||||
|
||||
// We expect admin block to be visible, otherwise go to homepage.
|
||||
if (!$this->getSession()->getPage()->find('css', '.block_settings')) {
|
||||
$this->getSession()->visit($this->locate_path('/'));
|
||||
$this->wait(self::get_timeout() * 1000, self::PAGE_READY_JS);
|
||||
$this->execute('behat_forms::i_am_on_homepage');
|
||||
}
|
||||
|
||||
// Search by label.
|
||||
$searchbox = $this->find_field(get_string('searchinsettings', 'admin'));
|
||||
$searchbox->setValue($label);
|
||||
$submitsearch = $this->find('css', 'form.adminsearchform input[type=submit]');
|
||||
$submitsearch->press();
|
||||
|
||||
$this->wait(self::get_timeout() * 1000, self::PAGE_READY_JS);
|
||||
$this->execute('behat_forms::i_set_the_field_to', [get_string('searchinsettings', 'admin'), $label]);
|
||||
$this->execute("behat_general::i_click_on_in_the", [get_string('search', 'admin'), 'button', '.block_settings', 'css_element']);
|
||||
|
||||
// Admin settings does not use the same DOM structure than other moodle forms
|
||||
// but we also need to use lib/behat/form_field/* to deal with the different moodle form elements.
|
||||
@@ -83,35 +77,17 @@ class behat_admin extends behat_base {
|
||||
"@id=//span[contains(normalize-space(.), $label)]/preceding-sibling::label[1]/@for]";
|
||||
$fieldnode = $this->find('xpath', $fieldxpath, $exception);
|
||||
|
||||
$formfieldtypenode = $this->find('xpath', $fieldxpath . "/ancestor::div[@class='form-setting']" .
|
||||
"/child::div[contains(concat(' ', @class, ' '), ' form-')]/child::*/parent::div");
|
||||
|
||||
} catch (ElementNotFoundException $e) {
|
||||
|
||||
// Multi element settings, interacting only the first one.
|
||||
$fieldxpath = "//*[label[normalize-space(.)= $label]|span[normalize-space(.)= $label]]/" .
|
||||
"ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' form-item ')]" .
|
||||
"/descendant::div[@class='form-group']/descendant::*[self::input | self::textarea | self::select]" .
|
||||
"[not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]";
|
||||
$fieldnode = $this->find('xpath', $fieldxpath);
|
||||
|
||||
// It is the same one that contains the type.
|
||||
$formfieldtypenode = $fieldnode;
|
||||
}
|
||||
|
||||
// Getting the class which contains the field type.
|
||||
$classes = explode(' ', $formfieldtypenode->getAttribute('class'));
|
||||
foreach ($classes as $class) {
|
||||
if (substr($class, 0, 5) == 'form-') {
|
||||
$type = substr($class, 5);
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiating the appropiate field type.
|
||||
$field = behat_field_manager::get_field_instance($type, $fieldnode, $this->getSession());
|
||||
$field->set_value($value);
|
||||
|
||||
$this->find_button(get_string('savechanges'))->press();
|
||||
$this->execute('behat_forms::i_set_the_field_with_xpath_to', [$fieldxpath, $value]);
|
||||
$this->execute("behat_general::i_click_on", [get_string('savechanges'), 'button']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
@tool_behat
|
||||
Feature: Behat steps for interacting with form work
|
||||
In order to test my Moodle code
|
||||
As a developer
|
||||
I need the Behat steps for form elements to work reliably
|
||||
|
||||
@javascript
|
||||
Scenario: Test fields in containers
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
When I log in as "admin"
|
||||
And I am on "Course 1" course homepage
|
||||
# Just get to any form.
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I set the field "Course full name" in the "General" "fieldset" to "Frog"
|
||||
And I set the following fields in the "Appearance" "fieldset" to these values:
|
||||
| Show activity reports | Yes |
|
||||
| Number of announcements | 1 |
|
||||
Then the field "Show activity reports" in the "Appearance" "fieldset" matches value "Yes"
|
||||
And the field "Show activity reports" in the "Appearance" "fieldset" does not match value "No"
|
||||
And the following fields in the "region-main" "region" match these values:
|
||||
| Course full name | Frog |
|
||||
| Number of announcements | 1 |
|
||||
And the following fields in the "region-main" "region" do not match these values:
|
||||
| Course full name | Course 1 |
|
||||
| Number of announcements | 5 |
|
||||
@@ -222,6 +222,48 @@ class api {
|
||||
}
|
||||
}
|
||||
|
||||
// Clean the legacy role assignments which are stale.
|
||||
$paramsclean['usercontext'] = CONTEXT_USER;
|
||||
$paramsclean['component'] = 'tool_cohortroles';
|
||||
$sql = 'SELECT DISTINCT(ra.id), ra.roleid, ra.userid, ra.contextid, ctx.instanceid
|
||||
FROM {role_assignments} ra
|
||||
JOIN {context} ctx ON ctx.id = ra.contextid AND ctx.contextlevel = :usercontext
|
||||
JOIN {cohort_members} cm ON cm.userid = ctx.instanceid
|
||||
LEFT JOIN {tool_cohortroles} tc ON tc.cohortid = cm.cohortid
|
||||
AND tc.userid = ra.userid
|
||||
AND tc.roleid = ra.roleid
|
||||
WHERE ra.component = :component
|
||||
AND tc.id is null';
|
||||
if ($candidatelegacyassignments = $DB->get_records_sql($sql, $paramsclean)) {
|
||||
$sql = 'SELECT DISTINCT(ra.id)
|
||||
FROM {role_assignments} ra
|
||||
JOIN {context} ctx ON ctx.id = ra.contextid AND ctx.contextlevel = :usercontext
|
||||
JOIN {cohort_members} cm ON cm.userid = ctx.instanceid
|
||||
JOIN {tool_cohortroles} tc ON tc.cohortid = cm.cohortid AND tc.userid = ra.userid
|
||||
WHERE ra.component = :component';
|
||||
if ($currentvalidroleassignments = $DB->get_records_sql($sql, $paramsclean)) {
|
||||
foreach ($candidatelegacyassignments as $candidate) {
|
||||
if (!array_key_exists($candidate->id, $currentvalidroleassignments)) {
|
||||
role_unassign($candidate->roleid, $candidate->userid, $candidate->contextid, 'tool_cohortroles');
|
||||
$rolesremoved[] = array(
|
||||
'useridassignedto' => $candidate->userid,
|
||||
'useridassignedover' => $candidate->instanceid,
|
||||
'roleid' => $candidate->roleid
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
foreach ($candidatelegacyassignments as $candidate) {
|
||||
role_unassign($candidate->roleid, $candidate->userid, $candidate->contextid, 'tool_cohortroles');
|
||||
$rolesremoved[] = array(
|
||||
'useridassignedto' => $candidate->userid,
|
||||
'useridassignedover' => $candidate->instanceid,
|
||||
'roleid' => $candidate->roleid
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array('rolesadded' => $rolesadded, 'rolesremoved' => $rolesremoved);
|
||||
}
|
||||
|
||||
|
||||
@@ -103,5 +103,4 @@ class cohort_role_assignment extends persistent {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -50,8 +50,6 @@ class tool_cohortroles_api_testcase extends advanced_testcase {
|
||||
* Setup function- we will create a course and add an assign instance to it.
|
||||
*/
|
||||
protected function setUp() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
// Create some users.
|
||||
@@ -133,14 +131,91 @@ class tool_cohortroles_api_testcase extends advanced_testcase {
|
||||
|
||||
public function test_delete_cohort_role_assignment() {
|
||||
$this->setAdminUser();
|
||||
$params = (object) array(
|
||||
// Create a cohort role assigment.
|
||||
$params = (object) [
|
||||
'userid' => $this->userassignto->id,
|
||||
'roleid' => $this->roleid,
|
||||
'cohortid' => $this->cohort->id
|
||||
);
|
||||
$result = api::create_cohort_role_assignment($params);
|
||||
$worked = api::delete_cohort_role_assignment($result->get('id'));
|
||||
$this->assertTrue($worked);
|
||||
];
|
||||
$cohortroleassignment = api::create_cohort_role_assignment($params);
|
||||
$sync = api::sync_all_cohort_roles();
|
||||
$rolesadded = [
|
||||
[
|
||||
'useridassignedto' => $this->userassignto->id,
|
||||
'useridassignedover' => $this->userassignover->id,
|
||||
'roleid' => $this->roleid
|
||||
]
|
||||
];
|
||||
$expected = [
|
||||
'rolesadded' => $rolesadded,
|
||||
'rolesremoved' => []
|
||||
];
|
||||
$this->assertEquals($sync, $expected);
|
||||
|
||||
// Delete the cohort role assigment and confirm the roles are removed.
|
||||
$result = api::delete_cohort_role_assignment($cohortroleassignment->get('id'));
|
||||
$this->assertTrue($result);
|
||||
$sync = api::sync_all_cohort_roles();
|
||||
$expected = [
|
||||
'rolesadded' => [],
|
||||
'rolesremoved' => $rolesadded
|
||||
];
|
||||
$this->assertEquals($expected, $sync);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case verifying that syncing won't remove role assignments if they are valid for another cohort role assignment.
|
||||
*/
|
||||
public function test_delete_cohort_role_assignment_cohorts_having_same_members() {
|
||||
$this->setAdminUser();
|
||||
|
||||
// Create 2 cohorts, with a 1 user (user1) present in both,
|
||||
// and user2 and user3 members of 1 cohort each.
|
||||
$cohort1 = $this->getDataGenerator()->create_cohort();
|
||||
$cohort2 = $this->getDataGenerator()->create_cohort();
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$user3 = $this->getDataGenerator()->create_user();
|
||||
cohort_add_member($cohort1->id, $user1->id);
|
||||
cohort_add_member($cohort1->id, $user2->id);
|
||||
cohort_add_member($cohort2->id, $user1->id);
|
||||
cohort_add_member($cohort2->id, $user3->id);
|
||||
|
||||
// And a role and a user to assign that role to.
|
||||
$user4 = $this->getDataGenerator()->create_user(); // A cohort manager, for example.
|
||||
$roleid = create_role('Role 1', 'myrole', 'test');
|
||||
|
||||
// Assign the role for user4 in both cohorts.
|
||||
$params = (object) [
|
||||
'userid' => $user4->id,
|
||||
'roleid' => $roleid,
|
||||
'cohortid' => $cohort1->id
|
||||
];
|
||||
$cohort1roleassignment = api::create_cohort_role_assignment($params);
|
||||
$params->cohortid = $cohort2->id;
|
||||
$cohort2roleassignment = api::create_cohort_role_assignment($params);
|
||||
|
||||
$sync = api::sync_all_cohort_roles();
|
||||
|
||||
// There is no guarantee about the order of roles assigned.
|
||||
// so confirm we have 3 role assignments, and they are for the users 1, 2 and 3.
|
||||
$this->assertCount(3, $sync['rolesadded']);
|
||||
$addedusers = array_column($sync['rolesadded'], 'useridassignedover');
|
||||
$this->assertContains($user1->id, $addedusers);
|
||||
$this->assertContains($user2->id, $addedusers);
|
||||
$this->assertContains($user3->id, $addedusers);
|
||||
|
||||
// Remove the role assignment for user4/cohort1.
|
||||
// Verify only 1 role is unassigned as the others are still valid for the other cohort role assignment.
|
||||
$result = api::delete_cohort_role_assignment($cohort1roleassignment->get('id'));
|
||||
$this->assertTrue($result);
|
||||
|
||||
$sync = api::sync_all_cohort_roles();
|
||||
|
||||
$this->assertCount(0, $sync['rolesadded']);
|
||||
$this->assertCount(1, $sync['rolesremoved']);
|
||||
$removedusers = array_column($sync['rolesremoved'], 'useridassignedover');
|
||||
$this->assertContains($user2->id, $removedusers);
|
||||
}
|
||||
|
||||
public function test_list_cohort_role_assignments() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
use \tool_monitor\privacy\provider;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
use \core_privacy\tests\provider_testcase;
|
||||
|
||||
/**
|
||||
* Privacy test for the event monitor
|
||||
@@ -37,7 +38,7 @@ use \core_privacy\local\request\approved_userlist;
|
||||
* @copyright 2018 Adrian Greeve <adriangreeve.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class tool_monitor_privacy_testcase extends advanced_testcase {
|
||||
class tool_monitor_privacy_testcase extends provider_testcase {
|
||||
|
||||
/**
|
||||
* Set up method.
|
||||
|
||||
@@ -123,9 +123,12 @@ class issuer extends persistent {
|
||||
$mform->addElement('checkbox', 'showonloginpage', get_string('issuershowonloginpage', 'tool_oauth2'));
|
||||
$mform->addHelpButton('showonloginpage', 'issuershowonloginpage', 'tool_oauth2');
|
||||
|
||||
// Require confirmation email for new accounts.
|
||||
$mform->addElement('advcheckbox', 'requireconfirmation', get_string('issuerrequireconfirmation', 'tool_oauth2'));
|
||||
$mform->addHelpButton('requireconfirmation', 'issuerrequireconfirmation', 'tool_oauth2');
|
||||
$showrequireconfirm = $this->_customdata['showrequireconfirm'] ?? false;
|
||||
if ($showrequireconfirm) {
|
||||
// Require confirmation email for new accounts.
|
||||
$mform->addElement('advcheckbox', 'requireconfirmation', get_string('issuerrequireconfirmation', 'tool_oauth2'));
|
||||
$mform->addHelpButton('requireconfirmation', 'issuerrequireconfirmation', 'tool_oauth2');
|
||||
}
|
||||
|
||||
$mform->addElement('hidden', 'sortorder');
|
||||
$mform->setType('sortorder', PARAM_INT);
|
||||
|
||||
@@ -58,7 +58,25 @@ if ($action == 'edit') {
|
||||
$PAGE->navbar->add(get_string('createnewissuer', 'tool_oauth2'));
|
||||
}
|
||||
|
||||
$mform = new \tool_oauth2\form\issuer(null, ['persistent' => $issuer]);
|
||||
if (!empty($issuerid)) {
|
||||
// Show the "Require confirmation email" checkbox for trusted issuers like Google, Facebook and Microsoft.
|
||||
$likefacebook = $DB->sql_like('url', ':facebook');
|
||||
$likegoogle = $DB->sql_like('url', ':google');
|
||||
$likemicrosoft = $DB->sql_like('url', ':microsoft');
|
||||
$params = [
|
||||
'issuerid' => $issuerid,
|
||||
'facebook' => '%facebook%',
|
||||
'google' => '%google%',
|
||||
'microsoft' => '%microsoft%',
|
||||
];
|
||||
$select = "issuerid = :issuerid AND ($likefacebook OR $likegoogle OR $likemicrosoft)";
|
||||
// We're querying from the oauth2_endpoint table because the base URLs of FB and Microsoft can be empty in the issuer table.
|
||||
$showrequireconfirm = $DB->record_exists_select('oauth2_endpoint', $select, $params);
|
||||
} else {
|
||||
$showrequireconfirm = optional_param('showrequireconfirm', false, PARAM_BOOL);
|
||||
}
|
||||
|
||||
$mform = new \tool_oauth2\form\issuer(null, ['persistent' => $issuer, 'showrequireconfirm' => $showrequireconfirm]);
|
||||
}
|
||||
|
||||
if ($mform && $mform->is_cancelled()) {
|
||||
@@ -157,21 +175,31 @@ if ($mform && $mform->is_cancelled()) {
|
||||
$issuers = core\oauth2\api::get_all_issuers();
|
||||
echo $renderer->issuers_table($issuers);
|
||||
|
||||
// Google template.
|
||||
$docs = 'admin/tool/oauth2/issuers/google';
|
||||
$params = ['action' => 'edittemplate', 'type' => 'google', 'sesskey' => sesskey(), 'docslink' => $docs];
|
||||
$params = ['action' => 'edittemplate', 'type' => 'google', 'sesskey' => sesskey(), 'docslink' => $docs,
|
||||
'showrequireconfirm' => true];
|
||||
$addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params);
|
||||
echo $renderer->single_button($addurl, get_string('createnewgoogleissuer', 'tool_oauth2'));
|
||||
|
||||
// Microsoft template.
|
||||
$docs = 'admin/tool/oauth2/issuers/microsoft';
|
||||
$params = ['action' => 'edittemplate', 'type' => 'microsoft', 'sesskey' => sesskey(), 'docslink' => $docs];
|
||||
$params = ['action' => 'edittemplate', 'type' => 'microsoft', 'sesskey' => sesskey(), 'docslink' => $docs,
|
||||
'showrequireconfirm' => true];
|
||||
$addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params);
|
||||
echo $renderer->single_button($addurl, get_string('createnewmicrosoftissuer', 'tool_oauth2'));
|
||||
|
||||
// Facebook template.
|
||||
$docs = 'admin/tool/oauth2/issuers/facebook';
|
||||
$params = ['action' => 'edittemplate', 'type' => 'microsoft', 'sesskey' => sesskey(), 'docslink' => $docs];
|
||||
$params = ['action' => 'edittemplate', 'type' => 'facebook', 'sesskey' => sesskey(), 'docslink' => $docs];
|
||||
$params = ['action' => 'edittemplate', 'type' => 'facebook', 'sesskey' => sesskey(), 'docslink' => $docs,
|
||||
'showrequireconfirm' => true];
|
||||
$addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params);
|
||||
echo $renderer->single_button($addurl, get_string('createnewfacebookissuer', 'tool_oauth2'));
|
||||
|
||||
// Generic issuer.
|
||||
$addurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['action' => 'edit']);
|
||||
echo $renderer->single_button($addurl, get_string('createnewissuer', 'tool_oauth2'));
|
||||
|
||||
echo $OUTPUT->footer();
|
||||
|
||||
}
|
||||
|
||||
@@ -131,11 +131,12 @@ if ($execute) {
|
||||
tool_phpunit_problem('Can not create configuration file');
|
||||
}
|
||||
}
|
||||
$configdir = escapeshellarg($configdir);
|
||||
// no cleanup of path - this is tricky because we can not use escapeshellarg and friends for escaping,
|
||||
// this is from admin user so PARAM_PATH must be enough
|
||||
chdir($CFG->dirroot);
|
||||
passthru("php $CFG->admin/tool/phpunit/cli/util.php --run -c $configdir $testclass $testpath", $code);
|
||||
$configdir = escapeshellarg($configdir);
|
||||
$cleanclass = escapeshellarg($testclass);
|
||||
$cleanpath = escapeshellarg($testpath);
|
||||
|
||||
passthru("php $CFG->admin/tool/phpunit/cli/util.php --run -c $configdir $cleanclass $cleanpath", $code);
|
||||
chdir($oldcwd);
|
||||
|
||||
echo '</pre>';
|
||||
@@ -146,7 +147,7 @@ if ($execute) {
|
||||
}
|
||||
|
||||
echo $OUTPUT->box_start('generalbox boxwidthwide boxaligncenter');
|
||||
echo '<form method="get" action="webrunner.php">';
|
||||
echo '<form method="post" action="webrunner.php">';
|
||||
echo '<fieldset class="invisiblefieldset">';
|
||||
echo '<label for="testpath">Test one file</label> ';
|
||||
echo '<input type="text" id="testpath" name="testpath" value="'.s($testpath).'" size="50" /> (all test cases from webrunner.xml if empty)';
|
||||
|
||||
@@ -47,5 +47,39 @@ function xmldb_auth_oauth2_upgrade($oldversion) {
|
||||
// Automatically generated Moodle v3.5.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
if ($oldversion < 2018051401) {
|
||||
// Fetch Facebook, Google, and Microsoft issuers. We use the URL field to determine the issuer type as it's the only
|
||||
// field that contains the keyword that can somewhat let us reliably determine the issuer type.
|
||||
$likefacebook = $DB->sql_like('oe.url', ':facebook');
|
||||
$likegoogle = $DB->sql_like('oe.url', ':google');
|
||||
$likemicrosoft = $DB->sql_like('oe.url', ':microsoft');
|
||||
|
||||
$params = [
|
||||
'facebook' => '%facebook%',
|
||||
'google' => '%google%',
|
||||
'microsoft' => '%microsoft%',
|
||||
];
|
||||
|
||||
// We're querying from the oauth2_endpoint table because the base URLs of FB and Microsoft can be empty in the issuer table.
|
||||
$subsql = "
|
||||
SELECT DISTINCT oe.issuerid
|
||||
FROM {oauth2_endpoint} oe
|
||||
WHERE $likefacebook
|
||||
OR $likegoogle
|
||||
OR $likemicrosoft";
|
||||
|
||||
// Update non-Facebook/Google/Microsoft issuers and set requireconfirmation to 1.
|
||||
$updatesql = "
|
||||
UPDATE {oauth2_issuer}
|
||||
SET requireconfirmation = 1
|
||||
WHERE id NOT IN ({$subsql})";
|
||||
$DB->execute($updatesql, $params);
|
||||
|
||||
// Delete linked logins for non-Facebook/Google/Microsoft issuers. They can easily re-link their logins anyway.
|
||||
$DB->delete_records_select('auth_oauth2_linked_login', "issuerid NOT IN ($subsql)", $params);
|
||||
|
||||
upgrade_plugin_savepoint(true, 2018051401, 'auth', 'oauth2');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,10 @@ then cut and paste the address into the address
|
||||
line at the top of your web browser window.
|
||||
|
||||
If you need help, please contact the site administrator,
|
||||
{$a->admin}';
|
||||
{$a->admin}
|
||||
|
||||
If you did not do this, someone else could be trying to compromise your account.
|
||||
Please contact the site administrator immediately.';
|
||||
$string['confirmaccountemailsubject'] = '{$a}: account confirmation';
|
||||
$string['confirmationinvalid'] = 'The confirmation link is either invalid, or has expired. Please start the login process again to generate a new confirmation email.';
|
||||
$string['confirmationpending'] = 'This account is pending email confirmation.';
|
||||
@@ -60,7 +63,10 @@ then cut and paste the address into the address
|
||||
line at the top of your web browser window.
|
||||
|
||||
If you need help, please contact the site administrator,
|
||||
{$a->admin}';
|
||||
{$a->admin}
|
||||
|
||||
If you did not do this, someone else could be trying to compromise your account.
|
||||
Please contact the site administrator immediately.';
|
||||
$string['confirmlinkedloginemailsubject'] = '{$a}: linked login confirmation';
|
||||
$string['createaccountswarning'] = 'This authentication plugin allows users to create accounts on your site. You may want to enable the setting "authpreventaccountcreation" if you use this plugin.';
|
||||
$string['createnewlinkedlogin'] = 'Link a new account ({$a})';
|
||||
|
||||
@@ -24,6 +24,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2018051401; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2018050800; // Requires this Moodle version.
|
||||
$plugin->component = 'auth_oauth2'; // Full name of the plugin (used for diagnostics).
|
||||
|
||||
@@ -42,10 +42,18 @@ class behat_auth extends behat_base {
|
||||
* Logs in the user. There should exist a user with the same value as username and password.
|
||||
*
|
||||
* @Given /^I log in as "(?P<username_string>(?:[^"]|\\")*)"$/
|
||||
* @param string $username the user to log in as.
|
||||
* @param moodle_url|null $wantsurl optional, URL to go to after logging in.
|
||||
*/
|
||||
public function i_log_in_as($username) {
|
||||
public function i_log_in_as(string $username, moodle_url $wantsurl = null) {
|
||||
|
||||
$loginurl = new moodle_url('/login/index.php');
|
||||
if ($wantsurl !== null) {
|
||||
$loginurl->param('wantsurl', $wantsurl->out_as_local_url());
|
||||
}
|
||||
|
||||
// Visit login page.
|
||||
$this->getSession()->visit($this->locate_path('login/index.php'));
|
||||
$this->getSession()->visit($this->locate_path($loginurl->out_as_local_url()));
|
||||
|
||||
// Enter username and password.
|
||||
$this->execute('behat_forms::i_set_the_field_to', array('Username', $this->escape($username)));
|
||||
|
||||
@@ -114,12 +114,8 @@ class behat_block_site_main_menu extends behat_base {
|
||||
protected function get_site_menu_activity_element($element, $selectortype, $activityname) {
|
||||
$activitynode = $this->get_site_menu_activity_node($activityname);
|
||||
|
||||
// Transforming to Behat selector/locator.
|
||||
list($selector, $locator) = $this->transform_selector($selectortype, $element);
|
||||
$exception = new ElementNotFoundException($this->getSession(), '"' . $element . '" "' .
|
||||
$selectortype . '" in "' . $activityname . '" ');
|
||||
|
||||
return $this->find($selector, $locator, $exception, $activitynode);
|
||||
$exception = new ElementNotFoundException($this->getSession(), "'{$element}' '{$selectortype}' in '${activityname}'");
|
||||
return $this->find($selectortype, $element, $exception, $activitynode);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -114,12 +114,8 @@ class behat_block_social_activities extends behat_base {
|
||||
protected function get_social_block_activity_element($element, $selectortype, $activityname) {
|
||||
$activitynode = $this->get_social_block_activity_node($activityname);
|
||||
|
||||
// Transforming to Behat selector/locator.
|
||||
list($selector, $locator) = $this->transform_selector($selectortype, $element);
|
||||
$exception = new ElementNotFoundException($this->getSession(), '"' . $element . '" "' .
|
||||
$selectortype . '" in "' . $activityname . '" ');
|
||||
|
||||
return $this->find($selector, $locator, $exception, $activitynode);
|
||||
$exception = new ElementNotFoundException($this->getSession(), "'{$element}' '{$selectortype}' in '${activityname}'");
|
||||
return $this->find($selectortype, $element, $exception, $activitynode);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "6.5.*",
|
||||
"phpunit/dbunit": "3.0.*",
|
||||
"moodlehq/behat-extension": "3.35.3",
|
||||
"moodlehq/behat-extension": "3.35.4",
|
||||
"mikey179/vfsstream": "^1.6",
|
||||
"instaclick/php-webdriver": "dev-local as 1.x-dev"
|
||||
}
|
||||
|
||||
Generated
+110
-111
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "727ed2d71a63f4567fde125863d19407",
|
||||
"content-hash": "6aaa4a5a81f6318809d9d614c3262c03",
|
||||
"packages": [],
|
||||
"packages-dev": [
|
||||
{
|
||||
@@ -619,27 +619,28 @@
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/guzzle",
|
||||
"version": "6.3.3",
|
||||
"version": "6.4.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/guzzle.git",
|
||||
"reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba"
|
||||
"reference": "0895c932405407fd3a7368b6910c09a24d26db11"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba",
|
||||
"reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba",
|
||||
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/0895c932405407fd3a7368b6910c09a24d26db11",
|
||||
"reference": "0895c932405407fd3a7368b6910c09a24d26db11",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"guzzlehttp/promises": "^1.0",
|
||||
"guzzlehttp/psr7": "^1.4",
|
||||
"guzzlehttp/psr7": "^1.6.1",
|
||||
"php": ">=5.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-curl": "*",
|
||||
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
|
||||
"psr/log": "^1.0"
|
||||
"psr/log": "^1.1"
|
||||
},
|
||||
"suggest": {
|
||||
"psr/log": "Required for using the Log middleware"
|
||||
@@ -651,12 +652,12 @@
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/functions_include.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"GuzzleHttp\\": "src/"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"src/functions_include.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
@@ -680,7 +681,7 @@
|
||||
"rest",
|
||||
"web service"
|
||||
],
|
||||
"time": "2018-04-22T15:46:56+00:00"
|
||||
"time": "2019-10-23T15:58:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/promises",
|
||||
@@ -903,8 +904,8 @@
|
||||
"authors": [
|
||||
{
|
||||
"name": "Frank Kleine",
|
||||
"role": "Developer",
|
||||
"homepage": "http://frankkleine.de/"
|
||||
"homepage": "http://frankkleine.de/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Virtual file system to mock the real file system in unit tests.",
|
||||
@@ -913,16 +914,16 @@
|
||||
},
|
||||
{
|
||||
"name": "moodlehq/behat-extension",
|
||||
"version": "v3.35.3",
|
||||
"version": "v3.35.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/moodlehq/moodle-behat-extension.git",
|
||||
"reference": "e7239c27be9ec24cdc1c6c99e073922775d86769"
|
||||
"reference": "5c54be1577b7e8a7c4a44f8859548fc44b595c1f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/e7239c27be9ec24cdc1c6c99e073922775d86769",
|
||||
"reference": "e7239c27be9ec24cdc1c6c99e073922775d86769",
|
||||
"url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/5c54be1577b7e8a7c4a44f8859548fc44b595c1f",
|
||||
"reference": "5c54be1577b7e8a7c4a44f8859548fc44b595c1f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -947,9 +948,9 @@
|
||||
"authors": [
|
||||
{
|
||||
"name": "David Monllaó",
|
||||
"role": "Developer",
|
||||
"email": "david.monllao@gmail.com",
|
||||
"homepage": "http://moodle.com"
|
||||
"homepage": "http://moodle.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Moodle behat extension",
|
||||
@@ -958,7 +959,7 @@
|
||||
"Behat",
|
||||
"moodle"
|
||||
],
|
||||
"time": "2019-08-26T21:52:50+00:00"
|
||||
"time": "2019-10-28T11:42:45+00:00"
|
||||
},
|
||||
{
|
||||
"name": "myclabs/deep-copy",
|
||||
@@ -1043,18 +1044,18 @@
|
||||
"authors": [
|
||||
{
|
||||
"name": "Arne Blankerts",
|
||||
"role": "Developer",
|
||||
"email": "arne@blankerts.de"
|
||||
"email": "arne@blankerts.de",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Sebastian Heuer",
|
||||
"role": "Developer",
|
||||
"email": "sebastian@phpeople.de"
|
||||
"email": "sebastian@phpeople.de",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"role": "Developer",
|
||||
"email": "sebastian@phpunit.de"
|
||||
"email": "sebastian@phpunit.de",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
|
||||
@@ -1090,18 +1091,18 @@
|
||||
"authors": [
|
||||
{
|
||||
"name": "Arne Blankerts",
|
||||
"role": "Developer",
|
||||
"email": "arne@blankerts.de"
|
||||
"email": "arne@blankerts.de",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Sebastian Heuer",
|
||||
"role": "Developer",
|
||||
"email": "sebastian@phpeople.de"
|
||||
"email": "sebastian@phpeople.de",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"role": "Developer",
|
||||
"email": "sebastian@phpunit.de"
|
||||
"email": "sebastian@phpunit.de",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Library for handling version information and constraints",
|
||||
@@ -1163,26 +1164,26 @@
|
||||
},
|
||||
{
|
||||
"name": "phpdocumentor/reflection-docblock",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
|
||||
"reference": "bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c"
|
||||
"reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c",
|
||||
"reference": "bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/b83ff7cfcfee7827e1e78b637a5904fe6a96698e",
|
||||
"reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.0",
|
||||
"phpdocumentor/reflection-common": "^1.0.0",
|
||||
"phpdocumentor/type-resolver": "^0.4.0",
|
||||
"phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0",
|
||||
"phpdocumentor/type-resolver": "~0.4 || ^1.0.0",
|
||||
"webmozart/assert": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/instantiator": "~1.0.5",
|
||||
"doctrine/instantiator": "^1.0.5",
|
||||
"mockery/mockery": "^1.0",
|
||||
"phpunit/phpunit": "^6.4"
|
||||
},
|
||||
@@ -1210,29 +1211,29 @@
|
||||
}
|
||||
],
|
||||
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
|
||||
"time": "2019-04-30T17:48:53+00:00"
|
||||
"time": "2019-09-12T14:27:41+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpdocumentor/type-resolver",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpDocumentor/TypeResolver.git",
|
||||
"reference": "9c977708995954784726e25d0cd1dddf4e65b0f7"
|
||||
"reference": "cf842904952e64e703800d094cdf34e715a8a3ae"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7",
|
||||
"reference": "9c977708995954784726e25d0cd1dddf4e65b0f7",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/cf842904952e64e703800d094cdf34e715a8a3ae",
|
||||
"reference": "cf842904952e64e703800d094cdf34e715a8a3ae",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^5.5 || ^7.0",
|
||||
"php": "^7.0",
|
||||
"phpdocumentor/reflection-common": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^0.9.4",
|
||||
"phpunit/phpunit": "^5.2||^4.8.24"
|
||||
"mockery/mockery": "^1.0",
|
||||
"phpunit/phpunit": "^6.4"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
@@ -1242,9 +1243,7 @@
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"phpDocumentor\\Reflection\\": [
|
||||
"src/"
|
||||
]
|
||||
"phpDocumentor\\Reflection\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
@@ -1257,26 +1256,26 @@
|
||||
"email": "me@mikevanriel.com"
|
||||
}
|
||||
],
|
||||
"time": "2017-07-14T14:27:02+00:00"
|
||||
"time": "2017-12-30T13:23:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpspec/prophecy",
|
||||
"version": "1.8.1",
|
||||
"version": "1.9.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpspec/prophecy.git",
|
||||
"reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76"
|
||||
"reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/1927e75f4ed19131ec9bcc3b002e07fb1173ee76",
|
||||
"reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76",
|
||||
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/f6811d96d97bdf400077a0cc100ae56aa32b9203",
|
||||
"reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/instantiator": "^1.0.2",
|
||||
"php": "^5.3|^7.0",
|
||||
"phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
|
||||
"phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0",
|
||||
"sebastian/comparator": "^1.1|^2.0|^3.0",
|
||||
"sebastian/recursion-context": "^1.0|^2.0|^3.0"
|
||||
},
|
||||
@@ -1320,7 +1319,7 @@
|
||||
"spy",
|
||||
"stub"
|
||||
],
|
||||
"time": "2019-06-13T12:50:23+00:00"
|
||||
"time": "2019-10-03T11:07:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/dbunit",
|
||||
@@ -1425,8 +1424,8 @@
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"role": "lead",
|
||||
"email": "sebastian@phpunit.de"
|
||||
"email": "sebastian@phpunit.de",
|
||||
"role": "lead"
|
||||
}
|
||||
],
|
||||
"description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
|
||||
@@ -1473,8 +1472,8 @@
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"role": "lead",
|
||||
"email": "sb@sebastian-bergmann.de"
|
||||
"email": "sb@sebastian-bergmann.de",
|
||||
"role": "lead"
|
||||
}
|
||||
],
|
||||
"description": "FilterIterator implementation that filters files based on a list of suffixes.",
|
||||
@@ -1515,8 +1514,8 @@
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"role": "lead",
|
||||
"email": "sebastian@phpunit.de"
|
||||
"email": "sebastian@phpunit.de",
|
||||
"role": "lead"
|
||||
}
|
||||
],
|
||||
"description": "Simple template engine.",
|
||||
@@ -1564,8 +1563,8 @@
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"role": "lead",
|
||||
"email": "sb@sebastian-bergmann.de"
|
||||
"email": "sb@sebastian-bergmann.de",
|
||||
"role": "lead"
|
||||
}
|
||||
],
|
||||
"description": "Utility class for timing",
|
||||
@@ -1695,8 +1694,8 @@
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"role": "lead",
|
||||
"email": "sebastian@phpunit.de"
|
||||
"email": "sebastian@phpunit.de",
|
||||
"role": "lead"
|
||||
}
|
||||
],
|
||||
"description": "The PHP Unit Testing framework.",
|
||||
@@ -1869,16 +1868,16 @@
|
||||
},
|
||||
{
|
||||
"name": "psr/log",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/log.git",
|
||||
"reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd"
|
||||
"reference": "bf73deb2b3b896a9d9c75f3f0d88185d2faa27e2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
|
||||
"reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
|
||||
"url": "https://api.github.com/repos/php-fig/log/zipball/bf73deb2b3b896a9d9c75f3f0d88185d2faa27e2",
|
||||
"reference": "bf73deb2b3b896a9d9c75f3f0d88185d2faa27e2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1887,7 +1886,7 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
"dev-master": "1.1.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@@ -1912,7 +1911,7 @@
|
||||
"psr",
|
||||
"psr-3"
|
||||
],
|
||||
"time": "2018-11-20T15:27:04+00:00"
|
||||
"time": "2019-10-25T08:06:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ralouphie/getallheaders",
|
||||
@@ -2167,16 +2166,16 @@
|
||||
},
|
||||
{
|
||||
"name": "sebastian/exporter",
|
||||
"version": "3.1.1",
|
||||
"version": "3.1.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/exporter.git",
|
||||
"reference": "06a9a5947f47b3029d76118eb5c22802e5869687"
|
||||
"reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/06a9a5947f47b3029d76118eb5c22802e5869687",
|
||||
"reference": "06a9a5947f47b3029d76118eb5c22802e5869687",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e",
|
||||
"reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2230,7 +2229,7 @@
|
||||
"export",
|
||||
"exporter"
|
||||
],
|
||||
"time": "2019-08-11T12:43:14+00:00"
|
||||
"time": "2019-09-14T09:02:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/global-state",
|
||||
@@ -2505,8 +2504,8 @@
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"role": "lead",
|
||||
"email": "sebastian@phpunit.de"
|
||||
"email": "sebastian@phpunit.de",
|
||||
"role": "lead"
|
||||
}
|
||||
],
|
||||
"description": "Library that helps with managing the version number of Git-hosted PHP projects",
|
||||
@@ -2515,16 +2514,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/browser-kit",
|
||||
"version": "v3.4.31",
|
||||
"version": "v3.4.32",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/browser-kit.git",
|
||||
"reference": "1a3406a6b9b61b492592a816ca056afcc9e976c0"
|
||||
"reference": "abe4bf2c934ddd1fd490a7d9147df7827b5fff0f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/browser-kit/zipball/1a3406a6b9b61b492592a816ca056afcc9e976c0",
|
||||
"reference": "1a3406a6b9b61b492592a816ca056afcc9e976c0",
|
||||
"url": "https://api.github.com/repos/symfony/browser-kit/zipball/abe4bf2c934ddd1fd490a7d9147df7827b5fff0f",
|
||||
"reference": "abe4bf2c934ddd1fd490a7d9147df7827b5fff0f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2568,11 +2567,11 @@
|
||||
],
|
||||
"description": "Symfony BrowserKit Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2019-08-26T07:52:58+00:00"
|
||||
"time": "2019-09-10T10:13:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/class-loader",
|
||||
"version": "v3.4.31",
|
||||
"version": "v3.4.32",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/class-loader.git",
|
||||
@@ -2628,16 +2627,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/config",
|
||||
"version": "v3.4.31",
|
||||
"version": "v3.4.32",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/config.git",
|
||||
"reference": "24a60c0d7ad98a0fa5d1f892e9286095a389404f"
|
||||
"reference": "717ad66b5257e9752ae3c5722b5810bb4c40b236"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/config/zipball/24a60c0d7ad98a0fa5d1f892e9286095a389404f",
|
||||
"reference": "24a60c0d7ad98a0fa5d1f892e9286095a389404f",
|
||||
"url": "https://api.github.com/repos/symfony/config/zipball/717ad66b5257e9752ae3c5722b5810bb4c40b236",
|
||||
"reference": "717ad66b5257e9752ae3c5722b5810bb4c40b236",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2688,7 +2687,7 @@
|
||||
],
|
||||
"description": "Symfony Config Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2019-08-26T07:52:57+00:00"
|
||||
"time": "2019-09-19T15:32:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
@@ -2760,16 +2759,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/css-selector",
|
||||
"version": "v3.4.31",
|
||||
"version": "v3.4.32",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/css-selector.git",
|
||||
"reference": "e18c5c4b35e7f17513448a25d02f7af34a4bdb41"
|
||||
"reference": "f819f71ae3ba6f396b4c015bd5895de7d2f1f85f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/css-selector/zipball/e18c5c4b35e7f17513448a25d02f7af34a4bdb41",
|
||||
"reference": "e18c5c4b35e7f17513448a25d02f7af34a4bdb41",
|
||||
"url": "https://api.github.com/repos/symfony/css-selector/zipball/f819f71ae3ba6f396b4c015bd5895de7d2f1f85f",
|
||||
"reference": "f819f71ae3ba6f396b4c015bd5895de7d2f1f85f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2809,20 +2808,20 @@
|
||||
],
|
||||
"description": "Symfony CssSelector Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2019-08-20T13:31:17+00:00"
|
||||
"time": "2019-10-01T11:57:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/debug",
|
||||
"version": "v3.4.31",
|
||||
"version": "v3.4.32",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/debug.git",
|
||||
"reference": "0b600300918780001e2821db77bc28b677794486"
|
||||
"reference": "b3e7ce815d82196435d16dc458023f8fb6b36ceb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/debug/zipball/0b600300918780001e2821db77bc28b677794486",
|
||||
"reference": "0b600300918780001e2821db77bc28b677794486",
|
||||
"url": "https://api.github.com/repos/symfony/debug/zipball/b3e7ce815d82196435d16dc458023f8fb6b36ceb",
|
||||
"reference": "b3e7ce815d82196435d16dc458023f8fb6b36ceb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2865,7 +2864,7 @@
|
||||
],
|
||||
"description": "Symfony Debug Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2019-08-20T13:31:17+00:00"
|
||||
"time": "2019-09-19T15:32:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/dependency-injection",
|
||||
@@ -2939,16 +2938,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/dom-crawler",
|
||||
"version": "v3.4.31",
|
||||
"version": "v3.4.32",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/dom-crawler.git",
|
||||
"reference": "8558d1bc4554f5cb0b66e50377457967a8969263"
|
||||
"reference": "29cffc38a38f2a8ed7e494c9cea2f890a40c2359"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/8558d1bc4554f5cb0b66e50377457967a8969263",
|
||||
"reference": "8558d1bc4554f5cb0b66e50377457967a8969263",
|
||||
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/29cffc38a38f2a8ed7e494c9cea2f890a40c2359",
|
||||
"reference": "29cffc38a38f2a8ed7e494c9cea2f890a40c2359",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2992,11 +2991,11 @@
|
||||
],
|
||||
"description": "Symfony DomCrawler Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2019-08-26T07:52:58+00:00"
|
||||
"time": "2019-08-30T17:42:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher",
|
||||
"version": "v3.4.31",
|
||||
"version": "v3.4.32",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/event-dispatcher.git",
|
||||
@@ -3059,7 +3058,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v3.4.31",
|
||||
"version": "v3.4.32",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
@@ -3426,8 +3425,8 @@
|
||||
"authors": [
|
||||
{
|
||||
"name": "Arne Blankerts",
|
||||
"role": "Developer",
|
||||
"email": "arne@blankerts.de"
|
||||
"email": "arne@blankerts.de",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
|
||||
|
||||
@@ -812,6 +812,10 @@ $CFG->admin = 'admin';
|
||||
// Example:
|
||||
// $CFG->behat_faildump_path = '/my/path/to/save/failure/dumps';
|
||||
//
|
||||
// You can make behat pause upon failure to help you diagnose and debug problems with your tests.
|
||||
//
|
||||
// $CFG->behat_pause_on_fail = true;
|
||||
//
|
||||
// You can specify db, selenium wd_host etc. for behat parallel run by setting following variable.
|
||||
// Example:
|
||||
// $CFG->behat_parallel_run = array (
|
||||
|
||||
+3
-5
@@ -161,11 +161,9 @@ if ($editform->is_cancelled()) {
|
||||
|
||||
if (!empty($CFG->creatornewroleid) and !is_viewing($context, NULL, 'moodle/role:assign') and !is_enrolled($context, NULL, 'moodle/role:assign')) {
|
||||
// Deal with course creators - enrol them internally with default role.
|
||||
if (user_can_assign($context, $CFG->creatornewroleid)) {
|
||||
enrol_try_internal_enrol($course->id, $USER->id, $CFG->creatornewroleid);
|
||||
} else {
|
||||
enrol_try_internal_enrol($course->id, $USER->id);
|
||||
}
|
||||
// Note: This does not respect capabilities, the creator will be assigned the default role.
|
||||
// This is an expected behaviour. See MDL-66683 for further details.
|
||||
enrol_try_internal_enrol($course->id, $USER->id, $CFG->creatornewroleid);
|
||||
}
|
||||
|
||||
// The URL to take them to if they chose save and display.
|
||||
|
||||
@@ -1123,11 +1123,8 @@ class behat_course extends behat_base {
|
||||
protected function get_activity_element($element, $selectortype, $activityname) {
|
||||
$activitynode = $this->get_activity_node($activityname);
|
||||
|
||||
// Transforming to Behat selector/locator.
|
||||
list($selector, $locator) = $this->transform_selector($selectortype, $element);
|
||||
$exception = new ElementNotFoundException($this->getSession(), '"' . $element . '" "' . $selectortype . '" in "' . $activityname . '" ');
|
||||
|
||||
return $this->find($selector, $locator, $exception, $activitynode);
|
||||
$exception = new ElementNotFoundException($this->getSession(), "'{$element}' '{$selectortype}' in '${activityname}'");
|
||||
return $this->find($selectortype, $element, $exception, $activitynode);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -85,7 +85,6 @@ Feature: Managers can create courses
|
||||
| moodle/course:create | Allow |
|
||||
| moodle/course:manageactivities | Allow |
|
||||
| moodle/course:viewparticipants | Allow |
|
||||
| moodle/role:assign | Allow |
|
||||
And I log out
|
||||
And I log in as "kevin"
|
||||
And I am on site homepage
|
||||
@@ -96,20 +95,4 @@ Feature: Managers can create courses
|
||||
And I press "Save and display"
|
||||
And I follow "Participants"
|
||||
Then I should see "Kevin the"
|
||||
And I should not see "Teacher"
|
||||
And I log out
|
||||
Given I log in as "admin"
|
||||
And I define the allowed role assignments for the "Creator" role as:
|
||||
| Teacher | Assignable |
|
||||
And I log out
|
||||
And I log in as "kevin"
|
||||
And I am on site homepage
|
||||
And I turn editing mode on
|
||||
When I press "Add a new course"
|
||||
And I set the following fields to these values:
|
||||
| Course full name | My second course |
|
||||
| Course short name | mysecondcourse |
|
||||
And I press "Save and display"
|
||||
And I follow "Participants"
|
||||
Then I should see "Kevin the"
|
||||
And I should see "Teacher"
|
||||
|
||||
@@ -128,6 +128,7 @@ class helper {
|
||||
* @return array sql and params list
|
||||
*/
|
||||
protected static function get_users_sql_and_params($context, $search = '', $count = false) {
|
||||
global $DB, $USER;
|
||||
|
||||
// Fields we need from the user table.
|
||||
$extrafields = get_extra_user_fields($context);
|
||||
@@ -147,13 +148,33 @@ class helper {
|
||||
$select = "SELECT DISTINCT $ufields ";
|
||||
$orderby = " ORDER BY u.lastname ASC, u.firstname ASC";
|
||||
}
|
||||
|
||||
$groupjoinsql = '';
|
||||
$groupwheresql = '';
|
||||
$courseid = $context->instanceid;
|
||||
$groupmode = groups_get_course_groupmode(get_course($courseid));
|
||||
|
||||
// We're only interested in separate groups mode because it's the only group mode that requires the user to be a member of
|
||||
// specific group(s), except when they have the 'moodle/site:accessallgroups' capability.
|
||||
if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $context)) {
|
||||
// Fetch the groups that the user can see.
|
||||
$groups = groups_get_all_groups($courseid, $USER->id, 0, 'g.id');
|
||||
|
||||
// Add join condition to include users that only belong to the same group as the user.
|
||||
list($insql, $inparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED, 'gid', true, 0);
|
||||
$groupjoinsql = " JOIN {groups_members} gm ON gm.userid = u.id ";
|
||||
$groupwheresql = " AND gm.groupid $insql ";
|
||||
$params = array_merge($params, $inparams);
|
||||
}
|
||||
|
||||
$sql = "$select
|
||||
FROM {user} u
|
||||
JOIN {grade_grades_history} ggh ON u.id = ggh.userid
|
||||
JOIN {grade_items} gi ON gi.id = ggh.itemid
|
||||
WHERE $filtersql gi.courseid = :courseid";
|
||||
$groupjoinsql
|
||||
WHERE $filtersql gi.courseid = :courseid $groupwheresql";
|
||||
$sql .= $orderby;
|
||||
$params['courseid'] = $context->instanceid;
|
||||
$params['courseid'] = $courseid;
|
||||
|
||||
return array($sql, $params);
|
||||
}
|
||||
@@ -166,18 +187,31 @@ class helper {
|
||||
* @return array list of graders.
|
||||
*/
|
||||
public static function get_graders($courseid) {
|
||||
global $DB;
|
||||
global $DB, $USER;
|
||||
|
||||
$groupjoinsql = $groupwheresql = '';
|
||||
$inparams = [];
|
||||
$groupmode = groups_get_course_groupmode(get_course($courseid));
|
||||
if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', \context_course::instance($courseid))) {
|
||||
// Fetch the groups that the user can see.
|
||||
$groups = groups_get_all_groups($courseid, $USER->id, 0, 'g.id');
|
||||
// Add join condition to include users that only belong to the same group as the user.
|
||||
list($insql, $inparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED, 'gid', true, 0);
|
||||
$groupjoinsql = " JOIN {groups_members} gm ON gm.userid = u.id ";
|
||||
$groupwheresql = " AND gm.groupid $insql ";
|
||||
}
|
||||
|
||||
$ufields = get_all_user_name_fields(true, 'u');
|
||||
$sql = "SELECT u.id, $ufields
|
||||
FROM {user} u
|
||||
JOIN {grade_grades_history} ggh ON ggh.usermodified = u.id
|
||||
JOIN {grade_items} gi ON gi.id = ggh.itemid
|
||||
WHERE gi.courseid = :courseid
|
||||
$groupjoinsql
|
||||
WHERE gi.courseid = :courseid $groupwheresql
|
||||
GROUP BY u.id, $ufields
|
||||
ORDER BY u.lastname ASC, u.firstname ASC";
|
||||
|
||||
$graders = $DB->get_records_sql($sql, array('courseid' => $courseid));
|
||||
$graders = $DB->get_records_sql($sql, array('courseid' => $courseid) + $inparams);
|
||||
$return = array(0 => get_string('allgraders', 'gradereport_history'));
|
||||
foreach ($graders as $grader) {
|
||||
$return[$grader->id] = fullname($grader);
|
||||
|
||||
@@ -324,7 +324,7 @@ class tablelog extends \table_sql implements \renderable {
|
||||
* @return array containing sql to use and an array of params.
|
||||
*/
|
||||
protected function get_filters_sql_and_params() {
|
||||
global $DB;
|
||||
global $DB, $USER;
|
||||
|
||||
$coursecontext = $this->context;
|
||||
$filter = 'gi.courseid = :courseid';
|
||||
@@ -355,6 +355,16 @@ class tablelog extends \table_sql implements \renderable {
|
||||
$params += array('grader' => $this->filters->grader);
|
||||
}
|
||||
|
||||
// If the course is separate group mode and the current user is not allowed to see all groups make sure
|
||||
// that we display only users from the same groups as current user.
|
||||
$groupmode = get_course($coursecontext->instanceid)->groupmode;
|
||||
if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $coursecontext)) {
|
||||
$groupids = array_column(groups_get_all_groups($coursecontext->instanceid, $USER->id, 0, 'g.id'), 'id');
|
||||
list($gsql, $gparams) = $DB->get_in_or_equal($groupids, SQL_PARAMS_NAMED, 'gmuparam', true, 0);
|
||||
$filter .= " AND EXISTS (SELECT 1 FROM {groups_members} gmu WHERE gmu.userid=ggh.userid AND gmu.groupid $gsql)";
|
||||
$params += $gparams;
|
||||
}
|
||||
|
||||
return array($filter, $params);
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,12 @@ class gradereport_history_report_testcase extends advanced_testcase {
|
||||
$u5 = $this->getDataGenerator()->create_user();
|
||||
$grader1 = $this->getDataGenerator()->create_user();
|
||||
$grader2 = $this->getDataGenerator()->create_user();
|
||||
self::getDataGenerator()->enrol_user($grader1->id, $c1->id, 'teacher');
|
||||
self::getDataGenerator()->enrol_user($grader2->id, $c1->id, 'teacher');
|
||||
self::getDataGenerator()->enrol_user($u2->id, $c1->id, 'student');
|
||||
self::getDataGenerator()->enrol_user($u3->id, $c1->id, 'student');
|
||||
self::getDataGenerator()->enrol_user($u4->id, $c1->id, 'student');
|
||||
self::getDataGenerator()->enrol_user($u5->id, $c1->id, 'student');
|
||||
|
||||
// Modules.
|
||||
$c1m1 = $this->getDataGenerator()->create_module('assign', array('course' => $c1));
|
||||
@@ -65,6 +71,8 @@ class gradereport_history_report_testcase extends advanced_testcase {
|
||||
$giparams = array('itemtype' => 'mod', 'itemmodule' => 'assign');
|
||||
$grades = array();
|
||||
|
||||
$this->setUser($grader1);
|
||||
|
||||
$gi = grade_item::fetch($giparams + array('iteminstance' => $c1m1->id));
|
||||
$grades['c1m1u1'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id,
|
||||
'timemodified' => time() - 3600));
|
||||
@@ -163,6 +171,20 @@ class gradereport_history_report_testcase extends advanced_testcase {
|
||||
$this->assertEquals(null, $results[$grades['c2m2u1a']->id]->prevgrade);
|
||||
$this->assertEquals($grades['c2m2u1a']->finalgrade, $results[$grades['c2m2u1c']->id]->prevgrade);
|
||||
$this->assertEquals($grades['c2m2u1c']->finalgrade, $results[$grades['c2m2u1e']->id]->prevgrade);
|
||||
|
||||
// Put course in separate groups mode, add grader1 and two students to the same group.
|
||||
$c1->groupmode = SEPARATEGROUPS;
|
||||
update_course($c1);
|
||||
$this->assertFalse(has_capability('moodle/site:accessallgroups', \context_course::instance($c1->id)));
|
||||
$g1 = self::getDataGenerator()->create_group(['courseid' => $c1->id, 'name' => 'g1']);
|
||||
self::getDataGenerator()->create_group_member(['groupid' => $g1->id, 'userid' => $grader1->id]);
|
||||
self::getDataGenerator()->create_group_member(['groupid' => $g1->id, 'userid' => $u1->id]);
|
||||
self::getDataGenerator()->create_group_member(['groupid' => $g1->id, 'userid' => $u2->id]);
|
||||
$this->assertEquals(2, $this->get_tablelog_results($c1ctx, array(), true));
|
||||
|
||||
// Grader2 is not in any groups.
|
||||
$this->setUser($grader2);
|
||||
$this->assertEquals(0, $this->get_tablelog_results($c1ctx, array(), true));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,6 +241,118 @@ class gradereport_history_report_testcase extends advanced_testcase {
|
||||
$this->assertEquals(1, \gradereport_history\helper::get_users_count($c1ctx, 'c'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider method for \gradereport_history_report_testcase::test_get_users_with_groups()
|
||||
*/
|
||||
public function get_users_provider() {
|
||||
return [
|
||||
'Visible groups, non-editing teacher, not in any group' => [
|
||||
VISIBLEGROUPS, 'teacher', ['g1', 'g2'], ['s1', 's2', 's3', 's4', 's5']
|
||||
],
|
||||
'Visible groups, non-editing teacher' => [
|
||||
VISIBLEGROUPS, 'teacher', [], ['s1', 's2', 's3', 's4', 's5']
|
||||
],
|
||||
'Visible groups, editing teacher' => [
|
||||
VISIBLEGROUPS, 'editingteacher', ['g1', 'g2'], ['s1', 's2', 's3', 's4', 's5']
|
||||
],
|
||||
'Separate groups, non-editing teacher' => [
|
||||
SEPARATEGROUPS, 'teacher', ['g1', 'g2'], ['s1', 's2']
|
||||
],
|
||||
'Separate groups, non-editing teacher, not in any group' => [
|
||||
SEPARATEGROUPS, 'teacher', [], []
|
||||
],
|
||||
'Separate groups, non-editing teacher and student share two groups' => [
|
||||
SEPARATEGROUPS, 'teacher', ['g4', 'g5'], ['s5']
|
||||
],
|
||||
'Separate groups, editing teacher' => [
|
||||
SEPARATEGROUPS, 'editingteacher', ['g1', 'g2'], ['s1', 's2', 's3', 's4', 's5']
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for helper::get_users() with course group mode set.
|
||||
*
|
||||
* @dataProvider get_users_provider
|
||||
* @param $groupmode
|
||||
* @param $teacherrole
|
||||
* @param $teachergroups
|
||||
* @param $expectedusers
|
||||
*/
|
||||
public function test_get_users_with_groups($groupmode, $teacherrole, $teachergroups, $expectedusers) {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = $this->getDataGenerator();
|
||||
|
||||
// Create a test course.
|
||||
$course = $generator->create_course(['groupmode' => $groupmode]);
|
||||
|
||||
// Create an assignment module.
|
||||
$assign = $generator->create_module('assign', ['course' => $course]);
|
||||
|
||||
// Fetch roles.
|
||||
$role = $DB->get_record('role', ['shortname' => $teacherrole], '*', MUST_EXIST);
|
||||
$studentrole = $DB->get_record('role', ['shortname' => 'student'], '*', MUST_EXIST);
|
||||
|
||||
// Create users.
|
||||
$t1 = $generator->create_user(['username' => 't1', 'email' => 't1@example.com']);
|
||||
$s1 = $generator->create_user(['username' => 's1', 'email' => 's1@example.com']);
|
||||
$s2 = $generator->create_user(['username' => 's2', 'email' => 's2@example.com']);
|
||||
$s3 = $generator->create_user(['username' => 's3', 'email' => 's3@example.com']);
|
||||
$s4 = $generator->create_user(['username' => 's4', 'email' => 's4@example.com']);
|
||||
$s5 = $generator->create_user(['username' => 's5', 'email' => 's5@example.com']);
|
||||
|
||||
// Enrol users.
|
||||
$generator->enrol_user($t1->id, $course->id, $role->id);
|
||||
$generator->enrol_user($s1->id, $course->id, $studentrole->id);
|
||||
$generator->enrol_user($s2->id, $course->id, $studentrole->id);
|
||||
$generator->enrol_user($s3->id, $course->id, $studentrole->id);
|
||||
$generator->enrol_user($s4->id, $course->id, $studentrole->id);
|
||||
$generator->enrol_user($s5->id, $course->id, $studentrole->id);
|
||||
|
||||
// Create groups.
|
||||
$groups = [];
|
||||
$groups['g1'] = $generator->create_group(['courseid' => $course->id, 'name' => 'g1']);
|
||||
$groups['g2'] = $generator->create_group(['courseid' => $course->id, 'name' => 'g2']);
|
||||
$groups['g3'] = $generator->create_group(['courseid' => $course->id, 'name' => 'g3']);
|
||||
$groups['g4'] = $generator->create_group(['courseid' => $course->id, 'name' => 'g4']);
|
||||
$groups['g5'] = $generator->create_group(['courseid' => $course->id, 'name' => 'g5']);
|
||||
|
||||
// Add teacher to the assigned groups.
|
||||
foreach ($teachergroups as $groupname) {
|
||||
$group = $groups[$groupname];
|
||||
$generator->create_group_member(['groupid' => $group->id, 'userid' => $t1->id]);
|
||||
}
|
||||
|
||||
// Add students to groups.
|
||||
$generator->create_group_member(['groupid' => $groups['g1']->id, 'userid' => $s1->id]);
|
||||
$generator->create_group_member(['groupid' => $groups['g2']->id, 'userid' => $s2->id]);
|
||||
$generator->create_group_member(['groupid' => $groups['g3']->id, 'userid' => $s3->id]);
|
||||
$generator->create_group_member(['groupid' => $groups['g4']->id, 'userid' => $s5->id]);
|
||||
$generator->create_group_member(['groupid' => $groups['g5']->id, 'userid' => $s5->id]);
|
||||
|
||||
// Creating grade history for the students.
|
||||
$gi = grade_item::fetch(['iteminstance' => $assign->id, 'itemtype' => 'mod', 'itemmodule' => 'assign']);
|
||||
$this->create_grade_history(['itemid' => $gi->id, 'userid' => $s1->id]);
|
||||
$this->create_grade_history(['itemid' => $gi->id, 'userid' => $s2->id]);
|
||||
$this->create_grade_history(['itemid' => $gi->id, 'userid' => $s3->id]);
|
||||
$this->create_grade_history(['itemid' => $gi->id, 'userid' => $s4->id]);
|
||||
$this->create_grade_history(['itemid' => $gi->id, 'userid' => $s5->id]);
|
||||
|
||||
// Log in as the teacher.
|
||||
$this->setUser($t1);
|
||||
|
||||
// Fetch the users.
|
||||
$users = \gradereport_history\helper::get_users(context_course::instance($course->id));
|
||||
// Confirm that the number of users fetched is the same as the count of expected users.
|
||||
$this->assertCount(count($expectedusers), $users);
|
||||
foreach ($users as $user) {
|
||||
// Confirm that each user returned is in the list of expected users.
|
||||
$this->assertTrue(in_array($user->username, $expectedusers));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the get graders helper method.
|
||||
*/
|
||||
@@ -228,9 +362,11 @@ class gradereport_history_report_testcase extends advanced_testcase {
|
||||
// Making the setup.
|
||||
$c1 = $this->getDataGenerator()->create_course();
|
||||
$c2 = $this->getDataGenerator()->create_course();
|
||||
$c3 = $this->getDataGenerator()->create_course(['groupmode' => SEPARATEGROUPS]);
|
||||
|
||||
$c1m1 = $this->getDataGenerator()->create_module('assign', array('course' => $c1));
|
||||
$c2m1 = $this->getDataGenerator()->create_module('assign', array('course' => $c2));
|
||||
$c3m1 = $this->getDataGenerator()->create_module('assign', array('course' => $c3));
|
||||
|
||||
// Users.
|
||||
$u1 = $this->getDataGenerator()->create_user(array('firstname' => 'Eric', 'lastname' => 'Cartman'));
|
||||
@@ -238,6 +374,12 @@ class gradereport_history_report_testcase extends advanced_testcase {
|
||||
$u3 = $this->getDataGenerator()->create_user(array('firstname' => 'Kyle', 'lastname' => 'Broflovski'));
|
||||
$u4 = $this->getDataGenerator()->create_user(array('firstname' => 'Kenny', 'lastname' => 'McCormick'));
|
||||
|
||||
foreach ([$c1, $c2, $c3] as $course) {
|
||||
foreach ([$u1, $u2, $u3, $u4] as $user) {
|
||||
self::getDataGenerator()->enrol_user($user->id, $course->id, 'student');
|
||||
}
|
||||
}
|
||||
|
||||
// Creating grade history for some users.
|
||||
$gi = grade_item::fetch(array('iteminstance' => $c1m1->id, 'itemtype' => 'mod', 'itemmodule' => 'assign'));
|
||||
$this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id, 'usermodified' => $u1->id));
|
||||
@@ -247,6 +389,10 @@ class gradereport_history_report_testcase extends advanced_testcase {
|
||||
$gi = grade_item::fetch(array('iteminstance' => $c2m1->id, 'itemtype' => 'mod', 'itemmodule' => 'assign'));
|
||||
$this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id, 'usermodified' => $u4->id));
|
||||
|
||||
$gi = grade_item::fetch(array('iteminstance' => $c3m1->id, 'itemtype' => 'mod', 'itemmodule' => 'assign'));
|
||||
$this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id, 'usermodified' => $u1->id));
|
||||
$this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u2->id, 'usermodified' => $u2->id));
|
||||
|
||||
// Checking fetching some users.
|
||||
$graders = \gradereport_history\helper::get_graders($c1->id);
|
||||
$this->assertCount(4, $graders); // Including "all graders" .
|
||||
@@ -256,6 +402,17 @@ class gradereport_history_report_testcase extends advanced_testcase {
|
||||
$graders = \gradereport_history\helper::get_graders($c2->id);
|
||||
$this->assertCount(2, $graders); // Including "all graders" .
|
||||
$this->assertArrayHasKey($u4->id, $graders);
|
||||
|
||||
// Third course is in separate groups mode. Only graders from the same group will be returned.
|
||||
$g = self::getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'g1']);
|
||||
self::getDataGenerator()->create_group_member(['groupid' => $g->id, 'userid' => $u1->id]);
|
||||
self::getDataGenerator()->create_group_member(['groupid' => $g->id, 'userid' => $u2->id]);
|
||||
$this->setUser($u1);
|
||||
$graders = \gradereport_history\helper::get_graders($c3->id);
|
||||
$this->assertCount(3, $graders); // Including "all graders" .
|
||||
$this->setUser($u3);
|
||||
$graders = \gradereport_history\helper::get_graders($c3->id);
|
||||
$this->assertCount(1, $graders); // Including "all graders" .
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -195,9 +195,9 @@ Feature: We can set the grade to pass value
|
||||
And I add a "Forum" to section "1" and I fill the form with:
|
||||
| Forum name | Test Forum 1 |
|
||||
| Description | Test |
|
||||
| Aggregate type | Average of ratings |
|
||||
| Grade to pass | 90 |
|
||||
| scale[modgrade_point] | 60 |
|
||||
| Ratings > Aggregate type | Average of ratings |
|
||||
| Ratings > scale[modgrade_point] | 60 |
|
||||
| Grade > Grade to pass | 90 |
|
||||
Then I should see "The grade to pass can not be greater than the maximum possible grade 60"
|
||||
|
||||
Scenario: Set a valid grade to pass for forum activity
|
||||
@@ -205,8 +205,8 @@ Feature: We can set the grade to pass value
|
||||
And I add a "Forum" to section "1" and I fill the form with:
|
||||
| Forum name | Test Forum 1 |
|
||||
| Description | Test |
|
||||
| Aggregate type | Average of ratings |
|
||||
| Grade to pass | 90 |
|
||||
| Ratings > Aggregate type | Average of ratings |
|
||||
| Grade > Grade to pass | 90 |
|
||||
And I navigate to "View > Grader report" in the course gradebook
|
||||
And I turn editing mode on
|
||||
And I click on "Edit forum Test Forum 1" "link"
|
||||
|
||||
@@ -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'] = 'ar';
|
||||
$string['thislanguage'] = 'بالعربي workplace';
|
||||
@@ -46,7 +46,7 @@ $string['downloadedfilecheckfailed'] = 'Ha fallat la comprovació del fitxer bai
|
||||
$string['invalidmd5'] = 'L\'md5 no és vàlid. Torneu a provar-ho';
|
||||
$string['missingrequiredfield'] = 'Falta algun camp necessari';
|
||||
$string['remotedownloaderror'] = '<p>No s\'ha pogut baixar el component al vostre servidor. Verifiqueu els paràmetres del servidor intermediari. Es recomana vivament l\'extensió cURL de PHP.</p>
|
||||
<p>Haureu de baixar manualment el fitxer <a href="{$a->url}">{$a->url}</a>, copiar-lo a la ubicació «{$a->dest}» del vostre servidor i descomprimir-lo allí.</p>';
|
||||
<p>Haureu de baixar manualment el fitxer <a href="{$a->url}">{$a->url}</a>, copiar-lo a la ubicació «{$a->dest}» del vostre servidor i descomprimir-lo allà.</p>';
|
||||
$string['wrongdestpath'] = 'El camí de destinació és erroni';
|
||||
$string['wrongsourcebase'] = 'L\'adreça (URL) base de la font és errònia';
|
||||
$string['wrongzipfilename'] = 'El nom del fitxer ZIP és erroni';
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* 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['thislanguage'] = 'Valencià_RACV';
|
||||
@@ -45,7 +45,7 @@ $string['datarootpermission'] = 'Zugriffsrechte zum Datenverzeichnis';
|
||||
$string['dbprefix'] = 'Tabellen-Prefix';
|
||||
$string['dirroot'] = 'Moodle-Verzeichnis';
|
||||
$string['environmenthead'] = 'Installationsvoraussetzungen werden geprüft ...';
|
||||
$string['environmentsub2'] = 'Jede Version hat Mindestvoraussetzungen für der PHP-Version und für verbindliche PHP-Extensions. Vor einer Installation oder einer Aktualisierung wird eine vollständige Prüfung durchgeführt. Bitte fragen Sie den Administrator des Servers, wenn Sie mit der Installation einer neuen Version oder mit der Aktivierung von PHP-Extensions nicht weiterkommen.';
|
||||
$string['environmentsub2'] = 'Jede Version hat Mindestvoraussetzungen für der PHP-Version und für verbindliche PHP-Extensions. Vor einer Installation oder einer Aktualisierung wird eine vollständige Prüfung durchgeführt. Bitte fragen Sie die Administrator/innen des Servers, wenn Sie mit der Installation einer neuen Version oder mit der Aktivierung von PHP-Extensions nicht weiterkommen.';
|
||||
$string['errorsinenvironment'] = 'Fehler bei der Prüfung der Systemvoraussetzungen!';
|
||||
$string['installation'] = 'Installation';
|
||||
$string['langdownloaderror'] = 'Das Sprachpaket \'{$a}\' konnte nicht heruntergeladen werden. Die Installation wird in englischer Sprache fortgesetzt.';
|
||||
@@ -63,7 +63,7 @@ $string['pathserrcreatedataroot'] = 'Das Datenverzeichnis ({$a->dataroot}) kann
|
||||
$string['pathshead'] = 'Pfade bestätigen';
|
||||
$string['pathsrodataroot'] = 'Das Verzeichnis dataroot ist schreibgeschützt.';
|
||||
$string['pathsroparentdataroot'] = 'Das Verzeichnis ({$a->parent}) ist schreibgeschützt. Deswegen kann das Datenverzeichnis ({$a->dataroot}) vom Installer nicht angelegt werden.';
|
||||
$string['pathssubadmindir'] = 'Einige Webserver benutzen /admin als speziellen Link, um auf Einstellungsseiten oder Ähnliches zu verweisen. Unglücklicherweise kollidiert dies mit dem standardmäßigen Verzeichnis für die Moodle-Administration. Sie können dieses Problem beheben, indem Sie das Verzeichnis admin in Ihrer Moodle-Installation umbenennen und den neuen Namen hier eingeben (z.B. <em>moodleadmin</em>). Mit dieser Änderung werden alle Admin-Links korrigiert.';
|
||||
$string['pathssubadmindir'] = 'Einige Webserver benutzen /admin als speziellen Link, um auf Einstellungsseiten oder Ähnliches zu verweisen. Unglücklicherweise kollidiert dies mit dem Verzeichnis für die Moodle-Administration. Sie können dieses Problem beheben, indem Sie das Verzeichnis admin in Ihrer Moodle-Installation umbenennen und den neuen Namen hier eingeben (z.B. <em>moodleadmin</em>). Mit dieser Änderung werden alle Admin-Links korrigiert.';
|
||||
$string['pathssubdataroot'] = '<p>Sie benötigen einen Platz, wo Moodle hochgeladene Dateien abspeichern kann. </p><p>Dieses Verzeichnis muss Lese- und Schreibrechte für das Nutzerkonto besitzen, mit dem Ihr Webservers läuft (üblicherweise \'nobody\', \'apache\' oder \'www-data).</p><p> Außerdem sollte das Verzeichnis nicht direkt aus dem Internet erreichbar sein. </p><p>Das Intallationsskript wird versuchen, ein solches Verzeichnis zu erstellen, falls es nicht existiert.</p>';
|
||||
$string['pathssubdirroot'] = '<p>Vollständiger Pfad der Moodle-Installation.</p>';
|
||||
$string['pathssubwwwroot'] = '<p>Webadresse, die ein Nutzer im Browser eingibt, um auf Moodle zuzugreifen.</p><p> Es ist nicht möglich, über unterschiedliche Adressen auf Moodle zuzugreifen. Sollte Ihre Website mehrere öffentliche Adressen verwenden, so müssen Sie eine Adresse festlegen und für die übrigen Adressen dauerhafte Weiterleitungen dorthin einrichten.</p>
|
||||
|
||||
@@ -36,8 +36,9 @@ $string['cliincorrectvalueerror'] = 'Σφάλμα, λανθασμένη τιμή
|
||||
$string['cliincorrectvalueretry'] = 'Λανθασμένη τιμή. Παρακαλούμε προσπαθήστε ξανά.';
|
||||
$string['clitypevalue'] = 'πληκτρολογήστε μια τιμή';
|
||||
$string['clitypevaluedefault'] = 'πληκτρολογήστε μια τιμή· πατήστε Enter για να χρησιμοποιήσετε τηνπροεπιλεγμένη τιμή ({$a})';
|
||||
$string['cliunknowoption'] = 'Μη αναγνωρίσιμες επιλογές: {$a}<br />
|
||||
Παρακαλούμε χρησιμοποιήστε την επιλογή --βοήθεια';
|
||||
$string['cliunknowoption'] = 'Μη αναγνωρίσιμες επιλογές:
|
||||
{$a}<br />
|
||||
Παρακαλούμε χρησιμοποιήστε την επιλογή --help (βοήθεια)';
|
||||
$string['cliyesnoprompt'] = 'πατήστε y (σημαίνει yes=ναι) ή πατήστε n (σημαίνει no=όχι)';
|
||||
$string['environmentrequireinstall'] = 'απαιτείται να εγκατασταθεί/ ενεργοποιηθεί';
|
||||
$string['environmentrequireversion'] = 'απαιτείται η έκδοση {$a->needed} ενώ εσείς έχετε την {$a->current}';
|
||||
|
||||
@@ -42,9 +42,9 @@ $string['cannotunzipfile'] = 'Αδυναμία αποσυμπίεσης αρχε
|
||||
$string['componentisuptodate'] = 'Το στοιχείο λογισμικού είναι ενημερωμένο.';
|
||||
$string['dmlexceptiononinstall'] = '<p>Παρουσιάστηκε κάποιο σφάλμα βάσης δεδομένων [{$a->errorcode}].<br />{$a->debuginfo}</p>';
|
||||
$string['downloadedfilecheckfailed'] = 'Αποτυχία ελέγχου αρχείου που έγινε λήψη';
|
||||
$string['invalidmd5'] = 'Μη έγκυρο md5';
|
||||
$string['invalidmd5'] = 'Η μεταβλητή ελέγχου ήταν λανθασμένη - δοκιμάστε ξανά';
|
||||
$string['missingrequiredfield'] = 'Κάποιο απαιτούμενο πεδίο λείπει';
|
||||
$string['remotedownloaderror'] = '<p>Απέτυχε η λήψη του τμήματος στον εξυπηρετητή σας. Παρακαλούμε επιβεβαιώστε τις ρυθμίσεις του διακομιστή μεσολάβησης (proxy)· η επέκταση PHP cURL συνιστάται θερμά.</p> <p>Πρέπει να κατεβάσετε το <a href="{$a->url}">{$a->url}</a> χειροκίνητα, να το αντιγράψετε στο «{$a->dest}» στον εξυπηρετητή σας και να το αποσυμπιέσετε εκεί.</p>';
|
||||
$string['wrongdestpath'] = 'Λανθασμένη διαδρομή προορισμού (πλήρες όνομα).';
|
||||
$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.';
|
||||
|
||||
@@ -37,7 +37,7 @@ $string['chooselanguagesub'] = 'Παρακαλούμε, επιλέξτε γλώ
|
||||
$string['clialreadyconfigured'] = 'Το αρχείο ρυθμίσεων config.php υπάρχει ήδη. Χρησιμοποιήστε το admin/cli/install_database.php για να εγκαταστήσετε το Moodle για αυτόν τον ιστότοπο.';
|
||||
$string['clialreadyinstalled'] = 'Το αρχείο ρυθμίσεων config.php υπάρχει ήδη. Χρησιμοποιήστε το admin/cli/install_database.php για να αναβαθμίσετε το Moodle για αυτόν τον ιστότοπο.';
|
||||
$string['cliinstallheader'] = 'Moodle {$a} πρόγραμμα εγκατάστασης γραμμής εντολών';
|
||||
$string['databasehost'] = 'Κεντρικός Υπολογιστής Βάσης Δεδομένων';
|
||||
$string['databasehost'] = 'Κεντρικός H/Y που φιλοξενεί την Βάση Δεδομένων';
|
||||
$string['databasename'] = 'Όνομα Βάσης Δεδομένων';
|
||||
$string['databasetypehead'] = 'Επιλογή οδηγού βάσης δεδομένων';
|
||||
$string['dataroot'] = 'Φάκελος δεδομένων';
|
||||
@@ -67,15 +67,15 @@ $string['pathserrcreatedataroot'] = 'Ο Φάκελος δεδομένων ({$a->
|
||||
$string['pathshead'] = 'Επιβεβαίωση Διαδρομών';
|
||||
$string['pathsrodataroot'] = 'Ο Φάκελος Δεδομένων δεν είναι εγγράψιμος.';
|
||||
$string['pathsroparentdataroot'] = 'Ο γονικός φάκελος ({$a->parent}) δεν είναι εγγράψιμος. Ο φάκελος δεδομένων ({$a->dataroot}) δεν μπορεί να δημιουργθεί από το πρόγραμμα εγκατάστασης.';
|
||||
$string['pathssubadmindir'] = 'Κάποιοι λίγοι κεντρικοί υπολογιστές ιστού χρησιμοποιούν το /admin ως ειδική διεύθυνση URL για την πρόσβαση σε κάποιο πίνακα ελέγχου ή κάτι τέτοιο. Δυστυχώς αυτό έρχεται σε αντίθεση με την τυπική τοποθεσία των σελίδων διαχείρισης (admin) του Moodle. Αυτό μπορεί να διορθωθεί με την μετονομασία του admin φακέλου στην εγκατάστασή σας, και βάζοντας αυτό το καινούργιο όνομα εδώ. Για παράδειγμα: <em>moodleadmin</em>. Αυτό θα διορθώσει όλους τους συνδέσμους με το admin στην διεύθυνσή τους σε όλη την εγκατάσταση του Moodle σας.';
|
||||
$string['pathssubadmindir'] = 'Κάποιοι λίγοι κεντρικοί υπολογιστές ιστού χρησιμοποιούν το /admin ως ειδική διεύθυνση URL για την πρόσβαση σε κάποιο πίνακα ελέγχου ή κάτι τέτοιο. Δυστυχώς αυτό έρχεται σε αντίθεση με την τυπική τοποθεσία των σελίδων διαχείρισης (admin) του Moodle. Αυτό μπορεί να διορθωθεί με την μετονομασία του admin φακέλου στην εγκατάστασή σας, και βάζοντας αυτό το καινούργιο όνομα εδώ. Για παράδειγμα: <em>moodleadmin</em>. Αυτό θα διορθώσει όλους τους συνδέσμους με το admin στην διεύθυνσή τους σε όλη την εγκατάσταση του Moodle σας.';
|
||||
$string['pathssubdataroot'] = '<p>Ένας φάκελος όπου το Moodle θα αποθηκεύει όλα τα ανεβασμένα από τους χρήστες αρχεία.</p> <p>Αυτος ο φάκελος θα πρέπει να είναι αναγνώσιμος ΚΑΙ ΕΓΓΡΑΨΙΜΟΣ από τον χρήστη του εξυπηρετητή ιστού (συνήθως «nobody» ή «apache»).</p> <p>Δεν πρέπει να είναι προσβάσιμος κατευθείαν από τον ιστό.</p> <p>Αν ο φάκελος δεν υπάρχει, η διαδικασία εγκατάστασης θα προσπαθήσει να τον δημιουργήσει.</p>';
|
||||
$string['pathssubdirroot'] = '<p>Η πλήρης διαδρομή του φακέλου που περιέχει τα αρχεία κώδικα του Moodle.</p>';
|
||||
$string['pathssubwwwroot'] = '<p>Η πλήρης διεύθυνση (ιστού) από την οποία θα γίνεται η πρόσβαση στο Moodle, δηλαδή η διεύθυνση που οι χρήστες θα εισάγουν στην γραμμή διεύθυνσης του περιηγητή, για να έχουν πρόσβαση στου Moodle.</p>
|
||||
$string['pathssubwwwroot'] = '<p>Η πλήρης διεύθυνση από την οποία θα γίνεται η πρόσβαση στο Moodle, δηλαδή η διεύθυνση που οι χρήστες θα εισάγουν στην γραμμή διεύθυνσης του περιηγητή, για να έχουν πρόσβαση στου Moodle.</p>
|
||||
<p>Δεν είναι δυνατόν να έχετε πρόβαση στο Moodle χρησιμοποιώντας πολλαπλές διευθύνσεις. Εάν ο ιστότοπος θα είναι προσβάσιμος μέσω πολλαπλών διευθύνσεων τότε επιλέξτε την ευκολότερη και εγκαταστήστε μια μόνιμη ανακατεύθυνση για καθεμία από τις άλλες διευθύνσεις.</p>
|
||||
<p>Εάν ο ιστότοπός σας είναι προσβάσιμος τόσο από το Διαδίκτυο όσο και από ένα εσωτερικό δίκτυο (που συχνά λέγεται intranet) τότε χρησιμοποιήστε εδώ την δημόσια διεύθυνση.</p>
|
||||
<p>Αν η τρέχουσα διεύθυνση δεν είναι σωστή, παρακαλούμε αλλάξτε την URL διεύθυνση στην γραμμή διευθύνσεων του περιηγητή σας και επανεκκινήστε την εγκατάσταση.</p>';
|
||||
$string['pathsunsecuredataroot'] = 'Η τοποθεσία του Φάκελου Δεδομένων δεν είναι ασφαλής';
|
||||
$string['pathswrongadmindir'] = 'Ο Φάκελος Admin δεν υπάρχει';
|
||||
$string['pathswrongadmindir'] = 'Ο φάκελος Admin δεν υπάρχει';
|
||||
$string['phpextension'] = 'Επέκταση {$a} της PHP';
|
||||
$string['phpversion'] = 'Έκδοση της PHP';
|
||||
$string['phpversionhelp'] = 'p>Το Moodle απαιτεί η έκδοση της PHP να είναι τουλάχιστον 5.6.5 ή 7.1 (η 7.0.x έχει κάποιους περιορισμούς στη μηχανή).</p>
|
||||
|
||||
@@ -31,4 +31,5 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['parentlanguage'] = 'es';
|
||||
$string['thisdirection'] = 'ltr';
|
||||
$string['thislanguage'] = 'Español para la Empresa';
|
||||
|
||||
@@ -46,7 +46,7 @@ $string['dmlexceptiononinstall'] = '<p>Datu-basearen errorea gertatu da [{$a->er
|
||||
$string['downloadedfilecheckfailed'] = 'Jaitsitako fitxategiaren egiaztatzeak huts egin du.';
|
||||
$string['invalidmd5'] = 'Kontrolerako aldagaia gaizki zegoen - saiatu berriz ere';
|
||||
$string['missingrequiredfield'] = 'Beharrezko eremuren bat falta da.';
|
||||
$string['remotedownloaderror'] = '<p>Errorea osagaia zure zerbitzarian jaistean, mesedez egiaztatu proxy-ezarpenak, PHP cURL luzapena erabat gomendatzen da.</p>
|
||||
$string['remotedownloaderror'] = '<p>Errorea osagaia zure zerbitzarian jaistean, mesedez egiaztatu proxy-ezarpenak, PHP cURL hedapena erabat gomendatzen da.</p>
|
||||
<p> <a href="{$a->url}">{$a->url}</a> fitxategia eskuz jaitsi beharko zenuke, zure zerbitzariko "{$a->dest}"-ra kopiatu eta bertan deskonprimatu.</p>';
|
||||
$string['wrongdestpath'] = 'Bide desegokia';
|
||||
$string['wrongsourcebase'] = 'URL iturriaren oinarri akastuna';
|
||||
|
||||
@@ -45,7 +45,7 @@ $string['datarootpermission'] = 'Datu-direktorioen baimena';
|
||||
$string['dbprefix'] = 'Taulen aurrizkia';
|
||||
$string['dirroot'] = 'Moodle direktorioa';
|
||||
$string['environmenthead'] = 'Zure ingurunea egiaztatzen...';
|
||||
$string['environmentsub2'] = 'Moodle-ko bertsio bakoitzak PHPko gutxieneko bertsioa eta derrigorrez instalatu beharreko PHP luzapen batzuk ditu. Ingurunearen azterketa oso bat egiten da instalazioa eta eguneraketa bakoitza egin aurretik. Mesedez, jarri harremanetan zerbitzariaren kudeatzailearekin ez badakizu bertsio berria edo PHP luzapenak nola instalatu.';
|
||||
$string['environmentsub2'] = 'Moodle-ko bertsio bakoitzak PHPko gutxieneko bertsioa eta derrigorrez instalatu beharreko PHP hedapen batzuk ditu. Ingurunearen azterketa oso bat egiten da instalazioa eta eguneraketa bakoitza egin aurretik. Mesedez, jarri harremanetan zerbitzariaren kudeatzailearekin ez badakizu bertsio berria edo PHP hedapenak nola instalatu.';
|
||||
$string['errorsinenvironment'] = 'Huts egin du ingurunearen egiaztatzeak!';
|
||||
$string['installation'] = 'Instalazioa';
|
||||
$string['langdownloaderror'] = 'Zoritxarrez "{$a}" hizkuntza ezin izan da jaitsi. Instalazio-prozesuak ingelesez jarraituko du.';
|
||||
@@ -76,14 +76,14 @@ $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>
|
||||
<p>Oraingo helbidea egokia ez bada, mesedez aldatu ezazu URLa zure nabigatzailean eta instalazioa berriz abiatu ezazu.</p>';
|
||||
$string['pathsunsecuredataroot'] = 'Dataroot-en kokapena ez da segurua';
|
||||
$string['pathswrongadmindir'] = 'Admin direktorioa ez da existitzen';
|
||||
$string['phpextension'] = '{$a} PHP luzapena';
|
||||
$string['phpextension'] = '{$a} PHP hedapena';
|
||||
$string['phpversion'] = 'PHP bertsioa';
|
||||
$string['phpversionhelp'] = '<p>Moodle-k PHP 5.6.5 edo 7.1 bertsioetako bat behar du (7.0.x bertsioek muga batzuk dituzte). </p>
|
||||
<p>Zure bertsioa: {$a}</p>
|
||||
|
||||
@@ -44,11 +44,11 @@ $string['dataroot'] = 'Dossier de données';
|
||||
$string['datarootpermission'] = 'Droits d\'accès au dossier de données';
|
||||
$string['dbprefix'] = 'Préfixe des tables';
|
||||
$string['dirroot'] = 'Dossier Moodle';
|
||||
$string['environmenthead'] = 'Vérification de l\'environnement...';
|
||||
$string['environmenthead'] = 'Vérification de l\'environnement…';
|
||||
$string['environmentsub2'] = 'Chaque version de Moodle nécessite une version minimale de certains composants PHP et des extensions de PHP obligatoires. Une vérification complète de l\'environnement est effectuée avec chaque installation et chaque mise à jour. Veuillez contacter l\'administrateur du serveur si vous ne savez pas comment installer une nouvelle version ou activer des extensions de PHP.';
|
||||
$string['errorsinenvironment'] = 'Échec de la vérification de l\'environnement !';
|
||||
$string['installation'] = 'Installation';
|
||||
$string['langdownloaderror'] = 'La langue {$a} n\'a pas pu être téléchargée. La suite de l\'installation se déroulera en anglais. Vous pourrez télécharger et installer d\'autres langues à la fin de l\'installation';
|
||||
$string['langdownloaderror'] = 'La langue « {$a} » n\'a pas pu être téléchargée. La suite de l\'installation se déroulera en anglais. Vous pourrez télécharger et installer d\'autres langues à la fin de l\'installation';
|
||||
$string['memorylimithelp'] = '<p>La limite de mémoire de PHP sur votre serveur est actuellement de {$a}.</p>
|
||||
<p>Cette valeur trop basse risque de générer des problèmes de manque de mémoire pour Moodle, notamment si vous utilisez beaucoup de modules et/ou si vous avez un grand nombre d\'utilisateurs.</p>
|
||||
<p>Il est recommandé de configurer PHP avec une limite de mémoire aussi élevée que possible, par exemple 40 Mo. Vous pouvez obtenir cela de différentes façons :</p>
|
||||
|
||||
@@ -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)';
|
||||
@@ -31,4 +31,5 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['parentlanguage'] = 'he';
|
||||
$string['thisdirection'] = 'rtl';
|
||||
$string['thislanguage'] = 'עברית בתי־ספר';
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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['clianswerno'] = 'ל';
|
||||
$string['cliansweryes'] = 'כ';
|
||||
$string['cliincorrectvalueretry'] = 'ערך לא תקין, נסו שוב בבקשה';
|
||||
@@ -37,7 +37,7 @@ $string['cliincorrectvalueretry'] = 'Netočna vrijednost, pokušajte ponovno';
|
||||
$string['clitypevalue'] = 'unesite vrijednost';
|
||||
$string['clitypevaluedefault'] = 'unesite vrijednost ili pritisnite Enter za korištenje zadane vrijednosti ({$a})';
|
||||
$string['cliunknowoption'] = 'Nepoznate opcije: {$a} Molimo koristite --help opciju.';
|
||||
$string['cliyesnoprompt'] = 'unesite y (znači da) ili n (znači ne)';
|
||||
$string['cliyesnoprompt'] = 'unesite d (znači da) ili n (znači ne)';
|
||||
$string['environmentrequireinstall'] = 'je neophodno instalirati/omogućiti';
|
||||
$string['environmentrequireversion'] = 'neophodna inačica je {$a->needed}, a vi trenutačno koristite inačicu {$a->current}';
|
||||
$string['upgradekeyset'] = 'Ključ za ažuriranje (ostavite prazno kako ga ne bi zadali)';
|
||||
|
||||
@@ -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'] = 'hr';
|
||||
$string['thislanguage'] = 'Croatian schools';
|
||||
@@ -57,7 +57,7 @@ $string['memorylimithelp'] = '<p>現在、サーバのPHPメモリ制限が {$a}
|
||||
<ol>
|
||||
<li>あなたがリコンパイル可能な場合、PHPを<i>--enable-memory-limit</i>オプションでコンパイルしてください。これにより、Moodle自身がメモリ制限を設定することが可能になります。</li>
|
||||
<li>あなたがphp.iniファイルにアクセスできる場合、<b>memory_limit</b>設定を40Mのように変更することができます。php.iniファイルにアクセスできない場合、管理者に変更を依頼してください。</li>
|
||||
<li>いくつかのPHPサーバでは下記の行を含む.htaccessファイルをMoodleディレクトリに作成することができます:
|
||||
<li>いくつかのPHPサーバでは以下の行を含む.htaccessファイルをMoodleディレクトリに作成することができます:
|
||||
<blockquote><div>php_value memory_limit 40M</div></blockquote>
|
||||
<p>しかし、この設定が<b>すべての</b>PHPページの動作を妨げる場合もあります。ページ閲覧中にエラーが表示される場合、.htaccessファイルを削除してください。</p></li>
|
||||
</ol>';
|
||||
|
||||
@@ -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'] = 'ja';
|
||||
$string['thislanguage'] = 'Workplace準拠の日本語';
|
||||
@@ -30,5 +30,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['clianswerno'] = 'ទេ';
|
||||
$string['environmentrequireinstall'] = 'ត្រូវតែបានដំឡើង និងអនុញ្ញាត';
|
||||
$string['environmentrequireversion'] = 'ត្រូវការកំណែ {$a->needed} ហើយអ្នកកំពុងរត់ {$a->current}';
|
||||
|
||||
@@ -30,4 +30,4 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['thislanguage'] = 'myanma bhasa';
|
||||
$string['thislanguage'] = 'ဗမာစာ';
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* 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['thislanguage'] = 'Afaan Oromoo';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Vendored
+10
@@ -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.
|
||||
@@ -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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -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.';
|
||||
@@ -315,6 +315,7 @@ $string['configrequestedstudentname'] = 'Word for student used in requested cour
|
||||
$string['configrequestedstudentsname'] = 'Word for students used in requested courses';
|
||||
$string['configrequestedteachername'] = 'Word for teacher used in requested courses';
|
||||
$string['configrequestedteachersname'] = 'Word for teachers used in requested courses';
|
||||
$string['configreverseproxyignore'] = 'If your server is behind multiple reverse proxies that append to the X-Forwarded-For header then you will need to specify a comma separated list of ip addresses or subnets of the reverse proxies to be ignored in order to find the users correct IP address.';
|
||||
$string['configuserquota'] = 'The maximum number of bytes that a user can store in their own private file area. {$a->bytes} bytes == {$a->displaysize}';
|
||||
$string['configsectioninterface'] = 'Interface';
|
||||
$string['configsectionmail'] = 'Mail';
|
||||
@@ -731,6 +732,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';
|
||||
@@ -987,6 +989,7 @@ $string['restorecourse'] = 'Restore course';
|
||||
$string['restorernewroleid'] = 'Restorers\' role in courses';
|
||||
$string['restorernewroleid_help'] = 'If the user does not already have the permission to manage the newly restored course, the user is automatically assigned this role and enrolled if necessary. Select "None" if you do not want restorers to be able to manage every restored course.';
|
||||
$string['reverseproxy'] = 'Reverse proxy';
|
||||
$string['reverseproxyignore'] = 'Ignore reverse proxies';
|
||||
$string['riskconfig'] = 'Users could change site configuration and behaviour';
|
||||
$string['riskconfigshort'] = 'Configuration risk';
|
||||
$string['riskdataloss'] = 'Users could destroy large amounts of content or information';
|
||||
@@ -1197,6 +1200,7 @@ $string['unsupportedphpversion7'] = 'PHP version 7 is not supported.';
|
||||
$string['unsupportedphpversion71'] = 'PHP version 7.1 is not supported.';
|
||||
$string['unsupportedphpversion72'] = 'PHP version 7.2 is not supported.';
|
||||
$string['unsupportedphpversion73'] = 'PHP version 7.3 is not supported.';
|
||||
$string['unsupportedphpversion74'] = 'PHP version 7.4 is not supported.';
|
||||
$string['unsuspenduser'] = 'Activate user account';
|
||||
$string['updateaccounts'] = 'Update existing accounts';
|
||||
$string['updatecomponent'] = 'Update component';
|
||||
|
||||
Vendored
+1
-1
@@ -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}});
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+43
-14
@@ -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
@@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
+233
-109
@@ -32,8 +32,12 @@ use Behat\Mink\Exception\DriverException;
|
||||
use Behat\Mink\Exception\ExpectationException;
|
||||
use Behat\Mink\Exception\ElementNotFoundException;
|
||||
use Behat\Mink\Element\NodeElement;
|
||||
use Behat\Mink\Element\Element;
|
||||
use Behat\Mink\Session;
|
||||
|
||||
require_once(__DIR__ . '/classes/component_named_selector.php');
|
||||
require_once(__DIR__ . '/classes/component_named_replacement.php');
|
||||
|
||||
/**
|
||||
* Steps definitions base class.
|
||||
*
|
||||
@@ -103,14 +107,6 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
|
||||
* @return NodeElement
|
||||
*/
|
||||
protected function find($selector, $locator, $exception = false, $node = false, $timeout = false) {
|
||||
|
||||
// Throw exception, so dev knows it is not supported.
|
||||
if ($selector === 'named') {
|
||||
$exception = 'Using the "named" selector is deprecated as of 3.1. '
|
||||
.' Use the "named_partial" or use the "named_exact" selector instead.';
|
||||
throw new ExpectationException($exception, $this->getSession());
|
||||
}
|
||||
|
||||
// Returns the first match.
|
||||
$items = $this->find_all($selector, $locator, $exception, $node, $timeout);
|
||||
return count($items) ? reset($items) : null;
|
||||
@@ -125,12 +121,11 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
|
||||
* @param string $selector The selector type (css, xpath, named...)
|
||||
* @param mixed $locator It depends on the $selector, can be the xpath, a name, a css locator...
|
||||
* @param Exception $exception Otherwise we throw expcetion with generic info
|
||||
* @param NodeElement $node Spins around certain DOM node instead of the whole page
|
||||
* @param NodeElement $container Restrict the search to just children of the specified container
|
||||
* @param int $timeout Forces a specific time out (in seconds). If 0 is provided the default timeout will be applied.
|
||||
* @return array NodeElements list
|
||||
*/
|
||||
protected function find_all($selector, $locator, $exception = false, $node = false, $timeout = false) {
|
||||
|
||||
protected function find_all($selector, $locator, $exception = false, $container = false, $timeout = false) {
|
||||
// Throw exception, so dev knows it is not supported.
|
||||
if ($selector === 'named') {
|
||||
$exception = 'Using the "named" selector is deprecated as of 3.1. '
|
||||
@@ -140,7 +135,6 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
|
||||
|
||||
// Generic info.
|
||||
if (!$exception) {
|
||||
|
||||
// With named selectors we can be more specific.
|
||||
if (($selector == 'named_exact') || ($selector == 'named_partial')) {
|
||||
$exceptiontype = $locator[0];
|
||||
@@ -159,12 +153,6 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
|
||||
$exception = new ElementNotFoundException($this->getSession(), $exceptiontype, null, $exceptionlocator);
|
||||
}
|
||||
|
||||
$params = array('selector' => $selector, 'locator' => $locator);
|
||||
// Pushing $node if required.
|
||||
if ($node) {
|
||||
$params['node'] = $node;
|
||||
}
|
||||
|
||||
// How much we will be waiting for the element to appear.
|
||||
if (!$timeout) {
|
||||
$timeout = self::get_timeout();
|
||||
@@ -176,24 +164,87 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
|
||||
$microsleep = true;
|
||||
}
|
||||
|
||||
// Normalise the values in order to perform the search.
|
||||
$normalised = $this->normalise_selector($selector, $locator, $container ?: $this->getSession()->getPage());
|
||||
$selector = $normalised['selector'];
|
||||
$locator = $normalised['locator'];
|
||||
$container = $normalised['container'];
|
||||
|
||||
if (empty($container)) {
|
||||
$container = $this->getSession()->getPage();
|
||||
}
|
||||
|
||||
// Waits for the node to appear if it exists, otherwise will timeout and throw the provided exception.
|
||||
return $this->spin(
|
||||
function($context, $args) {
|
||||
|
||||
// If no DOM node provided look in all the page.
|
||||
if (empty($args['node'])) {
|
||||
return $context->getSession()->getPage()->findAll($args['selector'], $args['locator']);
|
||||
}
|
||||
|
||||
return $args['node']->findAll($args['selector'], $args['locator']);
|
||||
},
|
||||
$params,
|
||||
$timeout,
|
||||
$exception,
|
||||
$microsleep
|
||||
function() use ($selector, $locator, $container) {
|
||||
return $container->findAll($selector, $locator);
|
||||
}, [], $timeout, $exception, $microsleep
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise the locator and selector.
|
||||
*
|
||||
* @param string $selector The type of thing to search
|
||||
* @param mixed $locator The locator value. Can be an array, but is more likely a string.
|
||||
* @param Element $container An optional container to search within
|
||||
* @return array The selector, locator, and container to search within
|
||||
*/
|
||||
public function normalise_selector(string $selector, $locator, Element $container): array {
|
||||
// Check for specific transformations for this selector type.
|
||||
$transformfunction = "transform_find_for_{$selector}";
|
||||
if (method_exists('behat_selectors', $transformfunction)) {
|
||||
// A selector-specific transformation exists.
|
||||
// Perform initial transformation of the selector within the current container.
|
||||
$normalised = behat_selectors::{$transformfunction}($this, $locator, $container);
|
||||
$selector = $normalised['selector'];
|
||||
$locator = $normalised['locator'];
|
||||
$container = $normalised['container'];
|
||||
}
|
||||
|
||||
// Normalise the css and xpath selector types.
|
||||
if ('css_element' === $selector) {
|
||||
$selector = 'css';
|
||||
} else if ('xpath_element' === $selector) {
|
||||
$selector = 'xpath';
|
||||
}
|
||||
|
||||
// Convert to a named selector where the selector type is not a known selector.
|
||||
$converttonamed = !$this->getSession()->getSelectorsHandler()->isSelectorRegistered($selector);
|
||||
$converttonamed = $converttonamed && 'xpath' !== $selector;
|
||||
if ($converttonamed) {
|
||||
if (behat_partial_named_selector::is_deprecated_selector($selector)) {
|
||||
if ($replacement = behat_partial_named_selector::get_deprecated_replacement($selector)) {
|
||||
error_log("The '{$selector}' selector has been replaced with {$replacement}");
|
||||
$selector = $replacement;
|
||||
}
|
||||
} else if (behat_exact_named_selector::is_deprecated_selector($selector)) {
|
||||
if ($replacement = behat_exact_named_selector::get_deprecated_replacement($selector)) {
|
||||
error_log("The '{$selector}' selector has been replaced with {$replacement}");
|
||||
$selector = $replacement;
|
||||
}
|
||||
}
|
||||
|
||||
$allowedpartialselectors = behat_partial_named_selector::get_allowed_selectors();
|
||||
$allowedexactselectors = behat_exact_named_selector::get_allowed_selectors();
|
||||
if (isset($allowedpartialselectors[$selector])) {
|
||||
$locator = behat_selectors::normalise_named_selector($allowedpartialselectors[$selector], $locator);
|
||||
$selector = 'named_partial';
|
||||
} else if (isset($allowedexactselectors[$selector])) {
|
||||
$locator = behat_selectors::normalise_named_selector($allowedexactselectors[$selector], $locator);
|
||||
$selector = 'named_exact';
|
||||
} else {
|
||||
throw new ExpectationException("The '{$selector}' selector type is not registered.", $this->getSession()->getDriver());
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'selector' => $selector,
|
||||
'locator' => $locator,
|
||||
'container' => $container,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds DOM nodes in the page using named selectors.
|
||||
*
|
||||
@@ -217,27 +268,14 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
|
||||
* @return NodeElement
|
||||
*/
|
||||
public function __call($name, $arguments) {
|
||||
|
||||
if (substr($name, 0, 5) !== 'find_') {
|
||||
throw new coding_exception('The "' . $name . '" method does not exist');
|
||||
if (substr($name, 0, 5) === 'find_') {
|
||||
return call_user_func_array([$this, 'find'], array_merge(
|
||||
[substr($name, 5)],
|
||||
$arguments
|
||||
));
|
||||
}
|
||||
|
||||
// Only the named selector identifier.
|
||||
$cleanname = substr($name, 5);
|
||||
|
||||
// All named selectors shares the interface.
|
||||
if (count($arguments) !== 1) {
|
||||
throw new coding_exception('The "' . $cleanname . '" named selector needs the locator as it\'s single argument');
|
||||
}
|
||||
|
||||
// Redirecting execution to the find method with the specified selector.
|
||||
// It will detect if it's pointing to an unexisting named selector.
|
||||
return $this->find('named_partial',
|
||||
array(
|
||||
$cleanname,
|
||||
behat_context_helper::escape($arguments[0])
|
||||
)
|
||||
);
|
||||
throw new coding_exception("The '{$name}' method does not exist");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -290,20 +328,11 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
|
||||
if (!$timeout) {
|
||||
$timeout = self::get_timeout();
|
||||
}
|
||||
if ($microsleep) {
|
||||
// Will sleep 1/10th of a second by default for self::get_timeout() seconds.
|
||||
$loops = $timeout * 10;
|
||||
} else {
|
||||
// Will sleep for self::get_timeout() seconds.
|
||||
$loops = $timeout;
|
||||
}
|
||||
|
||||
// DOM will never change on non-javascript case; do not wait or try again.
|
||||
if (!$this->running_javascript()) {
|
||||
$loops = 1;
|
||||
}
|
||||
$start = microtime(true);
|
||||
$end = $start + $timeout;
|
||||
|
||||
for ($i = 0; $i < $loops; $i++) {
|
||||
do {
|
||||
// We catch the exception thrown by the step definition to execute it again.
|
||||
try {
|
||||
// We don't check with !== because most of the time closures will return
|
||||
@@ -319,14 +348,13 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->running_javascript()) {
|
||||
if ($microsleep) {
|
||||
usleep(100000);
|
||||
} else {
|
||||
sleep(1);
|
||||
}
|
||||
if (!$this->running_javascript()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
usleep(100000);
|
||||
|
||||
} while (microtime(true) < $end);
|
||||
|
||||
// Using coding_exception as is a development issue if no exception has been provided.
|
||||
if (!$exception) {
|
||||
@@ -348,12 +376,7 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
|
||||
* @return NodeElement
|
||||
*/
|
||||
protected function get_selected_node($selectortype, $element) {
|
||||
|
||||
// Getting Mink selector and locator.
|
||||
list($selector, $locator) = $this->transform_selector($selectortype, $element);
|
||||
|
||||
// Returns the NodeElement.
|
||||
return $this->find($selector, $locator);
|
||||
return $this->find($selectortype, $element);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -365,7 +388,6 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
|
||||
* @return NodeElement
|
||||
*/
|
||||
protected function get_text_selector_node($selectortype, $element) {
|
||||
|
||||
// Getting Mink selector and locator.
|
||||
list($selector, $locator) = $this->transform_text_selector($selectortype, $element);
|
||||
|
||||
@@ -384,18 +406,13 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
|
||||
* @return NodeElement
|
||||
*/
|
||||
protected function get_node_in_container($selectortype, $element, $containerselectortype, $containerelement) {
|
||||
|
||||
// Gets the container, it will always be text based.
|
||||
$containernode = $this->get_text_selector_node($containerselectortype, $containerelement);
|
||||
|
||||
list($selector, $locator) = $this->transform_selector($selectortype, $element);
|
||||
|
||||
// Specific exception giving info about where can't we find the element.
|
||||
$locatorexceptionmsg = $element . '" in the "' . $containerelement. '" "' . $containerselectortype. '"';
|
||||
$exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $locatorexceptionmsg);
|
||||
|
||||
// Looks for the requested node inside the container node.
|
||||
return $this->find($selector, $locator, $exception, $containernode);
|
||||
return $this->find($selectortype, $element, $exception, $containernode);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -414,14 +431,14 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
|
||||
* @return array Contains the selector and the locator expected by Mink.
|
||||
*/
|
||||
protected function transform_selector($selectortype, $element) {
|
||||
|
||||
// Here we don't know if an allowed text selector is being used.
|
||||
$selectors = behat_selectors::get_allowed_selectors();
|
||||
if (!isset($selectors[$selectortype])) {
|
||||
throw new ExpectationException('The "' . $selectortype . '" selector type does not exist', $this->getSession());
|
||||
}
|
||||
|
||||
return behat_selectors::get_behat_selector($selectortype, $element, $this->getSession());
|
||||
$normalised = $this->normalise_selector($selectortype, $element, $this->getSession()->getPage());
|
||||
return [$normalised['selector'], $normalised['locator']];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -458,63 +475,66 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
|
||||
* Spins around an element until it exists
|
||||
*
|
||||
* @throws ExpectationException
|
||||
* @param string $element
|
||||
* @param string $locator
|
||||
* @param string $selectortype
|
||||
* @return void
|
||||
*/
|
||||
protected function ensure_element_exists($element, $selectortype) {
|
||||
|
||||
// Getting the behat selector & locator.
|
||||
list($selector, $locator) = $this->transform_selector($selectortype, $element);
|
||||
|
||||
protected function ensure_element_exists($locator, $selectortype) {
|
||||
// Exception if it timesout and the element is still there.
|
||||
$msg = 'The "' . $element . '" element does not exist and should exist';
|
||||
$msg = "The '{$locator}' element does not exist and should";
|
||||
$exception = new ExpectationException($msg, $this->getSession());
|
||||
|
||||
// Normalise the values in order to perform the search.
|
||||
$normalised = $this->normalise_selector($selectortype, $locator, $this->getSession()->getPage());
|
||||
$selector = $normalised['selector'];
|
||||
$locator = $normalised['locator'];
|
||||
$container = $normalised['container'];
|
||||
|
||||
// It will stop spinning once the find() method returns true.
|
||||
$this->spin(
|
||||
function($context, $args) {
|
||||
// We don't use behat_base::find as it is already spinning.
|
||||
if ($context->getSession()->getPage()->find($args['selector'], $args['locator'])) {
|
||||
function() use ($selector, $locator, $container) {
|
||||
if ($container->find($selector, $locator)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
array('selector' => $selector, 'locator' => $locator),
|
||||
[],
|
||||
self::get_extended_timeout(),
|
||||
$exception,
|
||||
true
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Spins until the element does not exist
|
||||
*
|
||||
* @throws ExpectationException
|
||||
* @param string $element
|
||||
* @param string $locator
|
||||
* @param string $selectortype
|
||||
* @return void
|
||||
*/
|
||||
protected function ensure_element_does_not_exist($element, $selectortype) {
|
||||
|
||||
// Getting the behat selector & locator.
|
||||
list($selector, $locator) = $this->transform_selector($selectortype, $element);
|
||||
|
||||
protected function ensure_element_does_not_exist($locator, $selectortype) {
|
||||
// Exception if it timesout and the element is still there.
|
||||
$msg = 'The "' . $element . '" element exists and should not exist';
|
||||
$msg = "The '{$locator}' element exists and should not exist";
|
||||
$exception = new ExpectationException($msg, $this->getSession());
|
||||
|
||||
// Normalise the values in order to perform the search.
|
||||
$normalised = $this->normalise_selector($selectortype, $locator, $this->getSession()->getPage());
|
||||
$selector = $normalised['selector'];
|
||||
$locator = $normalised['locator'];
|
||||
$container = $normalised['container'];
|
||||
|
||||
// It will stop spinning once the find() method returns false.
|
||||
$this->spin(
|
||||
function($context, $args) {
|
||||
// We don't use behat_base::find() as we are already spinning.
|
||||
if (!$context->getSession()->getPage()->find($args['selector'], $args['locator'])) {
|
||||
return true;
|
||||
function() use ($selector, $locator, $container) {
|
||||
if ($container->find($selector, $locator)) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
return true;
|
||||
},
|
||||
array('selector' => $selector, 'locator' => $locator),
|
||||
// Note: We cannot use $this because the find will then be $this->find(), which leads us to a nested spin().
|
||||
// We cannot nest spins because the outer spin times out before the inner spin completes.
|
||||
[],
|
||||
self::get_extended_timeout(),
|
||||
$exception,
|
||||
true
|
||||
@@ -959,6 +979,56 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert page names to URLs for steps like 'When I am on the "[page name]" page'.
|
||||
*
|
||||
* You should override this as appropriate for your plugin. The method
|
||||
* {@link behat_navigation::resolve_core_page_url()} is a good example.
|
||||
*
|
||||
* Your overridden method should document the recognised page types with
|
||||
* a table like this:
|
||||
*
|
||||
* Recognised page names are:
|
||||
* | Page | Description |
|
||||
*
|
||||
* @param string $page name of the page, with the component name removed e.g. 'Admin notification'.
|
||||
* @return moodle_url the corresponding URL.
|
||||
* @throws Exception with a meaningful error message if the specified page cannot be found.
|
||||
*/
|
||||
protected function resolve_page_url(string $page): moodle_url {
|
||||
throw new Exception('Component "' . get_class($this) .
|
||||
'" does not support the generic \'When I am on the "' . $page .
|
||||
'" page\' navigation step.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'.
|
||||
*
|
||||
* A typical example might be:
|
||||
* When I am on the "Test quiz" "mod_quiz > Responses report" page
|
||||
* which would cause this method in behat_mod_quiz to be called with
|
||||
* arguments 'Responses report', 'Test quiz'.
|
||||
*
|
||||
* You should override this as appropriate for your plugin. The method
|
||||
* {@link behat_navigation::resolve_core_page_instance_url()} is a good example.
|
||||
*
|
||||
* Your overridden method should document the recognised page types with
|
||||
* a table like this:
|
||||
*
|
||||
* Recognised page names are:
|
||||
* | Type | identifier meaning | Description |
|
||||
*
|
||||
* @param string $type identifies which type of page this is, e.g. 'Attempt review'.
|
||||
* @param string $identifier identifies the particular page, e.g. 'Test quiz > student > Attempt 1'.
|
||||
* @return moodle_url the corresponding URL.
|
||||
* @throws Exception with a meaningful error message if the specified page cannot be found.
|
||||
*/
|
||||
protected function resolve_page_instance_url(string $type, string $identifier): moodle_url {
|
||||
throw new Exception('Component "' . get_class($this) .
|
||||
'" does not support the generic \'When I am on the "' . $identifier .
|
||||
'" "' . $type . '" page\' navigation step.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the required timeout in seconds.
|
||||
*
|
||||
@@ -1008,4 +1078,58 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
|
||||
public static function get_extended_timeout() : int {
|
||||
return self::get_real_timeout(10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of the exact named selectors for the component.
|
||||
*
|
||||
* Named selectors are what make Behat steps like
|
||||
* Then I should see "Useful text" in the "General" "fieldset"
|
||||
* work. Here, "fieldset" is the named selector, and "General" is the locator.
|
||||
*
|
||||
* If you override this method in your plugin (e.g. mod_mymod), to define
|
||||
* new selectors specific to your plugin. For example, if you returned
|
||||
* new behat_component_named_selector('Thingy',
|
||||
* [".//some/xpath//img[contains(@alt, %locator%)]/.."])
|
||||
* then
|
||||
* Then I should see "Useful text" in the "Whatever" "mod_mymod > Thingy"
|
||||
* would work.
|
||||
*
|
||||
* This method should return a list of {@link behat_component_named_selector} and
|
||||
* the docs on that class explain how it works.
|
||||
*
|
||||
* @return behat_component_named_selector[]
|
||||
*/
|
||||
public static function get_exact_named_selectors(): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of the partial named selectors for the component.
|
||||
*
|
||||
* Like the exact named selectors above, but the locator only
|
||||
* needs to match part of the text. For example, the standard
|
||||
* "button" is a partial selector, so:
|
||||
* When I click "Save" "button"
|
||||
* will activate "Save changes".
|
||||
*
|
||||
* @return behat_component_named_selector[]
|
||||
*/
|
||||
public static function get_partial_named_selectors(): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of the Mink named replacements for the component.
|
||||
*
|
||||
* Named replacements allow you to define parts of an xpath that can be reused multiple times, or in multiple
|
||||
* xpaths.
|
||||
*
|
||||
* This method should return a list of {@link behat_component_named_replacement} and the docs on that class explain
|
||||
* how it works.
|
||||
*
|
||||
* @return behat_component_named_replacement[]
|
||||
*/
|
||||
public static function get_named_replacements(): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,28 +90,64 @@ class behat_context_helper {
|
||||
* @return behat_base
|
||||
*/
|
||||
public static function get($classname) {
|
||||
$definedclassname = self::get_theme_override($classname);
|
||||
if ($definedclassname) {
|
||||
return self::$environment->getContext($definedclassname);
|
||||
}
|
||||
|
||||
// Just fall back on getContext to ensure that we throw the correct exception.
|
||||
return self::$environment->getContext($classname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the context for the specified component or subsystem.
|
||||
*
|
||||
* @param string $component The component or subsystem to find the context for
|
||||
* @return behat_base|null
|
||||
*/
|
||||
public static function get_component_context(string $component) {
|
||||
$component = str_replace('core_', '', $component);
|
||||
|
||||
if ($classname = self::get_theme_override("behat_{$component}")) {
|
||||
return self::get($classname);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for any theme override of the specified class name.
|
||||
*
|
||||
* @param string $classname
|
||||
* @return string|null
|
||||
*/
|
||||
protected static function get_theme_override(string $classname) {
|
||||
$suitename = self::$environment->getSuite()->getName();
|
||||
// If default suite, then get the default theme name.
|
||||
if ($suitename == 'default') {
|
||||
$suitename = theme_config::DEFAULT_THEME;
|
||||
}
|
||||
$overridencontextname = 'behat_theme_'.$suitename.'_'.$classname;
|
||||
|
||||
// If contexts has not been checked before and doesn't exist then just use core one.
|
||||
if (!isset(self::$nonexistingcontexts[$overridencontextname])) {
|
||||
try {
|
||||
$subcontext = self::$environment->getContext($overridencontextname);
|
||||
|
||||
return $subcontext;
|
||||
} catch (Behat\Behat\Context\Exception\ContextNotFoundException $e) {
|
||||
// If context not found then it's not overridden.
|
||||
self::$nonexistingcontexts[$overridencontextname] = 1;
|
||||
}
|
||||
$overrideclassname = "behat_theme_{$suitename}_{$classname}";
|
||||
if (self::$environment->hasContextClass($overrideclassname)) {
|
||||
return $overrideclassname;
|
||||
}
|
||||
|
||||
// Get the actual context.
|
||||
return self::$environment->getContext($classname);
|
||||
if (self::$environment->hasContextClass($classname)) {
|
||||
return $classname;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether there is a context of the specified classname.
|
||||
*
|
||||
* @param string $classname
|
||||
* @return bool
|
||||
*/
|
||||
public static function has_context(string $classname): bool {
|
||||
return self::$environment->hasContextClass($classname);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,12 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/named_selector.php');
|
||||
require_once(__DIR__ . '/exact_named_selector.php');
|
||||
require_once(__DIR__ . '/partial_named_selector.php');
|
||||
|
||||
use Behat\Mink\Exception\ExpectationException as ExpectationException;
|
||||
use Behat\Mink\Element\Element;
|
||||
|
||||
/**
|
||||
* Moodle selectors manager.
|
||||
@@ -47,32 +49,13 @@ class behat_selectors {
|
||||
* @return array Contains the selector and the locator expected by Mink.
|
||||
*/
|
||||
public static function get_behat_selector($selectortype, $element, Behat\Mink\Session $session) {
|
||||
// Note: This function is not deprecated, but not the recommended way of doing things.
|
||||
$normalised = $session->normalise_selector($selectortype, $element, $session->getPage());
|
||||
$selector = $normalised['selector'];
|
||||
$locator = $normalised['locator'];
|
||||
|
||||
// CSS and XPath selectors locator is one single argument.
|
||||
if ($selectortype == 'css_element' || $selectortype == 'xpath_element') {
|
||||
$selector = str_replace('_element', '', $selectortype);
|
||||
$locator = $element;
|
||||
} else {
|
||||
// Named selectors uses arrays as locators including the type of named selector.
|
||||
$allowedselectors = self::get_allowed_selectors();
|
||||
if (!isset($allowedselectors[$selectortype])) {
|
||||
throw new ExpectationException('The "' . $selectortype . '" selector not registered.', $session);
|
||||
}
|
||||
$locator = array($allowedselectors[$selectortype], behat_context_helper::escape($element));
|
||||
|
||||
// Get the selector which should be used.
|
||||
$allowedpartialselectors = behat_partial_named_selector::get_allowed_selectors();
|
||||
$allowedexactselectors = behat_exact_named_selector::get_allowed_selectors();
|
||||
if (isset($allowedpartialselectors[$selectortype])) {
|
||||
$selector = 'named_partial';
|
||||
} else if (isset($allowedexactselectors[$selectortype])) {
|
||||
$selector = 'named_exact';
|
||||
} else {
|
||||
throw new ExpectationException('The "' . $selectortype . '" selector not registered.', $session);
|
||||
}
|
||||
}
|
||||
|
||||
return array($selector, $locator);
|
||||
return [$selector, $locator];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,4 +81,40 @@ class behat_selectors {
|
||||
behat_exact_named_selector::get_allowed_text_selectors()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise the selector and locator for a named partial.
|
||||
*
|
||||
* @param string $selector The selector name
|
||||
* @param string $locator The value to normalise
|
||||
* @return array
|
||||
*/
|
||||
public static function normalise_named_selector(string $selector, string $locator): array {
|
||||
return [
|
||||
$selector,
|
||||
behat_context_helper::escape($locator),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the selector for a field.
|
||||
*
|
||||
* @param string $label The label to find
|
||||
* @param Element $container The container to look within
|
||||
* @return array The selector, locator, and container to search within
|
||||
*/
|
||||
public static function transform_find_for_field(behat_base $context, string $label, Element $container): array {
|
||||
$hasfieldset = strpos($label, '>');
|
||||
if (false !== $hasfieldset) {
|
||||
list($containerlabel, $label) = explode(">", $label, 2);
|
||||
$container = $context->find_fieldset(trim($containerlabel), $container);
|
||||
$label = trim($label);
|
||||
}
|
||||
|
||||
return [
|
||||
'selector' => 'named_partial',
|
||||
'locator' => self::normalise_named_selector('field', $label),
|
||||
'container' => $container,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* A class for recording the definition of Mink replacements.
|
||||
*
|
||||
* @package core
|
||||
* @category test
|
||||
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* A class for recording the definition of Mink replacements for use in Mink selectors.
|
||||
*
|
||||
* These are comprised of a source string, and a replacement.
|
||||
*
|
||||
* During use the source string is converted from the string to be in the format:
|
||||
*
|
||||
* %[component]/[string]%
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* %mod_forum/title%
|
||||
*
|
||||
* Mink replacements are used in xpath translation to translate regularly used items such as title.
|
||||
* Here is an example from the upstream Mink project:
|
||||
*
|
||||
* '%tagTextMatch%' => 'contains(normalize-space(string(.)), %locator%)'
|
||||
*
|
||||
* And can be used in an xpath:
|
||||
*
|
||||
* .//label[%tagTextMatch%]
|
||||
*
|
||||
* This would be expanded to:
|
||||
*
|
||||
* .//label[contains(normalize-space(string(.)), %locator%)]
|
||||
*
|
||||
* Replacements can also be used in other replacements, as long as that replacement is defined later.
|
||||
*
|
||||
* '%linkMatch%' => '(%idMatch% or %tagTextMatch% or %titleMatch% or %relMatch%)'
|
||||
*
|
||||
* @package core
|
||||
* @category test
|
||||
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class behat_component_named_replacement {
|
||||
/** @var string */
|
||||
protected $from;
|
||||
|
||||
/** @var string */
|
||||
protected $to;
|
||||
|
||||
/**
|
||||
* Create the replacement.
|
||||
*
|
||||
* @param string $from this is the old selector that should no longer be used.
|
||||
* For example 'group_message'.
|
||||
* @param string $to this is the new equivalent that should be used instead.
|
||||
* For example 'core_message > Message'.
|
||||
*/
|
||||
public function __construct(string $from, string $to) {
|
||||
$this->from = $from;
|
||||
$this->to = $to;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the 'from' part of the replacement, formatted for the component.
|
||||
*
|
||||
* @param string $component
|
||||
* @return string
|
||||
*/
|
||||
public function get_from(string $component): string {
|
||||
return "%{$component}/{$this->from}%";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the 'to' part of the replacement.
|
||||
*
|
||||
* @return string Target xpath
|
||||
*/
|
||||
public function get_to(): string {
|
||||
return $this->to;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Class representing a named selector that can be used in Behat tests.
|
||||
*
|
||||
* @package core
|
||||
* @category test
|
||||
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class representing a named selector that can be used in Behat tests.
|
||||
*
|
||||
* Named selectors are what make Behat steps like
|
||||
* Then I should see "Useful text" in the "General" "fieldset"
|
||||
* Here, "fieldset" is the named selector, and "General" is the locator.
|
||||
*
|
||||
* Selectors can either be exact, in which case the locator needs to
|
||||
* match exactly, or can be partial, for example the way
|
||||
* When I click "Save" "button"
|
||||
* will trigger a "Save changes" button.
|
||||
*
|
||||
* Instances of this class get returned by the get_exact_named_selectors()
|
||||
* and get_partial_named_selectors() methods in classes like behat_mod_mymod.
|
||||
* The code that makes the magic work is in the trait behat_named_selector
|
||||
* used by both behat_exact_named_selector and behat_partial_named_selector.
|
||||
*
|
||||
* @package core
|
||||
* @category test
|
||||
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class behat_component_named_selector {
|
||||
/** @var string */
|
||||
protected $alias;
|
||||
|
||||
/** @var array List of xpaths */
|
||||
protected $xpaths;
|
||||
|
||||
/** @var string */
|
||||
protected $istextselector;
|
||||
|
||||
/**
|
||||
* Create the selector definition.
|
||||
*
|
||||
* As an example, if you define
|
||||
* new behat_component_named_selector('Message',
|
||||
* [".//*[@data-conversation-id]//img[contains(@alt, %locator%)]/.."])
|
||||
* in get_partial_named_selectors in behat_message in
|
||||
* message/tests/behat/behat_message.php, then steps like
|
||||
* When "Group 1" "core_message > Message" should exist
|
||||
* will work.
|
||||
*
|
||||
* Text selectors are things that contain other things (e.g. some particular text), e.g.
|
||||
* Then I can see "Some text" in the "Whatever" "text_selector"
|
||||
* whereas non-text selectors are atomic things, like
|
||||
* When I click the "Whatever" "widget".
|
||||
*
|
||||
* @param string $alias The 'friendly' name of the thing. This will be prefixed with the component name.
|
||||
* For example, if the mod_mymod plugin, says 'Thingy', then "mod_mymod > Thingy" becomes a selector.
|
||||
* @param array $xpaths A list of xpaths one or more XPaths that the selector gets transformed into.
|
||||
* @param bool $istextselector Whether this selector can also be used as a text selector.
|
||||
*/
|
||||
public function __construct(string $alias, array $xpaths, bool $istextselector = true) {
|
||||
$this->alias = $alias;
|
||||
$this->xpaths = $xpaths;
|
||||
$this->istextselector = $istextselector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this is a text selector.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_text_selector(): bool {
|
||||
return $this->istextselector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the selector.
|
||||
* This is a back-end feature and contains a namespaced md5 of the human-readable name.
|
||||
*
|
||||
* @param string $component
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(string $component): string {
|
||||
return implode('_', [$component, md5($this->alias)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the alias of the selector.
|
||||
* This is the human-readable name that you would typically interact with.
|
||||
*
|
||||
* @param string $component
|
||||
* @return string
|
||||
*/
|
||||
public function get_alias(string $component): string {
|
||||
return implode(" > ", [$component, $this->alias]);;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of combined xpaths.
|
||||
*
|
||||
* @return string The list of xpaths combined with the xpath | (OR) operator
|
||||
*/
|
||||
public function get_combined_xpath(): string {
|
||||
return implode(' | ', $this->xpaths);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,9 @@
|
||||
*/
|
||||
class behat_exact_named_selector extends \Behat\Mink\Selector\ExactNamedSelector {
|
||||
|
||||
// Use the named selector trait.
|
||||
use behat_named_selector;
|
||||
|
||||
/**
|
||||
* Creates selector instance.
|
||||
*/
|
||||
@@ -63,6 +66,9 @@ class behat_exact_named_selector extends \Behat\Mink\Selector\ExactNamedSelector
|
||||
'text_exact' => 'text',
|
||||
);
|
||||
|
||||
/** @var List of deprecated selectors */
|
||||
protected static $deprecatedselectors = [];
|
||||
|
||||
/**
|
||||
* Allowed selectors getter.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Moodle-specific common functions for named selectors.
|
||||
*
|
||||
* @package core
|
||||
* @category test
|
||||
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* Common functions for named selectors.
|
||||
*
|
||||
* This has to be a trait, because we need this in both the classes
|
||||
* behat_exact_named_selector and behat_partial_named_selector, and
|
||||
* those classes have to be subclasses of \Behat\Mink\Selector\ExactNamedSelector
|
||||
* and \Behat\Mink\Selector\PartialNamedSelector. This trait is a way achieve
|
||||
* that without duplciated code.
|
||||
*
|
||||
* @package core
|
||||
* @category test
|
||||
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
trait behat_named_selector {
|
||||
|
||||
/**
|
||||
* Registers new XPath selector with specified name.
|
||||
*
|
||||
* @param string $component
|
||||
* @param behat_component_named_selector $selector
|
||||
*/
|
||||
public function register_component_selector(string $component, behat_component_named_selector $selector) {
|
||||
$alias = $selector->get_alias($component);
|
||||
$name = $selector->get_name($component);
|
||||
static::$allowedselectors[$alias] = $name;
|
||||
|
||||
if ($selector->is_text_selector()) {
|
||||
static::$allowedtextselectors[$alias] = $name;
|
||||
}
|
||||
|
||||
// We must use Reflection here. The replacements property is private and cannot be accessed otherwise.
|
||||
// This is due to an API limitation in Mink.
|
||||
$rc = new \ReflectionClass(\Behat\Mink\Selector\NamedSelector::class);
|
||||
$r = $rc->getProperty('replacements');
|
||||
$r->setAccessible(true);
|
||||
$replacements = $r->getValue($this);
|
||||
|
||||
$selectorxpath = strtr($selector->get_combined_xpath(), $replacements);
|
||||
|
||||
parent::registerNamedXpath($name, $selectorxpath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers new XPath selector with specified name.
|
||||
*
|
||||
* @param string $component
|
||||
* @param behat_component_named_replacement $replacement
|
||||
*/
|
||||
public function register_replacement(string $component, behat_component_named_replacement $replacement) {
|
||||
// We must use Reflection here. The replacements property is private and cannot be accessed otherwise.
|
||||
// This is due to an API limitation in Mink.
|
||||
$rc = new \ReflectionClass(\Behat\Mink\Selector\NamedSelector::class);
|
||||
$r = $rc->getProperty('replacements');
|
||||
$r->setAccessible(true);
|
||||
$existing = $r->getValue($this);
|
||||
|
||||
$from = $replacement->get_from($component);
|
||||
|
||||
if (isset($existing[$from])) {
|
||||
throw new \coding_exception("A named replacement already exists in the partial named selector for '{$from}'. " .
|
||||
"Replacement names must be unique, and should be namespaced to the component");
|
||||
}
|
||||
|
||||
$translatedto = strtr($replacement->get_to(), $existing);
|
||||
$this->registerReplacement($from, $translatedto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the specified selector has been deprecated and marked for replacement.
|
||||
*
|
||||
* @param string $selector
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_deprecated_selector(string $selector): bool {
|
||||
return array_key_exists($selector, static::$deprecatedselectors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the replacement name of a deprecated selector if any exists.
|
||||
*
|
||||
* @param string $selector
|
||||
* @return string
|
||||
*/
|
||||
public static function get_deprecated_replacement(string $selector) {
|
||||
return static::$deprecatedselectors[$selector];
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,9 @@
|
||||
*/
|
||||
class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSelector {
|
||||
|
||||
// Use the named selector trait.
|
||||
use behat_named_selector;
|
||||
|
||||
/**
|
||||
* Creates selector instance.
|
||||
*/
|
||||
@@ -248,6 +251,10 @@ XPATH
|
||||
],
|
||||
];
|
||||
|
||||
/** @var List of deprecated selectors */
|
||||
protected static $deprecatedselectors = [
|
||||
];
|
||||
|
||||
/**
|
||||
* Allowed selectors getter.
|
||||
*
|
||||
|
||||
@@ -31,6 +31,9 @@ require_once(__DIR__ . '/behat_command.php');
|
||||
require_once(__DIR__ . '/behat_config_manager.php');
|
||||
|
||||
require_once(__DIR__ . '/../../filelib.php');
|
||||
require_once(__DIR__ . '/../../clilib.php');
|
||||
|
||||
use Behat\Mink\Session;
|
||||
|
||||
/**
|
||||
* Init/reset utilities for Behat database and dataroot
|
||||
@@ -373,4 +376,38 @@ class behat_util extends testing_util {
|
||||
// $CFG values from the old run. @see set_config.
|
||||
initialise_cfg();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause execution immediately.
|
||||
*
|
||||
* @param Session $session
|
||||
* @param string $message The message to show when pausing.
|
||||
* This will be passed through cli_ansi_format so appropriate ANSI formatting and features are available.
|
||||
*/
|
||||
public static function pause(Session $session, string $message) {
|
||||
$posixexists = function_exists('posix_isatty');
|
||||
|
||||
// Make sure this step is only used with interactive terminal (if detected).
|
||||
if ($posixexists && !@posix_isatty(STDOUT)) {
|
||||
throw new ExpectationException('Break point should only be used with interactive terminal.', $session);
|
||||
}
|
||||
|
||||
// Save the cursor position, ring the bell, and add a new line.
|
||||
fwrite(STDOUT, cli_ansi_format("<cursor:save><bell><newline>"));
|
||||
|
||||
// Output the formatted message and reset colour back to normal.
|
||||
$formattedmessage = cli_ansi_format("{$message}<colour:normal>");
|
||||
fwrite(STDOUT, $formattedmessage);
|
||||
|
||||
// Wait for input.
|
||||
fread(STDIN, 1024);
|
||||
|
||||
// Move the cursor back up to the previous position, then restore the original position stored earlier, and move
|
||||
// it back down again.
|
||||
fwrite(STDOUT, cli_ansi_format("<cursor:up><cursor:up><cursor:restore><cursor:down><cursor:down>"));
|
||||
|
||||
// Add any extra lines back if the provided message was spread over multiple lines.
|
||||
$linecount = count(explode("\n", $formattedmessage));
|
||||
fwrite(STDOUT, str_repeat(cli_ansi_format("<cursor:down>"), $linecount - 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,4 +224,25 @@ final class ip_utils {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is an ip in a given list of subnets?
|
||||
*
|
||||
* @param string $ip - the IP to test against the list
|
||||
* @param string $list - the list of IP subnets
|
||||
* @param string $delim a delimiter of the list
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_ip_in_subnet_list($ip, $list, $delim = "\n") {
|
||||
$list = explode($delim, $list);
|
||||
foreach ($list as $line) {
|
||||
$tokens = explode('#', $line);
|
||||
$subnet = trim($tokens[0]);
|
||||
if (address_in_subnet($ip, $subnet)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -260,7 +260,27 @@ class manager {
|
||||
|
||||
// Set configuration.
|
||||
session_name($sessionname);
|
||||
session_set_cookie_params(0, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $cookiesecure, $CFG->cookiehttponly);
|
||||
|
||||
if (version_compare(PHP_VERSION, '7.3.0', '>=')) {
|
||||
$sessionoptions = [
|
||||
'lifetime' => 0,
|
||||
'path' => $CFG->sessioncookiepath,
|
||||
'domain' => $CFG->sessioncookiedomain,
|
||||
'secure' => $cookiesecure,
|
||||
'httponly' => $CFG->cookiehttponly,
|
||||
];
|
||||
|
||||
if (self::should_use_samesite_none()) {
|
||||
// If $samesite is empty, we don't want there to be any SameSite attribute.
|
||||
$sessionoptions['samesite'] = 'None';
|
||||
}
|
||||
|
||||
session_set_cookie_params($sessionoptions);
|
||||
} else {
|
||||
// Once PHP 7.3 becomes our minimum, drop this in favour of the alternative call to session_set_cookie_params above,
|
||||
// as that does not require a hack to work with same site settings on cookies.
|
||||
session_set_cookie_params(0, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $cookiesecure, $CFG->cookiehttponly);
|
||||
}
|
||||
ini_set('session.use_trans_sid', '0');
|
||||
ini_set('session.use_only_cookies', '1');
|
||||
ini_set('session.hash_function', '0'); // For now MD5 - we do not have room for sha-1 in sessions table.
|
||||
@@ -420,6 +440,8 @@ class manager {
|
||||
if ($timedout) {
|
||||
$_SESSION['SESSION']->has_timed_out = true;
|
||||
}
|
||||
|
||||
self::append_samesite_cookie_attribute();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -487,6 +509,61 @@ class manager {
|
||||
|
||||
// Setup $USER object.
|
||||
self::set_user($user);
|
||||
self::append_samesite_cookie_attribute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a valid setting for the SameSite cookie attribute.
|
||||
*
|
||||
* @return string The desired setting for the SameSite attribute on the cookie. Empty string indicates the SameSite attribute
|
||||
* should not be set at all.
|
||||
*/
|
||||
private static function should_use_samesite_none() {
|
||||
// We only want None or no attribute at this point. When we have cookie handling compatible with Lax,
|
||||
// we can look at checking a setting.
|
||||
|
||||
// Browser support for none is not consistent yet. There are known issues with Safari, and IE11.
|
||||
// Things are stablising, however as they're not stable yet we will deal specifically with the version of chrome
|
||||
// that introduces a default of lax, setting it to none for the current version of chrome (2 releases before the change).
|
||||
// We also check you are using secure cookies and HTTPS because if you are not running over HTTPS
|
||||
// then setting SameSite=None will cause your session cookie to be rejected.
|
||||
if (\core_useragent::is_chrome() && \core_useragent::check_chrome_version('78') && is_moodle_cookie_secure()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally append the SameSite attribute to the session cookie if necessary.
|
||||
*
|
||||
* Contains a hack for versions of PHP lower than 7.3 as there is no API built into PHP cookie API
|
||||
* for adding the SameSite setting.
|
||||
*
|
||||
* This won't change the Set-Cookie headers if:
|
||||
* - PHP 7.3 or higher is being used. That already adds the SameSite attribute without any hacks.
|
||||
* - If the samesite setting is empty.
|
||||
* - If the samesite setting is None but the browser is not compatible with that setting.
|
||||
*/
|
||||
private static function append_samesite_cookie_attribute() {
|
||||
if (version_compare(PHP_VERSION, '7.3.0', '>=')) {
|
||||
// This hack is only necessary if we weren't able to set the samesite flag via the session_set_cookie_params API.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self::should_use_samesite_none()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cookies = headers_list();
|
||||
header_remove('Set-Cookie');
|
||||
$setcookiesession = 'Set-Cookie: ' . session_name() . '=';
|
||||
|
||||
foreach ($cookies as $cookie) {
|
||||
if (strpos($cookie, $setcookiesession) === 0) {
|
||||
$cookie .= '; SameSite=None';
|
||||
}
|
||||
header($cookie, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -523,6 +600,8 @@ class manager {
|
||||
self::add_session_record($_SESSION['USER']->id); // Do not use $USER here because it may not be set up yet.
|
||||
session_write_close();
|
||||
self::$sessionactive = false;
|
||||
|
||||
self::append_samesite_cookie_attribute();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -227,3 +227,59 @@ function cli_logo($padding=2, $return=false) {
|
||||
cli_write($logo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitute cursor, colour, and bell placeholders in a CLI output to ANSI escape characters when ANSI is available.
|
||||
*
|
||||
* @param string $message
|
||||
* @return string
|
||||
*/
|
||||
function cli_ansi_format(string $message): string {
|
||||
global $CFG;
|
||||
|
||||
$replacements = [
|
||||
"<newline>" => "\n",
|
||||
"<bell>" => "\007",
|
||||
|
||||
// Cursor movement: https://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x361.html.
|
||||
"<cursor:save>" => "\033[s",
|
||||
"<cursor:restore>" => "\033[u",
|
||||
"<cursor:up>" => "\033[1A",
|
||||
"<cursor:down>" => "\033[1B",
|
||||
"<cursor:forward>" => "\033[1C",
|
||||
"<cursor:back>" => "\033[1D",
|
||||
];
|
||||
|
||||
$colours = [
|
||||
'normal' => '0;0',
|
||||
'black' => '0;30',
|
||||
'darkGray' => '1;30',
|
||||
'blue' => '0;34',
|
||||
'lightBlue' => '1;34',
|
||||
'green' => '0;32',
|
||||
'lightGreen' => '1;32',
|
||||
'cyan' => '0;36',
|
||||
'lightCyan' => '1;36',
|
||||
'red' => '0;31',
|
||||
'lightRed' => '1;31',
|
||||
'purple' => '0;35',
|
||||
'lightPurple' => '1;35',
|
||||
'brown' => '0;33',
|
||||
'yellow' => '1;33',
|
||||
'lightYellow' => '0;93',
|
||||
'lightGray' => '0;37',
|
||||
'white' => '1;37',
|
||||
];
|
||||
|
||||
foreach ($colours as $colour => $code) {
|
||||
$replacements["<colour:{$colour}>"] = "\033[{$code}m";
|
||||
}
|
||||
|
||||
// Windows don't support ANSI code by default, but does if ANSICON is available.
|
||||
$isansicon = getenv('ANSICON');
|
||||
if (($CFG->ostype === 'WINDOWS') && empty($isansicon)) {
|
||||
return str_replace(array_keys($replacements), '', $message);
|
||||
}
|
||||
|
||||
return str_replace(array_keys($replacements), array_values($replacements), $message);
|
||||
}
|
||||
|
||||
@@ -1628,3 +1628,14 @@ function restrict_php_version_72(&$result) {
|
||||
function restrict_php_version_73(&$result) {
|
||||
return restrict_php_version($result, '7.3');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current PHP version is greater than or equal to
|
||||
* PHP version 7.4.
|
||||
*
|
||||
* @param object $result an environment_results instance
|
||||
* @return bool result of version check
|
||||
*/
|
||||
function restrict_php_version_74(&$result) {
|
||||
return restrict_php_version($result, '7.4');
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
+10
-12
@@ -8993,23 +8993,13 @@ function cleardoubleslashes ($path) {
|
||||
* @return bool
|
||||
*/
|
||||
function remoteip_in_list($list) {
|
||||
$inlist = false;
|
||||
$clientip = getremoteaddr(null);
|
||||
|
||||
if (!$clientip) {
|
||||
// Ensure access on cli.
|
||||
return true;
|
||||
}
|
||||
|
||||
$list = explode("\n", $list);
|
||||
foreach ($list as $subnet) {
|
||||
$subnet = trim($subnet);
|
||||
if (address_in_subnet($clientip, $subnet)) {
|
||||
$inlist = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $inlist;
|
||||
return \core\ip_utils::is_ip_in_subnet_list($clientip, $list);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9037,7 +9027,15 @@ function getremoteaddr($default='0.0.0.0') {
|
||||
if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
|
||||
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||
$address = $forwardedaddresses[0];
|
||||
|
||||
$forwardedaddresses = array_filter($forwardedaddresses, function($ip) {
|
||||
global $CFG;
|
||||
return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore, ',');
|
||||
});
|
||||
|
||||
// Multiple proxies can append values to this header including an
|
||||
// untrusted original request header so we must only trust the last ip.
|
||||
$address = end($forwardedaddresses);
|
||||
|
||||
if (substr_count($address, ":") > 1) {
|
||||
// Remove port and brackets from IPv6.
|
||||
|
||||
@@ -57,8 +57,10 @@ class fragment_requirements_manager extends page_requirements_manager {
|
||||
$output = '';
|
||||
|
||||
// First include must be to a module with no dependencies, this prevents multiple requests.
|
||||
$prefix = "require(['core/first'], function() {\n";
|
||||
$prefix = 'M.util.js_pending("core/first");';
|
||||
$prefix .= "require(['core/first'], function() {\n";
|
||||
$suffix = "\n});";
|
||||
$suffix .= 'M.util.js_complete("core/first");';
|
||||
$output .= html_writer::script($prefix . implode(";\n", $this->amdjscode) . $suffix);
|
||||
return $output;
|
||||
}
|
||||
|
||||
@@ -2828,8 +2828,8 @@ EOD;
|
||||
$output .= $this->header();
|
||||
}
|
||||
|
||||
$message = '<p class="errormessage">' . $message . '</p>'.
|
||||
'<p class="errorcode"><a href="' . $moreinfourl . '">' .
|
||||
$message = '<p class="errormessage">' . s($message) . '</p>'.
|
||||
'<p class="errorcode"><a href="' . s($moreinfourl) . '">' .
|
||||
get_string('moreinformation') . '</a></p>';
|
||||
if (empty($CFG->rolesactive)) {
|
||||
$message .= '<p class="errormessage">' . get_string('installproblem', 'error') . '</p>';
|
||||
|
||||
@@ -1015,6 +1015,8 @@ class page_requirements_manager {
|
||||
$module = clean_param($module, PARAM_ALPHANUMEXT);
|
||||
$func = clean_param($func, PARAM_ALPHANUMEXT);
|
||||
|
||||
$modname = "{$component}/{$module}";
|
||||
|
||||
$jsonparams = array();
|
||||
foreach ($params as $param) {
|
||||
$jsonparams[] = json_encode($param);
|
||||
@@ -1027,9 +1029,15 @@ class page_requirements_manager {
|
||||
'"). Generally there are better ways to pass lots of data from PHP to JavaScript, for example via Ajax, data attributes, ... . ' .
|
||||
'This warning is triggered if the argument string becomes longer than ' . $toomanyparamslimit . ' characters.', DEBUG_DEVELOPER);
|
||||
}
|
||||
}
|
||||
|
||||
$js = 'require(["' . $component . '/' . $module . '"], function(amd) { amd.' . $func . '(' . $strparams . '); });';
|
||||
}
|
||||
$js = <<<EOF
|
||||
M.util.js_pending('{$modname}');
|
||||
require(['{$modname}'], function(amd) {
|
||||
amd.{$func}({$strparams});
|
||||
M.util.js_complete('{$modname}');
|
||||
});
|
||||
EOF;
|
||||
|
||||
$this->js_amd_inline($js);
|
||||
}
|
||||
@@ -1343,8 +1351,10 @@ class page_requirements_manager {
|
||||
}
|
||||
|
||||
// First include must be to a module with no dependencies, this prevents multiple requests.
|
||||
$prefix = "require(['core/first'], function() {\n";
|
||||
$suffix = "\n});";
|
||||
$prefix = 'M.util.js_pending("core/first");';
|
||||
$prefix .= "require(['core/first'], function() {\n";
|
||||
$suffix = 'M.util.js_complete("core/first");';
|
||||
$suffix .= "\n});";
|
||||
$output .= html_writer::script($prefix . implode(";\n", $this->amdjscode) . $suffix);
|
||||
return $output;
|
||||
}
|
||||
|
||||
@@ -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,798 +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;
|
||||
// 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);
|
||||
|
||||
if (is_null(get_capability_info($data['capability']))) {
|
||||
throw new Exception('The \'' . $data['capability'] . '\' capability does not exist');
|
||||
// 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 . '.');
|
||||
}
|
||||
|
||||
role_change_permission($data['roleid'], $context, $data['capability'], $permission);
|
||||
$instance = new $componentclass($component);
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
$context = $this->get_context($data['contextlevel'], $data['reference']);
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ use Behat\Gherkin\Node\TableNode as TableNode,
|
||||
/**
|
||||
* Forms-related steps definitions.
|
||||
*
|
||||
* Note, Behat tests to verify that the steps defined here work as advertised
|
||||
* are kept in admin/tool/behat/tests/behat.
|
||||
*
|
||||
* @package core
|
||||
* @category test
|
||||
* @copyright 2012 David Monllaó
|
||||
@@ -90,6 +93,29 @@ class behat_forms extends behat_base {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills a form with field/value data.
|
||||
*
|
||||
* @Given /^I set the following fields in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" to these values:$/
|
||||
* @throws ElementNotFoundException Thrown by behat_base::find
|
||||
* @param string $containerelement Element we look in
|
||||
* @param string $containerselectortype The type of selector where we look in
|
||||
* @param TableNode $data
|
||||
*/
|
||||
public function i_set_the_following_fields_in_container_to_these_values(
|
||||
$containerelement, $containerselectortype, TableNode $data) {
|
||||
|
||||
// Expand all fields in case we have.
|
||||
$this->expand_all_fields();
|
||||
|
||||
$datahash = $data->getRowsHash();
|
||||
|
||||
// The action depends on the field type.
|
||||
foreach ($datahash as $locator => $value) {
|
||||
$this->set_field_value_in_container($locator, $value, $containerselectortype, $containerelement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands all moodleform's fields, including collapsed fieldsets and advanced fields if they are present.
|
||||
* @Given /^I expand all fieldsets$/
|
||||
@@ -195,6 +221,20 @@ class behat_forms extends behat_base {
|
||||
$this->set_field_value($field, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the specified value to the field.
|
||||
*
|
||||
* @Given /^I set the field "(?P<field_string>(?:[^"]|\\")*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" to "(?P<field_value_string>(?:[^"]|\\")*)"$/
|
||||
* @throws ElementNotFoundException Thrown by behat_base::find
|
||||
* @param string $field
|
||||
* @param string $containerelement Element we look in
|
||||
* @param string $containerselectortype The type of selector where we look in
|
||||
* @param string $value
|
||||
*/
|
||||
public function i_set_the_field_in_container_to($field, $containerelement, $containerselectortype, $value) {
|
||||
$this->set_field_value_in_container($field, $value, $containerselectortype, $containerelement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Press the key in the field to trigger the javascript keypress event
|
||||
*
|
||||
@@ -284,7 +324,6 @@ class behat_forms extends behat_base {
|
||||
* @throws ElementNotFoundException Thrown by behat_base::find
|
||||
* @param string $field
|
||||
* @param string $value
|
||||
* @return void
|
||||
*/
|
||||
public function the_field_does_not_match_value($field, $value) {
|
||||
|
||||
@@ -300,6 +339,58 @@ class behat_forms extends behat_base {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, the field matches the value.
|
||||
*
|
||||
* @Then /^the field "(?P<field_string>(?:[^"]|\\")*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" matches value "(?P<field_value_string>(?:[^"]|\\")*)"$/
|
||||
* @throws ElementNotFoundException Thrown by behat_base::find
|
||||
* @param string $field
|
||||
* @param string $containerelement Element we look in
|
||||
* @param string $containerselectortype The type of selector where we look in
|
||||
* @param string $value
|
||||
*/
|
||||
public function the_field_in_container_matches_value($field, $containerelement, $containerselectortype, $value) {
|
||||
|
||||
// Get the field.
|
||||
$node = $this->get_node_in_container('field', $field, $containerselectortype, $containerelement);
|
||||
$formfield = behat_field_manager::get_form_field($node, $this->getSession());
|
||||
|
||||
// Checks if the provided value matches the current field value.
|
||||
if (!$formfield->matches($value)) {
|
||||
$fieldvalue = $formfield->get_value();
|
||||
throw new ExpectationException(
|
||||
'The \'' . $field . '\' value is \'' . $fieldvalue . '\', \'' . $value . '\' expected' ,
|
||||
$this->getSession()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, the field does not match the value.
|
||||
*
|
||||
* @Then /^the field "(?P<field_string>(?:[^"]|\\")*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" does not match value "(?P<field_value_string>(?:[^"]|\\")*)"$/
|
||||
* @throws ExpectationException
|
||||
* @throws ElementNotFoundException Thrown by behat_base::find
|
||||
* @param string $field
|
||||
* @param string $containerelement Element we look in
|
||||
* @param string $containerselectortype The type of selector where we look in
|
||||
* @param string $value
|
||||
*/
|
||||
public function the_field_in_container_does_not_match_value($field, $containerelement, $containerselectortype, $value) {
|
||||
|
||||
// Get the field.
|
||||
$node = $this->get_node_in_container('field', $field, $containerselectortype, $containerelement);
|
||||
$formfield = behat_field_manager::get_form_field($node, $this->getSession());
|
||||
|
||||
// Checks if the provided value matches the current field value.
|
||||
if ($formfield->matches($value)) {
|
||||
throw new ExpectationException(
|
||||
'The \'' . $field . '\' value matches \'' . $value . '\' and it should not match it' ,
|
||||
$this->getSession()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, the field matches the value.
|
||||
*
|
||||
@@ -391,6 +482,52 @@ class behat_forms extends behat_base {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, the provided field/value matches.
|
||||
*
|
||||
* @Then /^the following fields in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" match these values:$/
|
||||
* @throws ExpectationException
|
||||
* @param string $containerelement Element we look in
|
||||
* @param string $containerselectortype The type of selector where we look in
|
||||
* @param TableNode $data Pairs of | field | value |
|
||||
*/
|
||||
public function the_following_fields_in_container_match_these_values(
|
||||
$containerelement, $containerselectortype, TableNode $data) {
|
||||
|
||||
// Expand all fields in case we have.
|
||||
$this->expand_all_fields();
|
||||
|
||||
$datahash = $data->getRowsHash();
|
||||
|
||||
// The action depends on the field type.
|
||||
foreach ($datahash as $locator => $value) {
|
||||
$this->the_field_in_container_matches_value($locator, $containerelement, $containerselectortype, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the provided field/value pairs don't match.
|
||||
*
|
||||
* @Then /^the following fields in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" do not match these values:$/
|
||||
* @throws ExpectationException
|
||||
* @param string $containerelement Element we look in
|
||||
* @param string $containerselectortype The type of selector where we look in
|
||||
* @param TableNode $data Pairs of | field | value |
|
||||
*/
|
||||
public function the_following_fields_in_container_do_not_match_these_values(
|
||||
$containerelement, $containerselectortype, TableNode $data) {
|
||||
|
||||
// Expand all fields in case we have.
|
||||
$this->expand_all_fields();
|
||||
|
||||
$datahash = $data->getRowsHash();
|
||||
|
||||
// The action depends on the field type.
|
||||
foreach ($datahash as $locator => $value) {
|
||||
$this->the_field_in_container_does_not_match_value($locator, $containerelement, $containerselectortype, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, that given select box contains the specified option.
|
||||
*
|
||||
@@ -497,6 +634,26 @@ class behat_forms extends behat_base {
|
||||
$field->set_value($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic field setter.
|
||||
*
|
||||
* Internal API method, a generic *I set "VALUE" to "FIELD" field*
|
||||
* could be created based on it.
|
||||
*
|
||||
* @param string $fieldlocator The pointer to the field, it will depend on the field type.
|
||||
* @param string $value the value to set
|
||||
* @param string $containerselectortype The type of selector where we look in
|
||||
* @param string $containerelement Element we look in
|
||||
*/
|
||||
protected function set_field_value_in_container($fieldlocator, $value, $containerselectortype, $containerelement) {
|
||||
|
||||
$node = $this->get_node_in_container('field', $fieldlocator, $containerselectortype, $containerelement);
|
||||
// We delegate to behat_form_field class, it will
|
||||
// guess the type properly as it is a select tag.
|
||||
$field = behat_field_manager::get_form_field($node, $this->getSession());
|
||||
$field->set_value($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a value from single select and redirect.
|
||||
*
|
||||
|
||||
@@ -412,24 +412,22 @@ class behat_general extends behat_base {
|
||||
* @param string $containerelement
|
||||
* @param string $containerselectortype
|
||||
*/
|
||||
public function i_drag_and_i_drop_it_in($element, $selectortype, $containerelement, $containerselectortype) {
|
||||
|
||||
list($sourceselector, $sourcelocator) = $this->transform_selector($selectortype, $element);
|
||||
$sourcexpath = $this->getSession()->getSelectorsHandler()->selectorToXpath($sourceselector, $sourcelocator);
|
||||
|
||||
list($containerselector, $containerlocator) = $this->transform_selector($containerselectortype, $containerelement);
|
||||
$destinationxpath = $this->getSession()->getSelectorsHandler()->selectorToXpath($containerselector, $containerlocator);
|
||||
|
||||
$node = $this->get_selected_node("xpath_element", $sourcexpath);
|
||||
if (!$node->isVisible()) {
|
||||
throw new ExpectationException('"' . $sourcexpath . '" "xpath_element" is not visible', $this->getSession());
|
||||
}
|
||||
$node = $this->get_selected_node("xpath_element", $destinationxpath);
|
||||
if (!$node->isVisible()) {
|
||||
throw new ExpectationException('"' . $destinationxpath . '" "xpath_element" is not visible', $this->getSession());
|
||||
public function i_drag_and_i_drop_it_in($source, $sourcetype, $target, $targettype) {
|
||||
if (!$this->running_javascript()) {
|
||||
throw new DriverException('Drag and drop steps require javascript');
|
||||
}
|
||||
|
||||
$this->getSession()->getDriver()->dragTo($sourcexpath, $destinationxpath);
|
||||
$source = $this->find($sourcetype, $source);
|
||||
$target = $this->find($targettype, $target);
|
||||
|
||||
if (!$source->isVisible()) {
|
||||
throw new ExpectationException("'{$source}' '{$sourcetype}' is not visible", $this->getSession());
|
||||
}
|
||||
if (!$target->isVisible()) {
|
||||
throw new ExpectationException("'{$target}' '{$targettype}' is not visible", $this->getSession());
|
||||
}
|
||||
|
||||
$this->getSession()->getDriver()->dragTo($source->getXpath(), $target->getXpath());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -767,7 +765,7 @@ class behat_general extends behat_base {
|
||||
* @Then :preelement :preselectortype should appear before :postelement :postselectortype in the :containerelement :containerselectortype
|
||||
* @throws ExpectationException
|
||||
* @param string $preelement The locator of the preceding element
|
||||
* @param string $preselectortype The locator of the preceding element
|
||||
* @param string $preselectortype The selector type of the preceding element
|
||||
* @param string $postelement The locator of the latest element
|
||||
* @param string $postselectortype The selector type of the latest element
|
||||
* @param string $containerelement
|
||||
@@ -781,7 +779,7 @@ class behat_general extends behat_base {
|
||||
$containerelement = null,
|
||||
$containerselectortype = null
|
||||
) {
|
||||
$msg = "'{$preelement}' '{$preselectortype}' does not appear after '{$postelement}' '{$postselectortype}'";
|
||||
$msg = "'{$preelement}' '{$preselectortype}' does not appear before '{$postelement}' '{$postselectortype}'";
|
||||
$this->check_element_order(
|
||||
$containerelement,
|
||||
$containerselectortype,
|
||||
@@ -802,7 +800,7 @@ class behat_general extends behat_base {
|
||||
* @param string $postelement The locator of the latest element
|
||||
* @param string $postselectortype The selector type of the latest element
|
||||
* @param string $preelement The locator of the preceding element
|
||||
* @param string $preselectortype The locator of the preceding element
|
||||
* @param string $preselectortype The selector type of the preceding element
|
||||
* @param string $containerelement
|
||||
* @param string $containerselectortype
|
||||
*/
|
||||
@@ -968,12 +966,8 @@ EOF;
|
||||
* @param string $selectortype The selector type
|
||||
*/
|
||||
public function should_exist($element, $selectortype) {
|
||||
|
||||
// Getting Mink selector and locator.
|
||||
list($selector, $locator) = $this->transform_selector($selectortype, $element);
|
||||
|
||||
// Will throw an ElementNotFoundException if it does not exist.
|
||||
$this->find($selector, $locator);
|
||||
$this->find($selectortype, $element);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -987,35 +981,23 @@ EOF;
|
||||
* @param string $selectortype The selector type
|
||||
*/
|
||||
public function should_not_exist($element, $selectortype) {
|
||||
|
||||
// Getting Mink selector and locator.
|
||||
list($selector, $locator) = $this->transform_selector($selectortype, $element);
|
||||
|
||||
// Will throw an ElementNotFoundException if it does not exist, but, actually it should not exist, so we try &
|
||||
// catch it.
|
||||
try {
|
||||
|
||||
// Using directly the spin method as we want a reduced timeout but there is no
|
||||
// need for a 0.1 seconds interval because in the optimistic case we will timeout.
|
||||
$params = array('selector' => $selector, 'locator' => $locator);
|
||||
// The exception does not really matter as we will catch it and will never "explode".
|
||||
$exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $element);
|
||||
|
||||
// Using the spin method as we want a reduced timeout but there is no need for a 0.1 seconds interval
|
||||
// because in the optimistic case we will timeout.
|
||||
// If all goes good it will throw an ElementNotFoundExceptionn that we will catch.
|
||||
$this->spin(
|
||||
function($context, $args) {
|
||||
return $context->getSession()->getPage()->findAll($args['selector'], $args['locator']);
|
||||
},
|
||||
$params,
|
||||
behat_base::get_reduced_timeout(),
|
||||
$exception,
|
||||
false
|
||||
);
|
||||
return $this->find($selectortype, $element, $exception, false, behat_base::get_reduced_timeout());
|
||||
} catch (ElementNotFoundException $e) {
|
||||
// It passes.
|
||||
// We expect the element to not be found.
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ExpectationException('The "' . $element . '" "' . $selectortype .
|
||||
'" exists in the current page', $this->getSession());
|
||||
// The element was found and should not have been. Throw an exception.
|
||||
throw new ExpectationException("The '{$element}' '{$selectortype}' exists in the current page", $this->getSession());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1159,16 +1141,14 @@ EOF;
|
||||
*/
|
||||
public function should_exist_in_the($element, $selectortype, $containerelement, $containerselectortype) {
|
||||
// Get the container node.
|
||||
$containernode = $this->get_selected_node($containerselectortype, $containerelement);
|
||||
|
||||
list($selector, $locator) = $this->transform_selector($selectortype, $element);
|
||||
$containernode = $this->find($containerselectortype, $containerelement);
|
||||
|
||||
// Specific exception giving info about where can't we find the element.
|
||||
$locatorexceptionmsg = $element . '" in the "' . $containerelement. '" "' . $containerselectortype. '"';
|
||||
$locatorexceptionmsg = "{$element} in the {$containerelement} {$containerselectortype}";
|
||||
$exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $locatorexceptionmsg);
|
||||
|
||||
// Looks for the requested node inside the container node.
|
||||
$this->find($selector, $locator, $exception, $containernode);
|
||||
$this->find($selectortype, $element, $exception, $containernode);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1184,26 +1164,24 @@ EOF;
|
||||
* @param string $containerselectortype The container locator
|
||||
*/
|
||||
public function should_not_exist_in_the($element, $selectortype, $containerelement, $containerselectortype) {
|
||||
// Get the container node.
|
||||
$containernode = $this->find($containerselectortype, $containerelement);
|
||||
|
||||
// Get the container node; here we throw an exception
|
||||
// if the container node does not exist.
|
||||
$containernode = $this->get_selected_node($containerselectortype, $containerelement);
|
||||
|
||||
list($selector, $locator) = $this->transform_selector($selectortype, $element);
|
||||
|
||||
// Will throw an ElementNotFoundException if it does not exist, but, actually
|
||||
// it should not exist, so we try & catch it.
|
||||
// Will throw an ElementNotFoundException if it does not exist, but, actually it should not exist, so we try &
|
||||
// catch it.
|
||||
try {
|
||||
// Would be better to use a 1 second sleep because the element should not be there,
|
||||
// but we would need to duplicate the whole find_all() logic to do it, the benefit of
|
||||
// changing to 1 second sleep is not significant.
|
||||
$this->find($selector, $locator, false, $containernode, behat_base::get_reduced_timeout());
|
||||
// Looks for the requested node inside the container node.
|
||||
$this->find($selectortype, $element, false, $containernode, behat_base::get_reduced_timeout());
|
||||
} catch (ElementNotFoundException $e) {
|
||||
// It passes.
|
||||
// We expect the element to not be found.
|
||||
return;
|
||||
}
|
||||
throw new ExpectationException('The "' . $element . '" "' . $selectortype . '" exists in the "' .
|
||||
$containerelement . '" "' . $containerselectortype . '"', $this->getSession());
|
||||
|
||||
// The element was found and should not have been. Throw an exception.
|
||||
throw new ExpectationException(
|
||||
"The '{$element}' '{$selectortype}' exists in the '{$containerelement}' '{$containerselectortype}'",
|
||||
$this->getSession()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1613,27 +1591,9 @@ EOF;
|
||||
*
|
||||
* @Then /^(?:|I )pause(?:| scenario execution)$/
|
||||
*/
|
||||
public function i_pause_scenario_executon() {
|
||||
global $CFG;
|
||||
|
||||
$posixexists = function_exists('posix_isatty');
|
||||
|
||||
// Make sure this step is only used with interactive terminal (if detected).
|
||||
if ($posixexists && !@posix_isatty(STDOUT)) {
|
||||
$session = $this->getSession();
|
||||
throw new ExpectationException('Break point should only be used with interative terminal.', $session);
|
||||
}
|
||||
|
||||
// Windows don't support ANSI code by default, but with ANSICON.
|
||||
$isansicon = getenv('ANSICON');
|
||||
if (($CFG->ostype === 'WINDOWS') && empty($isansicon)) {
|
||||
fwrite(STDOUT, "Paused. Press Enter/Return to continue.");
|
||||
fread(STDIN, 1024);
|
||||
} else {
|
||||
fwrite(STDOUT, "\033[s\n\033[0;93mPaused. Press \033[1;31mEnter/Return\033[0;93m to continue.\033[0m");
|
||||
fread(STDIN, 1024);
|
||||
fwrite(STDOUT, "\033[2A\033[u\033[2B");
|
||||
}
|
||||
public function i_pause_scenario_execution() {
|
||||
$message = "<colour:lightYellow>Paused. Press <colour:lightRed>Enter/Return<colour:lightYellow> to continue.";
|
||||
behat_util::pause($this->getSession(), $message);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1754,8 +1714,8 @@ EOF;
|
||||
if (!$this->running_javascript()) {
|
||||
throw new DriverException('Checking focus on an element requires JavaScript');
|
||||
}
|
||||
list($a, $b) = $this->transform_selector($nodeselectortype, $nodeelement);
|
||||
$element = $this->find($a, $b);
|
||||
|
||||
$element = $this->find($nodeselectortype, $nodeelement);
|
||||
$xpath = addslashes_js($element->getXpath());
|
||||
$script = 'return (function() { return document.activeElement === document.evaluate("' . $xpath . '",
|
||||
document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; })(); ';
|
||||
|
||||
@@ -317,6 +317,7 @@ class behat_hooks extends behat_base {
|
||||
|
||||
// Register behat selectors for theme, if suite is changed. We do it for every suite change.
|
||||
if ($suitename !== self::$runningsuite) {
|
||||
self::$runningsuite = $suitename;
|
||||
behat_context_helper::set_environment($scope->getEnvironment());
|
||||
|
||||
// We need the Mink session to do it and we do it only before the first scenario.
|
||||
@@ -337,6 +338,13 @@ class behat_hooks extends behat_base {
|
||||
|
||||
$this->getSession()->getSelectorsHandler()->registerSelector('named_partial', new $namedpartialclass());
|
||||
$this->getSession()->getSelectorsHandler()->registerSelector('named_exact', new $namedexactclass());
|
||||
|
||||
// Register component named selectors.
|
||||
foreach (\core_component::get_component_list() as $subsystem => $components) {
|
||||
foreach (array_keys($components) as $component) {
|
||||
$this->register_component_selectors_for_component($component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset mink session between the scenarios.
|
||||
@@ -368,7 +376,6 @@ class behat_hooks extends behat_base {
|
||||
// Set the theme if not default.
|
||||
if ($suitename !== "default") {
|
||||
set_config('theme', $suitename);
|
||||
self::$runningsuite = $suitename;
|
||||
}
|
||||
|
||||
// Reset the scenariorunning variable to ensure that Step 0 occurs.
|
||||
@@ -383,7 +390,7 @@ class behat_hooks extends behat_base {
|
||||
* Yes, this is in a strange location and should be in the BeforeScenario hook, but failures in the test setUp lead
|
||||
* to the test being incorrectly marked as skipped with no way to force the test to be failed.
|
||||
*
|
||||
* @param BeforeStepScope $scope
|
||||
* @param BeforeStepScope $scope
|
||||
* @BeforeStep
|
||||
*/
|
||||
public function before_step(BeforeStepScope $scope) {
|
||||
@@ -412,7 +419,6 @@ class behat_hooks extends behat_base {
|
||||
new ExpectationException($message, $session)
|
||||
);
|
||||
|
||||
self::$initprocessesfinished = true;
|
||||
}
|
||||
$this->scenariorunning = true;
|
||||
}
|
||||
@@ -466,11 +472,7 @@ class behat_hooks extends behat_base {
|
||||
throw new coding_exception("Step '" . $scope->getStep()->getText() . "'' is undefined.");
|
||||
}
|
||||
|
||||
// Save the page content if the step failed.
|
||||
if (!empty($CFG->behat_faildump_path) &&
|
||||
$scope->getTestResult()->getResultCode() === Behat\Testwork\Tester\Result\TestResult::FAILED) {
|
||||
$this->take_contentdump($scope);
|
||||
}
|
||||
$isfailed = $scope->getTestResult()->getResultCode() === Behat\Testwork\Tester\Result\TestResult::FAILED;
|
||||
|
||||
// Abort any open transactions to prevent subsequent tests hanging.
|
||||
// This does the same as abort_all_db_transactions(), but doesn't call error_log() as we don't
|
||||
@@ -482,17 +484,30 @@ class behat_hooks extends behat_base {
|
||||
}
|
||||
}
|
||||
|
||||
if ($isfailed && !empty($CFG->behat_faildump_path)) {
|
||||
// Save the page content (html).
|
||||
$this->take_contentdump($scope);
|
||||
|
||||
if ($this->running_javascript()) {
|
||||
// Save a screenshot.
|
||||
$this->take_screenshot($scope);
|
||||
}
|
||||
}
|
||||
|
||||
if ($isfailed && !empty($CFG->behat_pause_on_fail)) {
|
||||
$exception = $scope->getTestResult()->getException();
|
||||
$message = "<colour:lightRed>Scenario failed. ";
|
||||
$message .= "<colour:lightYellow>Paused for inspection. Press <colour:lightRed>Enter/Return<colour:lightYellow> to continue.<newline>";
|
||||
$message .= "<colour:lightRed>Exception follows:<newline>";
|
||||
$message .= trim($exception->getMessage());
|
||||
behat_util::pause($this->getSession(), $message);
|
||||
}
|
||||
|
||||
// Only run if JS.
|
||||
if (!$this->running_javascript()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save a screenshot if the step failed.
|
||||
if (!empty($CFG->behat_faildump_path) &&
|
||||
$scope->getTestResult()->getResultCode() === Behat\Testwork\Tester\Result\TestResult::FAILED) {
|
||||
$this->take_screenshot($scope);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->wait_for_pending_js();
|
||||
self::$currentstepexception = null;
|
||||
@@ -666,6 +681,50 @@ class behat_hooks extends behat_base {
|
||||
protected static function is_first_scenario() {
|
||||
return !(self::$initprocessesfinished);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a set of component selectors.
|
||||
*
|
||||
* @param string $component
|
||||
*/
|
||||
public function register_component_selectors_for_component(string $component) {
|
||||
$context = behat_context_helper::get_component_context($component);
|
||||
|
||||
if ($context === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$namedpartial = $this->getSession()->getSelectorsHandler()->getSelector('named_partial');
|
||||
$namedexact = $this->getSession()->getSelectorsHandler()->getSelector('named_exact');
|
||||
|
||||
// Replacements must come before selectors as they are used in the selectors.
|
||||
foreach ($context->get_named_replacements() as $replacement) {
|
||||
$namedpartial->register_replacement($component, $replacement);
|
||||
$namedexact->register_replacement($component, $replacement);
|
||||
}
|
||||
|
||||
foreach ($context->get_partial_named_selectors() as $selector) {
|
||||
$namedpartial->register_component_selector($component, $selector);
|
||||
}
|
||||
|
||||
foreach ($context->get_exact_named_selectors() as $selector) {
|
||||
$namedexact->register_component_selector($component, $selector);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the first step as having been completed.
|
||||
*
|
||||
* This must be the last BeforeStep hook in the setup.
|
||||
*
|
||||
* @param BeforeStepScope $scope
|
||||
* @BeforeStep
|
||||
*/
|
||||
public function first_step_setup_complete(BeforeStepScope $scope) {
|
||||
self::$initprocessesfinished = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -574,6 +574,196 @@ class behat_navigation extends behat_base {
|
||||
$USER = $globuser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a given page, belonging to a plugin or core component.
|
||||
*
|
||||
* The page-type are interpreted by each plugin to work out the
|
||||
* corresponding URL. See the resolve_url method in each class like
|
||||
* behat_mod_forum. That method should document which page types are
|
||||
* recognised, and how the name identifies them.
|
||||
*
|
||||
* For pages belonging to core, the 'core > ' bit is omitted.
|
||||
*
|
||||
* @When I am on the :page page
|
||||
* @param string $page the component and page name.
|
||||
* E.g. 'Admin notifications' or 'core_user > Preferences'.
|
||||
* @throws Exception if the specified page cannot be determined.
|
||||
*/
|
||||
public function i_am_on_page(string $page) {
|
||||
$this->getSession()->visit($this->locate_path(
|
||||
$this->resolve_page_helper($page)->out_as_local_url()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a given page logged in as a given user.
|
||||
*
|
||||
* This is like the combination
|
||||
* When I log in as "..."
|
||||
* And I am on the "..." page
|
||||
* but with the advantage that you go straight to the desired page, without
|
||||
* having to wait for the Dashboard to load.
|
||||
*
|
||||
* @When I am on the :page page logged in as :username
|
||||
* @param string $page the type of page. E.g. 'Admin notifications' or 'core_user > Preferences'.
|
||||
* @param string $username the name of the user to log in as. E.g. 'admin'.
|
||||
* @throws Exception if the specified page cannot be determined.
|
||||
*/
|
||||
public function i_am_on_page_logged_in_as(string $page, string $username) {
|
||||
self::execute('behat_auth::i_log_in_as', [$username, $this->resolve_page_helper($page)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper used by i_am_on_page() and i_am_on_page_logged_in_as().
|
||||
*
|
||||
* @param string $page the type of page. E.g. 'Admin notifications' or 'core_user > Preferences'.
|
||||
* @return moodle_url the corresponding URL.
|
||||
*/
|
||||
protected function resolve_page_helper(string $page): moodle_url {
|
||||
list($component, $name) = $this->parse_page_name($page);
|
||||
if ($component === 'core') {
|
||||
return $this->resolve_core_page_url($name);
|
||||
} else {
|
||||
$context = behat_context_helper::get('behat_' . $component);
|
||||
return $context->resolve_page_url($name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a full page name like 'Admin notifications' or 'core_user > Preferences'.
|
||||
*
|
||||
* E.g. parsing 'mod_quiz > View' gives ['mod_quiz', 'View'].
|
||||
*
|
||||
* @param string $page the full page name
|
||||
* @return array with two elements, component and page name.
|
||||
*/
|
||||
protected function parse_page_name(string $page): array {
|
||||
$dividercount = substr_count($page, ' > ');
|
||||
if ($dividercount === 0) {
|
||||
return ['core', $page];
|
||||
} else if ($dividercount === 1) {
|
||||
list($component, $name) = explode(' > ', $page);
|
||||
if ($component === 'core') {
|
||||
throw new coding_exception('Do not specify the component "core > ..." for core pages.');
|
||||
}
|
||||
return [$component, $name];
|
||||
} else {
|
||||
throw new coding_exception('The page name most be in the form ' .
|
||||
'"{page-name}" for core pages, or "{component} > {page-name}" ' .
|
||||
'for pages belonging to other components. ' .
|
||||
'For example "Admin notifications" or "mod_quiz > View".');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a given instance of a page, belonging to a plugin or core component.
|
||||
*
|
||||
* The instance identifier and page-type are interpreted by each plugin to
|
||||
* work out the corresponding URL. See the resolve_page_instance_url method
|
||||
* in each class like behat_mod_forum. That method should document which page
|
||||
* types are recognised, and how the name identifies them.
|
||||
*
|
||||
* For pages belonging to core, the 'core > ' bit is omitted.
|
||||
*
|
||||
* @When I am on the :identifier :type page
|
||||
* @param string $identifier identifies the particular page. E.g. 'Test quiz'.
|
||||
* @param string $type the component and page type. E.g. 'mod_quiz > View'.
|
||||
* @throws Exception if the specified page cannot be determined.
|
||||
*/
|
||||
public function i_am_on_page_instance(string $identifier, string $type) {
|
||||
$this->getSession()->visit($this->locate_path(
|
||||
$this->resolve_page_instance_helper($identifier, $type)->out_as_local_url()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a given page logged in as a given user.
|
||||
*
|
||||
* This is like the combination
|
||||
* When I log in as "..."
|
||||
* And I am on the "..." "..." page
|
||||
* but with the advantage that you go straight to the desired page, without
|
||||
* having to wait for the Dashboard to load.
|
||||
*
|
||||
* @When I am on the :identifier :type page logged in as :username
|
||||
* @param string $identifier identifies the particular page. E.g. 'Test quiz'.
|
||||
* @param string $type the component and page type. E.g. 'mod_quiz > View'.
|
||||
* @param string $username the name of the user to log in as. E.g. 'student'.
|
||||
* @throws Exception if the specified page cannot be determined.
|
||||
*/
|
||||
public function i_am_on_page_instance_logged_in_as(string $identifier,
|
||||
string $type, string $username) {
|
||||
self::execute('behat_auth::i_log_in_as',
|
||||
[$username, $this->resolve_page_instance_helper($identifier, $type)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper used by i_am_on_page() and i_am_on_page_logged_in_as().
|
||||
*
|
||||
* @param string $identifier identifies the particular page. E.g. 'Test quiz'.
|
||||
* @param string $pagetype the component and page type. E.g. 'mod_quiz > View'.
|
||||
* @return moodle_url the corresponding URL.
|
||||
*/
|
||||
protected function resolve_page_instance_helper(string $identifier, string $pagetype): moodle_url {
|
||||
list($component, $type) = $this->parse_page_name($pagetype);
|
||||
if ($component === 'core') {
|
||||
return $this->resolve_core_page_instance_url($type, $identifier);
|
||||
} else {
|
||||
$context = behat_context_helper::get('behat_' . $component);
|
||||
return $context->resolve_page_instance_url($type, $identifier);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert core page names to URLs for steps like 'When I am on the "[page name]" page'.
|
||||
*
|
||||
* Recognised page names are:
|
||||
* | Homepage | Homepage (normally dashboard). |
|
||||
* | Admin notifications | Admin notification screen. |
|
||||
*
|
||||
* @param string $name identifies which identifies this page, e.g. 'Homepage', 'Admin notifications'.
|
||||
* @return moodle_url the corresponding URL.
|
||||
* @throws Exception with a meaningful error message if the specified page cannot be found.
|
||||
*/
|
||||
protected function resolve_core_page_url(string $name): moodle_url {
|
||||
switch ($name) {
|
||||
case 'Homepage':
|
||||
return new moodle_url('/');
|
||||
|
||||
case 'Admin notifications':
|
||||
return new moodle_url('/admin/');
|
||||
|
||||
default:
|
||||
throw new Exception('Unrecognised core page type "' . $name . '."');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'.
|
||||
*
|
||||
* Recognised page names are:
|
||||
* | Page type | Identifier meaning | description |
|
||||
* | Category page | category idnumber | List of courses in that category. |
|
||||
*
|
||||
* @param string $type identifies which type of page this is, e.g. 'Category page'.
|
||||
* @param string $identifier identifies the particular page, e.g. 'test-cat'.
|
||||
* @return moodle_url the corresponding URL.
|
||||
* @throws Exception with a meaningful error message if the specified page cannot be found.
|
||||
*/
|
||||
protected function resolve_core_page_instance_url(string $type, string $identifier): moodle_url {
|
||||
global $DB;
|
||||
|
||||
switch ($type) {
|
||||
case 'Category page':
|
||||
$categoryid = $DB->get_field('course_categories', 'id', ['idnumber' => $identifier]);
|
||||
if (!$categoryid) {
|
||||
throw new Exception('The specified category with idnumber "' . $identifier . '" does not exist');
|
||||
}
|
||||
return new moodle_url('/course/category.php', ['id' => $categoryid]);
|
||||
|
||||
default:
|
||||
throw new Exception('Unrecognised core page type "' . $type . '."');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the course homepage.
|
||||
*
|
||||
|
||||
@@ -376,4 +376,32 @@ class core_ip_utils_testcase extends basic_testcase {
|
||||
[false, 'trouble.com.au'] // The allowed domain (above) has a space at the front and so will return false.
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_is_ip_in_subnet_list.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function data_is_ip_in_subnet_list() {
|
||||
return [
|
||||
[true, '1.1.1.1', '1.1.1.1', "\n"],
|
||||
[false, '1.1.1.1', '2.2.2.2', "\n"],
|
||||
[true, '1.1.1.1', "1.1.1.5\n1.1.1.1", "\n"],
|
||||
[true, '1.1.1.1', "1.1.1.5,1.1.1.1", ","],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test checking ips against a list of allowed domains.
|
||||
*
|
||||
* @param bool $expected Expected result
|
||||
* @param string $ip IP address
|
||||
* @param string $list list of IP subnets
|
||||
* @param string $delim delimiter of list
|
||||
* @dataProvider data_is_ip_in_subnet_list
|
||||
*/
|
||||
public function test_is_ip_in_subnet_list($expected, $ip, $list, $delim) {
|
||||
$this->assertEquals($expected, \core\ip_utils::is_ip_in_subnet_list($ip, $list, $delim));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ class core_messageinbound_testcase extends advanced_testcase {
|
||||
// Break on the --[TOKEN]-- tags in the file.
|
||||
$content = file_get_contents($file->getRealPath());
|
||||
$content = preg_replace("#\r\n#", "\n", $content);
|
||||
$tokens = preg_split('#(?:^|\n*)----([A-Z]+)----\n#', file_get_contents($file->getRealPath()),
|
||||
$tokens = preg_split('#(?:^|\n*)----([A-Z]+)----\n#', $content,
|
||||
null, PREG_SPLIT_DELIM_CAPTURE);
|
||||
$sections = array(
|
||||
// Key => Required.
|
||||
|
||||
@@ -3569,6 +3569,10 @@ class core_moodlelib_testcase extends advanced_testcase {
|
||||
* Tests the getremoteaddr() function.
|
||||
*/
|
||||
public function test_getremoteaddr() {
|
||||
global $CFG;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$CFG->getremoteaddrconf = GETREMOTEADDR_SKIP_HTTP_CLIENT_IP;
|
||||
$xforwardedfor = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : null;
|
||||
|
||||
$_SERVER['HTTP_X_FORWARDED_FOR'] = '';
|
||||
@@ -3585,27 +3589,27 @@ class core_moodlelib_testcase extends advanced_testcase {
|
||||
|
||||
$_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,127.0.0.2';
|
||||
$twoip = getremoteaddr();
|
||||
$this->assertEquals('127.0.0.1', $twoip);
|
||||
$this->assertEquals('127.0.0.2', $twoip);
|
||||
|
||||
$_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,127.0.0.2, 127.0.0.3';
|
||||
$_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,127.0.0.2,127.0.0.3';
|
||||
$threeip = getremoteaddr();
|
||||
$this->assertEquals('127.0.0.1', $threeip);
|
||||
$this->assertEquals('127.0.0.3', $threeip);
|
||||
|
||||
$_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1:65535,127.0.0.2';
|
||||
$_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,127.0.0.2:65535';
|
||||
$portip = getremoteaddr();
|
||||
$this->assertEquals('127.0.0.1', $portip);
|
||||
$this->assertEquals('127.0.0.2', $portip);
|
||||
|
||||
$_SERVER['HTTP_X_FORWARDED_FOR'] = '0:0:0:0:0:0:0:1,127.0.0.2';
|
||||
$_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,0:0:0:0:0:0:0:2';
|
||||
$portip = getremoteaddr();
|
||||
$this->assertEquals('0:0:0:0:0:0:0:1', $portip);
|
||||
$this->assertEquals('0:0:0:0:0:0:0:2', $portip);
|
||||
|
||||
$_SERVER['HTTP_X_FORWARDED_FOR'] = '0::1,127.0.0.2';
|
||||
$_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,0::2';
|
||||
$portip = getremoteaddr();
|
||||
$this->assertEquals('0:0:0:0:0:0:0:1', $portip);
|
||||
$this->assertEquals('0:0:0:0:0:0:0:2', $portip);
|
||||
|
||||
$_SERVER['HTTP_X_FORWARDED_FOR'] = '[0:0:0:0:0:0:0:1]:65535,127.0.0.2';
|
||||
$_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,[0:0:0:0:0:0:0:2]:65535';
|
||||
$portip = getremoteaddr();
|
||||
$this->assertEquals('0:0:0:0:0:0:0:1', $portip);
|
||||
$this->assertEquals('0:0:0:0:0:0:0:2', $portip);
|
||||
|
||||
$_SERVER['HTTP_X_FORWARDED_FOR'] = $xforwardedfor;
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
This files describes API changes in core libraries and APIs,
|
||||
information provided here is intended especially for developers.
|
||||
|
||||
=== 3.5.10 ===
|
||||
|
||||
* behat_data_generators::the_following_exist() has been removed, please use
|
||||
behat_data_generators::the_following_entities_exist() instead. See MDL-67691 for more info.
|
||||
|
||||
=== 3.5.6 ===
|
||||
|
||||
* Behat timeout constants behat_base::TIMEOUT, EXTENDED_TIMEOUT, and REDUCED_TIMEOUT will be
|
||||
|
||||
+12
-1
@@ -30,7 +30,18 @@ require_once('lib.php');
|
||||
redirect_if_major_upgrade_required();
|
||||
|
||||
$testsession = optional_param('testsession', 0, PARAM_INT); // test session works properly
|
||||
$anchor = optional_param('anchor', '', PARAM_RAW); // Used to restore hash anchor to wantsurl.
|
||||
$anchor = optional_param('anchor', '', PARAM_RAW); // Used to restore hash anchor to wantsurl.
|
||||
|
||||
// It might be safe to do this for non-Behat sites, or there might
|
||||
// be a security risk. For now we only allow it on Behat sites.
|
||||
// If you wants to do the analysis, you may be able to remove the
|
||||
// if (BEHAT_SITE_RUNNING).
|
||||
if (defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING) {
|
||||
$wantsurl = optional_param('wantsurl', '', PARAM_LOCALURL); // Overrides $SESSION->wantsurl if given.
|
||||
if ($wantsurl !== '') {
|
||||
$SESSION->wantsurl = (new moodle_url($wantsurl))->out(false);
|
||||
}
|
||||
}
|
||||
|
||||
$context = context_system::instance();
|
||||
$PAGE->set_url("$CFG->wwwroot/login/index.php");
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -44,9 +44,8 @@ Feature: Students can edit or delete their forum posts within a set time limit
|
||||
Scenario: Time limit expires
|
||||
Given I log out
|
||||
And I log in as "admin"
|
||||
And I navigate to "Security > Site security settings" in site administration
|
||||
And I set the field "Maximum time to edit posts" to "1 minutes"
|
||||
And I press "Save changes"
|
||||
And the following config values are set as admin:
|
||||
| maxeditingtime | 1 |
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add the "Recent activity" block
|
||||
And I log out
|
||||
@@ -54,7 +53,7 @@ Feature: Students can edit or delete their forum posts within a set time limit
|
||||
And I am on "Course 1" course homepage
|
||||
And I should see "New forum posts:" in the "Recent activity" "block"
|
||||
And I should see "Forum post subject" in the "Recent activity" "block"
|
||||
When I wait "61" seconds
|
||||
When I wait "2" seconds
|
||||
And I follow "Forum post subject"
|
||||
Then I should not see "Edit" in the "region-main" "region"
|
||||
And I should not see "Delete" in the "region-main" "region"
|
||||
|
||||
@@ -4,7 +4,6 @@ Feature: A teacher can set whether glossary entries are always editable or not
|
||||
As a teacher
|
||||
I need to prevent entries to be always editable
|
||||
|
||||
@javascript
|
||||
Scenario: Glossary entries are not always editable
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
@@ -18,7 +17,7 @@ Feature: A teacher can set whether glossary entries are always editable or not
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
And the following config values are set as admin:
|
||||
| maxeditingtime | 60 |
|
||||
| maxeditingtime | 5 |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Glossary" to section "1" and I fill the form with:
|
||||
@@ -34,7 +33,7 @@ Feature: A teacher can set whether glossary entries are always editable or not
|
||||
| Definition | Test concept description |
|
||||
Then "Delete entry: Test concept name" "link" should exist
|
||||
And "Edit entry: Test concept name" "link" should exist
|
||||
And I wait "65" seconds
|
||||
And I wait "6" seconds
|
||||
And I reload the page
|
||||
Then "Delete entry: Test concept name" "link" should not exist
|
||||
And "Delete entry: Test concept name" "link" should not exist
|
||||
And "Edit entry: Test concept name" "link" should not exist
|
||||
|
||||
@@ -32,8 +32,11 @@ $pageid = required_param('pageid', PARAM_INT);
|
||||
$id = required_param('id', PARAM_INT); // Course Module ID
|
||||
$qtype = optional_param('qtype', 0, PARAM_INT);
|
||||
$edit = optional_param('edit', false, PARAM_BOOL);
|
||||
$returnto = optional_param('returnto', null, PARAM_URL);
|
||||
if (empty($returnto)) {
|
||||
$returnto = optional_param('returnto', null, PARAM_LOCALURL);
|
||||
|
||||
if (!empty($returnto)) {
|
||||
$returnto = new moodle_url($returnto);
|
||||
} else {
|
||||
$returnto = new moodle_url('/mod/lesson/edit.php', array('id' => $id));
|
||||
$returnto->set_anchor('lesson-' . $pageid);
|
||||
}
|
||||
|
||||
@@ -581,7 +581,7 @@ function lesson_add_header_buttons($cm, $context, $extraeditbuttons=false, $less
|
||||
'id' => $cm->id,
|
||||
'pageid' => $lessonpageid,
|
||||
'edit' => 1,
|
||||
'returnto' => $PAGE->url->out(false)
|
||||
'returnto' => $PAGE->url->out_as_local_url(false)
|
||||
));
|
||||
$PAGE->set_button($OUTPUT->single_button($url, get_string('editpagecontent', 'lesson')));
|
||||
}
|
||||
@@ -1320,7 +1320,7 @@ abstract class lesson_add_page_form_base extends moodleform {
|
||||
|
||||
if (!empty($this->_customdata['returnto'])) {
|
||||
$mform->addElement('hidden', 'returnto', $this->_customdata['returnto']);
|
||||
$mform->setType('returnto', PARAM_URL);
|
||||
$mform->setType('returnto', PARAM_LOCALURL);
|
||||
}
|
||||
|
||||
$mform->addElement('hidden', 'id');
|
||||
|
||||
@@ -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:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user