Compare commits

...

103 Commits

Author SHA1 Message Date
Huong Nguyen fb57854de5 weekly release 4.5.10+ 2026-03-27 15:41:10 +07:00
Huong Nguyen 4fa5e7e6ea Merge branch 'install_405_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_405_STABLE 2026-03-27 15:40:57 +07:00
Andi Permana aedff688e9 MDL-87548 repository_wikimedia: Handle HTTP 429 rate limiting 2026-03-27 10:28:05 +07:00
Huong Nguyen a7a6d32f3b Merge branch 'MDL-81241-405' of https://github.com/junpataleta/moodle into MOODLE_405_STABLE 2026-03-26 10:51:27 +07:00
Huong Nguyen 1d39913329 Merge branch 'MDL-88279-405' of https://github.com/meirzamoodle/moodle into MOODLE_405_STABLE 2026-03-26 07:49:41 +07:00
AMOS bot 1cdbf1089b Automatically generated installer lang files 2026-03-26 00:07:41 +00:00
Huong Nguyen b1aacfaab4 Merge branch 'MDL-85451-405' of https://github.com/junpataleta/moodle into MOODLE_405_STABLE 2026-03-25 14:30:16 +07:00
meirzamoodle 4260b9a0bf MDL-88279 core_session: fix cluster test retry count assertion
Update session_redis_cluster_test to expect 3 connection attempts
instead of 5, matching the $maxretries default value in redis.php.
2026-03-24 14:40:17 +07:00
AMOS bot 7e150c3221 Automatically generated installer lang files 2026-03-21 00:07:44 +00:00
Mihail Geshoski 4d8935e15e Merge branch 'MDL-88122-navbar_empty-breadcrumb-M4.5' of https://github.com/stopfstedt/moodle into MOODLE_405_STABLE 2026-03-20 15:35:57 +08:00
Shamim Rezaie 8926183e8e weekly release 4.5.10+ 2026-03-20 15:32:43 +11:00
Shamim Rezaie 8288ac9e11 NOBUG: Add upgrade notes 2026-03-20 15:32:43 +11:00
Shamim Rezaie 6bd427fad3 Merge branch 'install_405_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_405_STABLE 2026-03-20 15:32:31 +11:00
Jun Pataleta 03608d8fc6 MDL-81241 theme_boost: Accessibility test for the footer region links 2026-03-19 20:32:07 +08:00
Jun Pataleta 01ae2a52cb MDL-81241 theme_boost: Ensure footer links have sufficient spacing 2026-03-19 20:32:07 +08:00
Safat ba49df6fbb MDL-87959 tool_installaddon: Fix behats for activity chooser footer 2026-03-19 12:16:12 +07:00
Mihail Geshoski 2c1e8c2d2d Merge branch 'MDL-87810-405' of https://github.com/junpataleta/moodle into MOODLE_405_STABLE 2026-03-19 09:50:33 +08:00
Ilya Tregubov 828b8f51b4 Merge branch 'MDL-75067-405' of https://github.com/paulholden/moodle into MOODLE_405_STABLE 2026-03-18 14:03:02 +01:00
Jun Pataleta 9b5861e5d1 MDL-85451 block_timeline: Add accessibility Behat test coverage 2026-03-18 13:26:07 +08:00
Jun Pataleta b4dddbfd75 MDL-85451 block_timeline: Group the due date filter options
To ensure that the filter options for the day filter menu matches
the menu pattern and pass accessibility checks, we need to:
* Group the due date filter options, labelled by the "Due date"
"heading".
* Set the "Due date" heading with a presentation role to prevent
assistive technologies from announcing it when the user goes through
the menu options.
2026-03-18 13:26:07 +08:00
Jun Pataleta d7eb914233 MDL-85451 block_timeline: Fix heading levels 2026-03-18 13:26:04 +08:00
Jake Dallimore 9e62c289ce Merge branch 'MDL-83096-405' of https://github.com/jason-platts/moodle into MOODLE_405_STABLE 2026-03-16 09:43:35 +08:00
Paul Holden 564da75657 MDL-88211 backup: remove redundant usage of task manager from tests.
The asynchronouse backup/restore tests are only concerned with testing
the implementation of the accompanying task class directly. When they
try to do so at a distance via the task manager API they introduce
random failures by failing to account for different queued tasks that
come from other components (e.g. Workplace).
2026-03-13 15:26:15 +01:00
Daniel Poggenpohl e7582fab55 MDL-83096 mod_lti: Register view completion when launching in new window 2026-03-13 08:59:48 +00:00
Huong Nguyen 47e76e08b6 Merge branch 'MDL-87959-405' of https://github.com/safatshahin/moodle into MOODLE_405_STABLE 2026-03-13 14:06:05 +07:00
Safat 8126a90efb MDL-87959 admin: Add marketplace availability in notifications 2026-03-13 15:58:30 +11:00
Safat e8b7ff09db MDL-87959 tool_installaddon: Add support for marketplace 2026-03-13 15:58:30 +11:00
Mihail Geshoski 71b78c2d21 Merge branch 'MDL-81608-405-2' of https://github.com/junpataleta/moodle into MOODLE_405_STABLE 2026-03-13 09:54:41 +08:00
Huong Nguyen f35845fcad Merge branch 'MDL-87100-405' of https://github.com/paulholden/moodle into MOODLE_405_STABLE 2026-03-13 08:39:25 +07:00
Safat a76cc39623 Merge branch 'MDL-87708-405' of https://github.com/HuongNV13/moodle into MOODLE_405_STABLE 2026-03-12 15:19:06 +11:00
Huong Nguyen f896d4b381 MDL-87708 tool_moodlenet: Remove post install task 2026-03-12 10:48:23 +07:00
Huong Nguyen 3ca9f3a5e0 MDL-87708 admin: Improve MoodleNet removal warnings message 2026-03-12 10:16:06 +07:00
Andi Permana e77c82dc1d MDL-87708 tool_moodlenet: Clean up moodle.net configs in stable
Part of MDL-87351

- Discourage tool_moodlenet unless pointing to custom installation
- Clear activity chooser footer if set to MoodleNet
- Cleanup happens for: moodle.net URL, www.moodle.net, or empty config
- Preserves custom/local MoodleNet installations
2026-03-12 10:16:05 +07:00
Stefan Topfstedt 857fa9d371 MDL-88122 navigation: omit empty breadcrumb container in output. 2026-03-11 17:46:10 -07:00
Mihail Geshoski 1abad7745b Merge branch 'MDL-87955-405-2' of https://github.com/junpataleta/moodle into MOODLE_405_STABLE 2026-03-11 11:56:07 +08:00
AMOS bot 4449bd5399 Automatically generated installer lang files 2026-03-11 00:09:24 +00:00
Jun Pataleta b7dcc4e960 MDL-81608 quiz: Add Add menu spacing to prevent overlap 2026-03-10 17:59:28 +08:00
Jun Pataleta b1d5327464 MDL-81608 quiz: Set more meaningful button labels
* Add/remove page break buttons
* Move question buttons
2026-03-10 17:59:28 +08:00
Jun Pataleta 1f2ecfb129 MDL-81608 core: Add alt text to the YUI spinner image
The accessibility Behat test added to the quiz editing page uncovered
that the loading spinner icon does not have an appropriate alt text.
2026-03-10 17:59:27 +08:00
Jun Pataleta e05f5eb00e MDL-81608 quiz: Adjust size and spacing for the quiz edit page buttons
* In order to meet WCAG 2.2 criterion for minimum target size and make
it easier for users who have mobility limitations to use the quiz
editing page, we need to use the Bootstrap icon button styles and
increase the size and the spacing for the following buttons:
  * Add/remove page break button
  * Lock/unlock question dependency button
* This commit also makes the icons within the buttons as decorative and
moves the accessible name to the button elements themselves via the
aria-label attribute.
* Adjust Behat tests for checking the dependency buttons.
* Add accessibility Behat coverage for the quiz edit page.
2026-03-10 17:59:27 +08:00
Jun Pataleta d348e7a481 MDL-81608 core: Label YUI dragdrop handles appropriately
The YUI dragdrop handles rely on the button's title attribute for its
accessible label. We should label it appropriately using the
aria-label attribute.
2026-03-10 17:59:27 +08:00
Huong Nguyen 67b633fa5b Merge branch 'MDL-86698-405' of https://github.com/mickhawkins/moodle into MOODLE_405_STABLE 2026-03-10 08:26:30 +07:00
Huong Nguyen e14847283c Merge branch 'MDL-87883-405' of https://github.com/paulholden/moodle into MOODLE_405_STABLE 2026-03-09 09:06:32 +07:00
Michael Hawkins 8bbcf2a84c MDL-86698 core: Update security.txt expiry 2026-03-06 19:10:00 +08:00
Jake Dallimore c5b9e1af04 weekly release 4.5.10+ 2026-03-06 12:10:31 +08:00
Jake Dallimore d52b85dfb5 NOBUG: Add upgrade notes 2026-03-06 12:10:31 +08:00
Jake Dallimore 971fc7f1a8 Merge branch 'install_405_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_405_STABLE 2026-03-06 12:10:23 +08:00
Jun Pataleta 465cbf2813 MDL-87810 quiz: Accessibility Behat coverage for quiz attempt 2026-03-05 08:47:42 +08:00
Jun Pataleta 0f51395604 MDL-87810 question: Fix colour contrast issues on question view
* Lighten the background for the question formulation container.
* Lighten the background for the question feedback container while
  slightly darkening the text font as well.
2026-03-04 23:14:37 +08:00
Jun Pataleta b9f78d526d Merge branch 'MDL-87949-405' of https://github.com/paulholden/moodle into MOODLE_405_STABLE 2026-03-04 21:26:03 +08:00
Jun Pataleta 0528058a20 MDL-87955 mod_forum: Manage subscribers accessibility improvements
* Replace the layout table with responsive Bootstrap columns.
* Add accessible labels for the existing and potential subscribers
  select boxes.
* Add accessibility Behat coverage.
* Remove unused CSS styles
2026-03-04 12:58:11 +08:00
Huong Nguyen 10837f283a Merge branch 'MDL-87905-m405' of https://github.com/sammarshallou/moodle into MOODLE_405_STABLE 2026-03-04 08:50:50 +07:00
AMOS bot 920eb9e0eb Automatically generated installer lang files 2026-03-04 00:07:43 +00:00
Huong Nguyen 56d29efbd1 Merge branch 'MDL-87993-405' of https://github.com/junpataleta/moodle into MOODLE_405_STABLE 2026-03-03 08:36:30 +07:00
Huong Nguyen 17ccafb6f0 Merge branch 'MDL-87710-405' of https://github.com/paulholden/moodle into MOODLE_405_STABLE 2026-03-03 08:28:38 +07:00
Paul Holden 6bfb046d86 MDL-75067 behat: always interact with filepicker inside active dialogue.
This allows for tests to interact with multiple elements of this type
during scenarios, whereas previously Behat would always try to interact
with the first (even when referring to latter file managers).
2026-03-02 15:53:57 +00:00
Huong Nguyen 1619586794 weekly release 4.5.10+ 2026-02-27 09:39:21 +07:00
Huong Nguyen 86079406d9 Merge branch 'install_405_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_405_STABLE 2026-02-27 09:39:09 +07:00
AMOS bot 8a79c5fb4b Automatically generated installer lang files 2026-02-27 00:07:36 +00:00
sam marshall 27c9a39712 MDL-87905 Behat: pause incorrectly thinks terminal not interactive
When run from behat/cli/run.php, the pause command may fail because
it thinks the terminal is not interactive, even if the input and
output is passed through from an interactive terminal run.
2026-02-26 09:38:23 +00:00
Huong Nguyen 5a75954abb Merge branch 'MDL-88020-405' of https://github.com/rxdelacruz/moodle into MOODLE_405_STABLE 2026-02-26 09:03:17 +07:00
Huong Nguyen 58168c958c Merge branch 'MDL-88036-405' of https://github.com/mioyu-ubiq/moodle into MOODLE_405_STABLE 2026-02-26 09:01:47 +07:00
Mihail Geshoski 80e9a89704 Merge branch 'MDL-85902-405' of https://github.com/junpataleta/moodle into MOODLE_405_STABLE 2026-02-26 09:58:25 +08:00
AMOS bot df91fc31a3 Automatically generated installer lang files 2026-02-26 00:07:34 +00:00
rxdelacruz 4b26a84586 MDL-88020 enrol_self: Add Behat test for cohort restriction 2026-02-25 14:57:54 +08:00
mioyu-ubiq 6ee622ad13 MDL-88036 mod_scorm: Fix duplicate scenario name 2026-02-25 14:38:51 +08:00
Jun Pataleta f7cf98ea86 MDL-85902 course: Better labeling of activity navigation components
* The arrow icons produced by the `$OUTPUT->larrow()` and
`$OUTPUT->rarrow()` functions are announced by screen readers, and they
do not make sense when heard by screen reader users. To fix this,
we need to override the accessible names of the previous and next
activity links using the `aria-label` attribute and specify whether the
activity links point to the previous or next activities.
* The label for the jump menu "Jump to" is not informative enough to
convey the element's purpose. Let's improve it's accessible label to
"Jump to activity".
2026-02-24 22:03:04 +08:00
Jun Pataleta 8c1fe31370 MDL-85902 course: Convert activity navigation to a navigation landmark 2026-02-24 21:04:51 +08:00
AMOS bot fe92590370 Automatically generated installer lang files 2026-02-24 00:07:36 +00:00
Jun Pataleta 1718b04587 MDL-87993 core: Use visually hidden core/toast for the srlogger 2026-02-20 16:19:49 +08:00
Jun Pataleta 44cb65c5a3 MDL-87993 core: Allow toasts to be visually hidden 2026-02-20 16:16:10 +08:00
Mihail Geshoski dc7dea0cf2 weekly release 4.5.10+ 2026-02-20 09:17:52 +08:00
Mihail Geshoski 81a7de4721 Merge branch 'install_405_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_405_STABLE 2026-02-20 09:17:41 +08:00
AMOS bot 4027f44c77 Automatically generated installer lang files 2026-02-19 00:07:34 +00:00
Huong Nguyen 5c572f8a6b Merge branch 'MDL-87482-405' of https://github.com/HuongNV13/moodle into MOODLE_405_STABLE 2026-02-18 15:40:21 +07:00
AMOS bot 08cc1f3d32 Automatically generated installer lang files 2026-02-18 00:07:37 +00:00
Shamim Rezaie 5a1fb35680 weekly release 4.5.10+ 2026-02-17 12:56:21 +11:00
Shamim Rezaie 6f18148684 Merge branch 'install_405_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_405_STABLE 2026-02-17 12:56:09 +11:00
Mihail Geshoski 6e6d5fa3cf Merge branch 'MDL-85658-405' of https://github.com/HuongNV13/moodle into MOODLE_405_STABLE 2026-02-16 13:47:58 +08:00
Paul Holden 37170e5714 MDL-87949 gradingform_rubric: accessible "Click to edit" colour.
Darken it to meet the WCAG AA standards for accessible text. This
colour is also consistent with the accompanying buttons.
2026-02-13 08:11:31 +00:00
Mihail Geshoski fb680bb33d Merge branch 'MDL-87600-405' of https://github.com/junpataleta/moodle into MOODLE_405_STABLE 2026-02-13 14:18:36 +08:00
Jun Pataleta 4c872ab444 Merge branch 'MDL-87946-405' of https://github.com/andrewnicols/moodle into MOODLE_405_STABLE 2026-02-13 11:59:31 +08:00
Huong Nguyen 7cbd576777 Merge branch 'MDL-77209-405' of https://github.com/junpataleta/moodle into MOODLE_405_STABLE 2026-02-13 09:42:36 +07:00
Andrew Nicols 2c8cfb9e13 MDL-87946 github: Update GHA Database connections
Both the pgsql and mysql steps we were using use `docker:stable`, which
has been abandoned and is not a recommended approach. They are also both
abandoned.

This commit moves to using a service entry for postgres; and a manual
step for mysql.

We can't use the services entry for MySQL because GHA does not support
providing arguments to service containers.
2026-02-13 10:34:28 +08:00
Huong Nguyen 1bfe81f05d MDL-87482 core: Better text comparison 2026-02-13 09:31:03 +07:00
Andi Permana 2cf09ff8f3 MDL-87482 core_message: Fix compatibility with libxml2 >= 2.14.0 2026-02-13 09:17:47 +07:00
Huong Nguyen d9284eee0f MDL-85658 core_h5p: Implement contentLoaded event handler
This improvement will help to avoid random errors
because H5P content needs time to be rendered.
2026-02-13 08:55:10 +07:00
Huong Nguyen aa1723e748 MDL-85658 behat: Wait until the iframe is completely loaded 2026-02-13 08:55:10 +07:00
Huong Nguyen c079d2c3ea Merge branch 'MDL-87095-405' of https://github.com/lameze/moodle into MOODLE_405_STABLE 2026-02-12 10:24:43 +07:00
Simey Lameze 39aa019a86 MDL-87095 behat: reorganize lesson question importing tests 2026-02-12 09:51:53 +08:00
Kim Jared Lucas 882235917f MDL-87095 mod_lesson: add behat test for importing questions into lesson 2026-02-12 09:51:53 +08:00
AMOS bot e12ffcdda0 Automatically generated installer lang files 2026-02-12 00:09:30 +00:00
Huong Nguyen b2c2f2a0c5 Moodle release 4.5.10 2026-02-11 08:44:13 +07:00
Huong Nguyen 7a7f029149 weekly release 4.5.9+ 2026-02-10 08:42:00 +07:00
Shamim Rezaie 6cfefde94b Merge branch 'MDL-87892-405' of https://github.com/HuongNV13/moodle into MOODLE_405_STABLE 2026-02-09 21:07:28 +11:00
Huong Nguyen be8d3d004f MDL-87892 admin: Fix MoodleNet check 2026-02-09 16:34:16 +07:00
Shamim Rezaie d42c5d9311 Merge branch 'MDL-87892-405' of https://github.com/HuongNV13/moodle into MOODLE_405_STABLE 2026-02-09 19:36:47 +11:00
Huong Nguyen 1c0e0f55bf MDL-87892 admin: Fix MoodleNet removal warning
Instead of introducing a new optional parameter,
we will make all the changes in the Admin Renderer
2026-02-09 09:05:49 +07:00
Paul Holden c5aebec657 MDL-87883 core: correct license page link in project readme. 2026-02-06 09:09:12 +00:00
Jun Pataleta 4e689e468e MDL-77209 core: Make More help links open on a new tab 2026-02-06 10:57:26 +08:00
Jun Pataleta 77e3a9f987 MDL-87600 gradereport_overview: Fix heading levels on the index page 2026-02-04 18:51:23 +08:00
Paul Holden 23a0e2a9a4 MDL-87100 core: infer upgrade note issue number from current branch. 2026-01-27 10:55:10 +00:00
Paul Holden d88cbc344d MDL-87710 behat: move upgrade notes into the correct location.
They were put in the wrong location by MDL-87443 (my bad).
2026-01-27 10:47:09 +00:00
127 changed files with 2214 additions and 637 deletions
+21 -20
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
+24
View File
@@ -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
View File
@@ -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
View File
@@ -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');
}
/**
+8
View File
@@ -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
+5 -1
View File
@@ -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
View File
@@ -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"}
+59
View File
@@ -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);
}
@@ -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');
}
}
+37
View File
@@ -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);
}
+41
View File
@@ -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 ...';
+48
View File
@@ -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
);
}
+6
View File
@@ -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"
@@ -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'),
);
}
}
+1 -1
View File
@@ -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;
+1 -8
View File
@@ -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);
}
+3 -1
View File
@@ -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:
+3 -20
View File
@@ -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();
}
}
+2 -18
View File
@@ -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>
+4 -4
View File
@@ -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"
+52 -52
View File
@@ -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."
+1 -1
View File
@@ -79,7 +79,7 @@
.gradingform_guide .plainvalue.empty {
font-style: italic;
color: #aaa;
color: #555;
}
.gradingform_guide .plainvalue.editname {
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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 |
+44
View File
@@ -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;
+3 -3
View File
@@ -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';
+7 -9
View File
@@ -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';
+41
View File
@@ -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)';
+51
View File
@@ -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'] = 'Dhfhàillig sgrùdadh an fhaidhle a chaidh a luchdachadh a-nuas';
$string['missingrequiredfield'] = 'Tha raon deatamach air choireigin a dhìth';
$string['remotedownloaderror'] = '<p>Dhfhà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';
+1
View File
@@ -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)';
+32
View File
@@ -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
View File
@@ -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';
+4
View File
@@ -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';
+14
View File
@@ -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
View File
@@ -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"}
+1 -1
View File
@@ -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
+2 -17
View File
@@ -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});
}
}
}
+14
View File
@@ -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) {
+6 -6
View File
@@ -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(
+1 -1
View File
@@ -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);
}
+17 -27
View File
@@ -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.
+16 -2
View File
@@ -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;
+48
View File
@@ -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;
}
+1
View File
@@ -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();
+1 -1
View File
@@ -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>
+2
View File
@@ -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}}
+62
View File
@@ -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.
*
+2 -2
View File
@@ -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
View File
@@ -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');
+1
View File
@@ -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) {
+17 -5
View File
@@ -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>';
}
}
}
}
}
+13
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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'));
+60 -24
View File
@@ -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',
+3
View File
@@ -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
View File
@@ -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