Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fb57854de5 | |||
| 4fa5e7e6ea | |||
| aedff688e9 | |||
| a7a6d32f3b | |||
| 1d39913329 | |||
| 1cdbf1089b | |||
| b1aacfaab4 | |||
| 4260b9a0bf | |||
| 7e150c3221 | |||
| 4d8935e15e | |||
| 8926183e8e | |||
| 8288ac9e11 | |||
| 6bd427fad3 | |||
| 03608d8fc6 | |||
| 01ae2a52cb | |||
| ba49df6fbb | |||
| 2c1e8c2d2d | |||
| 828b8f51b4 | |||
| 9b5861e5d1 | |||
| b4dddbfd75 | |||
| d7eb914233 | |||
| 9e62c289ce | |||
| 564da75657 | |||
| e7582fab55 | |||
| 47e76e08b6 | |||
| 8126a90efb | |||
| e8b7ff09db | |||
| 71b78c2d21 | |||
| f35845fcad | |||
| a76cc39623 | |||
| f896d4b381 | |||
| 3ca9f3a5e0 | |||
| e77c82dc1d | |||
| 857fa9d371 | |||
| 1abad7745b | |||
| 4449bd5399 | |||
| b7dcc4e960 | |||
| b1d5327464 | |||
| 1f2ecfb129 | |||
| e05f5eb00e | |||
| d348e7a481 | |||
| 67b633fa5b | |||
| e14847283c | |||
| 8bbcf2a84c | |||
| c5b9e1af04 | |||
| d52b85dfb5 | |||
| 971fc7f1a8 | |||
| 465cbf2813 | |||
| 0f51395604 | |||
| b9f78d526d | |||
| 0528058a20 | |||
| 10837f283a | |||
| 920eb9e0eb | |||
| 56d29efbd1 | |||
| 17ccafb6f0 | |||
| 6bfb046d86 | |||
| 1619586794 | |||
| 86079406d9 | |||
| 8a79c5fb4b | |||
| 27c9a39712 | |||
| 5a75954abb | |||
| 58168c958c | |||
| 80e9a89704 | |||
| df91fc31a3 | |||
| 4b26a84586 | |||
| 6ee622ad13 | |||
| f7cf98ea86 | |||
| 8c1fe31370 | |||
| fe92590370 | |||
| 1718b04587 | |||
| 44cb65c5a3 | |||
| dc7dea0cf2 | |||
| 81a7de4721 | |||
| 4027f44c77 | |||
| 5c572f8a6b | |||
| 08cc1f3d32 | |||
| 5a1fb35680 | |||
| 6f18148684 | |||
| 6e6d5fa3cf | |||
| 37170e5714 | |||
| fb680bb33d | |||
| 4c872ab444 | |||
| 7cbd576777 | |||
| 2c8cfb9e13 | |||
| 1bfe81f05d | |||
| 2cf09ff8f3 | |||
| d9284eee0f | |||
| aa1723e748 | |||
| c079d2c3ea | |||
| 39aa019a86 | |||
| 882235917f | |||
| e12ffcdda0 | |||
| b2c2f2a0c5 | |||
| 7a7f029149 | |||
| 6cfefde94b | |||
| be8d3d004f | |||
| d42c5d9311 | |||
| 1c0e0f55bf | |||
| c5aebec657 | |||
| 4e689e468e | |||
| 77e3a9f987 | |||
| 23a0e2a9a4 | |||
| d88cbc344d |
+21
-20
@@ -56,6 +56,14 @@ jobs:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
postgres:
|
||||
image: ${{ matrix.db == 'pgsql' && 'postgres:13' || '' }}
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_USER: test
|
||||
POSTGRES_PASSWORD: test
|
||||
ports:
|
||||
- 5432:5432
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -71,27 +79,20 @@ jobs:
|
||||
db: pgsql
|
||||
|
||||
steps:
|
||||
- name: Setting up DB mysql
|
||||
- name: Run MySQL Server
|
||||
if: ${{ matrix.db == 'mysqli' }}
|
||||
uses: moodlehq/mysql-action@v1
|
||||
with:
|
||||
collation server: utf8mb4_bin
|
||||
mysql version: 8.0
|
||||
mysql database: test
|
||||
mysql user: test
|
||||
mysql password: test
|
||||
use tmpfs: true
|
||||
tmpfs size: '1024M'
|
||||
extra conf: --skip-log-bin
|
||||
|
||||
- name: Setting up DB pgsql
|
||||
if: ${{ matrix.db == 'pgsql' }}
|
||||
uses: m4nu56/postgresql-action@v1
|
||||
with:
|
||||
postgresql version: 13
|
||||
postgresql db: test
|
||||
postgresql user: test
|
||||
postgresql password: test
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e MYSQL_DATABASE=test \
|
||||
-e MYSQL_USER=test \
|
||||
-e MYSQL_PASSWORD=test \
|
||||
-e MYSQL_ROOT_PASSWORD=test \
|
||||
-p 3306:3306 \
|
||||
-d \
|
||||
--tmpfs /var/lib/mysql:rw,noexec,nosuid,size=1024M \
|
||||
mysql:8.0 \
|
||||
--skip-log-bin \
|
||||
--collation-server=utf8mb4_bin
|
||||
|
||||
- name: Configuring git vars
|
||||
uses: rlespinasse/github-slug-action@v4
|
||||
|
||||
@@ -18,7 +18,7 @@ import inquirer from 'inquirer';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { createNote } from './note.mjs';
|
||||
import { getInitialValues } from './helpers.mjs';
|
||||
import { getInitialValues, getIssueNumberFromBranchName } from './helpers.mjs';
|
||||
import * as Prompts from './prompts.mjs';
|
||||
import logger from './logger.mjs';
|
||||
|
||||
@@ -29,7 +29,7 @@ export default async (options) => {
|
||||
// Fetch information.
|
||||
const messages = [];
|
||||
const { issueNumber } = await inquirer.prompt([
|
||||
Prompts.getIssuePrompt(),
|
||||
Prompts.getIssuePrompt(getIssueNumberFromBranchName()),
|
||||
], initialValues);
|
||||
|
||||
let selection = {};
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import chalk from 'chalk';
|
||||
import branchName from 'current-git-branch';
|
||||
import { isStandardComponent, isCommunityComponent, rewritePlugintypeAsSubsystem } from './components.mjs';
|
||||
import { isValidNoteName } from './noteTypes.mjs';
|
||||
import logger from './logger.mjs';
|
||||
@@ -59,6 +60,19 @@ export const formatIssueNumber = (input) => {
|
||||
return input;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the issue number, based off current git branch name
|
||||
*
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export const getIssueNumberFromBranchName = () => {
|
||||
const issueNumber = String(branchName()).match(/^[a-zA-Z]*-\d+/);
|
||||
if (issueNumber) {
|
||||
return formatIssueNumber(issueNumber[0]);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate a component.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
issueNumber: MDL-75067
|
||||
notes:
|
||||
tool_behat:
|
||||
- message: >-
|
||||
The `behat_session_trait::ensure_element_[does_not_]exists(...)` methods
|
||||
now accept optional `$container` parameter to define the parent node to
|
||||
look within
|
||||
type: improved
|
||||
@@ -0,0 +1,7 @@
|
||||
issueNumber: MDL-87100
|
||||
notes:
|
||||
core:
|
||||
- message: >-
|
||||
When creating upgrade notes, the issue number will be inferred from the
|
||||
current Git branch name by default
|
||||
type: improved
|
||||
@@ -0,0 +1,8 @@
|
||||
issueNumber: MDL-87993
|
||||
notes:
|
||||
core:
|
||||
- message: >-
|
||||
The `core/toast` JS module now accepts a `visuallyHidden` configuration
|
||||
parameter to render visually hidden toast messages for screen reader
|
||||
users.
|
||||
type: improved
|
||||
@@ -32,7 +32,7 @@ Moodle also offers hosting through both [MoodleCloud][11], and our [partner netw
|
||||
|
||||
## License
|
||||
|
||||
Moodle is provided freely as open source software, under version 3 of the GNU General Public License. For more information on our license see
|
||||
Moodle is provided freely as open source software, under version 3 of the GNU General Public License. See our [license page][12] for more information.
|
||||
|
||||
[1]: https://moodle.org
|
||||
[2]: https://moodle.com
|
||||
|
||||
@@ -6,6 +6,30 @@ More detailed information on key changes can be found in the [Developer update n
|
||||
|
||||
The format of this change log follows the advice given at [Keep a CHANGELOG](https://keepachangelog.com).
|
||||
|
||||
## 4.5.10+
|
||||
|
||||
### core
|
||||
|
||||
#### Added
|
||||
|
||||
- When creating upgrade notes, the issue number will be inferred from the current Git branch name by default
|
||||
|
||||
For more information see [MDL-87100](https://tracker.moodle.org/browse/MDL-87100)
|
||||
- There is a new Behat `toast_message` named selector to more easily assert the presence of Toast messages on the page
|
||||
|
||||
For more information see [MDL-87443](https://tracker.moodle.org/browse/MDL-87443)
|
||||
- The `core/toast` JS module now accepts a `visuallyHidden` configuration parameter to render visually hidden toast messages for screen reader users.
|
||||
|
||||
For more information see [MDL-87993](https://tracker.moodle.org/browse/MDL-87993)
|
||||
|
||||
### tool_behat
|
||||
|
||||
#### Added
|
||||
|
||||
- The `behat_session_trait::ensure_element_[does_not_]exists(...)` methods now accept optional `$container` parameter to define the parent node to look within
|
||||
|
||||
For more information see [MDL-75067](https://tracker.moodle.org/browse/MDL-75067)
|
||||
|
||||
## 4.5.9
|
||||
|
||||
### core
|
||||
|
||||
+24
-13
@@ -937,20 +937,31 @@ $result = new environment_results('custom_checks');
|
||||
$result = check_xmlrpc_usage($result);
|
||||
$xmlrpcwarning = !is_null($result) ? get_string($result->getFeedbackStr(), 'admin') : '';
|
||||
|
||||
// Check whether MoodleNet features are enabled and warn about removal.
|
||||
$moodlenetwarning = '';
|
||||
$moodlenetenabled = get_config('tool_moodlenet', 'enablemoodlenet');
|
||||
if ($moodlenetenabled) {
|
||||
$moodlenetwarning = get_string('moodlenetremovalwarning', 'admin');
|
||||
}
|
||||
|
||||
admin_externalpage_setup('adminnotifications');
|
||||
|
||||
$output = $PAGE->get_renderer('core', 'admin');
|
||||
|
||||
echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, $cronoverdue, $dbproblems,
|
||||
$maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
|
||||
$registered, $cachewarnings, $eventshandlers, $themedesignermode, $devlibdir,
|
||||
$mobileconfigured, $overridetossl, $invalidforgottenpasswordurl, $croninfrequent,
|
||||
$showcampaigncontent, $showfeedbackencouragement, $servicesandsupportcontent,
|
||||
$xmlrpcwarning, $moodlenetwarning);
|
||||
echo $output->admin_notifications_page(
|
||||
$maturity,
|
||||
$insecuredataroot,
|
||||
$errorsdisplayed,
|
||||
$cronoverdue,
|
||||
$dbproblems,
|
||||
$maintenancemode,
|
||||
$availableupdates,
|
||||
$availableupdatesfetch,
|
||||
$buggyiconvnomb,
|
||||
$registered,
|
||||
$cachewarnings,
|
||||
$eventshandlers,
|
||||
$themedesignermode,
|
||||
$devlibdir,
|
||||
$mobileconfigured,
|
||||
$overridetossl,
|
||||
$invalidforgottenpasswordurl,
|
||||
$croninfrequent,
|
||||
$showcampaigncontent,
|
||||
$showfeedbackencouragement,
|
||||
$servicesandsupportcontent,
|
||||
$xmlrpcwarning
|
||||
);
|
||||
|
||||
+44
-14
@@ -279,17 +279,33 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
* @param bool $showfeedbackencouragement Whether the feedback encouragement content should be displayed or not.
|
||||
* @param bool $showservicesandsupport Whether the services and support content should be displayed or not.
|
||||
* @param string $xmlrpcwarning XML-RPC deprecation warning message.
|
||||
* @param string $moodlenetwarning MoodleNet removal warning message.
|
||||
*
|
||||
* @return string HTML to output.
|
||||
*/
|
||||
public function admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed,
|
||||
$cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch,
|
||||
$buggyiconvnomb, $registered, array $cachewarnings = array(), $eventshandlers = 0,
|
||||
$themedesignermode = false, $devlibdir = false, $mobileconfigured = false,
|
||||
$overridetossl = false, $invalidforgottenpasswordurl = false, $croninfrequent = false,
|
||||
$showcampaigncontent = false, bool $showfeedbackencouragement = false, bool $showservicesandsupport = false,
|
||||
$xmlrpcwarning = '', $moodlenetwarning = '') {
|
||||
public function admin_notifications_page(
|
||||
$maturity,
|
||||
$insecuredataroot,
|
||||
$errorsdisplayed,
|
||||
$cronoverdue,
|
||||
$dbproblems,
|
||||
$maintenancemode,
|
||||
$availableupdates,
|
||||
$availableupdatesfetch,
|
||||
$buggyiconvnomb,
|
||||
$registered,
|
||||
array $cachewarnings = [],
|
||||
$eventshandlers = 0,
|
||||
$themedesignermode = false,
|
||||
$devlibdir = false,
|
||||
$mobileconfigured = false,
|
||||
$overridetossl = false,
|
||||
$invalidforgottenpasswordurl = false,
|
||||
$croninfrequent = false,
|
||||
$showcampaigncontent = false,
|
||||
bool $showfeedbackencouragement = false,
|
||||
bool $showservicesandsupport = false,
|
||||
$xmlrpcwarning = ''
|
||||
) {
|
||||
|
||||
global $CFG;
|
||||
$output = '';
|
||||
@@ -315,7 +331,8 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
$output .= $this->mobile_configuration_warning($mobileconfigured);
|
||||
$output .= $this->forgotten_password_url_warning($invalidforgottenpasswordurl);
|
||||
$output .= $this->mnet_deprecation_warning($xmlrpcwarning);
|
||||
$output .= $this->moodlenet_removal_warning($moodlenetwarning);
|
||||
$output .= $this->moodlenet_removal_warning();
|
||||
$output .= $this->marketplace_integration_notice();
|
||||
$output .= $this->userfeedback_encouragement($showfeedbackencouragement);
|
||||
$output .= $this->services_and_support_content($showservicesandsupport);
|
||||
$output .= $this->campaign_content($showcampaigncontent);
|
||||
@@ -2272,15 +2289,28 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
/**
|
||||
* Display a warning about the removal of MoodleNet integration.
|
||||
*
|
||||
* @param string $moodlenetwarning The warning message
|
||||
* @return string HTML to output.
|
||||
*/
|
||||
protected function moodlenet_removal_warning($moodlenetwarning) {
|
||||
if (empty($moodlenetwarning)) {
|
||||
return '';
|
||||
protected function moodlenet_removal_warning(): string {
|
||||
$moodlenetenabled = get_config('tool_moodlenet', 'enablemoodlenet');
|
||||
if (!empty($moodlenetenabled)) {
|
||||
$moodlenetwarning = get_string('moodlenetremovalwarning', 'admin');
|
||||
return $this->warning($moodlenetwarning);
|
||||
}
|
||||
|
||||
return $this->warning($moodlenetwarning);
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a notice about Moodle Marketplace integration.
|
||||
*
|
||||
* @return string HTML to output.
|
||||
*/
|
||||
protected function marketplace_integration_notice(): string {
|
||||
$installer = tool_installaddon_installer::instance();
|
||||
$url = $installer->get_marketplace_url();
|
||||
$notice = get_string('marketplaceavailablenotice', 'admin', $url->out());
|
||||
return $this->warning($notice, 'info');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# tool_behat Upgrade notes
|
||||
|
||||
## 4.5.10+
|
||||
|
||||
### Added
|
||||
|
||||
- The `behat_session_trait::ensure_element_[does_not_]exists(...)` methods now accept optional `$container` parameter to define the parent node to look within
|
||||
|
||||
For more information see [MDL-75067](https://tracker.moodle.org/browse/MDL-75067)
|
||||
|
||||
## 4.5.2
|
||||
|
||||
### Added
|
||||
|
||||
@@ -224,6 +224,11 @@ if (empty($parallelrun)) {
|
||||
$cmds['singlerun'] = $runtestscommand;
|
||||
|
||||
echo "Running single behat site:" . PHP_EOL;
|
||||
// The inner PHP process is not marked as having an interactive terminal even if it's passed
|
||||
// through from this one which does, so we need to pass it through as an environment variable.
|
||||
if (function_exists('posix_isatty') && posix_isatty(STDOUT)) {
|
||||
putenv('MOODLE_BEHAT_RUNNING_IN_TTY=1');
|
||||
}
|
||||
passthru("php $runtestscommand", $status);
|
||||
$exitcodes['singlerun'] = $status;
|
||||
chdir($cwd);
|
||||
@@ -525,4 +530,3 @@ function get_status_lines_from_run_op(Symfony\Component\Process\Process $process
|
||||
|
||||
return $statusstr;
|
||||
}
|
||||
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
define("tool_installaddon/footer",["exports","jquery","core_course/local/activitychooser/selectors"],(function(_exports,_jquery,_selectors){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
|
||||
/**
|
||||
* Activity chooser footer handlers for tool_installaddon.
|
||||
*
|
||||
* @module tool_installaddon/footer
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.footerClickListener=void 0,_jquery=_interopRequireDefault(_jquery),_selectors=_interopRequireDefault(_selectors);_exports.footerClickListener=(e,footerData,modal)=>{const closeOption=e.target.closest(_selectors.default.actions.closeOption);if(!closeOption)return;const moduleName=closeOption.dataset.modname;if(!moduleName)return;const carousel=(0,_jquery.default)(modal.getBody()[0].querySelector(_selectors.default.regions.carousel));carousel.carousel("prev"),modal.setFooter(footerData.customfootertemplate),carousel.one("slid.bs.carousel",(()=>{const caller=modal.getBody()[0].querySelector(_selectors.default.regions.modules).querySelector(_selectors.default.regions.getModuleSelector(moduleName));caller&&caller.focus()}))}}));
|
||||
|
||||
//# sourceMappingURL=footer.min.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"footer.min.js","sources":["../src/footer.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Activity chooser footer handlers for tool_installaddon.\n *\n * @module tool_installaddon/footer\n * @copyright 2026 Safat Shahin <safat.shahin@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport selectors from 'core_course/local/activitychooser/selectors';\n\n/**\n * Handle clicks in the chooser footer when installaddon is the active footer plugin.\n *\n * @param {Event} e The event being triggered\n * @param {Object} footerData The footer data generated for the chooser\n * @param {Object} modal The chooser modal\n */\nexport const footerClickListener = (e, footerData, modal) => {\n const closeOption = e.target.closest(selectors.actions.closeOption);\n\n if (!closeOption) {\n return;\n }\n\n const moduleName = closeOption.dataset.modname;\n\n if (!moduleName) {\n return;\n }\n\n const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));\n\n // Trigger the transition between 'pages'.\n carousel.carousel('prev');\n modal.setFooter(footerData.customfootertemplate);\n carousel.one('slid.bs.carousel', () => {\n const allModules = modal.getBody()[0].querySelector(selectors.regions.modules);\n const caller = allModules.querySelector(selectors.regions.getModuleSelector(moduleName));\n if (caller) {\n caller.focus();\n }\n });\n};\n"],"names":["e","footerData","modal","closeOption","target","closest","selectors","actions","moduleName","dataset","modname","carousel","getBody","querySelector","regions","setFooter","customfootertemplate","one","caller","modules","getModuleSelector","focus"],"mappings":";;;;;;;oNAiCmC,CAACA,EAAGC,WAAYC,eACzCC,YAAcH,EAAEI,OAAOC,QAAQC,mBAAUC,QAAQJ,iBAElDA,yBAICK,WAAaL,YAAYM,QAAQC,YAElCF,wBAICG,UAAW,mBAAET,MAAMU,UAAU,GAAGC,cAAcP,mBAAUQ,QAAQH,WAGtEA,SAASA,SAAS,QAClBT,MAAMa,UAAUd,WAAWe,sBAC3BL,SAASM,IAAI,oBAAoB,WAEvBC,OADahB,MAAMU,UAAU,GAAGC,cAAcP,mBAAUQ,QAAQK,SAC5CN,cAAcP,mBAAUQ,QAAQM,kBAAkBZ,aACxEU,QACAA,OAAOG"}
|
||||
@@ -0,0 +1,59 @@
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* Activity chooser footer handlers for tool_installaddon.
|
||||
*
|
||||
* @module tool_installaddon/footer
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import $ from 'jquery';
|
||||
import selectors from 'core_course/local/activitychooser/selectors';
|
||||
|
||||
/**
|
||||
* Handle clicks in the chooser footer when installaddon is the active footer plugin.
|
||||
*
|
||||
* @param {Event} e The event being triggered
|
||||
* @param {Object} footerData The footer data generated for the chooser
|
||||
* @param {Object} modal The chooser modal
|
||||
*/
|
||||
export const footerClickListener = (e, footerData, modal) => {
|
||||
const closeOption = e.target.closest(selectors.actions.closeOption);
|
||||
|
||||
if (!closeOption) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moduleName = closeOption.dataset.modname;
|
||||
|
||||
if (!moduleName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));
|
||||
|
||||
// Trigger the transition between 'pages'.
|
||||
carousel.carousel('prev');
|
||||
modal.setFooter(footerData.customfootertemplate);
|
||||
carousel.one('slid.bs.carousel', () => {
|
||||
const allModules = modal.getBody()[0].querySelector(selectors.regions.modules);
|
||||
const caller = allModules.querySelector(selectors.regions.getModuleSelector(moduleName));
|
||||
if (caller) {
|
||||
caller.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -70,6 +70,25 @@ class tool_installaddon_installer {
|
||||
$url = 'https://moodle.org/plugins/get.php';
|
||||
}
|
||||
|
||||
return $this->get_external_service_url($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns URL to Moodle Marketplace.
|
||||
*
|
||||
* @return moodle_url
|
||||
*/
|
||||
public function get_marketplace_url(): moodle_url {
|
||||
return $this->get_external_service_url('https://marketplace.moodle.com/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns URL to external service with optional encoded site information.
|
||||
*
|
||||
* @param string $url The external service base URL.
|
||||
* @return moodle_url
|
||||
*/
|
||||
protected function get_external_service_url(string $url): moodle_url {
|
||||
if (!$this->should_send_site_info()) {
|
||||
return new moodle_url($url);
|
||||
}
|
||||
|
||||
+9
-6
@@ -16,20 +16,23 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tool_moodlenet\task;
|
||||
namespace tool_installaddon\task;
|
||||
|
||||
/**
|
||||
* Ad-hoc task to perform post install tasks.
|
||||
* We use this to set the active activity chooser footer plugin to tool_moodlenet.
|
||||
* We use this to set the active activity chooser footer plugin to tool_installaddon.
|
||||
* We couldn't do this directly in install.php, because there is an admin_apply_default_settings() call after all plugins are
|
||||
* installed and that would reset whatever value we had set earlier to 'hidden'.
|
||||
*
|
||||
* @package tool_moodlenet
|
||||
* @copyright 2022 Shamim Rezaie <shamim@moodle.com>
|
||||
* @package tool_installaddon
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class post_install extends \core\task\adhoc_task {
|
||||
public function execute() {
|
||||
set_config('activitychooseractivefooter', 'tool_moodlenet');
|
||||
/**
|
||||
* Sets the active activity chooser footer plugin after install defaults are applied.
|
||||
*/
|
||||
public function execute(): void {
|
||||
set_config('activitychooseractivefooter', 'tool_installaddon');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Install script for tool_installaddon.
|
||||
*
|
||||
* @package tool_installaddon
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* Perform the post-install procedures.
|
||||
*/
|
||||
function xmldb_tool_installaddon_install(): void {
|
||||
// Use an ad-hoc task to set the active activity chooser footer plugin to tool_installaddon.
|
||||
// We couldn't do this in admin/settings/courses.php for 2 reasons:
|
||||
// - First, because it would be a breach of component communications principles to do so there.
|
||||
// - Second, because we can't call get_plugins_with_function() during install and upgrade (or it will return []).
|
||||
// We couldn't do this directly here either, because there is an admin_apply_default_settings() call after all plugins are
|
||||
// installed and that would reset whatever value we set here to 'hidden'.
|
||||
$postinstall = new tool_installaddon\task\post_install();
|
||||
core\task\manager::queue_adhoc_task($postinstall);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Upgrade script for tool_installaddon.
|
||||
*
|
||||
* @package tool_installaddon
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* Upgrade the plugin.
|
||||
*
|
||||
* @param int $oldversion
|
||||
* @return bool always true
|
||||
*/
|
||||
function xmldb_tool_installaddon_upgrade(int $oldversion): bool {
|
||||
if ($oldversion < 2024100701) {
|
||||
// Set the activity chooser active footer to include marketplace regardless of the previous setting.
|
||||
// We are deliberately setting this to increase awareness of marketplace.
|
||||
set_config('activitychooseractivefooter', 'tool_installaddon');
|
||||
|
||||
upgrade_plugin_savepoint(true, 2024100701, 'tool', 'installaddon');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['acknowledgement'] = 'Acknowledgement';
|
||||
$string['acknowledgementtext'] = 'I understand that it is my responsibility to have full backups of this site prior to installing additional plugins. I accept and understand that plugins (especially but not only those originating in unofficial sources) may contain security holes, can make the site unavailable, or cause private data leaks or loss.';
|
||||
$string['activitychooserfootertext'] = 'Browse more activities on';
|
||||
$string['featuredisabled'] = 'The plugin installer is disabled on this site.';
|
||||
$string['installaddon'] = 'Install plugin!';
|
||||
$string['installaddons'] = 'Install plugins';
|
||||
@@ -44,6 +45,8 @@ $string['installfromzipsubmit'] = 'Install plugin from the ZIP file';
|
||||
$string['installfromziptype'] = 'Plugin type';
|
||||
$string['installfromziptype_help'] = 'For plugins that correctly declare their component name, the installer is able to detect the plugin type automatically. If the auto-detection fails, choose the correct type of plugin manually. Warning: The installation procedure can fail badly if an incorrect plugin type is specified.';
|
||||
$string['installfromziptype_link'] = 'Development:Plugins';
|
||||
$string['marketplaceadminlinktext'] = 'Browse new plugins';
|
||||
$string['marketplacelink'] = 'Moodle Marketplace';
|
||||
$string['permcheck'] = 'Make sure the plugin type root location is writable by the web server process.';
|
||||
$string['permcheckerror'] = 'Error while checking for write permission';
|
||||
$string['permcheckprogress'] = 'Checking for write permission ...';
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Public API for tool_installaddon.
|
||||
*
|
||||
* @package tool_installaddon
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
use core_course\local\entity\activity_chooser_footer;
|
||||
|
||||
/**
|
||||
* Build activity chooser footer content for Marketplace.
|
||||
*
|
||||
* @param int $courseid The selected course id.
|
||||
* @param int $sectionid The selected section id.
|
||||
* @return activity_chooser_footer
|
||||
*/
|
||||
function tool_installaddon_custom_chooser_footer(int $courseid, int $sectionid): activity_chooser_footer {
|
||||
global $OUTPUT;
|
||||
|
||||
$installer = tool_installaddon_installer::instance();
|
||||
$marketplaceurl = $installer->get_marketplace_url();
|
||||
|
||||
$renderedfooter = $OUTPUT->render_from_template('tool_installaddon/chooser_footer', [
|
||||
'url' => $marketplaceurl->out(false),
|
||||
]);
|
||||
|
||||
return new activity_chooser_footer(
|
||||
'tool_installaddon/footer',
|
||||
$renderedfooter
|
||||
);
|
||||
}
|
||||
@@ -26,6 +26,12 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
if ($hassiteconfig and empty($CFG->disableupdateautodeploy)) {
|
||||
$installer = tool_installaddon_installer::instance();
|
||||
$ADMIN->add('modules', new admin_externalpage(
|
||||
'tool_installaddon_marketplace',
|
||||
get_string('marketplaceadminlinktext', 'tool_installaddon'),
|
||||
$installer->get_marketplace_url()->out(false)
|
||||
), 'modsettings');
|
||||
|
||||
$ADMIN->add('modules', new admin_externalpage('tool_installaddon_index',
|
||||
get_string('installaddons', 'tool_installaddon'),
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template tool_installaddon/chooser_footer
|
||||
|
||||
Activity chooser footer link to Marketplace.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"url": "https://marketplace.moodle.com/?site=hash"
|
||||
}
|
||||
}}
|
||||
<div class="w-100 d-flex px-2">
|
||||
<span class="my-auto me-1">{{#str}} activitychooserfootertext, tool_installaddon {{/str}}</span>
|
||||
{{> tool_installaddon/marketplace_link }}
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template tool_installaddon/marketplace_link
|
||||
|
||||
Reusable link to Moodle Marketplace.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"url": "https://marketplace.moodle.com/?site=hash"
|
||||
}
|
||||
}}
|
||||
<a class="d-inline my-auto" href="{{url}}" target="_blank" rel="noopener noreferrer">
|
||||
{{#str}} marketplacelink, tool_installaddon {{/str}}
|
||||
<i
|
||||
class="fa fa-external-link"
|
||||
role="img"
|
||||
aria-label="{{#str}} opensinnewwindow, core {{/str}}"
|
||||
title="{{#str}} opensinnewwindow, core {{/str}}"
|
||||
>
|
||||
</i>
|
||||
</a>
|
||||
@@ -0,0 +1,37 @@
|
||||
@tool @tool_installaddon @javascript
|
||||
Feature: Marketplace activity chooser footer
|
||||
In order to browse plugins from the activity chooser
|
||||
As a teacher
|
||||
I need to see the Marketplace footer link when installaddon is the active footer plugin
|
||||
|
||||
Background:
|
||||
Given the following config values are set as admin:
|
||||
| activitychooseractivefooter | tool_installaddon |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category | format |
|
||||
| Course 1 | C1 | 0 | topics |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
|
||||
Scenario: Activity chooser footer includes Marketplace link
|
||||
Given I log in as "teacher1"
|
||||
When I am on "Course 1" course homepage with editing mode on
|
||||
And I open the activity chooser
|
||||
Then "Browse more activities on" "text" should exist in the "Add an activity or resource" "dialogue"
|
||||
And "Marketplace" "link" should exist in the "Add an activity or resource" "dialogue"
|
||||
|
||||
Scenario: Back button works from activity help when the Marketplace footer is active
|
||||
Given I log in as "teacher1"
|
||||
When I am on "Course 1" course homepage with editing mode on
|
||||
And I open the activity chooser
|
||||
And I click on "Information about the Assignment activity" "button" in the "Add an activity or resource" "dialogue"
|
||||
Then "help" "core_course > Activity chooser screen" should be visible
|
||||
When I click on "Back" "button" in the "Add an activity or resource" "dialogue"
|
||||
Then "modules" "core_course > Activity chooser screen" should be visible
|
||||
And "help" "core_course > Activity chooser screen" should not be visible
|
||||
And "Marketplace" "link" should exist in the "Add an activity or resource" "dialogue"
|
||||
And "Back" "button" should not exist in the "Add an activity or resource" "dialogue"
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Provides a mock testable_tool_installaddon_installer_without_site_info class.
|
||||
*
|
||||
* @package tool_installaddon
|
||||
* @subpackage fixtures
|
||||
* @category test
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once(__DIR__ . '/testable_installer.php');
|
||||
|
||||
/**
|
||||
* Testable subclass with site-info sharing disabled.
|
||||
*
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class testable_tool_installaddon_installer_without_site_info extends testable_tool_installaddon_installer {
|
||||
/**
|
||||
* Disable site info sharing.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function should_send_site_info() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -18,12 +18,14 @@
|
||||
namespace tool_installaddon;
|
||||
|
||||
use testable_tool_installaddon_installer;
|
||||
use testable_tool_installaddon_installer_without_site_info;
|
||||
use tool_installaddon_installer;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once(__DIR__.'/fixtures/testable_installer.php');
|
||||
require_once(__DIR__ . '/fixtures/testable_installer.php');
|
||||
require_once(__DIR__ . '/fixtures/testable_installer_without_site_info.php');
|
||||
|
||||
/**
|
||||
* Unit tests for the {@link tool_installaddon_installer} class
|
||||
@@ -32,16 +34,14 @@ require_once(__DIR__.'/fixtures/testable_installer.php');
|
||||
* @category test
|
||||
* @copyright 2013 David Mudrak <david@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \tool_installaddon_installer
|
||||
*/
|
||||
final class installer_test extends \advanced_testcase {
|
||||
|
||||
public function test_get_addons_repository_url(): void {
|
||||
$installer = testable_tool_installaddon_installer::instance();
|
||||
$url = $installer->get_addons_repository_url();
|
||||
$query = parse_url($url, PHP_URL_QUERY);
|
||||
$this->assertEquals(1, preg_match('~^site=(.+)$~', $query, $matches));
|
||||
$site = rawurldecode($matches[1]);
|
||||
$site = json_decode(base64_decode($site), true);
|
||||
$site = $this->decode_site_info_from_url($url);
|
||||
$this->assertIsArray($site);
|
||||
$this->assertEquals(3, count($site));
|
||||
$this->assertSame('Nasty site', $site['fullname']);
|
||||
@@ -49,6 +49,34 @@ final class installer_test extends \advanced_testcase {
|
||||
$this->assertSame("2.5'; DROP TABLE mdl_user; --", $site['majorversion']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that marketplace URL includes expected host, scheme and site payload.
|
||||
*/
|
||||
public function test_get_marketplace_url(): void {
|
||||
$installer = testable_tool_installaddon_installer::instance();
|
||||
$addonsurl = $installer->get_addons_repository_url();
|
||||
$marketplaceurl = $installer->get_marketplace_url();
|
||||
|
||||
$this->assertSame('marketplace.moodle.com', parse_url($marketplaceurl, PHP_URL_HOST));
|
||||
$this->assertSame('https', parse_url($marketplaceurl, PHP_URL_SCHEME));
|
||||
$this->assertSame(
|
||||
$this->decode_site_info_from_url($addonsurl),
|
||||
$this->decode_site_info_from_url($marketplaceurl),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that marketplace URL has no query string when site info is unavailable.
|
||||
*/
|
||||
public function test_get_marketplace_url_without_site_info(): void {
|
||||
$installer = testable_tool_installaddon_installer_without_site_info::instance();
|
||||
$marketplaceurl = $installer->get_marketplace_url();
|
||||
|
||||
$this->assertSame('marketplace.moodle.com', parse_url($marketplaceurl, PHP_URL_HOST));
|
||||
$this->assertSame('https', parse_url($marketplaceurl, PHP_URL_SCHEME));
|
||||
$this->assertEmpty(parse_url($marketplaceurl, PHP_URL_QUERY));
|
||||
}
|
||||
|
||||
public function test_decode_remote_request(): void {
|
||||
$installer = testable_tool_installaddon_installer::instance();
|
||||
|
||||
@@ -132,7 +160,7 @@ $plugin->version = 2014121300;
|
||||
$versionphp = file_get_contents($fixtures.'/github/moodle-repository_mahara-master/version.php');
|
||||
$this->assertEquals('repository_mahara', $installer->testable_detect_plugin_component_from_versionphp($versionphp));
|
||||
|
||||
$versionphp = file_get_contents($fixtures.'/nocomponent/baz/version.php');
|
||||
$versionphp = file_get_contents($fixtures . '/nocomponent/baz/version.php');
|
||||
$this->assertFalse($installer->testable_detect_plugin_component_from_versionphp($versionphp));
|
||||
}
|
||||
|
||||
@@ -143,15 +171,28 @@ $plugin->version = 2014121300;
|
||||
$storage1 = $installer->make_installfromzip_storage();
|
||||
$this->assertTrue(is_dir($storage1));
|
||||
$this->assertTrue(is_writable($storage1));
|
||||
file_put_contents($storage1.'/hello.txt', 'Find me if you can!');
|
||||
file_put_contents($storage1 . '/hello.txt', 'Find me if you can!');
|
||||
|
||||
// Check we get unique directory on each call.
|
||||
$storage2 = $installer->make_installfromzip_storage();
|
||||
$this->assertTrue(is_dir($storage2));
|
||||
$this->assertTrue(is_writable($storage2));
|
||||
$this->assertFalse(file_exists($storage2.'/hello.txt'));
|
||||
$this->assertFalse(file_exists($storage2 . '/hello.txt'));
|
||||
|
||||
// Check both are in the same parent directory.
|
||||
$this->assertEquals(dirname($storage1), dirname($storage2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the encoded 'site' URL query parameter.
|
||||
*
|
||||
* @param moodle_url $url
|
||||
* @return array
|
||||
*/
|
||||
private function decode_site_info_from_url(\moodle_url $url): array {
|
||||
$query = parse_url($url, PHP_URL_QUERY);
|
||||
$this->assertEquals(1, preg_match('~^site=(.+)$~', $query, $matches));
|
||||
$site = rawurldecode($matches[1]);
|
||||
return json_decode(base64_decode($site), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Unit tests for tool_installaddon lib.
|
||||
*
|
||||
* @package tool_installaddon
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace tool_installaddon;
|
||||
|
||||
use core_course\local\entity\activity_chooser_footer;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/admin/tool/installaddon/lib.php');
|
||||
|
||||
/**
|
||||
* Test installaddon lib functions.
|
||||
*/
|
||||
final class lib_test extends \advanced_testcase {
|
||||
/**
|
||||
* Tests chooser footer generation for marketplace link content.
|
||||
*
|
||||
* @covers ::tool_installaddon_custom_chooser_footer
|
||||
*/
|
||||
public function test_tool_installaddon_custom_chooser_footer(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$footer = \tool_installaddon_custom_chooser_footer(1, 1);
|
||||
|
||||
$this->assertInstanceOf(activity_chooser_footer::class, $footer);
|
||||
$this->assertSame('tool_installaddon/footer', $footer->get_footer_js_file());
|
||||
$this->assertStringContainsString('https://marketplace.moodle.com/', $footer->get_footer_template());
|
||||
$this->assertStringContainsString('site=', $footer->get_footer_template());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?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/>.
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tool_installaddon\task;
|
||||
|
||||
/**
|
||||
* Unit tests for the post install task.
|
||||
*
|
||||
* @package tool_installaddon
|
||||
* @category test
|
||||
* @copyright 2026 Safat Shahin <safat.shahin@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \tool_installaddon\task\post_install
|
||||
*/
|
||||
final class post_install_test extends \advanced_testcase {
|
||||
/**
|
||||
* Tests that the task sets the activity chooser footer plugin to tool_installaddon.
|
||||
*/
|
||||
public function test_execute_sets_activitychooseractivefooter_config(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
set_config('activitychooseractivefooter', 'hidden');
|
||||
|
||||
$task = new post_install();
|
||||
$task->execute();
|
||||
|
||||
$this->assertSame(
|
||||
'tool_installaddon',
|
||||
get_config('core', 'activitychooseractivefooter'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,6 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->component = 'tool_installaddon';
|
||||
$plugin->version = 2024100700;
|
||||
$plugin->version = 2024100701;
|
||||
$plugin->requires = 2024100100;
|
||||
$plugin->maturity = MATURITY_STABLE;
|
||||
|
||||
@@ -28,12 +28,5 @@ declare(strict_types=1);
|
||||
* Perform the post-install procedures.
|
||||
*/
|
||||
function xmldb_tool_moodlenet_install() {
|
||||
// Use an ad-hoc task to set the active activity chooser footer plugin to tool_moodlenet.
|
||||
// We couldn't do this in admin/settings/courses.php for 2 reasons:
|
||||
// - First, because it would be a breach of component communications principles to do so there.
|
||||
// - Second, because we can't call get_plugins_with_function() during install and upgrade (or it will return []).
|
||||
// We couldn't do this directly here either, because there is an admin_apply_default_settings() call after all plugins are
|
||||
// installed and that would reset whatever value we set here to 'hidden'.
|
||||
$postinstall = new tool_moodlenet\task\post_install();
|
||||
core\task\manager::queue_adhoc_task($postinstall);
|
||||
|
||||
}
|
||||
|
||||
@@ -35,10 +35,12 @@ Feature: Test if the login form provides the correct feedback
|
||||
Given I log in as "admin"
|
||||
And I navigate to "Appearance > Logos" in site administration
|
||||
And I upload "course/tests/fixtures/image.jpg" file to "Logo" filemanager
|
||||
And I upload "course/tests/fixtures/image.jpg" file to "Favicon" filemanager
|
||||
And I press "Save changes"
|
||||
And I log out
|
||||
And I follow "Log in"
|
||||
Then "//img[@id='logoimage']" "xpath_element" should exist
|
||||
Then "//img[@id='logoimage' and contains(@src, '/image.jpg')]" "xpath_element" should exist
|
||||
And "//link[@rel='shortcut icon' and contains(@href, '/image.jpg')]" "xpath_element" should exist
|
||||
|
||||
Scenario: Add a custom welcome message
|
||||
Given the following config values are set as admin:
|
||||
|
||||
@@ -107,17 +107,12 @@ final class async_backup_test extends \advanced_testcase {
|
||||
$asynctask = new \core\task\asynchronous_backup_task();
|
||||
$asynctask->set_custom_data(['backupid' => $backupid]);
|
||||
$asynctask->set_userid($USER->id);
|
||||
\core\task\manager::queue_adhoc_task($asynctask);
|
||||
|
||||
// We are expecting trace output during this test.
|
||||
$this->expectOutputRegex("/$backupid/");
|
||||
|
||||
// Execute adhoc task.
|
||||
$now = time();
|
||||
$task = \core\task\manager::get_next_adhoc_task($now);
|
||||
$this->assertInstanceOf('\\core\\task\\asynchronous_backup_task', $task);
|
||||
$task->execute();
|
||||
\core\task\manager::adhoc_task_complete($task);
|
||||
$asynctask->execute();
|
||||
|
||||
$postbackuprec = $DB->get_record('backup_controllers', ['backupid' => $backupid]);
|
||||
|
||||
@@ -151,13 +146,9 @@ final class async_backup_test extends \advanced_testcase {
|
||||
// Create the adhoc task.
|
||||
$asynctask = new \core\task\asynchronous_backup_task();
|
||||
$asynctask->set_custom_data(['backupid' => $backupid]);
|
||||
\core\task\manager::queue_adhoc_task($asynctask);
|
||||
|
||||
// Execute adhoc task.
|
||||
$now = time();
|
||||
$task = \core\task\manager::get_next_adhoc_task($now);
|
||||
$task->execute();
|
||||
\core\task\manager::adhoc_task_complete($task);
|
||||
$asynctask->execute();
|
||||
|
||||
$postbackuprec = $DB->get_record('backup_controllers', ['backupid' => $backupid]);
|
||||
|
||||
@@ -241,19 +232,11 @@ final class async_backup_test extends \advanced_testcase {
|
||||
// Now queue an adhoc task and check it handles and completes gracefully.
|
||||
$asynctask = new \core\task\asynchronous_backup_task();
|
||||
$asynctask->set_custom_data(array('backupid' => $backupid));
|
||||
\core\task\manager::queue_adhoc_task($asynctask);
|
||||
|
||||
// We are expecting a specific message output during this test.
|
||||
$this->expectOutputRegex('/invalid controller/');
|
||||
|
||||
// Execute adhoc task.
|
||||
$now = time();
|
||||
$task = \core\task\manager::get_next_adhoc_task($now);
|
||||
$this->assertInstanceOf('\\core\\task\\asynchronous_backup_task', $task);
|
||||
$task->execute();
|
||||
\core\task\manager::adhoc_task_complete($task);
|
||||
|
||||
// Check the task record is removed.
|
||||
$this->assertEquals(0, $DB->count_records('task_adhoc'));
|
||||
$asynctask->execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,20 +223,12 @@ final class async_restore_test extends \advanced_testcase {
|
||||
// Create the adhoc task.
|
||||
$asynctask = new \core\task\asynchronous_restore_task();
|
||||
$asynctask->set_custom_data(['backupid' => $restoreid]);
|
||||
\core\task\manager::queue_adhoc_task($asynctask);
|
||||
|
||||
// We are expecting a specific message output during this test.
|
||||
$this->expectOutputRegex('/invalid controller/');
|
||||
|
||||
// Execute adhoc task.
|
||||
$now = time();
|
||||
$task = \core\task\manager::get_next_adhoc_task($now);
|
||||
$this->assertInstanceOf('\\core\\task\\asynchronous_restore_task', $task);
|
||||
$task->execute();
|
||||
\core\task\manager::adhoc_task_complete($task);
|
||||
|
||||
// Check the task record is removed.
|
||||
$this->assertEquals(0, $DB->count_records('task_adhoc'));
|
||||
$asynctask->execute();
|
||||
|
||||
// Now delete the record and confirm an entirely missing controller is handled.
|
||||
$DB->delete_records('backup_controllers');
|
||||
@@ -244,19 +236,11 @@ final class async_restore_test extends \advanced_testcase {
|
||||
// Create the adhoc task.
|
||||
$asynctask = new \core\task\asynchronous_restore_task();
|
||||
$asynctask->set_custom_data(['backupid' => $restoreid]);
|
||||
\core\task\manager::queue_adhoc_task($asynctask);
|
||||
|
||||
// We are expecting a specific message output during this test.
|
||||
$this->expectOutputRegex('/Unable to find restore controller/');
|
||||
|
||||
// Execute adhoc task.
|
||||
$now = time();
|
||||
$task = \core\task\manager::get_next_adhoc_task($now);
|
||||
$this->assertInstanceOf('\\core\\task\\asynchronous_restore_task', $task);
|
||||
$task->execute();
|
||||
\core\task\manager::adhoc_task_complete($task);
|
||||
|
||||
// Check the task record is removed.
|
||||
$this->assertEquals(0, $DB->count_records('task_adhoc'));
|
||||
$asynctask->execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
<div class="pb-2" data-region="event-list-wrapper">
|
||||
{{#eventsbyday}}
|
||||
<div class="mt-3" data-region="event-list-content-date" data-timestamp="{{dayTimestamp}}">
|
||||
<h5 class="h6 d-inline {{^courseview}}font-weight-bold px-2{{/courseview}}">{{#userdate}} {{dayTimestamp}}, {{#str}} strftimedaydate, core_langconfig {{/str}} {{/userdate}}</h5>
|
||||
<h4 class="h6 d-inline {{^courseview}}font-weight-bold px-2{{/courseview}}">{{#userdate}} {{dayTimestamp}}, {{#str}} strftimedaydate, core_langconfig {{/str}} {{/userdate}}</h4>
|
||||
</div>
|
||||
{{> block_timeline/event-list-items }}
|
||||
{{/eventsbyday}}
|
||||
|
||||
@@ -66,13 +66,13 @@
|
||||
</div>
|
||||
<div class="event-name-container flex-grow-1 line-height-3 nowrap text-truncate">
|
||||
<div class="d-flex">
|
||||
<h6 class="event-name mb-0 pb-1 text-truncate">
|
||||
<h5 class="h6 event-name mb-0 pb-1 text-truncate">
|
||||
{{#overdue}}<span class="badge rounded-pill bg-danger text-white ms-1 float-end">{{#str}} overdue, block_timeline {{/str}}</span>{{/overdue}}
|
||||
<a href="{{url}}"
|
||||
title="{{name}}"
|
||||
aria-label='{{#cleanstr}} ariaeventlistitem, block_timeline, { "name": {{#quote}}{{{activityname}}}{{/quote}}, "course": {{#quote}}{{{course.fullnamedisplay}}}{{/quote}}, "date": "{{#userdate}} {{timesort}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}}" } {{/cleanstr}}'>
|
||||
{{{activityname}}}</a>
|
||||
</h6>
|
||||
</h5>
|
||||
</div>
|
||||
<small class="mb-0">
|
||||
{{#courseview}}
|
||||
@@ -86,7 +86,7 @@
|
||||
</div>
|
||||
{{#action.actionable}}
|
||||
<div class="d-flex timeline-action-button">
|
||||
<h6 class="event-action">
|
||||
<h5 class="h6 event-action">
|
||||
<a class="list-group-item-action btn btn-outline-secondary btn-sm text-nowrap"
|
||||
href="{{action.url}}"
|
||||
aria-label="{{action.name}}"
|
||||
@@ -96,7 +96,7 @@
|
||||
<span class="badge bg-secondary text-dark">{{action.itemcount}}</span>
|
||||
{{/action.showitemcount}}
|
||||
</a>
|
||||
</h6>
|
||||
</h5>
|
||||
</div>
|
||||
{{/action.actionable}}
|
||||
</div>
|
||||
|
||||
@@ -60,54 +60,56 @@
|
||||
{{#str}} overdue, block_timeline {{/str}}
|
||||
</a>
|
||||
<div class="dropdown-divider" role="separator"></div>
|
||||
<h6 class="dropdown-header">{{#str}} duedate, block_timeline {{/str}}</h6>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
data-from="0"
|
||||
data-to="7"
|
||||
data-filtername="next7days"
|
||||
{{#next7days}}aria-current="true"{{/next7days}}
|
||||
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next7days, block_timeline {{/str}}{{/str}}"
|
||||
role="menuitem"
|
||||
>
|
||||
{{#str}} next7days, block_timeline {{/str}}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
data-from="0"
|
||||
data-to="30"
|
||||
data-filtername="next30days"
|
||||
{{#next30days}}aria-current="true"{{/next30days}}
|
||||
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next30days, block_timeline {{/str}}{{/str}}"
|
||||
role="menuitem"
|
||||
>
|
||||
{{#str}} next30days, block_timeline {{/str}}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
data-from="0"
|
||||
data-to="90"
|
||||
data-filtername="next3months"
|
||||
{{#next3months}}aria-current="true"{{/next3months}}
|
||||
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next3months, block_timeline {{/str}}{{/str}}"
|
||||
role="menuitem"
|
||||
>
|
||||
{{#str}} next3months, block_timeline {{/str}}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
data-from="0"
|
||||
data-to="180"
|
||||
data-filtername="next6months"
|
||||
{{#next6months}}aria-current="true"{{/next6months}}
|
||||
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next6months, block_timeline {{/str}}{{/str}}"
|
||||
role="menuitem"
|
||||
>
|
||||
{{#str}} next6months, block_timeline {{/str}}
|
||||
</a>
|
||||
<div role="group" aria-labelledby="duedatefiltergrouplabel">
|
||||
<div class="h6 dropdown-header" role="presentation" id="duedatefiltergrouplabel">{{#str}} duedate, block_timeline {{/str}}</div>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
data-from="0"
|
||||
data-to="7"
|
||||
data-filtername="next7days"
|
||||
{{#next7days}}aria-current="true"{{/next7days}}
|
||||
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next7days, block_timeline {{/str}}{{/str}}"
|
||||
role="menuitem"
|
||||
>
|
||||
{{#str}} next7days, block_timeline {{/str}}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
data-from="0"
|
||||
data-to="30"
|
||||
data-filtername="next30days"
|
||||
{{#next30days}}aria-current="true"{{/next30days}}
|
||||
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next30days, block_timeline {{/str}}{{/str}}"
|
||||
role="menuitem"
|
||||
>
|
||||
{{#str}} next30days, block_timeline {{/str}}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
data-from="0"
|
||||
data-to="90"
|
||||
data-filtername="next3months"
|
||||
{{#next3months}}aria-current="true"{{/next3months}}
|
||||
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next3months, block_timeline {{/str}}{{/str}}"
|
||||
role="menuitem"
|
||||
>
|
||||
{{#str}} next3months, block_timeline {{/str}}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
data-from="0"
|
||||
data-to="180"
|
||||
data-filtername="next6months"
|
||||
{{#next6months}}aria-current="true"{{/next6months}}
|
||||
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next6months, block_timeline {{/str}}{{/str}}"
|
||||
role="menuitem"
|
||||
>
|
||||
{{#str}} next6months, block_timeline {{/str}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,8 +37,10 @@ Feature: The timeline block allows users to see upcoming courses
|
||||
| student1 | C3 | student |
|
||||
| student1 | C4 | student |
|
||||
|
||||
@accessibility
|
||||
Scenario: Next 30 days in course view
|
||||
Given I log in as "student1"
|
||||
And the "Timeline" "block" should meet accessibility standards with "best-practice" extra tests
|
||||
And I click on "Filter timeline by date" "button" in the "Timeline" "block"
|
||||
And I click on "Next 30 days" "link" in the "Timeline" "block"
|
||||
And I click on "Sort timeline items" "button" in the "Timeline" "block"
|
||||
@@ -61,6 +63,7 @@ Feature: The timeline block allows users to see upcoming courses
|
||||
And "Test feedback 2" "link" should not exist in the "Timeline" "block"
|
||||
And "Test feedback 3" "link" should not exist in the "Timeline" "block"
|
||||
And "Test assign 1" "link" should not exist in the "Timeline" "block"
|
||||
And the "Timeline" "block" should meet accessibility standards with "best-practice" extra tests
|
||||
|
||||
Scenario: All in course view
|
||||
Given I log in as "student1"
|
||||
|
||||
@@ -50,9 +50,11 @@ Feature: The timeline block allows users to see upcoming activities
|
||||
And "Test assign 1" "link" should not exist in the "Timeline" "block"
|
||||
And I should not see "Assignment is due · Course 1" in the "Timeline" "block"
|
||||
|
||||
@accessibility
|
||||
Scenario: Overdue in date view
|
||||
Given I log in as "student1"
|
||||
And I click on "Filter timeline by date" "button" in the "Timeline" "block"
|
||||
And the "Timeline" "block" should meet accessibility standards with "best-practice" extra tests
|
||||
When I click on "Overdue" "link" in the "Timeline" "block"
|
||||
Then "Test assign 1" "link" should exist in the "Timeline" "block"
|
||||
And I should see "Assignment is due · Course 1" in the "Timeline" "block"
|
||||
@@ -63,6 +65,7 @@ Feature: The timeline block allows users to see upcoming activities
|
||||
And "Test choice 3" "link" should not exist in the "Timeline" "block"
|
||||
And "Test feedback 3" "link" should not exist in the "Timeline" "block"
|
||||
And "Test feedback 4" "link" should not exist in the "Timeline" "block"
|
||||
And the "Timeline" "block" should meet accessibility standards with "best-practice" extra tests
|
||||
|
||||
Scenario: All in date view
|
||||
Given I log in as "student1"
|
||||
|
||||
@@ -74,6 +74,7 @@ class activity_navigation implements renderable, templatable {
|
||||
$attributes = [
|
||||
'class' => 'btn btn-link',
|
||||
'id' => 'prev-activity-link',
|
||||
'aria-label' => get_string('activitynavigation:preva', 'course', $linkname),
|
||||
];
|
||||
$this->prevlink = new \action_link($linkurl, $OUTPUT->larrow() . ' ' . $linkname, null, $attributes);
|
||||
}
|
||||
@@ -89,15 +90,16 @@ class activity_navigation implements renderable, templatable {
|
||||
$attributes = [
|
||||
'class' => 'btn btn-link',
|
||||
'id' => 'next-activity-link',
|
||||
'aria-label' => get_string('activitynavigation:nexta', 'course', $linkname),
|
||||
];
|
||||
$this->nextlink = new \action_link($linkurl, $linkname . ' ' . $OUTPUT->rarrow(), null, $attributes);
|
||||
}
|
||||
|
||||
// Render the activity list dropdown menu if available.
|
||||
if (!empty($activitylist)) {
|
||||
$select = new url_select($activitylist, '', array('' => get_string('jumpto')));
|
||||
$select->set_label(get_string('jumpto'), array('class' => 'sr-only'));
|
||||
$select->attributes = array('id' => 'jump-to-activity');
|
||||
$select = new url_select($activitylist, '', ['' => get_string('activitynavigation:jumptoactivity', 'course')]);
|
||||
$select->set_label(get_string('activitynavigation:jumptoactivity', 'course'), ['class' => 'sr-only']);
|
||||
$select->attributes = ['id' => 'jump-to-activity'];
|
||||
$this->activitylist = $select;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,23 +39,23 @@ Feature: Activity navigation in a single activity course
|
||||
Scenario: Jump to a hidden activity as a teacher
|
||||
Given I log in as "teacher1"
|
||||
When I am on "Course 1" course homepage
|
||||
Then "Jump to..." "field" should exist
|
||||
Then "Jump to activity" "field" should exist
|
||||
# The current activity (Forum 1) will not be listed.
|
||||
And the "Jump to..." select box should not contain "Forum 1"
|
||||
And the "Jump to activity" select box should not contain "Forum 1"
|
||||
# Check drop down menu contents.
|
||||
And the "Jump to..." select box should contain "Assignment 1 (hidden)"
|
||||
And the "Jump to..." select box should contain "Lesson 1 (hidden)"
|
||||
And the "Jump to activity" select box should contain "Assignment 1 (hidden)"
|
||||
And the "Jump to activity" select box should contain "Lesson 1 (hidden)"
|
||||
# Jump to a hidden activity somewhere in the middle.
|
||||
When I select "Assignment 1 (hidden)" from the "Jump to..." singleselect
|
||||
When I select "Assignment 1 (hidden)" from the "Jump to activity" singleselect
|
||||
Then I should see "Assignment 1"
|
||||
And I should see "Forum 1" in the "#prev-activity-link" "css_element"
|
||||
And I should see "Lesson 1 (hidden)" in the "#next-activity-link" "css_element"
|
||||
# Jump to the first activity.
|
||||
And I select "Forum 1" from the "Jump to..." singleselect
|
||||
And I select "Forum 1" from the "Jump to activity" singleselect
|
||||
And I should see "Assignment 1 (hidden)" in the "#next-activity-link" "css_element"
|
||||
But "#prev-activity-link" "css_element" should not exist
|
||||
# Jump to the last activity.
|
||||
And I select "Lesson 1 (hidden)" from the "Jump to..." singleselect
|
||||
And I select "Lesson 1 (hidden)" from the "Jump to activity" singleselect
|
||||
And I should see "Assignment 1 (hidden)" in the "#prev-activity-link" "css_element"
|
||||
But "#next-activity-link" "css_element" should not exist
|
||||
|
||||
@@ -65,7 +65,7 @@ Feature: Activity navigation in a single activity course
|
||||
# The first activity won't have the previous activity link.
|
||||
Then "#prev-activity-link" "css_element" should not exist
|
||||
And "#next-activity-link" "css_element" should not exist
|
||||
And "Jump to..." "field" should not exist
|
||||
And "Jump to activity" "field" should not exist
|
||||
|
||||
Scenario: The activity navigation asks for login to guest user
|
||||
Given I log in as "guest"
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"id": "url_select_test",
|
||||
"action": "#",
|
||||
"options": [
|
||||
{"name": "Jump to...", "value": "#0"},
|
||||
{"name": "Jump to activity", "value": "#0"},
|
||||
{"name": "Activity A", "value": "#1"},
|
||||
{"name": "Activity B", "value": "#2"},
|
||||
{"name": "Activity C", "value": "#3"}
|
||||
@@ -64,7 +64,7 @@
|
||||
}
|
||||
}
|
||||
}}
|
||||
<div class="mt-5 mb-1 activity-navigation container-fluid">
|
||||
<nav class="mt-5 mb-1 activity-navigation container-fluid" aria-label="{{#str}}activitynavigation, course{{/str}}">
|
||||
{{< core/columns-1to1to1}}
|
||||
{{$column1}}
|
||||
<div class="float-start">
|
||||
@@ -82,4 +82,4 @@
|
||||
</div>
|
||||
{{/column3}}
|
||||
{{/ core/columns-1to1to1}}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -38,7 +38,7 @@ Feature: Display and choose from the available activities in course
|
||||
Scenario: The teacher can choose to add an activity from the activity summary in the activity chooser
|
||||
Given I click on "Add an activity or resource" "button" in the "New section" "section"
|
||||
When I click on "Information about the Assignment activity" "button" in the "Add an activity or resource" "dialogue"
|
||||
When I click on "Add a new Assignment" "link" in the "help" "core_course > Activity chooser screen"
|
||||
And I click on "Add a new Assignment" "link" in the ".modal-footer" "css_element"
|
||||
Then I should see "New Assignment"
|
||||
|
||||
Scenario: Show summary
|
||||
@@ -57,8 +57,8 @@ Feature: Display and choose from the available activities in course
|
||||
Given I click on "Add an activity or resource" "button" in the "New section" "section"
|
||||
When I click on "Information about the Assignment activity" "button" in the "modules" "core_course > Activity chooser screen"
|
||||
And I should see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback." in the "help" "core_course > Activity chooser screen"
|
||||
And I should see "Back" in the "help" "core_course > Activity chooser screen"
|
||||
When I click on "Back" "button" in the "help" "core_course > Activity chooser screen"
|
||||
And I should see "Back" in the ".modal-footer" "css_element"
|
||||
When I click on "Back" "button" in the ".modal-footer" "css_element"
|
||||
Then "modules" "core_course > Activity chooser screen" should be visible
|
||||
And "help" "core_course > Activity chooser screen" should not be visible
|
||||
And "Back" "button" should not exist in the "modules" "core_course > Activity chooser screen"
|
||||
@@ -67,7 +67,7 @@ Feature: Display and choose from the available activities in course
|
||||
And I am on "C2" course homepage with editing mode on
|
||||
And I click on "Add an activity or resource" "button" in the "13 January - 19 January" "section"
|
||||
And I click on "Information about the Assignment activity" "button" in the "Add an activity or resource" "dialogue"
|
||||
And I click on "Back" "button" in the "help" "core_course > Activity chooser screen"
|
||||
And I click on "Back" "button" in the ".modal-footer" "css_element"
|
||||
And "modules" "core_course > Activity chooser screen" should be visible
|
||||
And "help" "core_course > Activity chooser screen" should not be visible
|
||||
And "Back" "button" should not exist in the "modules" "core_course > Activity chooser screen"
|
||||
|
||||
@@ -191,49 +191,49 @@ Feature: Activity navigation
|
||||
Given I log in as "teacher1"
|
||||
When I am on "Course 1" course homepage
|
||||
And I follow "Assignment 1"
|
||||
Then "Jump to..." "field" should exist
|
||||
Then "Jump to activity" "field" should exist
|
||||
# The current activity will not be listed.
|
||||
And the "Jump to..." select box should not contain "Assignment 1"
|
||||
And the "Jump to activity" select box should not contain "Assignment 1"
|
||||
# Stealth activities will not be listed.
|
||||
And the "Jump to..." select box should not contain "Forum 1"
|
||||
And the "Jump to activity" select box should not contain "Forum 1"
|
||||
# Resources without view URL (e.g. labels) will not be listed.
|
||||
And the "Jump to..." select box should not contain "Label 1"
|
||||
And the "Jump to activity" select box should not contain "Label 1"
|
||||
# Check drop down menu contents.
|
||||
And the "Jump to..." select box should contain "Book 1"
|
||||
And the "Jump to..." select box should contain "Chat 1"
|
||||
And the "Jump to..." select box should contain "Choice 1"
|
||||
And the "Jump to..." select box should contain "Database 1"
|
||||
And the "Jump to..." select box should contain "Feedback 1"
|
||||
And the "Jump to..." select box should contain "Folder 1"
|
||||
And the "Jump to..." select box should contain "Imscp 1"
|
||||
And the "Jump to..." select box should contain "Lesson 1"
|
||||
And the "Jump to..." select box should contain "Lti 1"
|
||||
And the "Jump to..." select box should contain "Page 1"
|
||||
And the "Jump to..." select box should contain "Quiz 1"
|
||||
And the "Jump to..." select box should contain "Url 1"
|
||||
And the "Jump to..." select box should contain "Wiki 1"
|
||||
And the "Jump to..." select box should contain "Workshop 1"
|
||||
And the "Jump to activity" select box should contain "Book 1"
|
||||
And the "Jump to activity" select box should contain "Chat 1"
|
||||
And the "Jump to activity" select box should contain "Choice 1"
|
||||
And the "Jump to activity" select box should contain "Database 1"
|
||||
And the "Jump to activity" select box should contain "Feedback 1"
|
||||
And the "Jump to activity" select box should contain "Folder 1"
|
||||
And the "Jump to activity" select box should contain "Imscp 1"
|
||||
And the "Jump to activity" select box should contain "Lesson 1"
|
||||
And the "Jump to activity" select box should contain "Lti 1"
|
||||
And the "Jump to activity" select box should contain "Page 1"
|
||||
And the "Jump to activity" select box should contain "Quiz 1"
|
||||
And the "Jump to activity" select box should contain "Url 1"
|
||||
And the "Jump to activity" select box should contain "Wiki 1"
|
||||
And the "Jump to activity" select box should contain "Workshop 1"
|
||||
# Hidden activities will be rendered with a '(hidden)' text.
|
||||
And the "Jump to..." select box should contain "Glossary 1 (hidden)"
|
||||
And the "Jump to activity" select box should contain "Glossary 1 (hidden)"
|
||||
# Activities in hidden sections will be rendered with a '(hidden)' text.
|
||||
And the "Jump to..." select box should contain "Resource 1 (hidden)"
|
||||
And the "Jump to..." select box should contain "Scorm 1 (hidden)"
|
||||
And the "Jump to..." select box should contain "Survey 1 (hidden)"
|
||||
And the "Jump to activity" select box should contain "Resource 1 (hidden)"
|
||||
And the "Jump to activity" select box should contain "Scorm 1 (hidden)"
|
||||
And the "Jump to activity" select box should contain "Survey 1 (hidden)"
|
||||
# Jump to an activity somewhere in the middle.
|
||||
When I select "Page 1" from the "Jump to..." singleselect
|
||||
When I select "Page 1" from the "Jump to activity" singleselect
|
||||
Then I should see "Page 1"
|
||||
And I should see "Lti 1" in the "#prev-activity-link" "css_element"
|
||||
And I should see "Quiz 1" in the "#next-activity-link" "css_element"
|
||||
# Jump to the first activity.
|
||||
And I select "Assignment 1" from the "Jump to..." singleselect
|
||||
And I select "Assignment 1" from the "Jump to activity" singleselect
|
||||
And I should see "Book 1" in the "#next-activity-link" "css_element"
|
||||
But "#prev-activity-link" "css_element" should not exist
|
||||
# Jump to the last activity.
|
||||
And I select "Workshop 1" from the "Jump to..." singleselect
|
||||
And I select "Workshop 1" from the "Jump to activity" singleselect
|
||||
And I should see "Wiki 1" in the "#prev-activity-link" "css_element"
|
||||
But "#next-activity-link" "css_element" should not exist
|
||||
# Jump to a hidden activity.
|
||||
And I select "Glossary 1" from the "Jump to..." singleselect
|
||||
And I select "Glossary 1" from the "Jump to activity" singleselect
|
||||
And I should see "Folder 1" in the "#prev-activity-link" "css_element"
|
||||
And I should see "Imscp 1" in the "#next-activity-link" "css_element"
|
||||
|
||||
@@ -241,45 +241,45 @@ Feature: Activity navigation
|
||||
Given I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Assignment 1"
|
||||
And "Jump to..." "field" should exist
|
||||
And "Jump to activity" "field" should exist
|
||||
# The current activity will not be listed.
|
||||
And the "Jump to..." select box should not contain "Assignment 1"
|
||||
And the "Jump to activity" select box should not contain "Assignment 1"
|
||||
# Stealth activities will not be listed for students.
|
||||
And the "Jump to..." select box should not contain "Forum 1"
|
||||
And the "Jump to activity" select box should not contain "Forum 1"
|
||||
# Resources without view URL (e.g. labels) will not be listed.
|
||||
And the "Jump to..." select box should not contain "Label 1"
|
||||
And the "Jump to activity" select box should not contain "Label 1"
|
||||
# Hidden activities will not be listed for students.
|
||||
And the "Jump to..." select box should not contain "Glossary 1"
|
||||
And the "Jump to activity" select box should not contain "Glossary 1"
|
||||
# Activities in hidden sections will not be listed for students.
|
||||
And the "Jump to..." select box should not contain "Resource 1"
|
||||
And the "Jump to..." select box should not contain "Scorm 1"
|
||||
And the "Jump to..." select box should not contain "Survey 1"
|
||||
And the "Jump to activity" select box should not contain "Resource 1"
|
||||
And the "Jump to activity" select box should not contain "Scorm 1"
|
||||
And the "Jump to activity" select box should not contain "Survey 1"
|
||||
# Only activities visible to students will be listed.
|
||||
And the "Jump to..." select box should contain "Book 1"
|
||||
And the "Jump to..." select box should contain "Chat 1"
|
||||
And the "Jump to..." select box should contain "Choice 1"
|
||||
And the "Jump to..." select box should contain "Database 1"
|
||||
And the "Jump to..." select box should contain "Feedback 1"
|
||||
And the "Jump to..." select box should contain "Folder 1"
|
||||
And the "Jump to..." select box should contain "Imscp 1"
|
||||
And the "Jump to..." select box should contain "Lesson 1"
|
||||
And the "Jump to..." select box should contain "Lti 1"
|
||||
And the "Jump to..." select box should contain "Page 1"
|
||||
And the "Jump to..." select box should contain "Quiz 1"
|
||||
And the "Jump to..." select box should contain "Url 1"
|
||||
And the "Jump to..." select box should contain "Wiki 1"
|
||||
And the "Jump to..." select box should contain "Workshop 1"
|
||||
And the "Jump to activity" select box should contain "Book 1"
|
||||
And the "Jump to activity" select box should contain "Chat 1"
|
||||
And the "Jump to activity" select box should contain "Choice 1"
|
||||
And the "Jump to activity" select box should contain "Database 1"
|
||||
And the "Jump to activity" select box should contain "Feedback 1"
|
||||
And the "Jump to activity" select box should contain "Folder 1"
|
||||
And the "Jump to activity" select box should contain "Imscp 1"
|
||||
And the "Jump to activity" select box should contain "Lesson 1"
|
||||
And the "Jump to activity" select box should contain "Lti 1"
|
||||
And the "Jump to activity" select box should contain "Page 1"
|
||||
And the "Jump to activity" select box should contain "Quiz 1"
|
||||
And the "Jump to activity" select box should contain "Url 1"
|
||||
And the "Jump to activity" select box should contain "Wiki 1"
|
||||
And the "Jump to activity" select box should contain "Workshop 1"
|
||||
# Jump to an activity somewhere in the middle.
|
||||
When I select "Page 1" from the "Jump to..." singleselect
|
||||
When I select "Page 1" from the "Jump to activity" singleselect
|
||||
Then I should see "Page 1"
|
||||
And I should see "Lti 1" in the "#prev-activity-link" "css_element"
|
||||
And I should see "Quiz 1" in the "#next-activity-link" "css_element"
|
||||
# Jump to the first activity.
|
||||
And I select "Assignment 1" from the "Jump to..." singleselect
|
||||
And I select "Assignment 1" from the "Jump to activity" singleselect
|
||||
And I should see "Book 1" in the "#next-activity-link" "css_element"
|
||||
But "#prev-activity-link" "css_element" should not exist
|
||||
# Jump to the last activity.
|
||||
And I select "Workshop 1" from the "Jump to..." singleselect
|
||||
And I select "Workshop 1" from the "Jump to activity" singleselect
|
||||
And I should see "Wiki 1" in the "#prev-activity-link" "css_element"
|
||||
But "#next-activity-link" "css_element" should not exist
|
||||
|
||||
@@ -289,4 +289,4 @@ Feature: Activity navigation
|
||||
And I follow "Assignment 1"
|
||||
Then "#prev-activity-link" "css_element" should not exist
|
||||
And "#next-activity-link" "css_element" should not exist
|
||||
And "Jump to..." "field" should not exist
|
||||
And "Jump to activity" "field" should not exist
|
||||
|
||||
@@ -48,13 +48,13 @@ Feature: Activity navigation involving activities with access restrictions
|
||||
Given I am on the "Page 1" "page activity" page logged in as student1
|
||||
Then I should see "Page 2" in the "#next-activity-link" "css_element"
|
||||
# Activity that has access restriction should not show up in the dropdown.
|
||||
And the "Jump to..." select box should not contain "Page 3"
|
||||
And I select "Page 4" from the "Jump to..." singleselect
|
||||
And the "Jump to activity" select box should not contain "Page 3"
|
||||
And I select "Page 4" from the "Jump to activity" singleselect
|
||||
# Page 2 should be shown in the previous link since Page 3 is not yet available.
|
||||
And I should see "Page 2" in the "#prev-activity-link" "css_element"
|
||||
And the "Jump to..." select box should not contain "Page 3"
|
||||
And the "Jump to activity" select box should not contain "Page 3"
|
||||
# Navigate to Page 2.
|
||||
And I click on "Page 2" "link" in the "page-content" "region"
|
||||
# Since Page 2 has now been viewed and deemed completed, Page 3 can now be accessed.
|
||||
And I should see "Page 3" in the "#next-activity-link" "css_element"
|
||||
And the "Jump to..." select box should contain "Page 3"
|
||||
And the "Jump to activity" select box should contain "Page 3"
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
@enrol @enrol_self
|
||||
Feature: Self-enrolment cohort restriction
|
||||
In order to prevent unauthorized access
|
||||
As a admin
|
||||
I need to restrict self-enrolment to cohort members
|
||||
|
||||
Background:
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
| student2 | Student | 2 | student2@example.com |
|
||||
And the following "cohorts" exist:
|
||||
| name | idnumber | contextid |
|
||||
| Cohort A | CH1 | 1 |
|
||||
And the following "cohort members" exist:
|
||||
| user | cohort |
|
||||
| student1 | CH1 |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And I log in as "admin"
|
||||
And I add "Self enrolment" enrolment method in "Course 1" with:
|
||||
| Custom instance name | Test student enrolment |
|
||||
| Only cohort members | Cohort A |
|
||||
|
||||
Scenario: Self enrolment as cohort member
|
||||
Given I am on the "C1" "Course" page logged in as "student1"
|
||||
When I press "Enrol me"
|
||||
Then I should see "You are enrolled in the course."
|
||||
|
||||
Scenario: Self enrolment as non cohort member
|
||||
Given I am on the "C1" "Course" page logged in as "student2"
|
||||
Then I should see "Only members of cohort 'Cohort A' can self-enrol."
|
||||
@@ -79,7 +79,7 @@
|
||||
|
||||
.gradingform_guide .plainvalue.empty {
|
||||
font-style: italic;
|
||||
color: #aaa;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.gradingform_guide .plainvalue.editname {
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
|
||||
.gradingform_rubric .plainvalue.empty {
|
||||
font-style: italic;
|
||||
color: #aaa;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* Make invisible the buttons 'Move up' for the first criterion and
|
||||
|
||||
@@ -169,7 +169,7 @@ if (has_capability('moodle/grade:viewall', $context) && $courseid != SITEID) {
|
||||
|
||||
echo $OUTPUT->header();
|
||||
if ($report->fill_table(true, true)) {
|
||||
echo html_writer::tag('h3', get_string('coursesiamtaking', 'grades'));
|
||||
echo $OUTPUT->heading(get_string('coursesiamtaking', 'grades'));
|
||||
echo '<br />' . $report->print_table(true);
|
||||
}
|
||||
} else { // We have a course context. We must be navigating from the gradebook.
|
||||
@@ -189,7 +189,7 @@ if (has_capability('moodle/grade:viewall', $context) && $courseid != SITEID) {
|
||||
}
|
||||
|
||||
if (count($report->teachercourses)) {
|
||||
echo html_writer::tag('h3', get_string('coursesiamteaching', 'grades'));
|
||||
echo $OUTPUT->heading(get_string('coursesiamteaching', 'grades'));
|
||||
$report->print_teacher_table();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
@gradereport @gradereport_overview
|
||||
Feature: Grade overview report can be viewed
|
||||
In order to see the courses I am enrolled in
|
||||
As a user
|
||||
I should be able to access the grade overview report's index page
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Awesome course | C1 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | t1@example.com |
|
||||
| student1 | Student | 1 | s1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
|
||||
@javascript @accessibility
|
||||
Scenario Outline: The grade overview report index page should be accessible
|
||||
Given I am logged in as "<user>"
|
||||
When I follow "Grades" in the user menu
|
||||
Then "<headingname>" "heading" should exist
|
||||
And the page should meet accessibility standards with "best-practice" extra tests
|
||||
|
||||
Examples:
|
||||
| user | headingname |
|
||||
| student1 | Courses I am taking |
|
||||
| teacher1 | Courses I am teaching |
|
||||
@@ -226,6 +226,48 @@ document.onreadystatechange = async() => {
|
||||
const Pending = await getPendingClass();
|
||||
var resizePending = new Pending('core_h5p/iframe:resize');
|
||||
|
||||
// Track when the embedded H5P content is fully attached.
|
||||
const contentLoadedPending = new Pending('core_h5p/iframe:contentLoaded');
|
||||
let contentLoaded = false;
|
||||
const markContentLoaded = function() {
|
||||
const body = iFrame?.contentDocument?.body;
|
||||
const hasContent = body && body.querySelector('.h5p-container, .h5p-content');
|
||||
|
||||
// Check that H5P instance is actually ready with proper initialization
|
||||
if (contentLoaded || !hasContent || !H5P?.instances?.[0]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
contentLoaded = true;
|
||||
contentLoadedPending.resolve();
|
||||
H5PEmbedCommunicator.send('contentLoaded');
|
||||
return true;
|
||||
};
|
||||
|
||||
// If the content is already there, mark it immediately.
|
||||
markContentLoaded();
|
||||
|
||||
// Observe the iframe document for the first appearance of the H5P container.
|
||||
if (!contentLoaded && iFrame.contentDocument?.body) {
|
||||
const contentObserver = new MutationObserver(function() {
|
||||
if (markContentLoaded()) {
|
||||
contentObserver.disconnect();
|
||||
}
|
||||
});
|
||||
contentObserver.observe(iFrame.contentDocument.body, {childList: true, subtree: true});
|
||||
}
|
||||
|
||||
// Extended fallback timeout to ensure pending resolves even if content detection fails.
|
||||
if (!contentLoaded) {
|
||||
setTimeout(function() {
|
||||
if (!contentLoaded) {
|
||||
contentLoaded = true;
|
||||
contentLoadedPending.resolve();
|
||||
H5PEmbedCommunicator.send('contentLoaded');
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
H5P.on(instance, 'resize', function() {
|
||||
if (H5P.isFullscreen) {
|
||||
return; // Skip iframe resize.
|
||||
@@ -249,6 +291,8 @@ document.onreadystatechange = async() => {
|
||||
}, 150);
|
||||
});
|
||||
|
||||
H5P.externalDispatcher.on('domChanged', markContentLoaded);
|
||||
|
||||
// Get emitted xAPI data.
|
||||
H5P.externalDispatcher.on('xAPI', function(event) {
|
||||
statementPosted = false;
|
||||
|
||||
@@ -33,7 +33,7 @@ $string['cannotcreatedboninstall'] = '<p>No es pot crear la base de dades.</p> <
|
||||
<p>L\'administrador del lloc hauria de verificar la configuració de la base de dades.</p>';
|
||||
$string['cannotcreatelangdir'] = 'No s\'ha pogut crear el directori d\'idiomes';
|
||||
$string['cannotcreatetempdir'] = 'No s\'ha pogut crear el directori temporal';
|
||||
$string['cannotdownloadcomponents'] = 'No s\'han pogut baixar components';
|
||||
$string['cannotdownloadcomponents'] = 'No s\'han pogut descarregar els components';
|
||||
$string['cannotdownloadzipfile'] = 'No s\'ha pogut baixar el fitxer ZIP';
|
||||
$string['cannotfindcomponent'] = 'No s\'ha pogut trobar el component';
|
||||
$string['cannotsavemd5file'] = 'No s\'ha pogut desar el fitxer md5';
|
||||
@@ -46,6 +46,6 @@ $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>';
|
||||
$string['wrongdestpath'] = 'El camí de destinació és erroni';
|
||||
$string['wrongsourcebase'] = 'L\'adreça (URL) base de la font és errònia';
|
||||
$string['wrongdestpath'] = 'El camí de destinació és incorrecte';
|
||||
$string['wrongsourcebase'] = 'L\'URL base de la font és errònia';
|
||||
$string['wrongzipfilename'] = 'El nom del fitxer ZIP és erroni';
|
||||
|
||||
@@ -30,20 +30,20 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['admindirname'] = 'Directori d\'administració';
|
||||
$string['availablelangs'] = 'Llista d\'idiomes disponibles';
|
||||
$string['availablelangs'] = 'Paquets d\'idiomes disponibles';
|
||||
$string['chooselanguagehead'] = 'Trieu un idioma';
|
||||
$string['chooselanguagesub'] = 'Trieu un idioma per a la instal·lació. S\'utilitzarà també com a idioma per defecte del lloc, tot i que després podeu canviar-lo.';
|
||||
$string['clialreadyconfigured'] = 'El fitxer config.php ja existeix; feu servir admin/cli/install_database.php si voleu instal·lar Moodle en aquest lloc web.';
|
||||
$string['clialreadyinstalled'] = 'El fitxer de configuració config.php ja existeix. Feu servir admin/cli/upgrade.php si voleu actualitzar Moodle per a aquest lloc web.';
|
||||
$string['cliinstallheader'] = 'Programa d\'instal·lació de línia d\'ordres de Moodle {$a}';
|
||||
$string['clitablesexist'] = 'Les taules de la base de dades ja existeixen. La instal·lació per línia d\'ordres no pot continuar.';
|
||||
$string['databasehost'] = 'Servidor de base de dades:';
|
||||
$string['databasename'] = 'Nom de la base de dades:';
|
||||
$string['databasehost'] = 'Servidor de base de dades';
|
||||
$string['databasename'] = 'Nom de la base de dades';
|
||||
$string['databasetypehead'] = 'Trieu el controlador de la base de dades';
|
||||
$string['dataroot'] = 'Directori de dades';
|
||||
$string['datarootpermission'] = 'Permisos dels directoris de dades';
|
||||
$string['dbprefix'] = 'Prefix de taules';
|
||||
$string['dirroot'] = 'Directori de Moodle';
|
||||
$string['dirroot'] = 'Directori del Moodle';
|
||||
$string['environmenthead'] = 'S\'està comprovant el vostre entorn';
|
||||
$string['environmentsub2'] = 'Cada versió de Moodle té uns requeriments mínims de versió de PHP i un nombre d\'extensions de PHP necessàries.
|
||||
Abans de cada instal·lació o actualització es realitza una comprovació exhaustiva de l\'entorn. Contacteu amb l\'administrador si no sabeu com instal·lar una nova versió de PHP o com habilitar les extensions.';
|
||||
@@ -72,7 +72,7 @@ $string['pathssubdataroot'] = '<p>Directori on Moodle emmagatzemarà els materia
|
||||
<p>Aquest directori hauria de tenir permisos de lectura i escriptura per a l\'usuari del servidor web (normalment «www-data», «nobody», or «apache»).</p>
|
||||
<p>No ha de ser accessible directament via web.</p>
|
||||
<p>L\'instal·lador provarà de crear-lo si no existeix.</p>';
|
||||
$string['pathssubdirroot'] = 'Camí complet del directori d\'instal·lació de Moodle.';
|
||||
$string['pathssubdirroot'] = '<p>El camí complet al directori que conté el codi del Moodle.</p>';
|
||||
$string['pathssubwwwroot'] = '<p>L\'adreça web completa on s\'accedirà a Moodle; per exemple, l\'adreça que els usuaris introduiran a la barra d\'adreces del navegador per accedir a Moodle.</p> <p> No és possible accedir a Moodle utilitzant diferents adreces. Si el vostre lloc és accessible a través de diferents adreces, trieu-ne la més fàcil i configureu una redirecció permanent per a cadascuna de les altres adreces.</p> <p>
|
||||
Si el vostre lloc és accessible tant des d\'Internet com des d\'una xarxa interna (anomenada de vegades intranet), utilitzeu l\'adreça pública aquí.</p> <p>Si l\'adreça actual no és correcta, canvieu l\'URL a la barra d\'adreces del navegador i reinicieu la instal·lació.';
|
||||
$string['pathsunsecuredataroot'] = 'La ubicació del dataroot no és segura.';
|
||||
@@ -86,9 +86,7 @@ $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
|
||||
$string['welcomep20'] = 'Esteu veient aquesta pàgina perquè heu instal·lat amb èxit i heu executat el paquet <strong>{$a->packname} {$a->packversion}</strong>. Felicitacions.';
|
||||
$string['welcomep30'] = 'Aquesta versió de <strong>{$a->installername}</strong> inclou les aplicacions necessàries per crear un entorn en el qual funcioni <strong>Moodle</strong>, concretament:';
|
||||
$string['welcomep40'] = 'El paquet inclou també <strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong>.';
|
||||
$string['welcomep50'] = 'L\'ús de totes les aplicacions d\'aquest paquet és governat per les seves llicències respectives. El paquet <strong>{$a->installername}</strong> complet és
|
||||
<a href="http://www.opensource.org/docs/definition_plain.html">codi font obert</a> i es distribueix
|
||||
sota llicència <a href="http://www.gnu.org/copyleft/gpl.html">GPL</a>.';
|
||||
$string['welcomep60'] = 'Les pàgines següents us guiaran per una sèrie de passos fàcils de seguir per configurar <strong>Moodle</strong> en el vostre ordinador. Podeu acceptar els paràmetres per defecte o, opcionalment, modificar-los perquè s\'ajustin a les vostres necessitats.';
|
||||
$string['welcomep50'] = 'L\'ús de totes les aplicacions d\'aquest paquet es regeix per les seves respectives llicències. El paquet complet <strong>{$a->installername}</strong> és de <a href="https://www.opensource.org/docs/definition_plain.html">codi obert</a> i es distribueix sota la llicència <a href="https://www.gnu.org/copyleft/gpl.html">GPL</a>.';
|
||||
$string['welcomep60'] = 'Les pàgines següents us guiaran per una sèrie de passos fàcils de seguir per configurar el <strong>Moodle</strong> en el vostre ordinador. Podeu acceptar els paràmetres per defecte o, opcionalment, modificar-los perquè s\'ajustin a les vostres necessitats.';
|
||||
$string['welcomep70'] = 'Feu clic al botó «Següent» per continuar amb la configuració de <strong>Moodle</strong>.';
|
||||
$string['wwwroot'] = 'Adreça web';
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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['clianswerno'] = 'n';
|
||||
$string['cliansweryes'] = 's';
|
||||
$string['cliincorrectvalueerror'] = 'Error, valor incorrecto "{$a->value}" para "{$a->option}"';
|
||||
$string['cliincorrectvalueretry'] = 'Valor incorrecto, por favor intenta de nuevo';
|
||||
$string['clitypevalue'] = 'escribe el valor';
|
||||
$string['clitypevaluedefault'] = 'escribe el valor, presiona Enter para usar el valor predeterminado ({$a})';
|
||||
$string['cliunknowoption'] = 'Opciones no reconocidas:
|
||||
{$a}
|
||||
Por favor usa la opción --help.';
|
||||
$string['cliyesnoprompt'] = 'escribe s (significa sí) o n (significa no)';
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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>Chan urrainn an stòr-dàta a chruthachadh.</p>
|
||||
<p>Chan eil an stòr-dàta a chaidh a shònrachadh ann am bith agus chan eil cead aig an neach-cleachdaidh tugta an stòr-dàta a chruthachadh.</p>
|
||||
<p>Bu chòir do rianaire na làraich rèiteachadh an stòr-dàta a dhearbhadh.</p>';
|
||||
$string['cannotcreatelangdir'] = 'Chan urrainn iùl-lann cànain a chruthachadh';
|
||||
$string['cannotcreatetempdir'] = 'Chan urrainn iùl-lann shealach a chruthachadh';
|
||||
$string['cannotdownloadcomponents'] = 'Chan urrainn co-phàirtean a luchdachadh a-nuas';
|
||||
$string['cannotdownloadzipfile'] = 'Chan urrainn faidhle ZIP a luchdachadh a-nuas';
|
||||
$string['cannotfindcomponent'] = 'Chan urrainn a’ cho-phàirt a lorg';
|
||||
$string['cannotsavemd5file'] = 'Chan urrainn faidhle md5 a shàbhaladh';
|
||||
$string['cannotsavezipfile'] = 'Chan urrainn faidhle ZIP a shàbhaladh';
|
||||
$string['cannotunzipfile'] = 'Chan urrainn am faidhle siopach fhosgladh';
|
||||
$string['componentisuptodate'] = 'Tha a’ cho-phàirt ris an là';
|
||||
$string['dmlexceptiononinstall'] = '<p>Tha mearachd stòir-dàta air tachairt \\[{$a->errorcode}].<br />{$a->debuginfo}</p>';
|
||||
$string['downloadedfilecheckfailed'] = 'Dh’fhàillig sgrùdadh an fhaidhle a chaidh a luchdachadh a-nuas';
|
||||
$string['missingrequiredfield'] = 'Tha raon deatamach air choireigin a dhìth';
|
||||
$string['remotedownloaderror'] = '<p>Dh’fhàillig luchdachadh na co-phàirte a-nuas chun fhrithealaiche agad. Dearbh na suidheachaidhean procsaidh: thathas a’ moladh an leudachain PHP cURL gu mòr.</p>
|
||||
<p>Feumaidh tu am faidhle <a href="{$a->url}">{$a->url}</a> a luchdachadh a-nuas le làimh, lethbhreac dheth a chur ann an "{$a->dest}" san fhrithealaiche agad agus an siopadh fhosgladh an sin.</p>';
|
||||
$string['wrongdestpath'] = 'Slighe ceann-uidhe ceàrr';
|
||||
$string['wrongsourcebase'] = 'Bun-thùs URL ceàrr';
|
||||
$string['wrongzipfilename'] = 'Ainm ceàrr don fhaidhle ZIP';
|
||||
@@ -41,3 +41,4 @@ Per favor usa le option --help.';
|
||||
$string['cliyesnoprompt'] = 'typa s (significa si) o n (significa no)';
|
||||
$string['environmentrequireinstall'] = 'debe esser installate e habilitate';
|
||||
$string['environmentrequireversion'] = 'version {$a->needed} es requirite e tu es executante {$a->current}';
|
||||
$string['upgradekeyset'] = 'Clave de actualisation (lassa vacue pro non fixar lo)';
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?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['thislanguage'] = 'Provençal Occitan';
|
||||
+2
-1
@@ -871,6 +871,7 @@ $string['manageformatsgotosettings'] = 'Default format can be changed in {$a}';
|
||||
$string['managelang'] = 'Manage';
|
||||
$string['manageqbehaviours'] = 'Manage question behaviours';
|
||||
$string['manageqtypes'] = 'Manage question types';
|
||||
$string['marketplaceavailablenotice'] = 'Browse and download plugins on <a href="{$a}" target="_blank" rel="noopener noreferrer">Moodle Marketplace <i class="fa fa-external-link" role="img" aria-label="Opens in new window" title="Opens in new window"></i></a>.';
|
||||
$string['maturity50'] = 'Alpha';
|
||||
$string['maturity100'] = 'Beta';
|
||||
$string['maturity150'] = 'Release candidate';
|
||||
@@ -968,7 +969,7 @@ $string['maxtimelimit'] = 'Maximum time limit';
|
||||
$string['maxtimelimit_desc'] = 'To restrict the maximum PHP execution time that Moodle will allow without any output being displayed, enter a value in seconds here. 0 means that Moodle default restrictions are used. If you have a front-end server with its own time limit, set this value lower to receive PHP errors in logs. Does not apply to CLI scripts.';
|
||||
$string['moodlebrandedapp'] = 'Branded Moodle app';
|
||||
$string['moodlebrandedappreference'] = 'Alternatively, get a <a href="https://moodle.com/branded-app/">Branded Moodle app</a> with your own custom branding.';
|
||||
$string['moodlenetremovalwarning'] = 'The MoodleNet service will be shut down on 20 April 2026. If you wish to continue using MoodleNet on your site, install the MoodleNet plugin from the <a href="https://moodle.org/plugins" target="_blank">Moodle plugins directory <i class="fa fa-external-link" role="img" aria-label="Opens in new window" title="Opens in new window"></i></a> and connect it to a self-hosted MoodleNet instance. Following this, the MoodleNet profile ID field will be removed; please migrate that data if you are using it for other purposes.';
|
||||
$string['moodlenetremovalwarning'] = 'The MoodleNet service will be shut down on 20 April 2026. If you wish to continue using MoodleNet on your site, install the MoodleNet plugin from the <a href="https://github.com/moodlehq/moodle-tool_moodlenet" target="_blank">Moodle HQ GitHub repository <i class="fa fa-external-link" role="img" aria-label="Opens in new window" title="Opens in new window"></i></a> and connect it to a self-hosted MoodleNet instance. Following this, the MoodleNet profile ID field will be removed; please migrate that data if you are using it for other purposes.';
|
||||
$string['noreplyaddress'] = 'No-reply address';
|
||||
$string['noreplydomain'] = 'No-reply and domain';
|
||||
$string['noreplydomaindetail'] = 'Settings for No-reply and configured domains';
|
||||
|
||||
@@ -40,6 +40,10 @@ $string['activitydate:closed'] = 'Closed:';
|
||||
$string['activitydate:closes'] = 'Closes:';
|
||||
$string['activitydate:opened'] = 'Opened:';
|
||||
$string['activitydate:opens'] = 'Opens:';
|
||||
$string['activitynavigation'] = 'Activity navigation';
|
||||
$string['activitynavigation:jumptoactivity'] = 'Jump to activity';
|
||||
$string['activitynavigation:nexta'] = 'Next activity: {$a}';
|
||||
$string['activitynavigation:preva'] = 'Previous activity: {$a}';
|
||||
$string['aria:coursecategory'] = 'Course category';
|
||||
$string['aria:courseshortname'] = 'Course short name';
|
||||
$string['aria:coursename'] = 'Course name';
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# core (subsystem) Upgrade notes
|
||||
|
||||
## 4.5.10+
|
||||
|
||||
### Added
|
||||
|
||||
- When creating upgrade notes, the issue number will be inferred from the current Git branch name by default
|
||||
|
||||
For more information see [MDL-87100](https://tracker.moodle.org/browse/MDL-87100)
|
||||
- There is a new Behat `toast_message` named selector to more easily assert the presence of Toast messages on the page
|
||||
|
||||
For more information see [MDL-87443](https://tracker.moodle.org/browse/MDL-87443)
|
||||
- The `core/toast` JS module now accepts a `visuallyHidden` configuration parameter to render visually hidden toast messages for screen reader users.
|
||||
|
||||
For more information see [MDL-87993](https://tracker.moodle.org/browse/MDL-87993)
|
||||
|
||||
## 4.5.9
|
||||
|
||||
### Changed
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
define("core/local/reactive/srlogger",["exports","core/local/reactive/logger"],(function(_exports,_logger){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_logger=(obj=_logger)&&obj.__esModule?obj:{default:obj};class SRLogger extends _logger.default{add(entry){if(entry.feedbackMessage){let loggerFeedback=document.getElementById(SRLogger.liveRegionId);loggerFeedback||(loggerFeedback=document.createElement("div"),loggerFeedback.id=SRLogger.liveRegionId,loggerFeedback.classList.add("sr-only"),loggerFeedback.setAttribute("aria-live","polite"),document.body.append(loggerFeedback)),loggerFeedback.innerHTML=entry.feedbackMessage,setTimeout((()=>{loggerFeedback.innerHTML=""}),4e3)}}}return _exports.default=SRLogger,function(obj,key,value){key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}(SRLogger,"liveRegionId","sr-logger-feedback-container"),_exports.default}));
|
||||
define("core/local/reactive/srlogger",["exports","core/toast","core/local/reactive/logger"],(function(_exports,_toast,_logger){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_logger=(obj=_logger)&&obj.__esModule?obj:{default:obj};class SRLogger extends _logger.default{add(entry){entry.feedbackMessage&&(0,_toast.add)(entry.feedbackMessage,{visuallyHidden:!0})}}return _exports.default=SRLogger,function(obj,key,value){key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}(SRLogger,"liveRegionId","sr-logger-feedback-container"),_exports.default}));
|
||||
|
||||
//# sourceMappingURL=srlogger.min.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"srlogger.min.js","sources":["../../../src/local/reactive/srlogger.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Screen reader-only (sr-only) reactive mutations logger class.\n *\n * This logger can be used by the StateManager to log mutation feedbacks and actions.\n * The feedback messages logged by this logger will be rendered in a sr-only, ARIA live region.\n *\n * @module core/local/reactive/srlogger\n * @class SRLogger\n * @copyright 2023 Jun Pataleta <jun@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Logger from 'core/local/reactive/logger';\n\n/**\n * Logger entry structure.\n *\n * @typedef {object} LoggerEntry\n * @property {string} feedbackMessage Feedback message.\n */\n\n/**\n * Screen reader-only (sr-only) reactive mutations logger class.\n *\n * @class SRLogger\n */\nexport default class SRLogger extends Logger {\n /**\n * The element ID of the ARIA live region where the logger feedback will be rendered.\n *\n * @type {string}\n */\n static liveRegionId = 'sr-logger-feedback-container';\n\n /**\n * Add a log entry.\n * @param {LoggerEntry} entry Log entry.\n */\n add(entry) {\n if (entry.feedbackMessage) {\n // Fetch or create an ARIA live region that will serve as the container for the logger feedback.\n let loggerFeedback = document.getElementById(SRLogger.liveRegionId);\n if (!loggerFeedback) {\n loggerFeedback = document.createElement('div');\n loggerFeedback.id = SRLogger.liveRegionId;\n loggerFeedback.classList.add('sr-only');\n loggerFeedback.setAttribute('aria-live', 'polite');\n document.body.append(loggerFeedback);\n }\n // Set the ARIA live region's contents with the feedback.\n loggerFeedback.innerHTML = entry.feedbackMessage;\n\n // Clear the feedback message after 4 seconds to avoid the contents from being read out in case the user navigates\n // to this region. This is similar to the default timeout of toast messages before disappearing from view.\n setTimeout(() => {\n loggerFeedback.innerHTML = '';\n }, 4000);\n }\n }\n}\n"],"names":["SRLogger","Logger","add","entry","feedbackMessage","loggerFeedback","document","getElementById","liveRegionId","createElement","id","classList","setAttribute","body","append","innerHTML","setTimeout"],"mappings":"iQAyCqBA,iBAAiBC,gBAYlCC,IAAIC,UACIA,MAAMC,gBAAiB,KAEnBC,eAAiBC,SAASC,eAAeP,SAASQ,cACjDH,iBACDA,eAAiBC,SAASG,cAAc,OACxCJ,eAAeK,GAAKV,SAASQ,aAC7BH,eAAeM,UAAUT,IAAI,WAC7BG,eAAeO,aAAa,YAAa,UACzCN,SAASO,KAAKC,OAAOT,iBAGzBA,eAAeU,UAAYZ,MAAMC,gBAIjCY,YAAW,KACPX,eAAeU,UAAY,KAC5B,kLA9BMf,wBAMK"}
|
||||
{"version":3,"file":"srlogger.min.js","sources":["../../../src/local/reactive/srlogger.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Screen reader-only (sr-only) reactive mutations logger class.\n *\n * This logger can be used by the StateManager to log mutation feedbacks and actions.\n * The feedback messages logged by this logger will be rendered in a sr-only, ARIA live region.\n *\n * @module core/local/reactive/srlogger\n * @class SRLogger\n * @copyright 2023 Jun Pataleta <jun@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {add as addToast} from 'core/toast';\nimport Logger from 'core/local/reactive/logger';\n\n/**\n * Logger entry structure.\n *\n * @typedef {object} LoggerEntry\n * @property {string} feedbackMessage Feedback message.\n */\n\n/**\n * Screen reader-only (sr-only) reactive mutations logger class.\n *\n * @class SRLogger\n */\nexport default class SRLogger extends Logger {\n /**\n * The element ID of the ARIA live region where the logger feedback will be rendered.\n *\n * @type {string}\n */\n static liveRegionId = 'sr-logger-feedback-container';\n\n /**\n * Add a log entry.\n * @param {LoggerEntry} entry Log entry.\n */\n add(entry) {\n if (entry.feedbackMessage) {\n addToast(entry.feedbackMessage, {visuallyHidden: true});\n }\n }\n}\n"],"names":["SRLogger","Logger","add","entry","feedbackMessage","visuallyHidden"],"mappings":"qRA0CqBA,iBAAiBC,gBAYlCC,IAAIC,OACIA,MAAMC,gCACGD,MAAMC,gBAAiB,CAACC,gBAAgB,gLAdxCL,wBAMK"}
|
||||
Vendored
+1
-1
@@ -5,6 +5,6 @@ define("core/toast",["exports","core/templates","core/notification","core/pendin
|
||||
* @module core/toast
|
||||
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.removeToastRegion=_exports.addToastRegion=_exports.add=void 0,_templates=_interopRequireDefault(_templates),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);const addToastRegion=async parent=>{const pendingPromise=new _pending.default("addToastRegion");try{const{html:html,js:js}=await _templates.default.renderForPromise("core/local/toast/wrapper",{});_templates.default.prependNodeContents(parent,html,js)}catch(e){_notification.default.exception(e)}pendingPromise.resolve()};_exports.addToastRegion=addToastRegion;_exports.add=async(message,configuration)=>{const pendingPromise=new _pending.default("addToastRegion");configuration={type:"info",closeButton:!1,autohide:!0,delay:4e3,...configuration};try{const{html:html,js:js}=await _templates.default.renderForPromise("core/local/toast/message",{message:await message,...configuration}),targetNode=await getTargetNode();_templates.default.prependNodeContents(targetNode,html,js)}catch(e){_notification.default.exception(e)}pendingPromise.resolve()};const getTargetNode=async()=>{const regions=document.querySelectorAll(".toast-wrapper");return regions.length?regions[regions.length-1]:(await addToastRegion(document.body),getTargetNode())};_exports.removeToastRegion=async function(parent){let newParent=arguments.length>1&&void 0!==arguments[1]?arguments[1]:document;const pendingPromise=new _pending.default("core/toast:removeToastRegion"),getRegionFromParent=thisParent=>thisParent.querySelector(".toast-wrapper"),regionToRemove=getRegionFromParent(parent);if(regionToRemove){const targetRegion=getRegionFromParent(newParent);regionToRemove.children.forEach((node=>{targetRegion.insertBefore(node,targetRegion.firstChild)})),regionToRemove.remove()}pendingPromise.resolve()}}));
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.removeToastRegion=_exports.addToastRegion=_exports.add=void 0,_templates=_interopRequireDefault(_templates),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);const addToastRegion=async parent=>{const pendingPromise=new _pending.default("addToastRegion");try{const{html:html,js:js}=await _templates.default.renderForPromise("core/local/toast/wrapper",{});_templates.default.prependNodeContents(parent,html,js)}catch(e){_notification.default.exception(e)}pendingPromise.resolve()};_exports.addToastRegion=addToastRegion;_exports.add=async(message,configuration)=>{const pendingPromise=new _pending.default("addToastRegion");(configuration={type:"info",closeButton:!1,autohide:!0,delay:4e3,visuallyHidden:!1,...configuration}).visuallyHidden&&(configuration.autohide=!0,configuration.closeButton=!1);try{const{html:html,js:js}=await _templates.default.renderForPromise("core/local/toast/message",{message:await message,...configuration}),targetNode=await getTargetNode();_templates.default.prependNodeContents(targetNode,html,js)}catch(e){_notification.default.exception(e)}pendingPromise.resolve()};const getTargetNode=async()=>{const regions=document.querySelectorAll(".toast-wrapper");return regions.length?regions[regions.length-1]:(await addToastRegion(document.body),getTargetNode())};_exports.removeToastRegion=async function(parent){let newParent=arguments.length>1&&void 0!==arguments[1]?arguments[1]:document;const pendingPromise=new _pending.default("core/toast:removeToastRegion"),getRegionFromParent=thisParent=>thisParent.querySelector(".toast-wrapper"),regionToRemove=getRegionFromParent(parent);if(regionToRemove){const targetRegion=getRegionFromParent(newParent);regionToRemove.children.forEach((node=>{targetRegion.insertBefore(node,targetRegion.firstChild)})),regionToRemove.remove()}pendingPromise.resolve()}}));
|
||||
|
||||
//# sourceMappingURL=toast.min.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -25,6 +25,7 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import {add as addToast} from 'core/toast';
|
||||
import Logger from 'core/local/reactive/logger';
|
||||
|
||||
/**
|
||||
@@ -53,23 +54,7 @@ export default class SRLogger extends Logger {
|
||||
*/
|
||||
add(entry) {
|
||||
if (entry.feedbackMessage) {
|
||||
// Fetch or create an ARIA live region that will serve as the container for the logger feedback.
|
||||
let loggerFeedback = document.getElementById(SRLogger.liveRegionId);
|
||||
if (!loggerFeedback) {
|
||||
loggerFeedback = document.createElement('div');
|
||||
loggerFeedback.id = SRLogger.liveRegionId;
|
||||
loggerFeedback.classList.add('sr-only');
|
||||
loggerFeedback.setAttribute('aria-live', 'polite');
|
||||
document.body.append(loggerFeedback);
|
||||
}
|
||||
// Set the ARIA live region's contents with the feedback.
|
||||
loggerFeedback.innerHTML = entry.feedbackMessage;
|
||||
|
||||
// Clear the feedback message after 4 seconds to avoid the contents from being read out in case the user navigates
|
||||
// to this region. This is similar to the default timeout of toast messages before disappearing from view.
|
||||
setTimeout(() => {
|
||||
loggerFeedback.innerHTML = '';
|
||||
}, 4000);
|
||||
addToast(entry.feedbackMessage, {visuallyHidden: true});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ export const addToastRegion = async(parent) => {
|
||||
* @param {Boolean} [configuration.autohide=true]
|
||||
* @param {Boolean} [configuration.closeButton=false]
|
||||
* @param {Number} [configuration.delay=4000]
|
||||
* @param {Boolean} [configuration.visuallyHidden=false] Whether the toast should be visually hidden. This is intended for cases
|
||||
* where we want to announce ARIA live updates for screen reader users only.
|
||||
*
|
||||
* @example
|
||||
* import {add as addToast} from 'core/toast';
|
||||
@@ -73,6 +75,10 @@ export const addToastRegion = async(parent) => {
|
||||
* autohide: false,
|
||||
* closeButton: true,
|
||||
* });
|
||||
*
|
||||
* addToast('A string to be rendered visually hidden', {
|
||||
* visuallyHidden: true,
|
||||
* });
|
||||
*/
|
||||
export const add = async(message, configuration) => {
|
||||
const pendingPromise = new Pending('addToastRegion');
|
||||
@@ -81,15 +87,23 @@ export const add = async(message, configuration) => {
|
||||
closeButton: false,
|
||||
autohide: true,
|
||||
delay: 4000,
|
||||
visuallyHidden: false,
|
||||
...configuration,
|
||||
};
|
||||
|
||||
// Ensure that visually hidden toasts are always autohidden and do not have a close button that can receive keyboard focus.
|
||||
if (configuration.visuallyHidden) {
|
||||
configuration.autohide = true;
|
||||
configuration.closeButton = false;
|
||||
}
|
||||
|
||||
const templateName = `core/local/toast/message`;
|
||||
try {
|
||||
const {html, js} = await Templates.renderForPromise(templateName, {
|
||||
message: await message,
|
||||
...configuration
|
||||
});
|
||||
|
||||
const targetNode = await getTargetNode();
|
||||
Templates.prependNodeContents(targetNode, html, js);
|
||||
} catch (e) {
|
||||
|
||||
@@ -571,9 +571,9 @@ trait behat_session_trait {
|
||||
* @throws ExpectationException
|
||||
* @param string $locator
|
||||
* @param string $selectortype
|
||||
* @return void
|
||||
* @param NodeElement $container Restrict the search to just children of the specified container
|
||||
*/
|
||||
protected function ensure_element_exists($locator, $selectortype) {
|
||||
protected function ensure_element_exists($locator, $selectortype, $container = false) {
|
||||
// Exception if it timesout and the element is still there.
|
||||
$msg = "The '{$locator}' element does not exist and should";
|
||||
$exception = new ExpectationException($msg, $this->getSession());
|
||||
@@ -583,7 +583,7 @@ trait behat_session_trait {
|
||||
'selector' => $selector,
|
||||
'locator' => $locator,
|
||||
'container' => $container,
|
||||
] = $this->normalise_selector($selectortype, $locator, $this->getSession()->getPage());
|
||||
] = $this->normalise_selector($selectortype, $locator, $container ?: $this->getSession()->getPage());
|
||||
|
||||
// It will stop spinning once the find() method returns true.
|
||||
$this->spin(
|
||||
@@ -606,9 +606,9 @@ trait behat_session_trait {
|
||||
* @throws ExpectationException
|
||||
* @param string $locator
|
||||
* @param string $selectortype
|
||||
* @return void
|
||||
* @param NodeElement $container Restrict the search to just children of the specified container
|
||||
*/
|
||||
protected function ensure_element_does_not_exist($locator, $selectortype) {
|
||||
protected function ensure_element_does_not_exist($locator, $selectortype, $container = false) {
|
||||
// Exception if it timesout and the element is still there.
|
||||
$msg = "The '{$locator}' element exists and should not exist";
|
||||
$exception = new ExpectationException($msg, $this->getSession());
|
||||
@@ -618,7 +618,7 @@ trait behat_session_trait {
|
||||
'selector' => $selector,
|
||||
'locator' => $locator,
|
||||
'container' => $container,
|
||||
] = $this->normalise_selector($selectortype, $locator, $this->getSession()->getPage());
|
||||
] = $this->normalise_selector($selectortype, $locator, $container ?: $this->getSession()->getPage());
|
||||
|
||||
// It will stop spinning once the find() method returns false.
|
||||
$this->spin(
|
||||
|
||||
@@ -486,7 +486,7 @@ class behat_util extends testing_util {
|
||||
$posixexists = function_exists('posix_isatty');
|
||||
|
||||
// Make sure this step is only used with interactive terminal (if detected).
|
||||
if ($posixexists && !@posix_isatty(STDOUT)) {
|
||||
if ($posixexists && !@posix_isatty(STDOUT) && getenv('MOODLE_BEHAT_RUNNING_IN_TTY') !== '1') {
|
||||
throw new ExpectationException('Break point should only be used with interactive terminal.', $session);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,12 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
|
||||
require_once(__DIR__ . '/behat_base.php');
|
||||
|
||||
use Behat\Mink\Exception\ExpectationException;
|
||||
use Behat\Mink\Element\NodeElement;
|
||||
|
||||
/**
|
||||
* Files interactions with behat.
|
||||
*
|
||||
@@ -25,25 +31,6 @@
|
||||
* @copyright 2013 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.
|
||||
|
||||
require_once(__DIR__ . '/behat_base.php');
|
||||
|
||||
use Behat\Mink\Exception\ExpectationException as ExpectationException,
|
||||
Behat\Mink\Element\NodeElement as NodeElement;
|
||||
|
||||
/**
|
||||
* Files-related actions.
|
||||
*
|
||||
* Steps definitions related with filepicker or repositories should extend use this trait as it provides useful methods
|
||||
* to deal with the common filepicker issues.
|
||||
*
|
||||
* @package core
|
||||
* @category test
|
||||
* @copyright 2013 David Monllaó
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
trait core_behat_file_helper {
|
||||
|
||||
/**
|
||||
@@ -174,10 +161,8 @@ trait core_behat_file_helper {
|
||||
* @throws ExpectationException Thrown by behat_base::find
|
||||
* @param NodeElement $filemanagernode The filemanager or filepicker form element DOM node.
|
||||
* @param mixed $repositoryname The repo name.
|
||||
* @return void
|
||||
*/
|
||||
protected function open_add_file_window($filemanagernode, $repositoryname) {
|
||||
$exception = new ExpectationException('No files can be added to the specified filemanager', $this->getSession());
|
||||
|
||||
// We should deal with single-file and multiple-file filemanagers,
|
||||
// catching the exception thrown by behat_base::find() in case is not multiple
|
||||
@@ -186,13 +171,17 @@ trait core_behat_file_helper {
|
||||
$filemanagernode, 'NodeElement'
|
||||
]);
|
||||
|
||||
$filepickerdialogue = $this->find('dialogue', get_string('filepicker', 'core_repository'));
|
||||
|
||||
// Wait for the default repository (if any) to load. This checks that
|
||||
// the relevant div exists and that it does not include the loading image.
|
||||
$this->ensure_element_exists(
|
||||
"//div[contains(concat(' ', normalize-space(@class), ' '), ' file-picker ')]" .
|
||||
"//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-content ')]" .
|
||||
"[not(descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-content-loading ')])]",
|
||||
'xpath_element');
|
||||
"//div[contains(concat(' ', normalize-space(@class), ' '), ' file-picker ')]" .
|
||||
"//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-content ')]" .
|
||||
"[not(descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-content-loading ')])]",
|
||||
'xpath_element',
|
||||
$filepickerdialogue,
|
||||
);
|
||||
|
||||
// Getting the repository link and opening it.
|
||||
$repoexception = new ExpectationException('The "' . $repositoryname . '" repository has not been found', $this->getSession());
|
||||
@@ -200,13 +189,14 @@ trait core_behat_file_helper {
|
||||
// Avoid problems with both double and single quotes in the same string.
|
||||
$repositoryname = behat_context_helper::escape($repositoryname);
|
||||
|
||||
// Here we don't need to look inside the selected element because there can only be one modal window.
|
||||
// Select the repository link from inside the current filepicker dialogue.
|
||||
$repositorylink = $this->find(
|
||||
'xpath',
|
||||
"//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-area ')]" .
|
||||
"//descendant::span[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-name ')]" .
|
||||
"[normalize-space(.)=$repositoryname]",
|
||||
$repoexception
|
||||
$repoexception,
|
||||
$filepickerdialogue,
|
||||
);
|
||||
|
||||
// Selecting the repo.
|
||||
|
||||
@@ -304,7 +304,9 @@ class formatting {
|
||||
if ($blanktarget) {
|
||||
$domdoc = new \DOMDocument();
|
||||
libxml_use_internal_errors(true);
|
||||
$domdoc->loadHTML('<?xml version="1.0" encoding="UTF-8" ?>' . $text);
|
||||
// Use meta charset tag to properly handle UTF-8 instead of XML declaration hack.
|
||||
// The XML declaration approach no longer works with libxml2 >= 2.14.0.
|
||||
$domdoc->loadHTML('<meta http-equiv="Content-Type" content="text/html; charset=utf-8">' . $text);
|
||||
libxml_clear_errors();
|
||||
foreach ($domdoc->getElementsByTagName('a') as $link) {
|
||||
if ($link->hasAttribute('target') && strpos($link->getAttribute('target'), '_blank') === false) {
|
||||
@@ -320,11 +322,23 @@ class formatting {
|
||||
// $domdoc->loadHTML($text, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); however it seems like some libxml
|
||||
// versions don't work properly and end up leaving <html><body>, so I'm forced to use
|
||||
// this regex to remove those tags as a preventive measure.
|
||||
// Also strip head and meta tags added by the charset workaround.
|
||||
$text = trim(preg_replace(
|
||||
'~<(?:!DOCTYPE|/?(?:html|body))[^>]*>\s*~i',
|
||||
'~<(?:!DOCTYPE|/?(?:html|head|body)|meta\s[^>]*?)>\s*~i',
|
||||
'',
|
||||
$domdoc->saveHTML($domdoc->documentElement),
|
||||
));
|
||||
// Libxml2 >= 2.14.0 doesn't wrap plain text in <p> tags, so add them for consistency.
|
||||
if (LIBXML_VERSION >= 21400) {
|
||||
$trimmed = trim($text);
|
||||
if ($trimmed !== '' && !preg_match('/^</', $trimmed)) {
|
||||
$text = '<p>' . $text . '</p>';
|
||||
}
|
||||
}
|
||||
// The meta charset approach preserves leading/trailing whitespace in <p> tags more than the old XML
|
||||
// declaration approach. Normalize this by trimming whitespace inside <p> tags to match old behavior.
|
||||
$text = preg_replace('~<p>\s+~i', '<p>', $text);
|
||||
$text = preg_replace('~\s+</p>~i', '</p>', $text);
|
||||
}
|
||||
|
||||
return $text;
|
||||
|
||||
@@ -1622,5 +1622,53 @@ function xmldb_main_upgrade($oldversion) {
|
||||
upgrade_main_savepoint(true, 2024100705.09);
|
||||
}
|
||||
|
||||
if ($oldversion < 2024100710.03) {
|
||||
// Clean up tool_moodlenet configurations unless pointing to a custom installation.
|
||||
$moodleneturl = get_config('tool_moodlenet', 'defaultmoodlenet');
|
||||
|
||||
$shouldcleanup = true;
|
||||
|
||||
// Check if pointing to a custom MoodleNet installation.
|
||||
if (!empty($moodleneturl)) {
|
||||
$parsed = parse_url(strtolower(trim($moodleneturl)));
|
||||
$host = $parsed['host'] ?? '';
|
||||
|
||||
// Don't cleanup if it's a custom installation (not moodle.net).
|
||||
if ($host !== 'moodle.net' && $host !== 'www.moodle.net') {
|
||||
$shouldcleanup = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($shouldcleanup) {
|
||||
// Reset configs to defaults.
|
||||
set_config('defaultmoodlenet', '', 'tool_moodlenet');
|
||||
set_config('enablemoodlenet', 0, 'tool_moodlenet');
|
||||
|
||||
// Hide activity chooser footer if set to MoodleNet.
|
||||
$footer = get_config('core', 'activitychooseractivefooter');
|
||||
if ($footer === 'tool_moodlenet') {
|
||||
set_config('activitychooseractivefooter', 'hidden');
|
||||
}
|
||||
|
||||
// Remove the enablesharingtomoodlenet config setting.
|
||||
unset_config('enablesharingtomoodlenet');
|
||||
|
||||
// Remove MoodleNet outbound OAuth2 configuration.
|
||||
unset_config('oauthservice', 'moodlenet');
|
||||
$issuerids = $DB->get_fieldset_select('oauth2_issuer', 'id', "servicetype = ?", ['moodlenet']);
|
||||
if (!empty($issuerids)) {
|
||||
$DB->delete_records_list('oauth2_endpoint', 'issuerid', $issuerids);
|
||||
$DB->delete_records_list('oauth2_access_token', 'issuerid', $issuerids);
|
||||
$DB->delete_records_list('oauth2_refresh_token', 'issuerid', $issuerids);
|
||||
$DB->delete_records_list('oauth2_system_account', 'issuerid', $issuerids);
|
||||
$DB->delete_records_list('oauth2_user_field_mapping', 'issuerid', $issuerids);
|
||||
$DB->delete_records_list('oauth2_issuer', 'id', $issuerids);
|
||||
}
|
||||
}
|
||||
|
||||
// Main savepoint reached.
|
||||
upgrade_main_savepoint(true, 2024100710.03);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -840,6 +840,7 @@ M.util.add_spinner = function(Y, node) {
|
||||
|
||||
var spinner = Y.Node.create('<img />')
|
||||
.setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
|
||||
.setAttribute('alt', M.util.get_string('loading', 'core'))
|
||||
.addClass('spinner icon')
|
||||
.hide();
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"closebutton": true
|
||||
}
|
||||
}}
|
||||
<div id="toast-{{uniqid}}" class="toast mx-auto {{#type}}toast-{{type}}{{/type}}" data-delay="{{delay}}" {{^autohide}}data-autohide="false"{{/autohide}}>
|
||||
<div id="toast-{{uniqid}}" class="toast mx-auto {{#type}}toast-{{type}}{{/type}} {{#visuallyHidden}}sr-only{{/visuallyHidden}}" data-delay="{{delay}}" {{^autohide}}data-autohide="false"{{/autohide}}>
|
||||
{{#title}}
|
||||
<div class="toast-header">
|
||||
<span class="toast-title">{{{title}}}</span>
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
]
|
||||
}
|
||||
}}
|
||||
{{#get_items.0}}
|
||||
<nav aria-label="{{#str}}breadcrumb, access{{/str}}">
|
||||
<ol class="breadcrumb">{{!
|
||||
}}{{#get_items}}
|
||||
@@ -87,3 +88,4 @@
|
||||
{{/get_items}}{{!
|
||||
}}</ol>
|
||||
</nav>
|
||||
{{/get_items.0}}
|
||||
|
||||
@@ -203,6 +203,8 @@ class behat_general extends behat_base {
|
||||
$this->execute_js_on_node($iframe, "{{ELEMENT}}.name = '{$iframename}';");
|
||||
}
|
||||
$context->getSession()->switchToIFrame($iframename);
|
||||
// Wait until the iframe is completely loaded and all pending operations complete.
|
||||
$context->getSession()->wait(behat_base::get_extended_timeout(), behat_base::PAGE_READY_JS);
|
||||
|
||||
// If no exception we are done.
|
||||
return true;
|
||||
@@ -211,6 +213,66 @@ class behat_general extends behat_base {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until the specified iframe is interactable (visible, sized, and not occluded).
|
||||
*
|
||||
* @Given /^I wait until "(?P<iframe_name_string>(?:[^"]|\\")*)" iframe is interactable$/
|
||||
* @Given /^I wait until "(?P<iframe_name_string>(?:[^"]|\\")*)" class iframe is interactable$/
|
||||
* @param string $name The name or class of the iframe
|
||||
*/
|
||||
public function wait_until_iframe_interactable(string $name): void {
|
||||
if (!$this->running_javascript()) {
|
||||
throw new DriverException(
|
||||
'iFrame interactability checks are disabled in scenarios without Javascript support',
|
||||
);
|
||||
}
|
||||
$this->spin(
|
||||
function ($context) use ($name) {
|
||||
$iframe = $context->find('iframe', $name);
|
||||
$scrolljs = '{{ELEMENT}}.scrollIntoView({behavior: "auto", block: "center", inline: "center"});';
|
||||
$this->execute_js_on_node($iframe, $scrolljs);
|
||||
$this->ensure_node_is_visible($iframe);
|
||||
|
||||
// Check that the iframe is not occluded at its center point.
|
||||
$iframexpath = $iframe->getXpath();
|
||||
$js = <<<EOF
|
||||
(function() {
|
||||
const xpath = "{$iframexpath}";
|
||||
const el = document.evaluate(
|
||||
xpath,
|
||||
document,
|
||||
null,
|
||||
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
||||
null
|
||||
).singleNodeValue;
|
||||
if (!el) { return false; }
|
||||
const rect = el.getBoundingClientRect();
|
||||
if (rect.width === 0 || rect.height === 0) { return false; }
|
||||
const x = rect.left + rect.width / 2;
|
||||
const y = rect.top + rect.height / 2;
|
||||
const target = document.elementFromPoint(x, y);
|
||||
return target === el || el.contains(target);
|
||||
})()
|
||||
EOF;
|
||||
|
||||
return (bool) $this->evaluate_script($js);
|
||||
},
|
||||
behat_base::get_extended_timeout()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until the specified iframe is interactable (visible, sized, and not occluded) and switche to it.
|
||||
*
|
||||
* @Given /^I wait until "(?P<iframe_name_string>(?:[^"]|\\")*)" iframe is interactable and switch to it$/
|
||||
* @Given /^I wait until "(?P<iframe_name_string>(?:[^"]|\\")*)" class iframe is interactable and switch to it$/
|
||||
* @param string $name The name of the iframe
|
||||
*/
|
||||
public function wait_until_iframe_interactable_and_switch_to(string $name): void {
|
||||
$this->wait_until_iframe_interactable($name);
|
||||
$this->switch_to_iframe($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to the main Moodle frame.
|
||||
*
|
||||
|
||||
@@ -103,8 +103,8 @@ final class session_redis_cluster_test extends \advanced_testcase {
|
||||
$actual = $e->getMessage();
|
||||
}
|
||||
|
||||
$expected = "Failed to connect (try 5 out of 5) to Redis at";
|
||||
$this->assertDebuggingCalledCount(5);
|
||||
$expected = "Failed to connect (try 3 out of 3) to Redis at";
|
||||
$this->assertDebuggingCalledCount(3);
|
||||
$this->assertStringContainsString($expected, $actual);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -2693,8 +2693,7 @@ function get_formatted_help_string($identifier, $component, $ajax = false, $a =
|
||||
$data->doclink->linktext = $linktext;
|
||||
$data->doclink->class = ($CFG->doctonewwindow) ? 'helplinkpopup' : '';
|
||||
} else {
|
||||
$data->completedoclink = html_writer::tag('div', $OUTPUT->doc_link($link, $linktext),
|
||||
array('class' => 'helpdoclink'));
|
||||
$data->completedoclink = html_writer::div($OUTPUT->doc_link($link, $linktext, true), 'helpdoclink');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -183,6 +183,7 @@ Y.extend(DRAGDROP, Y.Base, {
|
||||
var dragelement = Y.Node.create('<span></span>')
|
||||
.addClass(classname)
|
||||
.setAttribute('title', title)
|
||||
.setAttribute('aria-label', title)
|
||||
.setAttribute('tabIndex', 0)
|
||||
.setAttribute('data-draggroups', this.groups)
|
||||
.setAttribute('role', 'button');
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -183,6 +183,7 @@ Y.extend(DRAGDROP, Y.Base, {
|
||||
var dragelement = Y.Node.create('<span></span>')
|
||||
.addClass(classname)
|
||||
.setAttribute('title', title)
|
||||
.setAttribute('aria-label', title)
|
||||
.setAttribute('tabIndex', 0)
|
||||
.setAttribute('data-draggroups', this.groups)
|
||||
.setAttribute('role', 'button');
|
||||
|
||||
Vendored
+1
@@ -181,6 +181,7 @@ Y.extend(DRAGDROP, Y.Base, {
|
||||
var dragelement = Y.Node.create('<span></span>')
|
||||
.addClass(classname)
|
||||
.setAttribute('title', title)
|
||||
.setAttribute('aria-label', title)
|
||||
.setAttribute('tabIndex', 0)
|
||||
.setAttribute('data-draggroups', this.groups)
|
||||
.setAttribute('role', 'button');
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -225,7 +225,8 @@ function(
|
||||
if (!isMedia) {
|
||||
// Try to get the text value of the content.
|
||||
// If that's not possible, we'll report it under the catch-all 'other media'.
|
||||
var messagePreview = $(lastMessage.text).text();
|
||||
// Use textContent to safely extract text without jQuery selector parsing.
|
||||
var messagePreview = tmpElement.textContent || tmpElement.innerText || '';
|
||||
if (messagePreview) {
|
||||
// The text value of the message must have no html/script tags.
|
||||
if (messagePreview.indexOf('<') == -1) {
|
||||
|
||||
@@ -712,13 +712,25 @@ class helper {
|
||||
if (!empty($message)) {
|
||||
$doc = new DOMDocument();
|
||||
$olderror = libxml_use_internal_errors(true);
|
||||
$doc->loadHTML('<?xml version="1.0" encoding="UTF-8" ?>' . $message);
|
||||
// Use meta charset tag to properly handle UTF-8 instead of XML declaration hack.
|
||||
// The XML declaration approach no longer works with libxml2 >= 2.14.0.
|
||||
$doc->loadHTML('<meta http-equiv="Content-Type" content="text/html; charset=utf-8">' . $message);
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($olderror);
|
||||
$html = $doc->getElementsByTagName('body')->item(0)->C14N(false, true);
|
||||
if ($removebody) {
|
||||
// Remove <body> element added in C14N function.
|
||||
$html = preg_replace('~<(/?(?:body))[^>]*>\s*~i', '', $html);
|
||||
$body = $doc->getElementsByTagName('body')->item(0);
|
||||
if ($body) {
|
||||
$html = $body->C14N(false, true);
|
||||
if ($removebody) {
|
||||
// Remove <body> element added in C14N function.
|
||||
$html = preg_replace('~<(/?(?:body))[^>]*>\s*~i', '', $html);
|
||||
// Libxml2 >= 2.14.0 doesn't wrap plain text in <p> tags, so add them for consistency.
|
||||
if (LIBXML_VERSION >= 21400) {
|
||||
$trimmed = trim($html);
|
||||
if ($trimmed !== '' && !preg_match('/^</', $trimmed)) {
|
||||
$html = '<p>' . $html . '</p>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -221,6 +221,13 @@ final class helper_test extends \advanced_testcase {
|
||||
$this->setAdminUser();
|
||||
|
||||
$html = \core_message\helper::prevent_unclosed_html_tags($message, $removebody);
|
||||
|
||||
// Libxml2 >= 2.14.0 closes unclosed comments instead of removing them.
|
||||
// Check if we're testing the unclosed comment case and adjust expectation based on libxml version.
|
||||
if ($message === '<h1>Title</h1><p>Paragraph</p><!-- Comments' && LIBXML_VERSION >= 21400) {
|
||||
$goodhtml = '<h1>Title</h1><p>Paragraph</p><!-- Comments-->';
|
||||
}
|
||||
|
||||
$this->assertSame($goodhtml, $html);
|
||||
}
|
||||
|
||||
@@ -248,6 +255,12 @@ final class helper_test extends \advanced_testcase {
|
||||
'Check encoding UTF-8 is working' => [
|
||||
'<body><h1>Title</h1><p>السلام عليكم</p></body>', '<body><h1>Title</h1><p>السلام عليكم</p></body>', false
|
||||
],
|
||||
'Script tag only returns empty' => [
|
||||
'<script>alert("test")</script>', '', true,
|
||||
],
|
||||
'Script tag with text' => [
|
||||
'<script>alert("test")</script>Some text', '<p>Some text</p>', true,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+44
-17
@@ -87,24 +87,51 @@ class mod_forum_renderer extends plugin_renderer_base {
|
||||
$output .= html_writer::start_tag('form', $formattributes);
|
||||
$output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'sesskey', 'value'=>sesskey()));
|
||||
|
||||
$existingcell = new html_table_cell();
|
||||
$existingcell->text = $existinguc->display(true);
|
||||
$existingcell->attributes['class'] = 'existing';
|
||||
$actioncell = new html_table_cell();
|
||||
$actioncell->text = html_writer::start_tag('div', array());
|
||||
$actioncell->text .= html_writer::empty_tag('input', array('type'=>'submit', 'name'=>'subscribe', 'value'=>$this->page->theme->larrow.' '.get_string('add'), 'class'=>'actionbutton'));
|
||||
$actioncell->text .= html_writer::empty_tag('br', array());
|
||||
$actioncell->text .= html_writer::empty_tag('input', array('type'=>'submit', 'name'=>'unsubscribe', 'value'=>$this->page->theme->rarrow.' '.get_string('remove'), 'class'=>'actionbutton'));
|
||||
$actioncell->text .= html_writer::end_tag('div', array());
|
||||
$actioncell->attributes['class'] = 'actions';
|
||||
$potentialcell = new html_table_cell();
|
||||
$potentialcell->text = $potentialuc->display(true);
|
||||
$potentialcell->attributes['class'] = 'potential';
|
||||
// Existing subscribers column.
|
||||
$existingsubslabel = html_writer::label(
|
||||
get_string('existingsubscribers', 'forum'),
|
||||
'existingsubscribers',
|
||||
false,
|
||||
['class' => 'sr-only']
|
||||
);
|
||||
$existingsubscol = html_writer::div($existingsubslabel . $existinguc->display(true), 'col-sm-5 px-0 existing');
|
||||
|
||||
$table = new html_table();
|
||||
$table->attributes['class'] = 'subscribertable boxaligncenter';
|
||||
$table->data = array(new html_table_row(array($existingcell, $actioncell, $potentialcell)));
|
||||
$output .= html_writer::table($table);
|
||||
// Actions column.
|
||||
$subscribeaction = html_writer::tag(
|
||||
'button',
|
||||
html_writer::span($this->page->theme->larrow, '', ['aria-hidden' => 'true']) . ' ' . get_string('add'),
|
||||
[
|
||||
'type' => 'submit',
|
||||
'name' => 'subscribe',
|
||||
'class' => 'actionbutton btn btn-secondary w-100 my-1 py-2',
|
||||
'value' => 'subscribe',
|
||||
]
|
||||
);
|
||||
$subscribeaction = html_writer::div($subscribeaction);
|
||||
$unsubscribeaction = html_writer::tag(
|
||||
'button',
|
||||
get_string('remove') . ' ' . html_writer::span($this->page->theme->rarrow, '', ['aria-hidden' => 'true']),
|
||||
[
|
||||
'type' => 'submit',
|
||||
'name' => 'unsubscribe',
|
||||
'class' => 'actionbutton btn btn-secondary w-100 my-1 py-2',
|
||||
'value' => 'unsubscribe',
|
||||
]
|
||||
);
|
||||
$unsubscribeaction = html_writer::div($unsubscribeaction);
|
||||
$actionscol = html_writer::div($subscribeaction . $unsubscribeaction, 'col-sm-2 actions py-6');
|
||||
|
||||
// Potential subscribers column.
|
||||
$potentialsubslabel = html_writer::label(
|
||||
get_string('potentialsubscribers', 'forum'),
|
||||
'potentialsubscribers',
|
||||
false,
|
||||
['class' => 'sr-only']
|
||||
);
|
||||
$potentialsubscol = html_writer::div($potentialsubslabel . $potentialuc->display(true), 'col-sm-5 px-0 potential');
|
||||
|
||||
$row = html_writer::div($existingsubscol . $actionscol . $potentialsubscol, 'row');
|
||||
$output .= html_writer::div($row, 'container-fluid');
|
||||
|
||||
$output .= html_writer::end_tag('form');
|
||||
return $output;
|
||||
|
||||
+1
-22
@@ -144,32 +144,11 @@
|
||||
}
|
||||
|
||||
/** Styles for subscribers.php */
|
||||
#page-mod-forum-subscribers .subscriberdiv,
|
||||
#page-mod-forum-subscribers .subscribertable {
|
||||
#page-mod-forum-subscribers .subscriberdiv {
|
||||
width: 100%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#page-mod-forum-subscribers .subscribertable tr td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#page-mod-forum-subscribers .subscribertable tr td.actions {
|
||||
width: 16%;
|
||||
padding-top: 3em;
|
||||
}
|
||||
|
||||
#page-mod-forum-subscribers .subscribertable tr td.actions .actionbutton {
|
||||
margin: 0.3em 0;
|
||||
padding: 0.5em 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#page-mod-forum-subscribers .subscribertable tr td.existing,
|
||||
#page-mod-forum-subscribers .subscribertable tr td.potential {
|
||||
width: 42%;
|
||||
}
|
||||
|
||||
/** Styles for discuss.php **/
|
||||
#page-mod-forum-discuss .discussioncontrols {
|
||||
width: 100%;
|
||||
|
||||
@@ -36,6 +36,7 @@ Feature: A teacher or admin can view subscriptions tab
|
||||
And I should see "Everyone can now choose to be subscribed"
|
||||
And I should see "View subscribers"
|
||||
|
||||
@accessibility
|
||||
Scenario: A teacher selects forced subscription and subscribers selector is not visible
|
||||
Given I am on the "Test forum name" "forum activity" page logged in as teacher
|
||||
And I navigate to "Subscriptions" in current page administration
|
||||
@@ -46,6 +47,7 @@ Feature: A teacher or admin can view subscriptions tab
|
||||
And I should not see "Forced subscription"
|
||||
And I should not see "Auto subscription"
|
||||
And I should not see "Subscription disabled"
|
||||
And the "region-main" "region" should meet accessibility standards with "best-practice" extra tests
|
||||
|
||||
Scenario: A teacher selects reports tab and verify the heading
|
||||
Given I am on the "Test forum name" "forum activity" page logged in as teacher
|
||||
|
||||
@@ -30,16 +30,14 @@ Feature: Users can see the H5P recent activity from the recent activity block
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| recent_activity | Course | C1 | course-view-* | side-pre |
|
||||
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
|
||||
# The H5P content needs some time to be displayed (so better to wait for 1 second to avoid random errors).
|
||||
And I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
And I wait until "h5p-player" iframe is interactable and switch to it
|
||||
And I wait until "h5p-iframe" iframe is interactable and switch to it
|
||||
And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element"
|
||||
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
|
||||
And I switch to the main frame
|
||||
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student2
|
||||
# The H5P content needs some time to be displayed (so better to wait for 1 second to avoid random errors).
|
||||
And I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
And I wait until "h5p-player" iframe is interactable and switch to it
|
||||
And I wait until "h5p-iframe" iframe is interactable and switch to it
|
||||
And I click on "Correct one" "text" in the ".h5p-question-content" "css_element"
|
||||
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
|
||||
And I switch to the main frame
|
||||
|
||||
@@ -34,8 +34,8 @@ Feature: Do a H5P attempt
|
||||
Scenario: Do an attempt and check on course log report
|
||||
When I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
|
||||
And I should not see "You are in preview mode."
|
||||
And I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
And I wait until "h5p-player" iframe is interactable and switch to it
|
||||
And I wait until "h5p-iframe" iframe is interactable and switch to it
|
||||
And I click on "Correct one" "text" in the ".h5p-question-content" "css_element"
|
||||
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
|
||||
And I switch to the main frame
|
||||
@@ -49,8 +49,8 @@ Feature: Do a H5P attempt
|
||||
Scenario: Do various attempts and check them with the attempts and user grades reports
|
||||
Given I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
|
||||
And I should not see "You are in preview mode."
|
||||
And I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
And I wait until "h5p-player" iframe is interactable and switch to it
|
||||
And I wait until "h5p-iframe" iframe is interactable and switch to it
|
||||
And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element"
|
||||
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
|
||||
And I click on "Retry" "button" in the ".h5p-question-buttons" "css_element"
|
||||
@@ -62,8 +62,8 @@ Feature: Do a H5P attempt
|
||||
# H5P does not allow to Retry if the user checks the correct answer, we need to refresh the page.
|
||||
And I switch to the main frame
|
||||
And I reload the page
|
||||
And I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
And I wait until "h5p-player" iframe is interactable and switch to it
|
||||
And I wait until "h5p-iframe" iframe is interactable and switch to it
|
||||
# Because of the steps above, the 2nd and 3rd attempts are enough "separated" and we don't
|
||||
# need to add any wait here.
|
||||
And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element"
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
@mod @mod_lesson
|
||||
Feature: In a lesson activity, teacher can import blackboard fill in the blank question
|
||||
As a teacher
|
||||
I need to import a fill in the blank question made in Blackboard in a lesson
|
||||
|
||||
@javascript @_file_upload
|
||||
Scenario: Import fill in the blank question in a lesson
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
And the following "activity" exists:
|
||||
| course | C1 |
|
||||
| activity | lesson |
|
||||
| name | Test lesson name |
|
||||
When I am on the "Test lesson name" "lesson activity" page logged in as teacher1
|
||||
And I follow "Import questions"
|
||||
And I set the field "File format" to "Blackboard"
|
||||
And I upload "mod/lesson/tests/fixtures/sample_blackboard_fib_qti.dat" file to "Upload" filemanager
|
||||
And I press "Import"
|
||||
Then I should see "Importing 1 questions"
|
||||
And I should see "Name an amphibian: __________"
|
||||
And I press "Continue"
|
||||
And I should not see "__________"
|
||||
And I should not see "Your answer"
|
||||
And I set the field "id_answer" to "frog"
|
||||
And I press "Submit"
|
||||
And I should see "Your answer : frog"
|
||||
And I should see "A frog is an amphibian"
|
||||
And I press "Continue"
|
||||
And I should see "Congratulations - end of lesson reached"
|
||||
@@ -1,35 +0,0 @@
|
||||
@mod @mod_lesson
|
||||
Feature: In a lesson activity, teacher can import embedded images in questions answers and responses
|
||||
As a teacher
|
||||
I need to import a question with images in answers and responses in a lesson
|
||||
|
||||
@javascript @_file_upload
|
||||
Scenario: Import questions with images in answers and responses in a lesson
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
And the following "activity" exists:
|
||||
| course | C1 |
|
||||
| activity | lesson |
|
||||
| name | Test lesson name |
|
||||
When I am on the "Test lesson name" "lesson activity" page logged in as teacher1
|
||||
And I follow "Import questions"
|
||||
And I set the field "File format" to "Moodle XML format"
|
||||
And I upload "mod/lesson/tests/fixtures/multichoice.xml" file to "Upload" filemanager
|
||||
And I press "Import"
|
||||
Then I should see "Importing 1 questions"
|
||||
And I should see " Listen to this greeting:"
|
||||
And I should see "What language is being spoken?"
|
||||
And I press "Continue"
|
||||
And I should see "What language is being spoken?"
|
||||
And "//audio[contains(@title, 'Listen to this greeting:')]/source[contains(@src, 'bonjour.mp3')]" "xpath_element" should exist
|
||||
And "//*[contains(@class, 'answeroption')]//img[contains(@src, 'pluginfile.php')]" "xpath_element" should exist
|
||||
And "//*[contains(@class, 'answeroption')]//img[contains(@src, 'flag-france.jpg')]" "xpath_element" should exist
|
||||
@@ -0,0 +1,70 @@
|
||||
@mod @mod_lesson
|
||||
Feature: Import questions into a lesson from multiple formats
|
||||
As a teacher
|
||||
I want to import question files in different formats into a lesson so images and audio are preserved
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | One | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course |
|
||||
| lesson | Test lesson name | C1 |
|
||||
|
||||
@javascript @_file_upload
|
||||
Scenario: Imports questions using GIFT format and edits page contents
|
||||
Given I am on the "Test lesson name" "lesson activity" page logged in as "teacher1"
|
||||
And I follow "Import questions"
|
||||
And I set the field "File format" to "GIFT"
|
||||
When I upload "mod/lesson/tests/fixtures/moodle_questions.gift" file to "Upload" filemanager
|
||||
And I press "Import"
|
||||
And I press "Continue"
|
||||
Then I should see "Lesson is currently being previewed."
|
||||
And I should see "Match the activity to the description."
|
||||
And I should see "An activity supporting asynchronous discussions."
|
||||
And I should see "A teacher asks a question and specifies a choice of multiple responses."
|
||||
And I press "Edit page contents"
|
||||
And I set the field "Page contents" to "Match the activity to the description (Edited)"
|
||||
And I press "Save page"
|
||||
And I should see "Match the activity to the description (Edited)"
|
||||
|
||||
@javascript @_file_upload
|
||||
Scenario: Import questions in XML format with images and audio in answers and responses
|
||||
Given I am on the "Test lesson name" "lesson activity" page logged in as teacher1
|
||||
And I follow "Import questions"
|
||||
And I set the field "File format" to "Moodle XML format"
|
||||
When I upload "mod/lesson/tests/fixtures/multichoice.xml" file to "Upload" filemanager
|
||||
And I press "Import"
|
||||
Then I should see "Importing 1 questions"
|
||||
And I should see " Listen to this greeting:"
|
||||
And I should see "What language is being spoken?"
|
||||
And I press "Continue"
|
||||
And I should see "What language is being spoken?"
|
||||
And "//audio[contains(@title, 'Listen to this greeting:')]/source[contains(@src, 'bonjour.mp3')]" "xpath_element" should exist
|
||||
And "//*[contains(@class, 'answeroption')]//img[contains(@src, 'pluginfile.php')]" "xpath_element" should exist
|
||||
And "//*[contains(@class, 'answeroption')]//img[contains(@src, 'flag-france.jpg')]" "xpath_element" should exist
|
||||
|
||||
@javascript @_file_upload
|
||||
Scenario: Import fill in the blank question in a lesson
|
||||
Given I am on the "Test lesson name" "lesson activity" page logged in as teacher1
|
||||
And I follow "Import questions"
|
||||
And I set the field "File format" to "Blackboard"
|
||||
When I upload "mod/lesson/tests/fixtures/sample_blackboard_fib_qti.dat" file to "Upload" filemanager
|
||||
And I press "Import"
|
||||
Then I should see "Importing 1 questions"
|
||||
And I should see "Name an amphibian: __________"
|
||||
And I press "Continue"
|
||||
And I should not see "__________"
|
||||
And I should not see "Your answer"
|
||||
And I set the field "id_answer" to "frog"
|
||||
And I press "Submit"
|
||||
And I should see "Your answer : frog"
|
||||
And I should see "A frog is an amphibian"
|
||||
And I press "Continue"
|
||||
And I should see "Congratulations - end of lesson reached"
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// question: 0 name: switch category to $course$/Default for New Features
|
||||
$CATEGORY: $course$/Default for New Features
|
||||
|
||||
// question: 44 name: Moodle activities
|
||||
::Moodle activities::[html]Match the activity to the description.{
|
||||
=An activity supporting asynchronous discussions. -> Forum
|
||||
=A teacher asks a question and specifies a choice of multiple responses. -> Choice
|
||||
=A bank of record entries which participants can add to. -> Database
|
||||
=A collection of web pages that anyone can add to or edit. -> Wiki
|
||||
= -> Chat
|
||||
}
|
||||
|
||||
// question: 46 name: Moodle user
|
||||
::Moodle user::[html]Anyone who uses Moodle is a ...{
|
||||
=%100%Moodler#
|
||||
}
|
||||
|
||||
// question: 45 name: Moodle acronym
|
||||
::Moodle acronym::[html]Moodle is an acronym for <span style\="font-style\: italic;">Modular Object-Oriented Dynamic Learning Environment</span>.{TRUE}
|
||||
+10
-6
@@ -57,6 +57,12 @@ $foruserid = optional_param('user', 0, PARAM_INT);
|
||||
|
||||
$cm = get_coursemodule_from_id('lti', $cmid, 0, false, MUST_EXIST);
|
||||
$lti = $DB->get_record('lti', array('id' => $cm->instance), '*', MUST_EXIST);
|
||||
$course = $DB->get_record('course', ['id' => $cm->course], '*', MUST_EXIST);
|
||||
|
||||
$context = context_module::instance($cm->id);
|
||||
|
||||
require_login($course, true, $cm);
|
||||
require_capability('mod/lti:view', $context);
|
||||
|
||||
$typeid = $lti->typeid;
|
||||
if (empty($typeid) && ($tool = lti_get_tool_by_url_match($lti->toolurl))) {
|
||||
@@ -73,6 +79,10 @@ if ($typeid) {
|
||||
if ($action === 'gradeReport') {
|
||||
$msgtype = 'LtiSubmissionReviewRequest';
|
||||
}
|
||||
// Do Completion and trigger events before launching the external tool and thus leaving Moodle.
|
||||
if ($triggerview) {
|
||||
lti_view($lti, $course, $cm, $context);
|
||||
}
|
||||
echo lti_initiate_login($cm->course, $cmid, $lti, $config, $msgtype, '', '', $foruserid);
|
||||
exit;
|
||||
} else {
|
||||
@@ -82,12 +92,6 @@ if ($typeid) {
|
||||
}
|
||||
}
|
||||
|
||||
$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
|
||||
|
||||
$context = context_module::instance($cm->id);
|
||||
|
||||
require_login($course, true, $cm);
|
||||
require_capability('mod/lti:view', $context);
|
||||
|
||||
if (!empty($missingtooltype)) {
|
||||
$PAGE->set_url(new moodle_url('/mod/lti/launch.php'));
|
||||
|
||||
@@ -94,8 +94,11 @@ class edit_renderer extends \plugin_renderer_base {
|
||||
|
||||
if ($structure->is_last_section($section)) {
|
||||
$output .= \html_writer::start_div('last-add-menu');
|
||||
$output .= html_writer::tag('span', $this->add_menu_actions($structure, 0,
|
||||
$pageurl, $contexts, $pagevars), ['class' => 'add-menu-outer']);
|
||||
$output .= html_writer::tag(
|
||||
'span',
|
||||
$this->add_menu_actions($structure, 0, $pageurl, $contexts, $pagevars),
|
||||
['class' => 'add-menu-outer pe-3']
|
||||
);
|
||||
$output .= \html_writer::end_div();
|
||||
}
|
||||
|
||||
@@ -578,9 +581,11 @@ class edit_renderer extends \plugin_renderer_base {
|
||||
|
||||
if ($structure->is_first_slot_on_page($slot)) {
|
||||
// Add the add-menu at the page level.
|
||||
$addmenu = html_writer::tag('span', $this->add_menu_actions($structure,
|
||||
$pagenumber, $pageurl, $contexts, $pagevars),
|
||||
['class' => 'add-menu-outer']);
|
||||
$addmenu = html_writer::tag(
|
||||
'span',
|
||||
$this->add_menu_actions($structure, $pagenumber, $pageurl, $contexts, $pagevars),
|
||||
['class' => 'add-menu-outer pe-2']
|
||||
);
|
||||
|
||||
$addquestionform = $this->add_question_form($structure,
|
||||
$pagenumber, $pageurl, $pagevars);
|
||||
@@ -861,9 +866,15 @@ class edit_renderer extends \plugin_renderer_base {
|
||||
* @return string The markup for the move action.
|
||||
*/
|
||||
public function question_move_icon(structure $structure, $slot) {
|
||||
return html_writer::link(new \moodle_url('#'),
|
||||
$this->pix_icon('i/dragdrop', get_string('move'), 'moodle', ['class' => 'iconsmall', 'title' => '']),
|
||||
['class' => 'editing_move', 'data-action' => 'move']
|
||||
$slotnumber = $structure->get_displayed_number_for_slot($slot);
|
||||
return html_writer::link(
|
||||
new \moodle_url('#'),
|
||||
$this->pix_icon('i/dragdrop', '', 'moodle', ['class' => 'iconsmall']),
|
||||
[
|
||||
'class' => 'editing_move',
|
||||
'data-action' => 'move',
|
||||
'aria-label' => get_string('movequestionnumber', 'quiz', $slotnumber),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -950,13 +961,14 @@ class edit_renderer extends \plugin_renderer_base {
|
||||
$url = new \moodle_url('repaginate.php', ['quizid' => $structure->get_quizid(),
|
||||
'slot' => $slot, 'repag' => $insertpagebreak ? 2 : 1, 'sesskey' => sesskey()]);
|
||||
|
||||
$slotname = $structure->get_displayed_number_for_slot($slot);
|
||||
if ($insertpagebreak) {
|
||||
$title = get_string('addpagebreak', 'quiz');
|
||||
$image = $this->image_icon('e/insert_page_break', $title);
|
||||
$title = get_string('addpagebreakafter', 'quiz', $slotname);
|
||||
$image = $this->image_icon('e/insert_page_break', '');
|
||||
$action = 'addpagebreak';
|
||||
} else {
|
||||
$title = get_string('removepagebreak', 'quiz');
|
||||
$image = $this->image_icon('e/remove_page_break', $title);
|
||||
$title = get_string('removepagebreakafter', 'quiz', $slotname);
|
||||
$image = $this->image_icon('e/remove_page_break', '');
|
||||
$action = 'removepagebreak';
|
||||
}
|
||||
|
||||
@@ -965,9 +977,22 @@ class edit_renderer extends \plugin_renderer_base {
|
||||
if (!$structure->can_be_edited()) {
|
||||
$disabled = 'disabled';
|
||||
}
|
||||
return html_writer::span($this->action_link($url, $image, null, ['title' => $title,
|
||||
'class' => 'page_split_join cm-edit-action', 'disabled' => $disabled, 'data-action' => $action]),
|
||||
'page_split_join_wrapper');
|
||||
return html_writer::span(
|
||||
$this->action_link(
|
||||
$url,
|
||||
$image,
|
||||
null,
|
||||
[
|
||||
'title' => $title,
|
||||
'aria-label' => $title,
|
||||
'class' => 'page_split_join cm-edit-action btn btn-sm icon-no-margin',
|
||||
'disabled' => $disabled,
|
||||
'data-action' => $action,
|
||||
'role' => 'button',
|
||||
]
|
||||
),
|
||||
'page_split_join_wrapper'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -985,13 +1010,11 @@ class edit_renderer extends \plugin_renderer_base {
|
||||
];
|
||||
if ($structure->is_question_dependent_on_previous_slot($slot)) {
|
||||
$title = get_string('questiondependencyremove', 'quiz', $a);
|
||||
$image = $this->pix_icon('t/locked', get_string('questiondependsonprevious', 'quiz'),
|
||||
'moodle', ['title' => '']);
|
||||
$image = $this->pix_icon('t/locked', '');
|
||||
$action = 'removedependency';
|
||||
} else {
|
||||
$title = get_string('questiondependencyadd', 'quiz', $a);
|
||||
$image = $this->pix_icon('t/unlocked', get_string('questiondependencyfree', 'quiz'),
|
||||
'moodle', ['title' => '']);
|
||||
$image = $this->pix_icon('t/unlocked', '');
|
||||
$action = 'adddependency';
|
||||
}
|
||||
|
||||
@@ -1004,9 +1027,22 @@ class edit_renderer extends \plugin_renderer_base {
|
||||
if (!$structure->can_question_depend_on_previous_slot($slot)) {
|
||||
$extraclass = ' question_dependency_cannot_depend';
|
||||
}
|
||||
return html_writer::span($this->action_link('#', $image, null, ['title' => $title,
|
||||
'class' => 'cm-edit-action', 'disabled' => $disabled, 'data-action' => $action]),
|
||||
'question_dependency_wrapper' . $extraclass);
|
||||
return html_writer::span(
|
||||
$this->action_link(
|
||||
'#',
|
||||
$image,
|
||||
null,
|
||||
[
|
||||
'title' => $title,
|
||||
'aria-label' => $title,
|
||||
'class' => 'cm-edit-action btn btn-link btn-sm icon-no-margin',
|
||||
'disabled' => $disabled,
|
||||
'data-action' => $action,
|
||||
'role' => 'button',
|
||||
]
|
||||
),
|
||||
'question_dependency_wrapper' . $extraclass
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1243,7 +1279,7 @@ class edit_renderer extends \plugin_renderer_base {
|
||||
], 'moodle');
|
||||
|
||||
$this->page->requires->strings_for_js([
|
||||
'addpagebreak',
|
||||
'addpagebreakafter',
|
||||
'cannotremoveallsectionslots',
|
||||
'cannotremoveslots',
|
||||
'confirmremovesectionheading',
|
||||
@@ -1254,7 +1290,7 @@ class edit_renderer extends \plugin_renderer_base {
|
||||
'sectionheadingedit',
|
||||
'sectionheadingremove',
|
||||
'sectionnoname',
|
||||
'removepagebreak',
|
||||
'removepagebreakafter',
|
||||
'questiondependencyadd',
|
||||
'questiondependencyfree',
|
||||
'questiondependencyremove',
|
||||
|
||||
@@ -44,6 +44,7 @@ $string['addnewpagesafterselected'] = 'Add new pages after selected questions';
|
||||
$string['addnewquestionsqbank'] = 'Add questions to the category {$a->catname}: {$a->link}';
|
||||
$string['addnewuseroverride'] = 'Add user override';
|
||||
$string['addpagebreak'] = 'Add page break';
|
||||
$string['addpagebreakafter'] = 'Add page break after question {$a}';
|
||||
$string['addpagehere'] = 'Add page here';
|
||||
$string['addquestion'] = 'Add question';
|
||||
$string['addquestionfrombankatend'] = 'Add from the question bank at the end';
|
||||
@@ -575,6 +576,7 @@ Quizzes may be used
|
||||
* For self-assessment';
|
||||
$string['modulename_link'] = 'mod/quiz/view';
|
||||
$string['modulenameplural'] = 'Quizzes';
|
||||
$string['movequestionnumber'] = 'Move question: {$a}';
|
||||
$string['moveselectedonpage'] = 'Move selected questions to page: {$a}';
|
||||
$string['multichoice'] = 'Multiple choice';
|
||||
$string['multipleanswers'] = 'Choose at least one answer.';
|
||||
@@ -885,6 +887,7 @@ $string['removeallquizattempts'] = 'All quiz attempts';
|
||||
$string['removealluseroverrides'] = 'All user overrides';
|
||||
$string['removeemptypage'] = 'Remove empty page';
|
||||
$string['removepagebreak'] = 'Remove page break';
|
||||
$string['removepagebreakafter'] = 'Remove page break after question {$a}';
|
||||
$string['removeselected'] = 'Remove selected';
|
||||
$string['rename'] = 'Rename';
|
||||
$string['renderingserverconnectfailed'] = 'The server {$a} failed to process an RQP request. Check that the URL is correct.';
|
||||
|
||||
+8
-5
@@ -618,7 +618,7 @@ table.quizreviewsummary td.cell {
|
||||
|
||||
#page-mod-quiz-edit ul.section {
|
||||
margin: 0;
|
||||
padding: 0 20px;
|
||||
padding: 0 38px;
|
||||
}
|
||||
|
||||
#page-mod-quiz-edit ul.slots {
|
||||
@@ -683,8 +683,11 @@ table.quizreviewsummary td.cell {
|
||||
|
||||
#page-mod-quiz-edit ul.slots li.section li.activity .page_split_join_wrapper {
|
||||
position: absolute;
|
||||
left: -20px;
|
||||
bottom: -11px;
|
||||
left: -38px;
|
||||
bottom: -18px;
|
||||
.icon {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
#page-mod-quiz-edit ul.slots li.section li.activity .instancemaxmark.decimalplaces_0 {
|
||||
@@ -737,8 +740,8 @@ table.quizreviewsummary td.cell {
|
||||
|
||||
#page-mod-quiz-edit ul.slots li.section li.activity .question_dependency_wrapper {
|
||||
position: absolute;
|
||||
top: -1em;
|
||||
right: -20px;
|
||||
top: -1.25em;
|
||||
right: -38px;
|
||||
}
|
||||
|
||||
#page-mod-quiz-edit ul.slots li.section li.activity .question_dependency_wrapper.question_dependency_cannot_depend {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user