Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 239be346a1 | |||
| e23a5c6b85 | |||
| fd6667f694 | |||
| 6d990ff157 | |||
| 5ad39879ea | |||
| 0e8027d3dd | |||
| 13ab8ce3f6 | |||
| 321f20bde4 | |||
| ccb94debcf | |||
| 596f290ca7 | |||
| 3cf753aea9 | |||
| 2689a8fdd3 | |||
| 0ef7b2853d | |||
| 3389ddbe77 | |||
| 9c0d3f8037 | |||
| 7b5bdf9b6e | |||
| 78468dffaa | |||
| d314c4719d | |||
| 4b30d08025 | |||
| bc29ef5d9e | |||
| 3030cde4ba | |||
| bfef5e4581 | |||
| 17c4412033 | |||
| 5b9e5416d5 | |||
| d3777d8a06 | |||
| 3613faf8d6 | |||
| 9b060f43ce | |||
| d63b359944 | |||
| 144c57c4ff | |||
| 9956df769b | |||
| b2fab52908 | |||
| 787199fa99 | |||
| 0afa5bfdb5 | |||
| 9247cf1bd6 | |||
| 42eeec6261 | |||
| 8b234f6690 | |||
| f4795ade4a | |||
| fc1a79d882 | |||
| 6b715e7e7f | |||
| bd96199040 | |||
| 127b856a73 | |||
| 28f675da81 | |||
| f2552bb9c4 |
@@ -0,0 +1,24 @@
|
||||
issueNumber: MDL-86231
|
||||
notes:
|
||||
core:
|
||||
- message: >
|
||||
The Behat `::execute()` method now accepts an array-style callable in
|
||||
addition to the string `classname::method` format.
|
||||
|
||||
|
||||
The following formats are now accepted:
|
||||
|
||||
|
||||
```php
|
||||
|
||||
// String format:
|
||||
|
||||
$this->execute('behat_general::i_click_on', [...]);
|
||||
|
||||
|
||||
// Array format:
|
||||
|
||||
$this->execute([behat_general::class],' i_click_on'], [...]);
|
||||
|
||||
```
|
||||
type: improved
|
||||
@@ -51,6 +51,14 @@ if ($hassiteconfig) {
|
||||
|
||||
$temp->add(new admin_setting_countrycodes('allcountrycodes', new lang_string('allcountrycodes', 'core_admin'),
|
||||
new lang_string('configallcountrycodes', 'core_admin')));
|
||||
|
||||
$temp->add(new admin_setting_configtext(
|
||||
'geopluginapikey',
|
||||
new lang_string('geopluginapikey', 'core_admin'),
|
||||
new lang_string('geopluginapikey_desc', 'core_admin'),
|
||||
'',
|
||||
PARAM_TEXT,
|
||||
));
|
||||
}
|
||||
|
||||
$ADMIN->add('location', $temp);
|
||||
|
||||
@@ -73,7 +73,15 @@ class auth_plugin_webservice extends auth_plugin_base {
|
||||
function user_login_webservice($username, $password) {
|
||||
global $CFG, $DB;
|
||||
// special web service login
|
||||
if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) {
|
||||
$user = $DB->get_record('user', [
|
||||
'username' => $username,
|
||||
'auth' => 'webservice',
|
||||
'deleted' => '0',
|
||||
'suspended' => '0',
|
||||
'mnethostid' => $CFG->mnet_localhost_id,
|
||||
]);
|
||||
|
||||
if ($user) {
|
||||
return validate_internal_user_password($user, $password);
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -911,10 +911,10 @@ class core_calendar_external extends external_api {
|
||||
$formoptions['eventtypes'] = $allowedeeventtypes;
|
||||
if ($courseid) {
|
||||
require_once($CFG->libdir . '/grouplib.php');
|
||||
$groupcoursedata = groups_get_course_data($courseid);
|
||||
if (!empty($groupcoursedata->groups)) {
|
||||
$groupcoursedata = groups_get_all_groups($courseid);
|
||||
if (!empty($groupcoursedata)) {
|
||||
$formoptions['groups'] = [];
|
||||
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
|
||||
foreach ($groupcoursedata as $groupid => $groupdata) {
|
||||
$formoptions['groups'][$groupid] = $groupdata->name;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -3608,9 +3608,9 @@ function calendar_output_fragment_event_form($args) {
|
||||
|
||||
if (is_null($eventid)) {
|
||||
if (!empty($courseid)) {
|
||||
$groupcoursedata = groups_get_course_data($courseid);
|
||||
$groupcoursedata = groups_get_all_groups($courseid);
|
||||
$formoptions['groups'] = [];
|
||||
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
|
||||
foreach ($groupcoursedata as $groupid => $groupdata) {
|
||||
$formoptions['groups'][$groupid] = $groupdata->name;
|
||||
}
|
||||
}
|
||||
@@ -3652,9 +3652,9 @@ function calendar_output_fragment_event_form($args) {
|
||||
$formoptions['event'] = $event;
|
||||
|
||||
if (!empty($event->courseid)) {
|
||||
$groupcoursedata = groups_get_course_data($event->courseid);
|
||||
$groupcoursedata = groups_get_all_groups($event->courseid);
|
||||
$formoptions['groups'] = [];
|
||||
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
|
||||
foreach ($groupcoursedata as $groupid => $groupdata) {
|
||||
$formoptions['groups'][$groupid] = $groupdata->name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,6 +380,12 @@ class core_cohort_external extends external_api {
|
||||
$results = array_merge($results, cohort_get_available_cohorts($context, COHORT_ALL, $limitfrom, $limitnum, $query));
|
||||
}
|
||||
} else if ($includes == 'all') {
|
||||
$contextsystem = context_system::instance();
|
||||
if (!$context instanceof context_system &&
|
||||
!has_any_capability(['moodle/cohort:view', 'moodle/cohort:manage'], $contextsystem)) {
|
||||
|
||||
throw new required_capability_exception($contextsystem, 'moodle/cohort:view', 'nopermissions', '');
|
||||
}
|
||||
$results = cohort_get_all_cohorts($limitfrom, $limitnum, $query);
|
||||
$results = $results['cohorts'];
|
||||
} else {
|
||||
|
||||
@@ -555,6 +555,7 @@ final class externallib_test extends externallib_advanced_testcase {
|
||||
role_assign($userrole, $catuser->id, $catcontext->id);
|
||||
|
||||
// Enrol user in the course.
|
||||
$this->getDataGenerator()->enrol_user($creator->id, $course->id);
|
||||
$this->getDataGenerator()->enrol_user($courseuser->id, $course->id, 'courserole');
|
||||
|
||||
$syscontext = array('contextid' => \context_system::instance()->id);
|
||||
@@ -612,18 +613,27 @@ final class externallib_test extends externallib_advanced_testcase {
|
||||
$this->assertEquals(3, count($result['cohorts']));
|
||||
|
||||
// A user in the course context with the system cohort:view capability. Check that all the system cohorts are returned.
|
||||
$this->setUser($courseuser);
|
||||
$result = core_cohort_external::search_cohorts("Cohortsearch", $coursecontext, 'all');
|
||||
$this->assertEquals(1, count($result['cohorts']));
|
||||
$this->assertEquals('Cohortsearch 1', $result['cohorts'][$cohort1->id]->name);
|
||||
$this->assertEquals(3, count($result['cohorts']));
|
||||
|
||||
// A user in the course context without the ability to view system cohorts.
|
||||
$this->setUser($courseuser);
|
||||
try {
|
||||
$result = core_cohort_external::search_cohorts("Cohortsearch", $coursecontext, 'all');
|
||||
$this->fail('Exception expected');
|
||||
} catch (\Throwable $e) {
|
||||
$this->assertInstanceOf(\required_capability_exception::class, $e);
|
||||
$this->assertStringContainsString('(View site-wide cohorts)', $e->getMessage());
|
||||
}
|
||||
|
||||
// Detect invalid parameter $includes.
|
||||
$this->setUser($creator);
|
||||
try {
|
||||
$result = core_cohort_external::search_cohorts("Cohortsearch", $syscontext, 'invalid');
|
||||
$this->fail('Invalid parameter includes');
|
||||
} catch (\coding_exception $e) {
|
||||
// All good.
|
||||
$this->fail('Exception expected');
|
||||
} catch (\Throwable $e) {
|
||||
$this->assertInstanceOf(\coding_exception::class, $e);
|
||||
$this->assertStringContainsString('Invalid parameter value for \'includes\'', $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <https://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 https://moodledev.io/general/projects/api/amos}) using the
|
||||
* list of strings defined in install/stringnames.txt file.
|
||||
*
|
||||
* @package installer
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['cliincorrectvalueerror'] = 'Mearachd, luach ceàrr "{$a->value}" airson "{$a->option}"';
|
||||
$string['cliincorrectvalueretry'] = 'Luach ceàrr, feuch a-rithist';
|
||||
$string['cliunknowoption'] = 'Roghainnean nach deach an aithneachadh:
|
||||
{$a}
|
||||
Cleachd— an roghainn airson cuideachadh';
|
||||
$string['environmentrequireversion'] = 'tha tionndadh {$a->needed} deatamach agus tha thusa a’ ruith {$a->current}';
|
||||
@@ -32,3 +32,4 @@ defined('MOODLE_INTERNAL') || die();
|
||||
$string['language'] = 'Cànan';
|
||||
$string['moodlelogo'] = 'Logo Mhoodle';
|
||||
$string['previous'] = 'Roimhe';
|
||||
$string['reload'] = 'Ath-luchdaich';
|
||||
|
||||
@@ -45,7 +45,7 @@ $string['dmlexceptiononinstall'] = '<p>データベースエラーが発生し
|
||||
$string['downloadedfilecheckfailed'] = 'ダウンロードファイルのチェックに失敗しました。';
|
||||
$string['invalidmd5'] = 'チェック変数が正しくありません - 再度お試しください。';
|
||||
$string['missingrequiredfield'] = 'いくつかの必須入力フィールドが入力されていません。';
|
||||
$string['remotedownloaderror'] = '<p>あなたのサーバへのコンポーネントのダウンロードに失敗しました。プロキシ設定を確認してください。PHP cURL拡張モジュールの使用を強くお勧めします。</p>
|
||||
$string['remotedownloaderror'] = '<p>あなたのサーバへのコンポーネントのダウンロードに失敗しました。プロキシ設定をご確認ください。PHP cURL拡張モジュールの使用を強くお勧めします。</p>
|
||||
<p><a href="{$a->url}">{$a->url}</a>ファイルを手動でダウンロードした後、あなたのサーバの「{$a->dest}」にコピーおよび展開してください。</p>';
|
||||
$string['wrongdestpath'] = '宛先パスが正しくありません。';
|
||||
$string['wrongsourcebase'] = 'ソースURLベースが正しくありません。';
|
||||
|
||||
@@ -32,10 +32,13 @@ defined('MOODLE_INTERNAL') || die();
|
||||
$string['clianswerno'] = 'n';
|
||||
$string['cliansweryes'] = 'y';
|
||||
$string['cliincorrectvalueerror'] = 'Xato, “{$a->option}” uchun noto‘g‘ri “{$a->value}” qiymati';
|
||||
$string['cliincorrectvalueretry'] = 'Noto\'g\'ri qiymat, iltimos qayta urinib ko\'ring';
|
||||
$string['cliincorrectvalueretry'] = 'Noto‘g‘ri qiymat, iltimos qayta urinib ko‘ring';
|
||||
$string['clitypevalue'] = 'qiymatni kiritish';
|
||||
$string['clitypevaluedefault'] = 'qiymatni kiriting, ({$a}) odatiy qiymatni ishlatish uchun Enter tumgasini bosing';
|
||||
$string['cliunknowoption'] = 'Noma\'lum parametrlar:
|
||||
{$a}
|
||||
Iltimos, -- help parametridan foydalaning';
|
||||
$string['cliyesnoprompt'] = 'y tugmasini bosing (ha degani) yoki n tugmasini bosing (yo‘q degani)';
|
||||
$string['environmentrequireinstall'] = 'o‘rnatilishi va yoqilishi shart.';
|
||||
$string['environmentrequireversion'] = '{$a->needed} versiyasi talab etiladi, sizda esa hozirda {$a->current} ishlatilmoqda';
|
||||
$string['upgradekeyset'] = 'Yangilash kaliti (o‘rnatmaslik uchun bo‘sh qoldiring)';
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <https://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 https://moodledev.io/general/projects/api/amos}) using the
|
||||
* list of strings defined in install/stringnames.txt file.
|
||||
*
|
||||
* @package installer
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['cannotcreatedboninstall'] = '<p>Ma\'lumotlar bazasini yaratib bo\'lmadi.</p>
|
||||
<p>Ko\'rsatilgan ma\'lumotlar bazasi mavjud emas va berilgan foydalanuvchida uni yaratish huquqi yo\'q.</p>
|
||||
<p>Sayt administratori ma\'lumotlar bazasi konfiguratsiyasini tekshirishi kerak.</p>';
|
||||
$string['cannotcreatelangdir'] = 'Til katalogini yaratib bo\'lmadi';
|
||||
$string['cannotcreatetempdir'] = 'Vaqtinchalik jildni yaratib bo‘lmadi';
|
||||
$string['cannotdownloadcomponents'] = 'Komponentlarni yuklab olib bo\'lmadi';
|
||||
$string['cannotdownloadzipfile'] = 'ZIP faylini yuklab olib bo\'lmadi';
|
||||
$string['cannotfindcomponent'] = 'Komponentni topib bo\'lmadi';
|
||||
@@ -29,9 +29,9 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['admindirname'] = 'Administrator katalogi nomi';
|
||||
$string['availablelangs'] = 'Mavjud(foydalanish mumkin bo\'lgan)til paketlari';
|
||||
$string['dataroot'] = 'Ma\'lumotlar katalogi';
|
||||
$string['admindirname'] = 'Admin katalogi';
|
||||
$string['availablelangs'] = 'Mavjud til paketlari';
|
||||
$string['dataroot'] = 'Ma’lumotlar katalogi';
|
||||
$string['dbprefix'] = 'Jadvallar prefiksi';
|
||||
$string['dirroot'] = 'Moodle kataloglari';
|
||||
$string['dirroot'] = 'Moodle katalogi';
|
||||
$string['installation'] = 'O\'rnatish';
|
||||
|
||||
+10
-5
@@ -64,7 +64,7 @@ function iplookup_find_location($ip) {
|
||||
|
||||
return $info;
|
||||
|
||||
} else {
|
||||
} else if (!empty($CFG->geopluginapikey)) {
|
||||
require_once($CFG->libdir.'/filelib.php');
|
||||
|
||||
if (strpos($ip, ':') !== false) {
|
||||
@@ -73,11 +73,13 @@ function iplookup_find_location($ip) {
|
||||
return $info;
|
||||
}
|
||||
|
||||
$ipdata = download_file_content('http://www.geoplugin.net/json.gp?ip='.$ip);
|
||||
if ($ipdata) {
|
||||
$ipdata = preg_replace('/^geoPlugin\((.*)\)\s*$/s', '$1', $ipdata);
|
||||
$ipdata = json_decode($ipdata, true);
|
||||
$requesturl = new moodle_url('https://api.geoplugin.com', ['ip' => $ip, 'auth' => $CFG->geopluginapikey]);
|
||||
$response = download_file_content($requesturl->out(false), null, null, true);
|
||||
if ($response->response_code != 200) {
|
||||
$info['error'] = get_string('cannotgeoplugin', 'error');
|
||||
return $info;
|
||||
}
|
||||
$ipdata = json_decode($response->results, true);
|
||||
if (!is_array($ipdata)) {
|
||||
$info['error'] = get_string('cannotgeoplugin', 'error');
|
||||
return $info;
|
||||
@@ -103,4 +105,7 @@ function iplookup_find_location($ip) {
|
||||
return $info;
|
||||
}
|
||||
|
||||
$info['error'] = get_string('iplookupfailed', 'error', $ip);
|
||||
return $info;
|
||||
|
||||
}
|
||||
|
||||
@@ -50,8 +50,12 @@ final class geoip_test extends \advanced_testcase {
|
||||
* @param string $ip The IP to test
|
||||
*/
|
||||
public function test_ip($ip): void {
|
||||
global $CFG;
|
||||
if (!defined('TEST_GEOIP_APIKEY') || empty(TEST_GEOIP_APIKEY)) {
|
||||
$this->markTestSkipped('External geo tests are disabled.');
|
||||
}
|
||||
$this->resetAfterTest();
|
||||
|
||||
$CFG->geopluginapikey = TEST_GEOIP_APIKEY;
|
||||
$this->setup_geoip2file();
|
||||
|
||||
// Note: The results we get from the iplookup tests are beyond our control.
|
||||
|
||||
@@ -35,12 +35,20 @@ final class geoplugin_test extends \advanced_testcase {
|
||||
}
|
||||
|
||||
/**
|
||||
* In order to execute this test PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php
|
||||
* In order to execute this test:
|
||||
* - PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php
|
||||
* - GeoPlugin API key should be defined in config.php as TEST_GEOIP_APIKEY
|
||||
*/
|
||||
public function setUp(): void {
|
||||
global $CFG;
|
||||
if (!PHPUNIT_LONGTEST) {
|
||||
$this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
|
||||
}
|
||||
|
||||
if (!defined('TEST_GEOIP_APIKEY') || empty(TEST_GEOIP_APIKEY)) {
|
||||
$this->markTestSkipped('External geo tests are disabled.');
|
||||
}
|
||||
$CFG->geopluginapikey = TEST_GEOIP_APIKEY;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -667,6 +667,8 @@ $string['fullnamedisplayprivate'] = 'Full name format - private';
|
||||
$string['gdrequired'] = 'The GD extension is now required by Moodle for image conversion.';
|
||||
$string['generalsettings'] = 'General settings';
|
||||
$string['geoipfile'] = 'GeoLite2 City MaxMind DB';
|
||||
$string['geopluginapikey'] = 'GeoPlugin API key';
|
||||
$string['geopluginapikey_desc'] = 'The API key used to access the GeoPlugin service. Get your own key at <a href="https://www.geoplugin.com/" target="_blank">GeoPlugin page</a>.';
|
||||
$string['getremoteaddrconf'] = 'Logged IP address source';
|
||||
$string['globalsearch'] = 'Global search';
|
||||
$string['globalsearchmanage'] = 'Manage global search';
|
||||
|
||||
@@ -1053,19 +1053,37 @@ EOF;
|
||||
/**
|
||||
* Helper function to execute api in a given context.
|
||||
*
|
||||
* @param string $contextapi context in which api is defined.
|
||||
* @param array $params list of params to pass.
|
||||
* Note: The contextapi does not support a callback.
|
||||
*
|
||||
* @param string|array $contextapi context in which api is defined.
|
||||
* @param array|mixed $params list of params to pass or a single parameter
|
||||
* @throws Exception
|
||||
* @throws DriverException
|
||||
*/
|
||||
protected function execute($contextapi, $params = array()) {
|
||||
protected function execute(
|
||||
$contextapi,
|
||||
$params = []
|
||||
): void {
|
||||
if (!is_array($params)) {
|
||||
$params = array($params);
|
||||
$params = [$params];
|
||||
}
|
||||
|
||||
if (is_string($contextapi)) {
|
||||
$contextapi = explode('::', $contextapi);
|
||||
}
|
||||
|
||||
if (count($contextapi) !== 2) {
|
||||
throw new DriverException('Invalid contextapi format, expected "context::api" or ["context", "api"]');
|
||||
}
|
||||
|
||||
// Get required context and execute the api.
|
||||
$contextapi = explode("::", $contextapi);
|
||||
$context = behat_context_helper::get($contextapi[0]);
|
||||
call_user_func_array(array($context, $contextapi[1]), $params);
|
||||
[$classname, $method] = $contextapi;
|
||||
if (!is_string($classname) || !is_string($method)) {
|
||||
throw new DriverException('Invalid contextapi format, expected "context::api" or ["context", "api"]');
|
||||
}
|
||||
|
||||
$context = behat_context_helper::get($classname);
|
||||
call_user_func_array([$context, $method], $params);
|
||||
|
||||
// NOTE: Wait for pending js and look for exception are not optional, as this might lead to unexpected results.
|
||||
// Don't make them optional for performance reasons.
|
||||
|
||||
@@ -293,11 +293,11 @@ class site_registration_form extends \moodleform {
|
||||
if ($data = parent::get_data()) {
|
||||
// Never return '*newemail' checkboxes, always return 'emailalertemail' and 'commnewsemail' even if not applicable.
|
||||
if (empty($data->emailalert) || empty($data->emailalertnewemail)) {
|
||||
$data->emailalertemail = null;
|
||||
$data->emailalertemail = '';
|
||||
}
|
||||
unset($data->emailalertnewemail);
|
||||
if (empty($data->commnews) || empty($data->commnewsnewemail)) {
|
||||
$data->commnewsemail = null;
|
||||
$data->commnewsemail = '';
|
||||
}
|
||||
unset($data->commnewsnewemail);
|
||||
// Always return 'contactable'.
|
||||
|
||||
@@ -998,7 +998,21 @@ class navigation_node implements renderable {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all static data.
|
||||
*
|
||||
* @throws coding_exception if called outside of a unit test
|
||||
*/
|
||||
public static function reset_all_data(): void {
|
||||
if (!defined('PHPUNIT_TEST') || !PHPUNIT_TEST) {
|
||||
throw new coding_exception('Resetting all data is not allowed outside of PHPUnit tests.');
|
||||
}
|
||||
|
||||
self::$fullmeurl = null;
|
||||
self::$autofindactive = true;
|
||||
self::$loadadmintree = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -199,6 +199,7 @@ class phpunit_util extends testing_util {
|
||||
// reinitialise following globals
|
||||
$OUTPUT = new bootstrap_renderer();
|
||||
$PAGE = new moodle_page();
|
||||
\navigation_node::reset_all_data();
|
||||
$FULLME = null;
|
||||
$ME = null;
|
||||
$SCRIPT = null;
|
||||
|
||||
@@ -145,6 +145,29 @@ STRING;
|
||||
self::assertTag(['id' => 'testid'], "<div><div>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the navigation node URL is overridden correctly.
|
||||
*/
|
||||
public function test_set_navigation_url(): void {
|
||||
\navigation_node::override_active_url(new \moodle_url('/foo/bar/baz'));
|
||||
$property = new \ReflectionProperty(\navigation_node::class, 'fullmeurl');
|
||||
$property->setAccessible(true);
|
||||
|
||||
$this->assertNotNull($property->getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the after-test teardown correctly resets the navigation node URL.
|
||||
*
|
||||
* @depends test_set_navigation_url
|
||||
*/
|
||||
public function test_navigation_url_reset(): void {
|
||||
$property = new \ReflectionProperty(\navigation_node::class, 'fullmeurl');
|
||||
$property->setAccessible(true);
|
||||
|
||||
$this->assertNull($property->getValue());
|
||||
}
|
||||
|
||||
// Uncomment following tests to see logging of unexpected changes in global state and database.
|
||||
/*
|
||||
public function test_db_modification() {
|
||||
|
||||
@@ -78,7 +78,7 @@ class behat_general extends behat_base {
|
||||
* @Given /^I am on homepage$/
|
||||
*/
|
||||
public function i_am_on_homepage() {
|
||||
$this->execute('behat_general::i_visit', ['/']);
|
||||
$this->execute([self::class, 'i_visit'], ['/']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,7 +87,7 @@ class behat_general extends behat_base {
|
||||
* @Given /^I am on site homepage$/
|
||||
*/
|
||||
public function i_am_on_site_homepage() {
|
||||
$this->execute('behat_general::i_visit', ['/?redirect=0']);
|
||||
$this->execute([self::class, 'i_visit'], ['/?redirect=0']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,7 +96,7 @@ class behat_general extends behat_base {
|
||||
* @Given /^I am on course index$/
|
||||
*/
|
||||
public function i_am_on_course_index() {
|
||||
$this->execute('behat_general::i_visit', ['/course/index.php']);
|
||||
$this->execute([self::class, 'i_visit'], ['/course/index.php']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -442,7 +442,7 @@ class behat_general extends behat_base {
|
||||
*/
|
||||
public function i_click_on_confirming_the_dialogue($element, $selectortype) {
|
||||
$this->i_click_on($element, $selectortype);
|
||||
$this->execute('behat_general::accept_currently_displayed_alert_dialog', []);
|
||||
$this->execute([self::class, 'accept_currently_displayed_alert_dialog'], []);
|
||||
$this->wait_until_the_page_is_ready();
|
||||
}
|
||||
|
||||
@@ -456,7 +456,7 @@ class behat_general extends behat_base {
|
||||
*/
|
||||
public function i_click_on_dismissing_the_dialogue($element, $selectortype) {
|
||||
$this->i_click_on($element, $selectortype);
|
||||
$this->execute('behat_general::dismiss_currently_displayed_alert_dialog', []);
|
||||
$this->execute([self::class, 'dismiss_currently_displayed_alert_dialog'], []);
|
||||
$this->wait_until_the_page_is_ready();
|
||||
}
|
||||
|
||||
@@ -1145,7 +1145,7 @@ EOF;
|
||||
* @Given /^I trigger cron$/
|
||||
*/
|
||||
public function i_trigger_cron() {
|
||||
$this->execute('behat_general::i_visit', ['/admin/cron.php']);
|
||||
$this->execute([self::class, 'i_visit'], ['/admin/cron.php']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2138,8 +2138,8 @@ EOF;
|
||||
}
|
||||
// Gets the node based on the requested selector type and locator.
|
||||
$node = $this->get_selected_node($selectortype, $element);
|
||||
$this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
|
||||
$this->execute('behat_general::i_press_named_key', ['', 'tab']);
|
||||
$this->execute([self::class, 'i_click_on'], [$node, 'NodeElement']);
|
||||
$this->execute([self::class, 'i_press_named_key'], ['', 'tab']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2252,9 +2252,9 @@ EOF;
|
||||
*/
|
||||
public function i_manually_press_tab($shift = '') {
|
||||
if (empty($shift)) {
|
||||
$this->execute('behat_general::i_press_named_key', ['', 'tab']);
|
||||
$this->execute([self::class, 'i_press_named_key'], ['', 'tab']);
|
||||
} else {
|
||||
$this->execute('behat_general::i_press_named_key', ['shift', 'tab']);
|
||||
$this->execute([self::class, 'i_press_named_key'], ['shift', 'tab']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2317,7 +2317,7 @@ EOF;
|
||||
* @throws DriverException
|
||||
*/
|
||||
public function i_manually_press_enter() {
|
||||
$this->execute('behat_general::i_press_named_key', ['', 'enter']);
|
||||
$this->execute([self::class, 'i_press_named_key'], ['', 'enter']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2352,7 +2352,7 @@ EOF;
|
||||
*/
|
||||
public function i_click_on_the_dynamic_tab(string $tabname): void {
|
||||
$xpath = "//*[@id='dynamictabs-tabs'][descendant::a[contains(text(), '" . $this->escape($tabname) . "')]]";
|
||||
$this->execute('behat_general::i_click_on_in_the',
|
||||
$this->execute([self::class, 'i_click_on_in_the'],
|
||||
[$tabname, 'link', $xpath, 'xpath_element']);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,9 @@ final class navigationlib_test extends \advanced_testcase {
|
||||
protected function setup_node() {
|
||||
global $PAGE, $SITE;
|
||||
|
||||
// Perform a reset between tests to reset the PAGE.
|
||||
$this->resetAfterTest();
|
||||
|
||||
$PAGE->set_url('/');
|
||||
$PAGE->set_course($SITE);
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
This files describes API changes in core libraries and APIs,
|
||||
information provided here is intended especially for developers.
|
||||
|
||||
=== 4.1.21 ===
|
||||
|
||||
* Moodle's Behat `::execute()` method now supports the use of array callable syntax for calling other steps.
|
||||
|
||||
=== 4.1.20 ===
|
||||
|
||||
* Add a new method has_valid_group in \core\report_helper that will return true or false depending if the user has a valid group. This is mainly
|
||||
|
||||
@@ -89,6 +89,16 @@ class user_submission_actionmenu implements templatable, renderable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Has the submission started.
|
||||
*
|
||||
* @return bool The status of the submission; true if started, otherwise false.
|
||||
*/
|
||||
protected function is_submission_started(): bool {
|
||||
return (is_object($this->teamsubmission) && isset($this->teamsubmission->timestarted))
|
||||
|| (is_object($this->submission) && isset($this->submission->timestarted));
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the submission buttons for the page.
|
||||
*
|
||||
@@ -125,19 +135,20 @@ class user_submission_actionmenu implements templatable, renderable {
|
||||
$data['edit']['help'] = $newattempthelp->export_for_template($output);
|
||||
}
|
||||
if ($status === ASSIGN_SUBMISSION_STATUS_NEW) {
|
||||
|
||||
if ($this->timelimit && empty($this->submission->timestarted)) {
|
||||
$timelimitenabled = get_config('assign', 'enabletimelimit');
|
||||
if ($timelimitenabled && $this->timelimit && !$this->is_submission_started()) {
|
||||
$confirmation = new \confirm_action(
|
||||
get_string('confirmstart', 'assign', format_time($this->timelimit)),
|
||||
null,
|
||||
get_string('beginassignment', 'assign')
|
||||
);
|
||||
$urlparams = array('id' => $this->cmid, 'action' => 'editsubmission');
|
||||
// The 'begin' flag indicates that the user is starting a timed assignment.
|
||||
$urlparams = ['id' => $this->cmid, 'action' => 'editsubmission', 'begin' => 1];
|
||||
$beginbutton = new \action_link(
|
||||
new moodle_url('/mod/assign/view.php', $urlparams),
|
||||
get_string('beginassignment', 'assign'),
|
||||
$confirmation,
|
||||
['class' => 'btn btn-primary']
|
||||
get_string('beginassignment', 'assign'),
|
||||
$confirmation,
|
||||
['class' => 'btn btn-primary']
|
||||
);
|
||||
$data['edit']['button'] = $beginbutton->export_for_template($output);
|
||||
$data['edit']['begin'] = true;
|
||||
|
||||
@@ -27,22 +27,33 @@ class PdfType
|
||||
* @param PdfType $value
|
||||
* @param PdfParser $parser
|
||||
* @param bool $stopAtIndirectObject
|
||||
* @param array $ensuredObjectsList A list of all ensured indirect objects to prevent recursion
|
||||
* @return PdfType
|
||||
* @throws CrossReferenceException
|
||||
* @throws PdfParserException
|
||||
*/
|
||||
public static function resolve(PdfType $value, PdfParser $parser, $stopAtIndirectObject = false)
|
||||
{
|
||||
public static function resolve(
|
||||
PdfType $value,
|
||||
PdfParser $parser,
|
||||
$stopAtIndirectObject = false,
|
||||
array &$ensuredObjectsList = []
|
||||
) {
|
||||
if ($value instanceof PdfIndirectObjectReference) {
|
||||
$value = $parser->getIndirectObject($value->value);
|
||||
}
|
||||
|
||||
if ($value instanceof PdfIndirectObject) {
|
||||
if ($stopAtIndirectObject === true) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return self::resolve($value->value, $parser, $stopAtIndirectObject);
|
||||
}
|
||||
|
||||
if ($value instanceof PdfIndirectObjectReference) {
|
||||
return self::resolve($parser->getIndirectObject($value->value), $parser, $stopAtIndirectObject);
|
||||
if (\in_array($value->objectNumber, $ensuredObjectsList, true)) {
|
||||
throw new PdfParserException(
|
||||
\sprintf('Indirect reference recursion detected (%s).', $value->objectNumber)
|
||||
);
|
||||
}
|
||||
$ensuredObjectsList[] = $value->objectNumber;
|
||||
return self::resolve($value->value, $parser, $stopAtIndirectObject, $ensuredObjectsList);
|
||||
}
|
||||
|
||||
return $value;
|
||||
|
||||
@@ -139,12 +139,19 @@ class PdfReader
|
||||
$page = $this->pages[$pageNumber - 1];
|
||||
|
||||
if ($page instanceof PdfIndirectObjectReference) {
|
||||
$readPages = function ($kids) use (&$readPages) {
|
||||
$alreadyReadKids = [];
|
||||
$readPages = function ($kids) use (&$readPages, &$alreadyReadKids) {
|
||||
$kids = PdfArray::ensure($kids);
|
||||
|
||||
/** @noinspection LoopWhichDoesNotLoopInspection */
|
||||
foreach ($kids->value as $reference) {
|
||||
$reference = PdfIndirectObjectReference::ensure($reference);
|
||||
|
||||
if (\in_array($reference->value, $alreadyReadKids, true)) {
|
||||
throw new PdfReaderException('Recursive pages dictionary detected.');
|
||||
}
|
||||
$alreadyReadKids[] = $reference->value;
|
||||
|
||||
$object = $this->parser->getIndirectObject($reference->value);
|
||||
$type = PdfDictionary::get($object->value, 'Type');
|
||||
|
||||
@@ -168,6 +175,7 @@ class PdfReader
|
||||
if ($type->value === 'Pages') {
|
||||
$kids = PdfType::resolve(PdfDictionary::get($dict, 'Kids'), $this->parser);
|
||||
try {
|
||||
$alreadyReadKids[] = $page->objectNumber;
|
||||
$page = $this->pages[$pageNumber - 1] = $readPages($kids);
|
||||
} catch (PdfReaderException $e) {
|
||||
if ($e->getCode() !== PdfReaderException::KIDS_EMPTY) {
|
||||
@@ -202,7 +210,8 @@ class PdfReader
|
||||
return;
|
||||
}
|
||||
|
||||
$readPages = function ($kids, $count) use (&$readPages, $readAll) {
|
||||
$alreadyReadKids = [];
|
||||
$readPages = function ($kids, $count) use (&$readPages, &$alreadyReadKids, $readAll) {
|
||||
$kids = PdfArray::ensure($kids);
|
||||
$isLeaf = ($count->value === \count($kids->value));
|
||||
|
||||
@@ -214,6 +223,11 @@ class PdfReader
|
||||
continue;
|
||||
}
|
||||
|
||||
if (\in_array($reference->value, $alreadyReadKids, true)) {
|
||||
throw new PdfReaderException('Recursive pages dictionary detected.');
|
||||
}
|
||||
$alreadyReadKids[] = $reference->value;
|
||||
|
||||
$object = $this->parser->getIndirectObject($reference->value);
|
||||
$type = PdfDictionary::get($object->value, 'Type');
|
||||
|
||||
|
||||
+31
-4
@@ -3145,7 +3145,9 @@ class assign {
|
||||
if ($create) {
|
||||
$action = optional_param('action', '', PARAM_TEXT);
|
||||
if ($action == 'editsubmission') {
|
||||
if (empty($submission->timestarted) && $this->get_instance()->timelimit) {
|
||||
$starttimer = optional_param('begin', 0, PARAM_INT);
|
||||
// Only start the timer if the user has clicked the 'Begin assignment' button.
|
||||
if (empty($submission->timestarted) && $this->get_instance()->timelimit && $starttimer) {
|
||||
$submission->timestarted = time();
|
||||
$DB->update_record('assign_submission', $submission);
|
||||
}
|
||||
@@ -3788,7 +3790,9 @@ class assign {
|
||||
if ($create) {
|
||||
$action = optional_param('action', '', PARAM_TEXT);
|
||||
if ($action == 'editsubmission') {
|
||||
if (empty($submission->timestarted) && $this->get_instance()->timelimit) {
|
||||
$starttimer = optional_param('begin', 0, PARAM_INT);
|
||||
// Only start the timer if the user has clicked the 'Begin assignment' button.
|
||||
if (empty($submission->timestarted) && $this->get_instance()->timelimit && $starttimer) {
|
||||
$submission->timestarted = time();
|
||||
$DB->update_record('assign_submission', $submission);
|
||||
}
|
||||
@@ -4796,7 +4800,7 @@ class assign {
|
||||
* @return string The page output.
|
||||
*/
|
||||
protected function view_edit_submission_page($mform, $notices) {
|
||||
global $CFG, $USER, $DB, $PAGE;
|
||||
global $CFG, $USER, $DB, $PAGE, $OUTPUT;
|
||||
|
||||
$o = '';
|
||||
require_once($CFG->dirroot . '/mod/assign/submission_form.php');
|
||||
@@ -4881,7 +4885,30 @@ class assign {
|
||||
$o .= $this->get_renderer()->notification($notice);
|
||||
}
|
||||
|
||||
$o .= $this->get_renderer()->render(new assign_form('editsubmissionform', $mform));
|
||||
if (
|
||||
$submission->status == ASSIGN_SUBMISSION_STATUS_NEW && $this->get_instance()->timelimit &&
|
||||
empty($submission->timestarted)
|
||||
) {
|
||||
// Timed assignment should always get a confirmation that the user wants to start it.
|
||||
$confirmation = new \confirm_action(
|
||||
get_string('confirmstart', 'assign', format_time($this->get_instance()->timelimit)),
|
||||
null,
|
||||
get_string('beginassignment', 'assign')
|
||||
);
|
||||
// The 'begin' flag indicates that the user is starting a timed assignment.
|
||||
$urlparams = ['id' => $this->get_course_module()->id, 'action' => 'editsubmission', 'begin' => 1];
|
||||
$beginbutton = new \action_link(
|
||||
new moodle_url('/mod/assign/view.php', $urlparams),
|
||||
get_string('beginassignment', 'assign'),
|
||||
$confirmation,
|
||||
['class' => 'btn btn-primary']
|
||||
);
|
||||
|
||||
$o .= $OUTPUT->render($beginbutton);
|
||||
} else {
|
||||
$o .= $this->get_renderer()->render(new assign_form('editsubmissionform', $mform));
|
||||
}
|
||||
|
||||
$o .= $this->view_footer();
|
||||
|
||||
\mod_assign\event\submission_form_viewed::create_from_user($this, $user)->trigger();
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
@mod @mod_assign
|
||||
Feature: In a timed assignment, students should confirm before starting the timer
|
||||
In order to submit a timed assignment
|
||||
As a student
|
||||
I need to confirm to begin the assignment before the timer starts
|
||||
|
||||
Background:
|
||||
Given the following config values are set as admin:
|
||||
| config | value | plugin |
|
||||
| enabletimelimit | 1 | assign |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
| student2 | Student | 2 | student2@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
| student2 | C1 | student |
|
||||
And the following "activities" exist:
|
||||
| activity | course | name | assignsubmission_file_enabled | assignsubmission_file_maxfiles | assignsubmission_file_maxsizebytes | teamsubmission | duedate | timelimit |
|
||||
| assign | C1 | Timed assignment 1 | 1 | 1 | 2097152 | 0 | ##+1 hours 30 minutes## | 60 |
|
||||
| assign | C1 | Group assignment 2 | 1 | 1 | 2097152 | 1 | ##+1 hours 30 minutes## | 60 |
|
||||
|
||||
Scenario: Access a timed assignment from the course page
|
||||
Given I am on the "Timed assignment 1" Activity page logged in as student1
|
||||
And "Begin assignment" "link" should exist
|
||||
When I reload the page
|
||||
Then "Begin assignment" "link" should exist
|
||||
And "#mod_assign_timelimit_block" "css_element" should not exist
|
||||
|
||||
@javascript
|
||||
Scenario: Access a timed assignment from the Dashboard
|
||||
Given I am logged in as student1
|
||||
When I click on "Timed assignment 1" "link" in the "Calendar" "block"
|
||||
And I click on "Add submission" "link" in the ".modal-footer" "css_element"
|
||||
Then "Begin assignment" "link" should exist
|
||||
And I reload the page
|
||||
And "Begin assignment" "link" should exist
|
||||
And "#mod_assign_timelimit_block" "css_element" should not exist
|
||||
# Repeat the steps to confirm timer doesn't start automatically.
|
||||
And I follow "Dashboard"
|
||||
And I click on "Timed assignment 1" "link" in the "Calendar" "block"
|
||||
And I click on "Add submission" "link" in the ".modal-footer" "css_element"
|
||||
And "Begin assignment" "link" should exist
|
||||
And "#mod_assign_timelimit_block" "css_element" should not exist
|
||||
# Now start the timer.
|
||||
And I click on "Begin assignment" "link"
|
||||
And I click on "Begin assignment" "button" in the ".confirmation-buttons" "css_element"
|
||||
And "#mod_assign_timelimit_block" "css_element" should exist
|
||||
|
||||
@javascript
|
||||
Scenario: Access a timed group assignment from the Dashboard
|
||||
Given the following "groups" exist:
|
||||
| name | course | idnumber |
|
||||
| Group 1 | C1 | CG1 |
|
||||
And the following "group members" exist:
|
||||
| user | group |
|
||||
| student1 | CG1 |
|
||||
| student2 | CG1 |
|
||||
And I am logged in as student1
|
||||
When I click on "Group assignment 2" "link" in the "Calendar" "block"
|
||||
And I click on "Add submission" "link" in the ".modal-footer" "css_element"
|
||||
Then "Begin assignment" "link" should exist
|
||||
And I reload the page
|
||||
And "Begin assignment" "link" should exist
|
||||
And "#mod_assign_timelimit_block" "css_element" should not exist
|
||||
# Repeat the steps to confirm timer doesn't start automatically.
|
||||
And I follow "Dashboard"
|
||||
And I click on "Group assignment 2" "link" in the "Calendar" "block"
|
||||
And I click on "Add submission" "link" in the ".modal-footer" "css_element"
|
||||
And "Begin assignment" "link" should exist
|
||||
And "#mod_assign_timelimit_block" "css_element" should not exist
|
||||
# Now start the timer.
|
||||
And I click on "Begin assignment" "link"
|
||||
And I click on "Begin assignment" "button" in the ".confirmation-buttons" "css_element"
|
||||
And "#mod_assign_timelimit_block" "css_element" should exist
|
||||
And I log out
|
||||
# Now check the submission has started for the other group member too.
|
||||
And I am on the "Group assignment 2" "assign activity" page logged in as student2
|
||||
And "Begin assignment" "link" should not exist
|
||||
And "Add submission" "button" should exist
|
||||
@@ -2430,6 +2430,8 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase {
|
||||
|
||||
// Set optional param to indicate start time required.
|
||||
$_GET['action'] = 'editsubmission';
|
||||
// Set optional param to indicate that assignment submission timer should start.
|
||||
$_GET['begin'] = 1;
|
||||
$cm = get_coursemodule_from_instance('assign', $instance->id);
|
||||
$context = \context_module::instance($cm->id);
|
||||
(new \assign($context, $cm, $cm->course))->get_user_submission(0, true);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
@mod_quiz @quizaccess @quizaccess_seb
|
||||
Feature: SEB settings in quiz access rule
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
|
||||
Scenario Outline: Require the use of Safe Exam Browser set to Yes – Configure manually shows message to students
|
||||
Given the following "activities" exist:
|
||||
| activity | course | name | seb_requiresafeexambrowser | idnumber |
|
||||
| quiz | C1 | Quiz 1 | <sebsetting> | quiz1 |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Activity module | quiz1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Test questions | truefalse | Reading | Can you read this? |
|
||||
And quiz "Quiz 1" contains the following questions:
|
||||
| question | page |
|
||||
| Reading | 1 |
|
||||
When I am on the "Quiz 1" "quiz activity" page logged in as student1
|
||||
Then I <messagevisibility> see "This quiz has been configured so that students may only attempt it using the Safe Exam Browser."
|
||||
And "Attempt quiz" "button" <attemptbuttonvisibility> exist
|
||||
|
||||
Examples:
|
||||
| sebsetting | messagevisibility | attemptbuttonvisibility |
|
||||
| 0 | should not | should |
|
||||
| 1 | should | should not |
|
||||
|
||||
Scenario Outline: Show Safe Exam Browser download button setting controls visibility of download link
|
||||
Given the following "activities" exist:
|
||||
| activity | course | name | idnumber | seb_requiresafeexambrowser | seb_showsebdownloadlink |
|
||||
| quiz | C1 | Quiz 1 | quiz1 | 1 | <seb_showsebdownloadlink> |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Activity module | quiz1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Test questions | truefalse | Reading | Can you read this? |
|
||||
And quiz "Quiz 1" contains the following questions:
|
||||
| question | page |
|
||||
| Reading | 1 |
|
||||
When I am on the "Quiz 1" "quiz activity" page logged in as student1
|
||||
Then "Download Safe Exam Browser" "button" <downloadseblinkvisibility> exist
|
||||
And I should see "This quiz has been configured so that students may only attempt it using the Safe Exam Browser."
|
||||
|
||||
Examples:
|
||||
| seb_showsebdownloadlink | downloadseblinkvisibility |
|
||||
| 0 | should not |
|
||||
| 1 | should |
|
||||
+1
-1
@@ -15,4 +15,4 @@ Policy: https://moodledev.io/general/development/process/security
|
||||
|
||||
# Expiry date of this document
|
||||
# This information is considered current and up to date until 3 weeks after the next major Moodle LMS release
|
||||
Expires: 2025-10-27T01:00:00.000Z
|
||||
Expires: 2026-05-11T01:00:00.000Z
|
||||
|
||||
+2
-2
@@ -29,9 +29,9 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2022112820.00; // 20221128 = branching date YYYYMMDD - do not modify!
|
||||
$version = 2022112821.00; // 20221128 = branching date YYYYMMDD - do not modify!
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
$release = '4.1.20 (Build: 20250811)'; // Human-friendly version name
|
||||
$release = '4.1.21 (Build: 20251006)'; // Human-friendly version name
|
||||
$branch = '401'; // This version's branch.
|
||||
$maturity = MATURITY_STABLE; // This version's maturity level.
|
||||
|
||||
Reference in New Issue
Block a user