Compare commits

..

136 Commits

Author SHA1 Message Date
Jun Pataleta fc01a52823 Moodle release 3.9.19 2023-01-14 14:16:56 +08:00
Jun Pataleta d84283e8cc weekly release 3.9.18+ 2023-01-13 10:52:29 +08:00
Jake Dallimore 9f1768c098 MDL-76862 user: Unit test for setting user home page preferences 2023-01-12 11:25:57 +08:00
Paul Holden 4babd8cdb7 MDL-76862 user: ensure user can only update own preferred start page. 2023-01-12 11:17:25 +08:00
Tim Hunt 312318201b MDL-76810 redirect handling: ensure return URLs are properly sanitised 2023-01-11 11:44:41 +08:00
Paul Holden 3d9ac7137e Merge branch 'MDL-76811-39' of https://github.com/andrewnicols/moodle into MOODLE_39_STABLE 2023-01-10 16:40:08 +00:00
Andrew Nicols 838e230d54 MDL-76811 mod_data: Remove duplicate tests 2023-01-10 11:19:14 +08:00
Andrew Nicols c26ac2e85d MDL-76811 mod_data: strpos check should be strict 2023-01-10 11:19:13 +08:00
Andrew Nicols ee3504f673 MDL-76811 mod_data: Generator test cleanup 2023-01-10 11:19:13 +08:00
Paul Holden 31256fae74 weekly release 3.9.18+ 2023-01-05 10:53:18 +00:00
Sara Arjona 0c91f5a454 Merge branch 'MDL-76375-39' of https://github.com/lucaboesch/moodle into MOODLE_39_STABLE 2023-01-04 16:22:07 +01:00
Luca Bösch 1ab21607a5 MDL-76375 quiz generator: Allow more than one attempt by Behat tests. 2023-01-04 15:15:57 +01:00
Sara Arjona 05fc7360c9 Merge branch 'MDL-76760-39' of https://github.com/paulholden/moodle into MOODLE_39_STABLE 2023-01-02 12:27:59 +01:00
Sara Arjona 4c3211871d weekly release 3.9.18+ 2022-12-29 12:58:01 +01:00
Sara Arjona 95f7d16801 Merge branch 'MDL-76399-patch-39' of https://github.com/ilyatregubov/moodle into MOODLE_39_STABLE 2022-12-29 09:13:54 +01:00
Ilya Tregubov 400b2086ff MDL-76399 behat: switch to appropriate Behat step for selects. 2022-12-29 14:33:55 +07:00
Ilya Tregubov 7ef99eb8cf Merge branch 'MDL-76399-39' of https://github.com/paulholden/moodle into MOODLE_39_STABLE 2022-12-29 09:44:59 +07:00
Paul Holden ab4b2404c7 MDL-76760 tool_behat: defer library loading until service execution.
Ensures that API documentation can be generated without requiring
the presence of the `vendor/` directory.
2022-12-28 10:47:59 +00:00
Ilya Tregubov d859c2875c Merge branch 'MDL-76731_39' of https://github.com/PhilippImhof/moodle into MOODLE_39_STABLE 2022-12-28 15:15:31 +07:00
Philipp Imhof d14ff96dfa MDL-76731 behat: add steps to check for version
Co-authored-by: Andrew Nicols <andrew@nicols.co.uk>
2022-12-22 17:10:57 +01:00
Ilya Tregubov bc53f961e7 weekly release 3.9.18+ 2022-12-22 19:16:44 +07:00
Andrew Nicols 91b2170fdb MDL-75105 tool_behat: Coding style fixes 2022-12-22 12:34:07 +07:00
Andrew Nicols fadadf8a64 Merge branch 'MDL-75105_39_STABLE' of https://github.com/marxjohnson/moodle into MOODLE_39_STABLE 2022-12-22 12:34:07 +07:00
Mark Johnson 65be624c43 MDL-75105 tool_behat: List available data generators
This will find all data generators that can be used in behat via the
'the following "something" exist:' step, and display them in a select
list on the step definitions page.

When a generator is selected, it will fetch the required fields for that
generator and display them on the page.
2022-12-21 08:38:49 +00:00
Paul Holden 3e5d4ba208 MDL-76399 question: switch to appropriate Behat step for selects. 2022-12-16 13:14:02 +00:00
Paul Holden dfea1a4f0d MDL-76399 behat: improvements to "should exist in" thrown exception. 2022-12-16 12:29:06 +00:00
Andrew Nicols 2409994d32 weekly release 3.9.18+ 2022-12-16 09:06:07 +08:00
Sara Arjona 05dd1161a4 Merge branch 'MDL-76674_39' of https://github.com/stronk7/moodle into MOODLE_39_STABLE 2022-12-14 18:35:14 +01:00
Eloy Lafuente (stronk7) 7ed61f310b MDL-76674 core_lang: Add string for new America/Ciudad_Juarez 2022-12-14 15:52:36 +01:00
Paul Holden 15b8b5cdf0 Merge branch 'MDL-76666_39' of https://github.com/stronk7/moodle into MOODLE_39_STABLE 2022-12-14 13:22:11 +00:00
Eloy Lafuente (stronk7) e6eb619c08 MDL-76666 local_langimport: Better handling of long locales
Under some linux versions, and depending of the configured
locale categories, it's possible to get a current locally
which length > 255 when calling to setlocale(LC_ALL, 0).

Later, if that long locale is tried to be restored, there
is a "setlocale(): Specified locale name is too long" warning
error.

When that happens we need to split the long locale into
individual chunks and set all the (six) locale categories
supported one by one.

Covered with tests, note that, in practice, this only
happens with linux because it supports 12 locale categories
@ OS level. Both BSD (6) and Windows (5) hardly can reach the limit.

No matter of that, the tests have been designed to ensure that
they pass on all OSs, just the new code only will be executed
on linux.
2022-12-14 12:40:38 +01:00
Andrew Nicols 8826294d5d Merge branch 'MDL-76398-39' of https://github.com/paulholden/moodle into MOODLE_39_STABLE 2022-12-11 20:21:33 +08:00
Sara Arjona cf2780d5c2 weekly release 3.9.18+ 2022-12-09 08:57:16 +01:00
Jun Pataleta 154bf3d9f5 Merge branch 'MDL-73017-M39_add-environment-check-for-versions-not-supporting-php-8-1' of https://github.com/ziegenberg/moodle into MOODLE_39_STABLE 2022-12-07 13:47:35 +08:00
Ilya Tregubov 768d683beb Merge branch 'MDL-76490-39' of https://github.com/paulholden/moodle into MOODLE_39_STABLE 2022-12-06 13:11:31 +03:00
Andrew Nicols 7f8aca035d Merge branch 'MDL-75446-39' of https://github.com/andelacruz/moodle into MOODLE_39_STABLE 2022-12-06 15:21:49 +08:00
Paul Holden 8db1cdf462 MDL-76490 behat: use defined step for interaction with autocomplete. 2022-12-05 14:27:46 +00:00
Marina Glancy 48dc7ef01e MDL-76490 behat: correct step for opening autocomplete in container. 2022-12-05 14:22:08 +00:00
Daniel Ziegenberg d782e76640 MDL-73017 env: Moodle 3.9.x, 3.11.x and 4.0.x do not support PHP 8.1
This commit adds an additional environment check for the unsupported
PHP 8.1 version to Moodle 3.9.x, 3.11.x and 4.0.x.

It also updates the lang string for unsupported PHP version.

Signed-off-by: Daniel Ziegenberg <daniel@ziegenberg.at>
2022-12-05 11:44:03 +01:00
Angelia Dela Cruz e693c811f0 MDL-75446 behat: Workshop behat generators use UI
Replace steps that manually add Workshop instances via the UI and use
Behat generators. This improves the speed of the Behat test runs.
2022-12-05 17:56:43 +08:00
Sara Arjona d2f37aab56 weekly release 3.9.18+ 2022-12-01 16:08:12 +01:00
Jun Pataleta 8d08094ac3 Merge branch 'MDL-76429_39' of https://github.com/stronk7/moodle into MOODLE_39_STABLE 2022-11-30 10:52:21 +08:00
Andrew Nicols 8483f7adef Merge branch 'MDL-75960-39' of https://github.com/andelacruz/moodle into MOODLE_39_STABLE 2022-11-29 17:06:06 +08:00
info@eWallah.net 4cffe42b56 MDL-65939 phpunit: Maildigest tests can query a wrong adhoc task 2022-11-29 15:43:45 +08:00
Andrew Nicols 84935b9585 Merge branch 'MDL-76058-39' of https://github.com/lameze/moodle into MOODLE_39_STABLE 2022-11-29 15:38:39 +08:00
Angelia Dela Cruz bf8f9164c0 MDL-75960 behat: Survey behat generators use UI
Replace steps that manually add Survey instances via the UI and use
Behat generators. This improves the speed of the Behat test runs.
2022-11-28 10:22:29 +08:00
Eloy Lafuente (stronk7) 64b5af4b58 MDL-76429 composer: Bump to phpwebdriver 1.2.1 for php81 compat
This fix is really only required for Moodle 4.1 and up, that will
be the one supporting php81, but we always apply composer changes
to all branches (supported and security), so we keep them running
the same stuff.

Note this comes with a bunch of other small updates, all them
pretty minor and should not be problematic. Better we get them
than keeping them apart.
2022-11-25 16:27:39 +01:00
Ilya Tregubov 959721b58a weekly release 3.9.18+ 2022-11-25 15:38:36 +03:00
Jun Pataleta 746fa817c1 Merge branch 'MDL-76340-39' of https://github.com/stevandoMoodle/moodle into MOODLE_39_STABLE 2022-11-25 08:21:32 +08:00
Stevani Andolo cfce223e9b MDL-76340 VideoJS: Fix video controls not showing
- Fixed the menu item not being displayed as expected
2022-11-24 22:57:30 +08:00
Paul Holden 375d8ffb56 MDL-76398 behat: broader action menu partial matching by element.
For action menu elements whose trigger element isn't plain text
(e.g. a pix icon), we should be able to match via the title of
that element.
2022-11-21 16:59:31 +00:00
Jun Pataleta f89a47eff8 Moodle release 3.9.18 2022-11-12 12:55:12 +08:00
Sara Arjona 6a16596f78 weekly release 3.9.17+ 2022-11-11 15:02:06 +01:00
Mathew May 7e22b75c28 MDL-71920 libraries: Migrate LTI lib away from pure curl 2022-11-11 12:55:19 +08:00
Mathew May 7d547f726f MDL-71920 core: update filelib opt name definition 2022-11-11 12:55:19 +08:00
Paul Holden 75c9e608ce MDL-75862 backup: omit sesskey for continue buttons using get method.
It's automatically added by the rendered single_button instance when
using post method, so no need to add it unconditionally.
2022-11-10 10:17:50 +08:00
Jun Pataleta fa8591ea3e Merge branch 'MDL-76054_39' of https://github.com/stronk7/moodle into MOODLE_39_STABLE 2022-11-09 20:08:49 +08:00
David Woloszyn ae8c5e16e9 MDL-76091 lib: Add additional check to local URLs params 2022-11-09 19:24:56 +08:00
Simey Lameze 4fe95b0e61 MDL-76058 behat: send message using data generator 2022-11-09 09:52:22 +08:00
Eloy Lafuente (stronk7) 5425233b37 MDL-76054 environment: Remove the php-xmlrpc extension check
For Moodle 4.1 and up, the php-xmlrpc is not needed anymore:
- All the MNet stuff has been moved to use php library (MDL-76055).
- The webservice/xmlrpc has been moved from core to contrib (MDL-76052).

So we just remove the check here. Starting with 4.1, it's not
needed for any core functionality.

No lang string changes in this branch, only in master (4.1dev).
2022-11-08 16:47:53 +01:00
Stevani Andolo dcc561762b MDL-75278 VideoJS: Update VideoJS to use validation helper 2022-11-08 13:57:20 +00:00
Víctor Déniz 6307f8dd89 weekly release 3.9.17+ 2022-11-08 13:44:57 +00:00
Jun Pataleta 1a3e679835 Merge branch 'MDL-76207-39' of https://github.com/andrewnicols/moodle into MOODLE_39_STABLE 2022-11-08 10:25:12 +08:00
Andrew Nicols 6cfdd1bbae Merge branch 'MDL-75880_39' of https://github.com/stronk7/moodle into MOODLE_39_STABLE 2022-11-07 21:57:45 +08:00
Andrew Nicols 56a368facf MDL-76207 contentbank: Switch to opened window 2022-11-06 21:00:33 +08:00
Eloy Lafuente (stronk7) 4842ae967c MDL-75880 phpunit: fix all @coversDefaultClass missing @covers
Because @coversDefaultClass doesn't indicate any coverage (it's
just an alias to avoid having to write the class name in @covers),
this commit fixes all the files that were using that annotation
and missing any @covers.

Basically, replacing one by the other and done.
2022-11-04 16:51:36 +01:00
Sara Arjona 6f62e05a96 weekly release 3.9.17+ 2022-11-04 14:52:11 +01:00
Jun Pataleta 6f23e028dd Merge branch 'MDL-76103-39' of https://github.com/junpataleta/moodle into MOODLE_39_STABLE 2022-11-04 11:34:02 +08:00
Marina Glancy ff013438d6 MDL-76103 search: skip solr behat tests if solr is not installed 2022-11-04 11:22:27 +08:00
Jun Pataleta 3e0941fb5b Merge branch 'MDL-76072-39' of https://github.com/lameze/moodle into MOODLE_39_STABLE 2022-11-04 10:37:19 +08:00
Simey Lameze e667c191f8 MDL-76072 tool_lp: only init inlineGrader module if user can grade 2022-11-04 08:04:30 +08:00
Sara Arjona 4566a44f14 Merge branch 'MDL-76169-39' of https://github.com/andrewnicols/moodle into MOODLE_39_STABLE 2022-11-03 17:06:28 +01:00
Andrew Nicols 8b2c0d969d MDL-76169 behat: Normalise exception message in should (not) steps
These steps have accepted a NodeElement instance as an argument for some
time, but were trying to cast it to string when formulating exception
messages, making it harder to debug and, in the case of the 'should see'
step, not work at all.

This patch introduces a new function to produce a consistent naming for
them.
2022-11-03 13:28:51 +08:00
Andrew Nicols 32d337a934 MDL-76169 behat: Add improved steps for using action menus
This commit adds new steps related to action menus to support:
* Choosing an item in a named action menu
* Choosing an item in a named action menu within a container
* Confirming that an action menu item does or does not exist in a named
  action menu
* Confirming that an action menu item does or does not exist in a named
  action menu within a container

The existing action menu steps were insufficient as they assume that
there is only one action menu within he container, which is not
necessarily the case.

The existing action menu steps are not non-JS friendly and will error if
JS is disabled, without providing any fallback when one is easily
available.

Unfortunately these steps cannot be used to replace the existing steps
without manual intervention.
2022-11-03 13:28:51 +08:00
Sara Arjona 4e1d36c8bc weekly release 3.9.17+ 2022-10-28 14:09:25 +02:00
Víctor Déniz 87c2aae85c Merge branch 'MDL-75969-39' of https://github.com/andrewnicols/moodle into MOODLE_39_STABLE 2022-10-27 15:45:39 +01:00
Jun Pataleta 8c6831f0fe weekly release 3.9.17+ 2022-10-25 17:17:19 +08:00
Andrew Nicols 5dbc672825 Merge branch 'MDL-76049_39' of https://github.com/timhunt/moodle into MOODLE_39_STABLE 2022-10-25 11:50:46 +08:00
Ilya Tregubov b97d313c11 weekly release 3.9.17+ 2022-10-21 15:33:55 +03:00
Tim Hunt ee0b0b4fc8 MDL-76049 quiz generator: update calendar events for new overrides 2022-10-20 23:44:54 +01:00
Andrew Nicols 5b707aa422 MDL-75985 behat: Return newline character for \n
Somehow part of this commit was lost during the push and/or squash.
2022-10-19 22:02:55 +08:00
Sara Arjona 519ba9e103 Merge branch 'MDL-76010_39' of https://github.com/stronk7/moodle into MOODLE_39_STABLE 2022-10-19 12:56:39 +02:00
Jun Pataleta c3a318e16a Merge branch 'MDL-75985-39' of https://github.com/andrewnicols/moodle into MOODLE_39_STABLE 2022-10-19 10:55:28 +08:00
Eloy Lafuente (stronk7) faf63d076d MDL-76010 behat: New scenario to verify that manual attempts work
This is basically the manual background that was removed in the
previous commit, with 4 attempts here. Note it includes the 2
waits that are REQUIRED to make it pass in too-quick environments.

If they are removed, they begin to fail because it seems that
the attempts need a minimum of separation between them.
2022-10-18 14:28:14 +02:00
Eloy Lafuente (stronk7) b1fe51cf0d MDL-76010 behat: Move feature to use attempts generator
This moves away from manual attempts and uses the activity
attempts generator instead. Quicker and not exposed to the
problems with clicks, refreshes and missing attempts.

Note that there is a small change in the attempt generator
so, now, when a new attempt is created, if the activity has
tracking enabled, the final result / grades are recalculated.

That's the real behaviour when the activity settings are
changes in the edition form and it's natural to get them
recalculated without having to visit the form. Specifically,
this allows the 1st scenario to pass, because in that scenario
no changes to the form are being performed.

Also, little detail, amend a dupe scenario name.
2022-10-18 14:28:14 +02:00
Sara Arjona 651a97efc0 weekly release 3.9.17+ 2022-10-18 13:10:01 +02:00
Víctor Déniz cd8af2d23a Merge branch 'MDL-75983_39' of https://github.com/stronk7/moodle into MOODLE_39_STABLE 2022-10-18 01:09:35 +01:00
Sara Arjona 8b75bb1772 Merge branch 'MDL-72836-39' of https://github.com/mickhawkins/moodle into MOODLE_39_STABLE 2022-10-17 16:40:07 +02:00
Andrew Nicols b672263e04 MDL-75969 behat: Allow the editor field to request a save
Some editors do not immediately store their content to the textarea they
represent and only do so when the form is submitted or they are
requested to.

This change adds to the existing API to allow an (optional)
`store_current_value` function to be defined for the editor type so that
it can use the JavaScript API to persist any current content to the
editor.
2022-10-14 23:46:25 +08:00
Andrew Nicols 1b506c9639 MDL-75985 behat: Standardise HTML output when comparing editor content
This commit adds a standardise_html function and updates the matches
function to compare normalised content.

This allows for a wider variety of valid editor output to be handled
using the standard value matching steps in Behat, thus supporting
editors other than Atto better.
2022-10-14 23:44:11 +08:00
Eloy Lafuente (stronk7) c463d72821 MDL-75983 gha: Update used actions to be node16 compatible
Node12 is being deprecated in github for actions:

https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/

So we need to ensure we are using node16 actions.
2022-10-14 13:28:33 +02:00
Sara Arjona 162053448e weekly release 3.9.17+ 2022-10-14 12:49:26 +02:00
Sara Arjona 2034d440af Merge branch 'MDL-75959-39' of https://github.com/andrewnicols/moodle into MOODLE_39_STABLE 2022-10-13 16:03:50 +02:00
Jun Pataleta 70f7efe0fe Merge branch 'MDL-75887-39' of https://github.com/andrewnicols/moodle into MOODLE_39_STABLE 2022-10-12 09:22:17 +08:00
Simey Lameze f1447db751 MDL-75887 behat: Add ability to set value regardless of the editor
Prior to this commit, the behat_form_editor integration was hard-coded
to use Atto.

This change allows other editors to be used to set editor values, and
breaks that hard bond. Following this change, any editor is able to
define a behat_editor_[name].php file defining a function which meets
the following signature:

  set_editor_value(string $editorid, string $value): void

Each editor is responsible for checking whether its own API is available
and calling any relevant functions to force the editor to set the
content.

Please note: Behat is unable to determine the current editor in use on
the page automatically.
2022-10-12 06:44:30 +08:00
Andrew Nicols 7fc01a94e2 MDL-75959 customfield: Correct inappropriate class import
I suspect this was an incorrect tab completion import by an IDE which
was missed.
2022-10-12 06:40:26 +08:00
Jun Pataleta 568e72e7dc weekly release 3.9.17+ 2022-10-11 23:16:45 +08:00
Jun Pataleta 5d4db3dce3 Merge branch 'MDL-75909-39' of https://github.com/andrewnicols/moodle into MOODLE_39_STABLE 2022-10-10 18:07:58 +08:00
Jun Pataleta e5c7b50d85 Merge branch 'MDL-75922-39' of https://github.com/andrewnicols/moodle into MOODLE_39_STABLE 2022-10-10 14:54:04 +08:00
Jun Pataleta 9fda41582c Merge branch 'MDL-75910-39' of https://github.com/andrewnicols/moodle into MOODLE_39_STABLE 2022-10-10 11:06:49 +08:00
Simey Lameze bddacfd5d7 MDL-75922 behat: Apply @editor_atto tag to tests requiring Atto
These steps are currently using Atto-specific UI features and must
therefore make use of the @editor_atto tag to ensure that they continue
to run using Atto.

A new issue will be raised to migrate each of these to be
editor-agnostic.
2022-10-09 20:28:07 +08:00
Michael Hawkins f3d9759375 MDL-72836 core: Updated security.txt expiry and policy URL 2022-10-07 14:12:23 +08:00
Andrew Nicols 1d776476b6 weekly release 3.9.17+ 2022-10-07 13:04:06 +08:00
Simey Lameze fca9778023 MDL-75910 core_form: Allow editor field to set a hidden label
When an editor is used in a form as part of a group, it is missing its
label element.

This happens because there is no inline template for the editor, and
therefore it reverts to using the old `toHtml` function. The legacy
function does not cater to the editor being in a group, and therefore
does not add the hidden label.

In addition, the hiddenLabel attribute was missing from the editor
element. This is a semi-standard element but must be added to each
supporting element.

Both the missing inline template for the editor, and the hidden label
attribute must be present:
- If the hidden label attribute is not present, then the standard inline
  template will add a visible label within the group.
- If the inline editor template is missing, then the label is not shown
  at all.
2022-10-06 22:54:20 +08:00
Andrew Nicols 482de4e370 MDL-75909 mod_data: Make template editor use html editor
When the template editor is configured to use the HTML Editor, it should
indicate this to Behat by setting the `data-fieldtype` to "editor".

This indicates to the behat field manager that the editor is in use,
which ensures that the correct value setting can take place.

This is important in the context of MDL-75887.
2022-10-06 21:49:08 +08:00
Shamim Rezaie 91f43dd47f Merge branch 'MDL-75886-39' of https://github.com/andrewnicols/moodle into MOODLE_39_STABLE 2022-10-06 12:25:07 +11:00
Andrew Nicols fa9ba48533 MDL-75886 atto: Always use Atto for @atto tests
This change, which should be easy to mimic for other editors.

This will ensure that the correct editor is used for tests relating to
that editor, or its subplugins.
2022-10-05 09:57:54 +08:00
Jake Dallimore 141657a483 weekly release 3.9.17+ 2022-09-30 10:17:43 +08:00
Jun Pataleta c8be5a9ab3 Merge branch 'MDL-75559-39' of https://github.com/andrewnicols/moodle into MOODLE_39_STABLE 2022-09-29 10:53:14 +08:00
Jun Pataleta 507f737266 Merge branch 'MDL-75726_39' of https://github.com/stronk7/moodle into MOODLE_39_STABLE 2022-09-28 08:12:26 +08:00
Jun Pataleta 6d75761b94 Merge branch 'MDL-75776_39' of https://github.com/stronk7/moodle into MOODLE_39_STABLE 2022-09-28 08:06:32 +08:00
Eloy Lafuente (stronk7) 52a9113c6d MDL-75726 ci: Bump GHA to Ubuntu 22.04
Have tried to also bump Travis to 22.04 (jammy) but it
was not working ok, with problems trying to find both
PHP and PostgreSQL versions.

Surely that's because 22.04 support is really new (Sep 9, 2022):

https://blog.travis-ci.com/22-9-9-ubuntujammy

So we'll have to wait a little more before being able to, also,
bump Travis to use 22.04
2022-09-22 17:45:23 +02:00
Eloy Lafuente (stronk7) 94f703e1d1 MDL-75776 composer: minor updates
In stables, just minors updates to components.

Generated using the instructions @ https://docs.moodle.org/dev/Composer
and using PHP 7.2 (always the min version supported) to generate the
lock file.
2022-09-22 17:44:51 +02:00
Eloy Lafuente (stronk7) d9ceef45de weekly release 3.9.17+ 2022-09-22 17:05:56 +02:00
Jun Pataleta 1ad332bbde Merge branch 'MDL-75757_39' of https://github.com/stronk7/moodle into MOODLE_39_STABLE 2022-09-19 13:01:43 +08:00
Eloy Lafuente (stronk7) d23efa1ff2 MDL-75757 phpunit: Add some tolerance to a few failing assertions
Recently, PHPUnit (all versions) did some changes about how the
comparators worked and this has caused some float comparisons
to stop working.

We need to move them to assertEqualsWithDelta(), allowing a small
tolerance to workaround the floats comparison problem.

0.00001 has been decided. And applied to all the similar assertions
within the unittest function, so if more cases are added to them
better be copied with the agreed tolerance.
2022-09-18 16:02:18 +02:00
Andrew Nicols bf1ebf3b44 MDL-75559 behat: Sort behat_hooks to top of context list 2022-09-12 12:33:46 +08:00
Eloy Lafuente (stronk7) 33e9b8c394 Moodle release 3.9.17 2022-09-10 12:15:47 +02:00
Jun Pataleta 5bb0f00c0e weekly release 3.9.16+ 2022-09-09 08:36:54 +08:00
Paul Holden 670b636c47 MDL-72012 mod_h5pactivity: respect user access when getting report. 2022-09-08 21:17:57 +08:00
Paul Holden e48460c8cc MDL-75695 mod_h5pactivity: add group filter to attempts report.
This is a backport from MDL-71662.
2022-09-08 17:14:00 +04:00
Michael Hawkins ad5bc5b5ca MDL-68066 output: Apply disable lambda rendering to Mustache renderer 2022-09-07 23:57:05 +08:00
Lars Bonczek 013cb2fb64 MDL-68066 output: Mustache - Add option to disable lambda rendering
This commit is based on an outstanding pull request to the Mustache repo
2022-09-07 23:57:05 +08:00
stevando c4df60bb8d MDL-75283 admin: Update admin user fetching to use sort helper 2022-09-07 13:18:55 +08:00
Paul Holden 786a4956dd MDL-75405 block_html: safer unserialize during old block conversion. 2022-09-06 17:57:15 +02:00
Jun Pataleta 323e272c85 Merge branch 'MDL-75231-39' of https://github.com/lameze/moodle into MOODLE_39_STABLE 2022-09-06 23:49:01 +08:00
Sara Arjona 6e769797fd Merge branch 'MDL-75550-39' of https://github.com/mickhawkins/moodle into MOODLE_39_STABLE 2022-09-05 08:57:43 +02:00
Jun Pataleta 0a48ddad8f Merge branch 'MDL-75641_39' of https://github.com/stronk7/moodle into MOODLE_39_STABLE 2022-09-05 14:37:02 +08:00
Eloy Lafuente (stronk7) c939cd4b6d MDL-75641 core_lang: Add string for new Europe/Kyiv timezone
With IANA TZDB (timezone database) release 2022b, Europe/Kiev
has been renamed to Europe/Kyiv. So we need to add this string
to our list to keep tests passing.

Note that the old Europe/Kiev continues working (is an alias) and,
also, there are places in core where we are using it. Just we cannot
remove/replace those uses in core until we ensure that everybody is
using PHP 8.0.23 (TZDB 2022b) or more recent versions. Hence,
not changing those uses now.
2022-09-04 10:06:39 +02:00
Ilya Tregubov fd27acfcb1 weekly release 3.9.16+ 2022-09-02 08:49:23 +04:00
Jun Pataleta 9ae0e4a58b Merge branch 'MDL-75019-39' of https://github.com/andelacruz/moodle into MOODLE_39_STABLE 2022-08-31 20:24:37 +08:00
Ilya Tregubov 874472a85d Merge branch 'MDL-74861-39' of https://github.com/andelacruz/moodle into MOODLE_39_STABLE 2022-08-31 11:51:54 +04:00
Michael Hawkins 5f893f890f MDL-75550 lib: Update Mustache const to reflect correct version
Mustache version 2.14.1 is already included, but the VERSION const
incorrectly listed 2.14.0 in the tagged Mustache release. A later
upstream commit corrected this in the library, which this now matches.
2022-08-31 15:29:59 +08:00
Simey Lameze 69e03eacba MDL-75231 files: make bucket calculations more precise
This commit changes changes the unit test calculations to
keep track of the time the bucket starts/finishes bursting
and use milliseconds instead of seconds to have a precise
time to sleep.
2022-08-22 14:24:09 +08:00
Angelia Dela Cruz 1dfd13841a MDL-74861 behat: H5P behat generators use UI
Replace steps that manually add H5Pactivity instances via the UI
and use Behat generators. This improves the speed of the Behat test
runs.

Co-authored-by: Simey Lameze <simey@moodle.com>
2022-08-08 12:54:02 +08:00
Angelia Dela Cruz 5d15e96a98 MDL-75019 behat: Folder behat generators use UI
Replace steps that manually add Folder instances via the UI and
use Behat generators. This improves the speed of the Behat test
runs.
2022-08-05 15:53:57 +08:00
128 changed files with 2728 additions and 975 deletions
+7 -7
View File
@@ -13,14 +13,14 @@ env:
jobs:
Grunt:
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
steps:
- name: Checking out code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Configuring node & npm
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
@@ -54,10 +54,10 @@ jobs:
fail-fast: false
matrix:
include:
- os: ubuntu-18.04
- os: ubuntu-22.04
php: 7.2
db: mysqli
- os: ubuntu-18.04
- os: ubuntu-22.04
php: 7.4
db: pgsql
@@ -83,7 +83,7 @@ jobs:
postgresql password: test
- name: Configuring git vars
uses: rlespinasse/github-slug-action@v3.x
uses: rlespinasse/github-slug-action@v4
- name: Setting up PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
@@ -92,7 +92,7 @@ jobs:
coverage: none
- name: Checking out code from ${{ env.GITHUB_REF_SLUG }}
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Setting up PHPUnit
env:
+2 -5
View File
@@ -3371,6 +3371,7 @@
<VENDOR name="oracle" version="11.2" />
</DATABASE>
<PHP version="7.3.0" level="required">
<RESTRICT function="restrict_php_version_81" message="unsupportedphpversion81" />
</PHP>
<PCREUNICODE level="optional">
<FEEDBACK>
@@ -3557,6 +3558,7 @@
<VENDOR name="oracle" version="11.2" />
</DATABASE>
<PHP version="7.3.0" level="required">
<RESTRICT function="restrict_php_version_81" message="unsupportedphpversion81" />
</PHP>
<PCREUNICODE level="optional">
<FEEDBACK>
@@ -3781,11 +3783,6 @@
<ON_CHECK message="tokenizerrecommended" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="xmlrpc" level="optional">
<FEEDBACK>
<ON_CHECK message="xmlrpcrecommended" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="soap" level="optional">
<FEEDBACK>
<ON_CHECK message="soaprecommended" />
+12
View File
@@ -0,0 +1,12 @@
define("tool_behat/steps",["exports","core/ajax","core/templates","core/pending"],(function(_exports,_ajax,_templates,_pending){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function asyncGeneratorStep(gen,resolve,reject,_next,_throw,key,arg){try{var info=gen[key](arg),value=info.value}catch(error){return void reject(error)}info.done?resolve(value):Promise.resolve(value).then(_next,_throw)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_templates=_interopRequireDefault(_templates),_pending=_interopRequireDefault(_pending);
/**
* Enhancements for the step definitions page.
*
* @module tool_behat/steps
* @copyright 2022 Catalyst IT EU
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
var getRequiredFieldsContent=function(entityData){var _entityData$required;return null!==(_entityData$required=entityData.required)&&void 0!==_entityData$required&&_entityData$required.length?_templates.default.renderForPromise("tool_behat/steprequiredfields",{fields:entityData.required}):Promise.resolve({html:"",js:""})};_exports.init=function(){var fn,_ref;document.addEventListener("change",(fn=regeneratorRuntime.mark((function _callee(e){var entityElement,stepElement,pendingPromise,entityData,_yield$getRequiredFie,html,js,stepRequiredFields;return regeneratorRuntime.wrap((function(_context){for(;;)switch(_context.prev=_context.next){case 0:if(entityElement=e.target.closest(".entities"),stepElement=e.target.closest(".stepcontent"),entityElement&&stepElement){_context.next=4;break}return _context.abrupt("return");case 4:return pendingPromise=new _pending.default("tool_behat/steps:change"),_context.next=7,entityType=e.target.value,_ajax.default.call([{methodname:"tool_behat_get_entity_generator",args:{entitytype:entityType}}])[0];case 7:return entityData=_context.sent,_context.next=10,getRequiredFieldsContent(entityData);case 10:if(_yield$getRequiredFie=_context.sent,html=_yield$getRequiredFie.html,js=_yield$getRequiredFie.js,!(stepRequiredFields=stepElement.querySelector(".steprequiredfields"))){_context.next=19;break}return _context.next=17,_templates.default.replaceNode(stepRequiredFields,html,js);case 17:_context.next=21;break;case 19:return _context.next=21,_templates.default.appendNodeContents(stepElement,html,js);case 21:pendingPromise.resolve();case 22:case"end":return _context.stop()}var entityType}),_callee)})),_ref=function(){var self=this,args=arguments;return new Promise((function(resolve,reject){var gen=fn.apply(self,args);function _next(value){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"next",value)}function _throw(err){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"throw",err)}_next(void 0)}))},function(_x){return _ref.apply(this,arguments)}))}}));
//# sourceMappingURL=steps.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"steps.min.js","sources":["../src/steps.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\nimport Ajax from 'core/ajax';\nimport Templates from 'core/templates';\nimport PendingJS from 'core/pending';\n\n/**\n * Enhancements for the step definitions page.\n *\n * @module tool_behat/steps\n * @copyright 2022 Catalyst IT EU\n * @author Mark Johnson <mark.johnson@catalyst-eu.net>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * Call the get_entity_generator web service function\n *\n * Takes the name of an entity generator and returns an object containing a list of the required fields.\n *\n * @param {String} entityType\n * @returns {Promise}\n */\nconst getGeneratorEntities = (entityType) => Ajax.call([{\n methodname: 'tool_behat_get_entity_generator',\n args: {entitytype: entityType}\n}])[0];\n\n/**\n * Render HTML for required fields\n *\n * Takes the entity data returned from getGeneratorEntities and renders the HTML to display the required fields.\n *\n * @param {String} entityData\n * @return {Promise}\n */\nconst getRequiredFieldsContent = (entityData) => {\n if (!entityData.required?.length) {\n return Promise.resolve({\n html: '',\n js: ''\n });\n }\n return Templates.renderForPromise('tool_behat/steprequiredfields', {fields: entityData.required});\n};\n\nexport const init = () => {\n // When an entity is selected in the \"the following exist\" step, fetch and display the required fields.\n document.addEventListener('change', async(e) => {\n const entityElement = e.target.closest('.entities');\n const stepElement = e.target.closest('.stepcontent');\n if (!entityElement || !stepElement) {\n return;\n }\n\n const pendingPromise = new PendingJS('tool_behat/steps:change');\n\n const entityData = await getGeneratorEntities(e.target.value);\n const {html, js} = await getRequiredFieldsContent(entityData);\n\n const stepRequiredFields = stepElement.querySelector('.steprequiredfields');\n if (stepRequiredFields) {\n await Templates.replaceNode(stepRequiredFields, html, js);\n } else {\n await Templates.appendNodeContents(stepElement, html, js);\n }\n pendingPromise.resolve();\n });\n};\n"],"names":["getRequiredFieldsContent","entityData","required","_entityData$required","length","Templates","renderForPromise","fields","Promise","resolve","html","js","document","addEventListener","e","entityElement","target","closest","stepElement","pendingPromise","PendingJS","entityType","value","Ajax","call","methodname","args","entitytype","stepRequiredFields","querySelector","replaceNode","appendNodeContents"],"mappings":";;;;;;;;;IAiDMA,yBAA2B,SAACC,yEACzBA,WAAWC,0CAAXC,qBAAqBC,OAMnBC,mBAAUC,iBAAiB,gCAAiC,CAACC,OAAQN,WAAWC,WAL5EM,QAAQC,QAAQ,CACnBC,KAAM,GACNC,GAAI,oBAMI,uBAEhBC,SAASC,iBAAiB,sCAAU,iBAAMC,oNAChCC,cAAgBD,EAAEE,OAAOC,QAAQ,aACjCC,YAAcJ,EAAEE,OAAOC,QAAQ,gBAChCF,eAAkBG,kFAIjBC,eAAiB,IAAIC,iBAAU,2CAhCfC,WAkCwBP,EAAEE,OAAOM,MAlClBC,cAAKC,KAAK,CAAC,CACpDC,WAAY,kCACZC,KAAM,CAACC,WAAYN,eACnB,iBA+BUpB,0CACmBD,yBAAyBC,2DAA3CS,2BAAAA,KAAMC,yBAAAA,KAEPiB,mBAAqBV,YAAYW,cAAc,wEAE3CxB,mBAAUyB,YAAYF,mBAAoBlB,KAAMC,mEAEhDN,mBAAU0B,mBAAmBb,YAAaR,KAAMC,YAE1DQ,eAAeV,mDA3CM,IAACY"}
+82
View File
@@ -0,0 +1,82 @@
// 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/>.
import Ajax from 'core/ajax';
import Templates from 'core/templates';
import PendingJS from 'core/pending';
/**
* Enhancements for the step definitions page.
*
* @module tool_behat/steps
* @copyright 2022 Catalyst IT EU
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Call the get_entity_generator web service function
*
* Takes the name of an entity generator and returns an object containing a list of the required fields.
*
* @param {String} entityType
* @returns {Promise}
*/
const getGeneratorEntities = (entityType) => Ajax.call([{
methodname: 'tool_behat_get_entity_generator',
args: {entitytype: entityType}
}])[0];
/**
* Render HTML for required fields
*
* Takes the entity data returned from getGeneratorEntities and renders the HTML to display the required fields.
*
* @param {String} entityData
* @return {Promise}
*/
const getRequiredFieldsContent = (entityData) => {
if (!entityData.required?.length) {
return Promise.resolve({
html: '',
js: ''
});
}
return Templates.renderForPromise('tool_behat/steprequiredfields', {fields: entityData.required});
};
export const init = () => {
// When an entity is selected in the "the following exist" step, fetch and display the required fields.
document.addEventListener('change', async(e) => {
const entityElement = e.target.closest('.entities');
const stepElement = e.target.closest('.stepcontent');
if (!entityElement || !stepElement) {
return;
}
const pendingPromise = new PendingJS('tool_behat/steps:change');
const entityData = await getGeneratorEntities(e.target.value);
const {html, js} = await getRequiredFieldsContent(entityData);
const stepRequiredFields = stepElement.querySelector('.steprequiredfields');
if (stepRequiredFields) {
await Templates.replaceNode(stepRequiredFields, html, js);
} else {
await Templates.appendNodeContents(stepElement, html, js);
}
pendingPromise.resolve();
});
};
@@ -0,0 +1,92 @@
<?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/>.
/**
* Return data about an entity generator.
*
* @package tool_behat
* @copyright 2022 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_behat\external;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/externallib.php');
/**
* External function for getting properties of entity generators.
*/
class get_entity_generator extends \external_api {
/**
* Define parameters for external function.
*
* The parameter is either in the format 'entity' or 'component_name > entity'. There is no appropriate param type for a
* string like this containing angle brackets, so we will do PARAM_RAW. The value will be parsed by
* behat_data_generators::parse_entity_type, which validates the format of the parameter and throws an exception if it is not
* correct.
*
* @return \external_function_parameters
*/
public static function execute_parameters(): \external_function_parameters {
return new \external_function_parameters([
'entitytype' => new \external_value(PARAM_RAW, 'Entity type that can be created by a generator.'),
]);
}
/**
* Return a list of the required fields for a given entity type.
*
* @param string $entitytype
* @return array
*/
public static function execute(string $entitytype): array {
global $CFG;
// Ensure we can load Behat and Facebook namespaces in behat libraries.
require_once("{$CFG->dirroot}/vendor/autoload.php");
require_once("{$CFG->libdir}/tests/behat/behat_data_generators.php");
$params = self::validate_parameters(self::execute_parameters(), ['entitytype' => $entitytype]);
$context = \context_system::instance();
self::validate_context($context);
require_capability('moodle/site:config', $context);
$generators = new \behat_data_generators();
$entity = $generators->get_entity($params['entitytype']);
return ['required' => $entity['required']];
}
/**
* Define return values.
*
* Return required fields
*
* @return \external_single_structure
*/
public static function execute_returns(): \external_single_structure {
return new \external_single_structure([
'required' => new \external_multiple_structure(
new \external_value(PARAM_TEXT, 'Required field'),
'Required fields',
VALUE_OPTIONAL
),
]);
}
}
+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/>.
/**
* Define web service functions for tool_behat
*
* @package tool_behat
* @copyright 2022 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$functions = [
'tool_behat_get_entity_generator' => [
'classname' => 'tool_behat\\external\\get_entity_generator',
'methodname' => 'execute',
'description' => 'Get the generator details for an entity',
'type' => 'read',
'ajax' => true,
'capabilities' => 'moodle/site:config'
]
];
+1
View File
@@ -56,6 +56,7 @@ if ($components) {
$form = new steps_definitions_form(null, array('components' => $componentswithsteps));
// Output contents.
$PAGE->requires->js_call_amd('tool_behat/steps', 'init');
$renderer = $PAGE->get_renderer('tool_behat');
echo $renderer->render_stepsdefinitions($steps, $form);
+28
View File
@@ -24,6 +24,8 @@
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/behat/classes/behat_generator_base.php');
/**
* Renderer for behat tool web features
*
@@ -106,6 +108,32 @@ class tool_behat_renderer extends plugin_renderer_base {
},
$stepsdefinitions
);
$elementstrings = [];
$count = 1;
$stepsdefinitions = preg_replace_callback('/(the following ")ELEMENT\d?_STRING(" exist:)/',
function($matches) use (&$elementstrings, &$count) {
// Replace element type arguments with a user-friendly select.
if (empty($elementstrings)) {
$behatgenerators = new behat_data_generators();
$componententities = $behatgenerators->get_all_entities();
ksort($componententities);
$elementstrings = [];
foreach ($componententities as $component => $entities) {
asort($entities);
foreach ($entities as $entity) {
$string = ($component === 'core') ? $entity : $component . ' > ' . $entity;
$elementstrings[$string] = $string;
}
}
}
$select = html_writer::select($elementstrings, 'entities' . $count, '', ['' => 'choosedots'],
['class' => 'entities']);
$count++;
return $matches[1] . $select . $matches[2];
},
$stepsdefinitions
);
}
// Steps definitions.
+6
View File
@@ -29,3 +29,9 @@
#page-admin-tool-behat-index .steps-definitions .stepregex {
color: #060;
}
#page-admin-tool-behat-index .steprequiredfields {
font-weight: bold;
font-size: 1em;
margin-top: 1em;
}
@@ -0,0 +1,33 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template tool_behat/steprequiredfields
Display a Gherkin-style table row showing required columns for a step.
Example context (json):
{
"fields": [
"user",
"role",
"context"
]
}
}}
<pre class="steprequiredfields">
|{{#fields}} {{{.}}} |{{/fields}}
</pre>
@@ -13,6 +13,10 @@ Feature: List the system steps definitions
Scenario: Accessing the list
Then I should see "Step definitions"
And I should not see "There aren't steps definitions matching this filter"
And I should not see "the following \"ELEMENT_STRING\" exist:"
And "entities1" "select" should exist
And the "entities1" select box should contain "users"
And the "entities1" select box should contain "mod_assign > submissions"
@javascript
Scenario: Filtering by type
@@ -34,3 +38,11 @@ Feature: List the system steps definitions
Then I should not see "There aren't steps definitions matching this filter"
And I should see "Checks the provided element and selector type exists in the current page."
And I should see "Checks that an element and selector type exists in another element and selector type on the current page."
@javascript
Scenario: Get required fields
Given I set the field "entities1" to "users"
And I should see "| username |"
When I set the field "entities1" to "mod_quiz > user overrides"
Then I should not see "| username |"
And I should see "| quiz | user |"
@@ -0,0 +1,104 @@
<?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 get_entity_generator web service
*
* @package tool_behat
* @copyright 2022 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_behat\external;
/**
* Tests for get_entity_generator web service
*
* @covers \tool_behat\external\get_entity_generator
*/
class get_entity_generator_test extends \advanced_testcase {
/**
* Log in as admin
*
* @return void
*/
public function setUp(): void {
$this->resetAfterTest();
$this->setAdminUser();
}
/**
* Get the generator for a core entity.
*
* @return void
*/
public function test_execute_core_entity(): void {
$generator = get_entity_generator::execute('users');
$this->assertEquals(['required' => ['username']], $generator);
}
/**
* Get the generator for the plugin entity.
*
* @return void
*/
public function test_execute_plugin_entity(): void {
$generator = get_entity_generator::execute('mod_book > chapters');
$this->assertEquals(['required' => ['book', 'title', 'content']], $generator);
}
/**
* Get the generator for an entity with no required fields.
*
* @return void
*/
public function test_execute_no_requried(): void {
$generator = get_entity_generator::execute('mod_forum > posts');
$this->assertEquals(['required' => []], $generator);
}
/**
* Attempt to get the generator for a core entity that does not exist.
*
* @return void
*/
public function test_execute_invalid_entity(): void {
$this->expectException('coding_exception');
get_entity_generator::execute('foo');
}
/**
* Attempt to get a generator form a plugin that does not exist.
*
* @return void
*/
public function test_execute_invalid_plugin(): void {
$this->expectException('coding_exception');
get_entity_generator::execute('foo > bar');
}
/**
* Attempt to get a generator for an entity that does not exist, from a plugin that does.
*
* @return void
*/
public function test_execute_invalid_plugin_entity(): void {
$this->expectException('coding_exception');
get_entity_generator::execute('mod_book > bar');
}
}
+1 -1
View File
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2020061500; // The current plugin version (Date: YYYYMMDDXX)
$plugin->version = 2020061501; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2020060900; // Requires this Moodle version
$plugin->component = 'tool_behat'; // Full name of the plugin (used for diagnostics)
+14 -1
View File
@@ -79,6 +79,19 @@ class locale {
* @return string|false Returns the new current locale, or FALSE on error.
*/
protected function set_locale(int $category = LC_ALL, string $locale = '0') {
return setlocale($category, $locale);
if (strlen($locale) <= 255 || PHP_OS_FAMILY === 'BSD' || PHP_OS_FAMILY === 'Darwin') {
// We can set the whole locale all together.
return setlocale($category, $locale);
}
// Too long locale with linux or windows, let's split it into known and supported categories.
$split = explode(';', $locale);
foreach ($split as $element) {
[$category, $value] = explode('=', $element);
if (defined($category)) { // Only if the category exists, there are OS differences.
setlocale(constant($category), $value);
}
}
return setlocale(LC_ALL, 0); // Finally, return the complete configured locale.
}
}
@@ -27,6 +27,7 @@ defined('MOODLE_INTERNAL') || die();
/**
* Tests for \tool_langimport\locale class.
*
* @coversDefaultClass \tool_langimport\locale
* @copyright 2018 Université Rennes 2 {@link https://www.univ-rennes2.fr}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@@ -34,6 +35,7 @@ class locale_testcase extends \advanced_testcase {
/**
* Test that \tool_langimport\locale::check_locale_availability() works as expected.
*
* @covers ::check_locale_availability
* @return void
*/
public function test_check_locale_availability() {
@@ -68,4 +70,100 @@ class locale_testcase extends \advanced_testcase {
$this->expectException(coding_exception::class);
$locale->check_locale_availability('');
}
/**
* Test \tool_langimport\locale::set_locale() own logic.
*
* We have to explicitly test set_locale() own logic and results,
* that effectively sets the current locale, so we need to restore
* the original locale after every test (ugly, from a purist unit test
* point of view, but needed).
*
* @dataProvider set_locale_provider
* @covers ::set_locale
*
* @param string $set locale string to be set.
* @param string $ret expected results returned after setting the locale.
*/
public function test_set_locale(string $set, string $ret) {
// Make set_locale() public.
$loc = new \tool_langimport\locale();
$rc = new \ReflectionClass(\tool_langimport\locale::class);
$rm = $rc->getMethod('set_locale');
$rm->setAccessible(true);
// Capture current locale for later restore (funnily, using the set_locale() method itself.
$originallocale = $rm->invokeArgs($loc, [LC_ALL, 0]);
// Assert we get the locale defined as expected.
$this->assertEquals($ret, $rm->invokeArgs($loc, [LC_ALL, $set]));
// We have finished, restore the original locale, so this doesn't affect other tests at distance.
// (again, funnily, using the very same set_locale() method).
$rm->invokeArgs($loc, [LC_ALL, $originallocale]);
}
/**
* Data provider for test_set_locale().
*
* Provides a locale to be set (as 'set') and a expected return value (as 'ret'). Note that
* some of the locales are OS dependent, so only the ones matching the OS will be provided.
*
* We make extensive use of the en_AU.UTF-8/English_Australia.1252 locale that is mandatory to
* be installed in any system running PHPUnit tests.
*/
public function set_locale_provider(): array {
// Let's list the allowed categories by OS.
$bsdallowed = ['LC_COLLATE', 'LC_CTYPE', 'LC_MESSAGES', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME'];
$winallowed = ['LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME'];
$linuxallowed = [
'LC_COLLATE', 'LC_CTYPE', 'LC_MESSAGES', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME',
'LC_PAPER', 'LC_NAME', 'LC_ADDRESS', 'LC_TELEPHONE', 'LC_MEASUREMENT', 'LC_IDENTIFICATION'
];
// The base locale name is also OS dependent.
$baselocale = get_string('locale', 'langconfig');
if (PHP_OS_FAMILY === 'Windows') {
$baselocale = get_string('localewin', 'langconfig');
}
// Here we'll go accumulating cases to be provided.
$cases = [];
// First, the simplest case, just pass a locale name, without categories.
$cases['rawlocale'] = [
'set' => $baselocale,
'ret' => $baselocale,
];
// Now, let's fill ALL LC categories, we should get back the locale name if all them are set with same value.
// Note that this case is the one that, under Linux only, covers the changes performed to the set_locale() method.
// Pick the correct categories depending on the OS.
$oscategories = $bsdallowed; // Default to BSD/Dawrwin ones because they are the standard 6 supported by PHP.
if (PHP_OS_FAMILY === 'Windows') {
$oscategories = $winallowed;
} else if (PHP_OS_FAMILY === 'Linux') {
$oscategories = $linuxallowed;
}
$localestr = '';
foreach ($oscategories as $category) {
// Format is different by OS too, so let build the string conditionally.
if (PHP_OS_FAMILY === 'BSD' || PHP_OS_FAMILY === 'Darwin') {
// BSD uses slashes (/) separated list of the 6 values in exact order.
$localestr .= '/' . $baselocale;
} else {
// Linux/Windows use semicolon (;) separated list of category=value pairs.
$localestr .= ';' . $category . '=' . $baselocale;
}
}
$cases['allcategories'] = [
'set' => trim($localestr, ';/'),
'ret' => $baselocale,
];
// Return all the built cases.
return $cases;
}
}
@@ -77,9 +77,10 @@
ucw.registerEvents('[data-region="user-competency-status"]');
ucw.on('status-changed', infoReloader.reload.bind(infoReloader));
ucw.on('error-occured', infoReloader.reload.bind(infoReloader));
var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '{{plan.id}}', '', '{{#str}}chooserating, tool_lp{{/str}}');
inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader));
{{#cangrade}}
var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '{{plan.id}}', '', '{{#str}}chooserating, tool_lp{{/str}}');
inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader));
{{/cangrade}}
});
{{/js}}
{{/usercompetency}}
@@ -95,13 +95,14 @@
{{#js}}
require(['jquery', 'tool_lp/grade_user_competency_inline', 'tool_lp/user_competency_info'], function($, mod, info) {
var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '', '{{course.id}}', '{{#str}}chooserating, tool_lp{{/str}}');
var competencyElement = $('[data-region-id="{{uniqid}}"]');
var displayuser = ('{{displayuser}}' == 'true') ? true : false;
var infoReloader = new info(competencyElement, '{{competency.competency.id}}', '{{user.id}}', '', '{{course.id}}', displayuser);
inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader));
{{#cangrade}}
var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '', '{{course.id}}', '{{#str}}chooserating, tool_lp{{/str}}');
inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader));
{{/cangrade}}
});
{{/js}}
+1 -1
View File
@@ -48,7 +48,7 @@ class backup_files_edit_form extends moodleform {
$mform->setType('component', PARAM_COMPONENT);
$mform->addElement('hidden', 'returnurl', $this->_customdata['returnurl']);
$mform->setType('returnurl', PARAM_URL);
$mform->setType('returnurl', PARAM_LOCALURL);
$this->add_action_buttons(true, get_string('savechanges'));
$this->set_data($this->_customdata['data']);
+1 -1
View File
@@ -643,7 +643,7 @@ class backup_ui_stage_complete extends backup_ui_stage_final {
}
$output .= $renderer->get_samesite_notification();
$output .= $renderer->notification(get_string('executionsuccess', 'backup'), 'notifysuccess');
$output .= $renderer->continue_button($restorerul);
$output .= $renderer->continue_button($restorerul, 'get');
$output .= $renderer->box_end();
return $output;
+2 -2
View File
@@ -553,7 +553,8 @@ class core_backup_renderer extends plugin_renderer_base {
}
/**
* Displays a continue button
* Displays a continue button, overriding core renderer method of the same in order
* to override submission method of the button form
*
* @param string|moodle_url $url
* @param string $method
@@ -566,7 +567,6 @@ class core_backup_renderer extends plugin_renderer_base {
if ($method != 'post') {
$method = 'get';
}
$url->param('sesskey', sesskey());
$button = new single_button($url, get_string('continue'), $method, true);
$button->class = 'continuebutton';
return $this->render($button);
+4 -2
View File
@@ -1161,8 +1161,10 @@ class restore_ui_stage_complete extends restore_ui_stage_process {
$html .= $renderer->notification(get_string('restorefileweremissing', 'backup'), 'notifyproblem');
}
$html .= $renderer->notification(get_string('restoreexecutionsuccess', 'backup'), 'notifysuccess');
$html .= $renderer->continue_button(new moodle_url('/course/view.php', array(
'id' => $this->get_ui()->get_controller()->get_courseid())), 'get');
$courseurl = course_get_url($this->get_ui()->get_controller()->get_courseid());
$html .= $renderer->continue_button($courseurl, 'get');
$html .= $renderer->box_end();
return $html;
+2 -2
View File
@@ -36,7 +36,7 @@ class moodle1_block_html_handler extends moodle1_block_handler {
$contextid = $this->converter->get_contextid(CONTEXT_BLOCK, $olddata['id']);
$decodeddata = base64_decode($olddata['configdata']);
list($updated, $configdata) = upgrade_fix_serialized_objects($decodeddata);
$configdata = unserialize($configdata);
$configdata = unserialize_object($configdata);
// get a fresh new file manager for this instance
$this->fileman = $this->converter->get_file_manager($contextid, 'block_html');
@@ -44,7 +44,7 @@ class moodle1_block_html_handler extends moodle1_block_handler {
// convert course files embedded in the block content
$this->fileman->filearea = 'content';
$this->fileman->itemid = 0;
$configdata->text = moodle1_converter::migrate_referenced_files($configdata->text, $this->fileman);
$configdata->text = moodle1_converter::migrate_referenced_files($configdata->text ?? '', $this->fileman);
$configdata->format = FORMAT_HTML;
return base64_encode(serialize($configdata));
+6 -7
View File
@@ -14,12 +14,12 @@ Feature: Block appearances
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | course | name | element |
| survey | C1 | Test survey name | 4 |
And I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
And I add a "Survey" to section "1" and I fill the form with:
| Name | Test survey name |
| Survey type | ATTLS (20 item version) |
| Description | Test survey description |
And I add a "Book" to section "1" and I fill the form with:
| Name | Test book name |
| Description | Test book description |
@@ -28,9 +28,8 @@ Feature: Block appearances
| Chapter title | Book title |
| Content | Book content test test |
And I press "Save changes"
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I am on the "Course 1" course page logged in as teacher1
And I turn editing mode on
And I add the "Comments" block
And I configure the "Comments" block
And I set the following fields to these values:
+3 -4
View File
@@ -14,12 +14,11 @@ Feature: Block region moving
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | course | template |
| survey | Test survey name | C1 | 4 |
And I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
And I add a "Survey" to section "1" and I fill the form with:
| Name | Test survey name |
| Survey type | ATTLS (20 item version) |
| Description | Test survey description |
And I add a "Book" to section "1" and I fill the form with:
| Name | Test book name |
| Description | Test book description |
+1 -1
View File
@@ -64,7 +64,7 @@ class blog_edit_external_form extends moodleform {
$mform->setDefault('id', 0);
$mform->addElement('hidden', 'returnurl');
$mform->setType('returnurl', PARAM_URL);
$mform->setType('returnurl', PARAM_LOCALURL);
$mform->setDefault('returnurl', 0);
}
+5 -5
View File
@@ -7,13 +7,13 @@
"require-dev": {
"phpunit/phpunit": "7.5.*",
"phpunit/dbunit": "4.0.*",
"mikey179/vfsstream": "^1.6",
"behat/mink": "~1.8",
"mikey179/vfsstream": "1.6.*",
"behat/mink": "^1.10.0",
"behat/mink-extension": "~2.3",
"behat/mink-goutte-driver": "~1.2",
"behat/mink-goutte-driver": "^1.3",
"symfony/process": "^4.0 || ^5.0",
"behat/behat": "3.10.*",
"oleg-andreyev/mink-phpwebdriver": "^1.0"
"behat/behat": "3.11.*",
"oleg-andreyev/mink-phpwebdriver": "^1.2.1"
},
"autoload-dev": {
"psr-0": {
Generated
+385 -338
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -34,7 +34,7 @@ require_once($CFG->dirroot.'/course/lib.php');
$id = required_param('id', PARAM_INT);
$switchrole = optional_param('switchrole', -1, PARAM_INT);
$returnurl = optional_param('returnurl', '', PARAM_RAW);
$returnurl = optional_param('returnurl', '', PARAM_LOCALURL);
if (strpos($returnurl, '?') === false) {
// Looks like somebody did not set proper page url, better go to course page.
@@ -43,7 +43,7 @@ if (strpos($returnurl, '?') === false) {
if (strpos($returnurl, $CFG->wwwroot) !== 0) {
$returnurl = $CFG->wwwroot.$returnurl;
}
$returnurl = clean_param($returnurl, PARAM_URL);
$returnurl = clean_param($returnurl, PARAM_LOCALURL);
}
$PAGE->set_url('/course/switchrole.php', array('id'=>$id, 'switchrole'=>$switchrole));
@@ -14,13 +14,10 @@ Feature: View subfolders in a course in-line
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I add a "Folder" to section "3" and I fill the form with:
| Name | Test folder |
| Display folder contents | On a separate page |
| Show subfolders expanded | |
And I am on the "Test folder" "folder activity" page
And the following "activities" exist:
| activity | course | name | display | showexpanded |
| folder | C1 | Test folder | 0 | 0 |
And I am on the "Test folder" "folder activity" page logged in as "teacher1"
And I press "Edit"
And I press "Create folder"
And I set the field "New folder name" to "Test subfolder 1"
+1 -2
View File
@@ -33,7 +33,6 @@ use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\writer;
use core_privacy\manager;
use Horde\Socket\Client\Exception;
/**
* Class provider
@@ -196,7 +195,7 @@ class provider implements
$field = array_key_exists($record->fieldid, $fields) ? $fields[$record->fieldid] : null;
$data = data_controller::create(0, $record, $field);
self::export_customfield_data($data, array_merge($subcontext, [$record->id]));
} catch (Exception $e) {
} catch (\Exception $e) {
// We store some data that we can not initialise controller for. We still need to export it.
self::export_customfield_data_unknown($record, $field, array_merge($subcontext, [$record->id]));
}
@@ -1,4 +1,5 @@
@customfield @customfield_textarea @javascript @_file_upload
# This feature has Atto-specific steps. See MDL-75913 for further details.
@customfield @customfield_textarea @javascript @_file_upload @editor_atto
Feature: Default value for the textarea custom field can contain images
In order to see images on custom fields
As a manager
+1 -2
View File
@@ -28,9 +28,8 @@ Feature: Users can be added to multiple groups at once
And I am on "Course 1" course homepage
And I follow "Participants"
And I click on "Edit groups for \"Student 1\"" "link" in the "student1" "table_row"
And I click on ".form-autocomplete-downarrow" "css_element" in the "student1" "table_row"
And I open the autocomplete suggestions list in the "student1" "table_row"
And I click on "Group 1" item in the autocomplete list
And I click on ".form-autocomplete-downarrow" "css_element" in the "student1" "table_row"
And I click on "Group 2" item in the autocomplete list
And I press the escape key
And I click on "Save changes" "link" in the "student1" "table_row"
@@ -49,7 +49,8 @@ Feature: Render H5P content using filters
And I wait until the page is ready
Then ".h5p-iframe" "css_element" should not exist
@javascript
# This scenario has Atto-specific steps. See MDL-75913 for further details.
@javascript @editor_atto
Scenario: Render a local H5P file as admin
Given I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
@@ -83,7 +84,8 @@ Feature: Render H5P content using filters
And I should not see "you don't have access"
And I should see "Lorum ipsum"
@javascript
# This scenario has Atto-specific steps. See MDL-75913 for further details.
@javascript @editor_atto
Scenario: Render a local H5P file as teacher
Given I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
@@ -107,7 +109,8 @@ Feature: Render H5P content using filters
Then I should see "Note that the libraries may exist in the file you uploaded, but you're not allowed to upload new libraries."
And I should see "missing-required-library"
@javascript
# This scenario has Atto-specific steps. See MDL-75913 for further details.
@javascript @editor_atto
Scenario: Render a local H5P file with existing libraries
Given I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
+8 -13
View File
@@ -26,8 +26,7 @@ Feature: We can set the grade to pass value
| name | Test Assignment 1 |
| intro | Submit your online text |
| assignsubmission_onlinetext_enabled | 1 |
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I am on the "Course 1" course page logged in as teacher1
@javascript
Scenario: Validate that switching the type of grading used correctly disables grade to pass
@@ -115,11 +114,10 @@ Feature: We can set the grade to pass value
Scenario: Set a valid grade to pass for workshop activity
Given the following "activities" exist:
| activity | name | intro | course | section | idnumber |
| workshop | Test Workshop 1 | Test workshop | C1 | 1 | workshop1 |
| activity | name | course | idnumber |
| workshop | Test Workshop 1 | C1 | workshop1 |
And I am on "Course 1" course homepage with editing mode on
And I follow "Test Workshop 1"
And I navigate to "Edit settings" in current page administration
And I am on the "Test Workshop 1" "workshop activity editing" page
And I set the following fields to these values:
| grade | 80 |
| Submission grade to pass | 40 |
@@ -138,19 +136,16 @@ Feature: We can set the grade to pass value
And the field "Grade to pass" matches value "10"
And I set the field "Grade to pass" to "15"
And I press "Save changes"
And I am on "Course 1" course homepage
And I follow "Test Workshop 1"
And I follow "Edit settings"
And I am on the "Test Workshop 1" "workshop activity editing" page
And the field "Submission grade to pass" matches value "45"
And the field "Assessment grade to pass" matches value "15"
Scenario: Set an invalid grade to pass for workshop activity
Given the following "activities" exist:
| activity | name | intro | course | section | idnumber |
| workshop | Test Workshop 1 | Test workshop | C1 | 1 | workshop1 |
| activity | name | course | idnumber |
| workshop | Test Workshop 1 | C1 | workshop1 |
And I am on "Course 1" course homepage with editing mode on
And I follow "Test Workshop 1"
And I navigate to "Edit settings" in current page administration
And I am on the "Test Workshop 1" "workshop activity editing" page
And I set the following fields to these values:
| grade | 80 |
| Submission grade to pass | 90 |
+7 -6
View File
@@ -1346,12 +1346,13 @@ $string['unsupporteddbfilepertable'] = 'For full support of UTF-8 both MySQL and
$string['unsupporteddblargeprefix'] = 'For full support of UTF-8 both MySQL and MariaDB require you to change your MySQL setting \'innodb_large_prefix\' to \'ON\'. See the documentation for further details.';
$string['unsupporteddbstorageengine'] = 'The database storage engine being used is no longer supported.';
$string['unsupporteddbtablerowformat'] = 'Your database has tables using Antelope as the file format. You are recommended to convert the tables to the Barracuda file format. See the documentation <a href="https://docs.moodle.org/en/cli">Administration via command line</a> for details of a tool for converting InnoDB tables to Barracuda.';
$string['unsupportedphpversion7'] = 'PHP version 7 is not supported.';
$string['unsupportedphpversion71'] = 'PHP version 7.1 is not supported.';
$string['unsupportedphpversion72'] = 'PHP version 7.2 is not supported.';
$string['unsupportedphpversion73'] = 'PHP version 7.3 is not supported.';
$string['unsupportedphpversion74'] = 'PHP version 7.4 is not supported.';
$string['unsupportedphpversion80'] = 'PHP version 8.0 is not supported.';
$string['unsupportedphpversion7'] = 'PHP version 7 and higher are not supported.';
$string['unsupportedphpversion71'] = 'PHP version 7.1 and higher are not supported.';
$string['unsupportedphpversion72'] = 'PHP version 7.2 and higher are not supported.';
$string['unsupportedphpversion73'] = 'PHP version 7.3 and higher are not supported.';
$string['unsupportedphpversion74'] = 'PHP version 7.4 and higher are not supported.';
$string['unsupportedphpversion80'] = 'PHP version 8.0 and higher are not supported.';
$string['unsupportedphpversion81'] = 'PHP version 8.1 and higher are not supported.';
$string['unsuspenduser'] = 'Activate user account';
$string['updateaccounts'] = 'Update existing accounts';
$string['updatecomponent'] = 'Update component';
+2
View File
@@ -112,6 +112,7 @@ $string['america/cayenne'] = 'America/Cayenne';
$string['america/cayman'] = 'America/Cayman';
$string['america/chicago'] = 'America/Chicago';
$string['america/chihuahua'] = 'America/Chihuahua';
$string['america/ciudad_juarez'] = 'America/Ciudad_Juarez';
$string['america/costa_rica'] = 'America/Costa_Rica';
$string['america/creston'] = 'America/Creston';
$string['america/cuiaba'] = 'America/Cuiaba';
@@ -363,6 +364,7 @@ $string['europe/jersey'] = 'Europe/Jersey';
$string['europe/kaliningrad'] = 'Europe/Kaliningrad';
$string['europe/kiev'] = 'Europe/Kiev';
$string['europe/kirov'] = 'Europe/Kirov';
$string['europe/kyiv'] = 'Europe/Kyiv';
$string['europe/lisbon'] = 'Europe/Lisbon';
$string['europe/ljubljana'] = 'Europe/Ljubljana';
$string['europe/london'] = 'Europe/London';
+30 -1
View File
@@ -333,6 +333,34 @@ class behat_config_util {
return $this->get_component_contexts($component);
}
/**
* Sort the list of components contexts.
*
* This ensures that contexts are sorted consistently.
* Core hooks defined in the behat_hooks class _must_ be defined first.
*
* @param array $contexts
* @return array The sorted context list
*/
protected function sort_component_contexts(array $contexts): array {
// Ensure that the lib_tests are first as they include the root of all tests, hooks, and more.
uksort($contexts, function($a, $b): int {
if ($a === 'behat_hooks') {
return -1;
}
if ($b === 'behat_hooks') {
return 1;
}
if ($a == $b) {
return 0;
}
return ($a < $b) ? -1 : 1;
});
return $contexts;
}
/**
* Behat config file specifing the main context class,
* the required Behat extensions and Moodle test wwwroot.
@@ -1537,8 +1565,9 @@ class behat_config_util {
}
// Sort the list of contexts.
ksort($contexts);
$contexts = $this->sort_component_contexts($contexts);
// Cache it for subsequent fetches.
$this->themecontexts[$theme] = $contexts;
return $contexts;
@@ -26,6 +26,7 @@
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
use Behat\Testwork\Environment\Environment;
use Behat\Mink\Exception\DriverException;
/**
* Helper to get behat contexts.
@@ -115,6 +116,34 @@ class behat_context_helper {
return null;
}
/**
* Find all Behat contexts which match the specified context class name prefix.
*
* Moodle uses a consistent class naming scheme for all Behat contexts, whereby the context name is in the format:
*
* behat_{component}
*
* This method will return all contexts which match the specified prefix.
*
* For example, to find all editors, you would pass in 'behat_editor', and this might return:
* - behat_editor_atto
* - behat_editor_textarea
*
* @param string $prefix The prefix to search for
* @return \Behat\Behat\Context\Context[]
*/
public static function get_prefixed_contexts(string $prefix): array {
if (!is_a(self::$environment, \Behat\Behat\Context\Environment\InitializedContextEnvironment::class)) {
throw new DriverException(
'Cannot get prefixed contexts - the environment is not an InitializedContextEnvironment'
);
}
return array_filter(self::$environment->getContexts(), function($context) use ($prefix): bool {
return (strpos(get_class($context), $prefix) === 0);
});
}
/**
* Check for any theme override of the specified class name.
*
@@ -165,6 +165,15 @@ abstract class behat_generator_base {
*/
protected abstract function get_creatable_entities(): array;
/**
* Get the list of available generators for this class.
*
* @return array
*/
final public function get_available_generators(): array {
return $this->get_creatable_entities();
}
/**
* Do the work to generate an entity.
*
+100 -2
View File
@@ -30,6 +30,7 @@ use Behat\Mink\Exception\ExpectationException;
use Behat\Mink\Exception\ElementNotFoundException;
use Behat\Mink\Exception\NoSuchWindowException;
use Behat\Mink\Session;
use Behat\Testwork\Hook\Scope\HookScope;
use Facebook\WebDriver\Exception\ScriptTimeoutException;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverElement;
@@ -223,6 +224,22 @@ trait behat_session_trait {
];
}
/**
* Get a description of the selector and locator to use in an exception message.
*
* @param string $selector The type of locator
* @param mixed $locator The locator text
* @return string
*/
protected function get_selector_description(string $selector, $locator): string {
if ($selector === 'NodeElement') {
$description = $locator->getText();
return "'{$description}' {$selector}";
}
return "'{$locator}' {$selector}";
}
/**
* Send key presses straight to the currently active element.
*
@@ -423,13 +440,12 @@ trait behat_session_trait {
if ($containerselectortype === 'NodeElement' && is_a($containerelement, NodeElement::class)) {
// Support a NodeElement being passed in for use in step chaining.
$containernode = $containerelement;
$locatorexceptionmsg = $element;
} else {
// Gets the container, it will always be text based.
$containernode = $this->get_text_selector_node($containerselectortype, $containerelement);
$locatorexceptionmsg = $element . '" in the "' . $containerelement. '" "' . $containerselectortype. '"';
}
$locatorexceptionmsg = $element . '" in the "' . $this->get_selector_description($containerselectortype, $containerelement);
$exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $locatorexceptionmsg);
return $this->find($selectortype, $element, $exception, $containernode);
@@ -1029,6 +1045,29 @@ EOF;
$this->look_for_exceptions();
}
/**
* Execute a function in a specific behat context.
*
* For example, to call the 'set_editor_value' function for all editors, you would call:
*
* behat_base::execute_in_matching_contexts('editor', 'set_editor_value', ['Some value']);
*
* This would find all behat contexts whose class name starts with 'behat_editor_' and
* call the 'set_editor_value' function on that context.
*
* @param string $prefix
* @param string $method
* @param array $params
*/
public static function execute_in_matching_contexts(string $prefix, string $method, array $params): void {
$contexts = behat_context_helper::get_prefixed_contexts("behat_{$prefix}_");
foreach ($contexts as $context) {
if (method_exists($context, $method) && is_callable([$context, $method])) {
call_user_func_array([$context, $method], $params);
}
}
}
/**
* Get the actual user in the behat session (note $USER does not correspond to the behat session's user).
* @return mixed
@@ -1578,4 +1617,63 @@ EOF;
return get_fast_modinfo($course)->get_cm($result->cmid);
}
/**
* Check whether any of the tags availble to the current scope match using the given callable.
*
* This function is typically called from within a Behat Hook, such as BeforeFeature, BeforeScenario, AfterStep, etc.
*
* The callable is used as the second argument to `array_filter()`, and is passed a single string argument for each of the
* tags available in the scope.
*
* The tags passed will include:
* - For a FeatureScope, the Feature tags only
* - For a ScenarioScope, the Feature and Scenario tags
* - For a StepScope, the Feature, Scenario, and Step tags
*
* An example usage may be:
*
* // Note: phpDoc beforeStep attribution not shown.
* public function before_step(StepScope $scope) {
* $callback = function (string $tag): bool {
* return $tag === 'editor_atto' || substr($tag, 0, 5) === 'atto_';
* };
*
* if (!self::scope_tags_match($scope, $callback)) {
* return;
* }
*
* // Do something here.
* }
*
* @param HookScope $scope The scope to check
* @param callable $callback The callable to use to check the scope
* @return boolean Whether any of the scope tags match
*/
public static function scope_tags_match(HookScope $scope, callable $callback): bool {
$tags = [];
if (is_subclass_of($scope, \Behat\Behat\Hook\Scope\FeatureScope::class)) {
$tags = $scope->getFeature()->getTags();
}
if (is_subclass_of($scope, \Behat\Behat\Hook\Scope\ScenarioScope::class)) {
$tags = array_merge(
$scope->getFeature()->getTags(),
$scope->getScenario()->getTags()
);
}
if (is_subclass_of($scope, \Behat\Behat\Hook\Scope\StepScope::class)) {
$tags = array_merge(
$scope->getFeature()->getTags(),
$scope->getScenario()->getTags(),
$scope->getStep()->getTags()
);
}
$matches = array_filter($tags, $callback);
return !empty($matches);
}
}
@@ -86,6 +86,7 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
*/
protected static $allowedselectors = array(
'activity' => 'activity',
'actionmenu' => 'actionmenu',
'block' => 'block',
'button' => 'button',
'checkbox' => 'checkbox',
@@ -136,6 +137,17 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
protected static $moodleselectors = array(
'activity' => <<<XPATH
.//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][descendant::*[contains(normalize-space(.), %locator%)]]
XPATH
, 'actionmenu' => <<<XPATH
.//*[
contains(concat(' ', normalize-space(@class), ' '), ' action-menu ')
and
descendant::*[
contains(concat(' ', normalize-space(@class), ' '), ' dropdown-toggle ')
and
(contains(normalize-space(.), %locator%) or descendant::*[%titleMatch%])
]
]
XPATH
, 'block' => <<<XPATH
.//*[@data-block][contains(concat(' ', normalize-space(@class), ' '), concat(' ', %locator%, ' ')) or
+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/>.
/**
* Editor interface for setting editor values.
*
* @package behat
* @category test
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_behat;
interface settable_editor {
/**
* Set the editor value.
*
* @param string $editorid The id of the editor within the page
* @param string $value The intended content of the editor
*/
public function set_editor_value(string $editorid, string $value): void;
}
+78 -18
View File
@@ -32,7 +32,6 @@ require_once(__DIR__ . '/behat_form_textarea.php');
/**
* Moodle editor field.
*
* @todo Support for multiple editors
* @package core_form
* @category test
* @copyright 2012 David Monllaó
@@ -44,31 +43,40 @@ class behat_form_editor extends behat_form_textarea {
* Sets the value to a field.
*
* @param string $value
* @return void
*/
public function set_value($value) {
public function set_value($value): void {
$editorid = $this->field->getAttribute('id');
if ($this->running_javascript()) {
$value = addslashes($value);
// This will be transported in JSON, which doesn't allow newlines in strings, so we must escape them.
$value = str_replace("\n", "\\n", $value);
$js = '
(function() {
var editor = Y.one(document.getElementById("'.$editorid.'editable"));
if (editor) {
editor.setHTML("' . $value . '");
}
editor = Y.one(document.getElementById("'.$editorid.'"));
editor.set("value", "' . $value . '");
})();
';
behat_base::execute_script_in_session($this->session, $js);
behat_base::execute_in_matching_contexts('editor', 'set_editor_value', [
$editorid,
$value,
]);
} else {
parent::set_value($value);
}
}
/**
* Returns the current value of the select element.
*
* @return string
*/
public function get_value(): string {
if ($this->running_javascript()) {
// Give any listening editors a chance to persist the value to the textarea.
// Some editors only do this on form submission or similar events.
behat_base::execute_in_matching_contexts('editor', 'store_current_value', [
$this->field->getAttribute('id'),
]);
}
return parent::get_value();
}
/**
* Select all the text in the form field.
*
@@ -102,8 +110,60 @@ class behat_form_editor extends behat_form_textarea {
* @return bool The provided value matches the field value?
*/
public function matches($expectedvalue) {
// A text editor may silently wrap the content in p tags (or not). Neither is an error.
return $this->text_matches($expectedvalue) || $this->text_matches('<p>' . $expectedvalue . '</p>');
// Fetch the actual value to save fetching it multiple times.
$actualvalue = $this->get_value();
if ($this->text_matches($expectedvalue, $actualvalue)) {
// The text is an exact match already.
return true;
}
if ($this->text_matches("<p>{$expectedvalue}</p>", $actualvalue)) {
// A text editor may silently wrap the content in p tags.
return true;
}
// Standardise both the expected value and the actual field value.
// We are likely dealing with HTML content, given this is an editor.
$expectedvalue = $this->standardise_html($expectedvalue);
$actualvalue = $this->standardise_html($actualvalue);
// Note: We don't need to worry about the floats here that we care about in text_matches.
// That condition isn't relevant to the content of an editor.
if ($expectedvalue === $actualvalue) {
return true;
}
return false;
}
/**
* Standardises the HTML content for comparison.
*
* @param string $html The HTML content to standardise
* @return string The standardised HTML content
*/
protected function standardise_html(string $html): string {
$document = new DOMDocument();
$errorstate = libxml_use_internal_errors(true);
// Format the whitespace nicely.
$document->preserveWhiteSpace = false;
$document->formatOutput = true;
// Wrap the content in a DIV element so that it is not parsed weirdly.
// Note: We must remove newlines too because DOMDocument does not do so, despite preserveWhiteSpace being false.
// Unfortunately this is slightly limited in that it will also remove newlines from <pre> content and similar.
$document->loadHTML(str_replace("\n", "", "<div>{$html}</div>"), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
$document->normalizeDocument();
libxml_clear_errors();
libxml_use_internal_errors($errorstate);
// Save the content of the 'div' element, removing the <div> and </div> tags at the start and end.
return trim(substr(
$document->saveHTML($document->getElementsByTagName('div')->item(0)),
5,
-6
));
}
}
+6 -2
View File
@@ -244,10 +244,14 @@ class behat_form_field implements behat_session_interface {
* Checks if the provided text matches the field value.
*
* @param string $expectedvalue
* @param string|null $actualvalue The actual value. If not specified, this will be fetched from $this->get_value().
* @return bool
*/
protected function text_matches($expectedvalue) {
if (trim($expectedvalue) != trim($this->get_value())) {
protected function text_matches($expectedvalue, ?string $actualvalue = null): bool {
$actualvalue = $actualvalue ?? $this->get_value();
// Non strict string comparison.
if (trim($expectedvalue) != trim($actualvalue)) {
return false;
}
return true;
+3 -2
View File
@@ -991,8 +991,9 @@ class core_user {
$preferences['user_home_page_preference'] = array('type' => PARAM_INT, 'null' => NULL_ALLOWED, 'default' => HOMEPAGE_MY,
'choices' => array(HOMEPAGE_SITE, HOMEPAGE_MY),
'permissioncallback' => function ($user, $preferencename) {
global $CFG;
return (!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_USER));
global $CFG, $USER;
return $user->id == $USER->id &&
(!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_USER));
}
);
+13 -7
View File
@@ -506,21 +506,27 @@ function get_users_listing($sort='lastaccess', $dir='ASC', $page=0, $recordsperp
$params = $params + (array)$extraparams;
}
if ($sort) {
$sort = " ORDER BY $sort $dir";
}
// If a context is specified, get extra user fields that the current user
// is supposed to see.
$extrafields = '';
$includedfields = ['id', 'username', 'email', 'firstname', 'lastname', 'city', 'country',
'lastaccess', 'confirmed', 'mnethostid', 'suspended'];
if ($extracontext) {
$extrafields = get_extra_user_fields_sql($extracontext, '', '',
array('id', 'username', 'email', 'firstname', 'lastname', 'city', 'country',
'lastaccess', 'confirmed', 'mnethostid'));
$extrafields = get_extra_user_fields_sql($extracontext, '', '', $includedfields);
}
$namefields = get_all_user_name_fields(true);
$extrafields = "$extrafields, $namefields";
if ($sort) {
$orderbymap = trim($extrafields, ', ');
$orderbymap = array_merge(explode(',', $orderbymap), $includedfields);
$neworderbymap = ['default' => 'lastaccess'];
foreach($orderbymap as $value) {
$neworderbymap[$value] = $value;
}
$sort = get_safe_orderby($neworderbymap, $sort, $dir);
}
// warning: will return UNCONFIRMED USERS
return $DB->get_records_sql("SELECT id, username, email, city, country, lastaccess, confirmed, mnethostid, suspended $extrafields
FROM {user}
@@ -23,9 +23,12 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/../../../../behat/behat_base.php');
require_once(__DIR__ . '/../../../../behat/classes/settable_editor.php');
/**
* Steps definitions to deal with the atto text editor
@@ -35,7 +38,30 @@ require_once(__DIR__ . '/../../../../behat/behat_base.php');
* @copyright 2014 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_editor_atto extends behat_base {
class behat_editor_atto extends behat_base implements \core_behat\settable_editor {
/**
* Set the value for the editor.
*
* @param string $editorid
* @param string $value
*/
public function set_editor_value(string $editorid, string $value): void {
$js = <<<EOF
(function() {
const editableEditor = document.getElementById("${editorid}editable");
if (editableEditor && editableEditor.classList.contains('editor_atto_content')) {
editableEditor.innerHTML = "${value}";
}
const editor = document.getElementById("${editorid}");
if (editor) {
editor.value = "${value}";
}
})();
EOF;
$this->execute_script($js);
}
/**
* Select the text in an Atto field.
@@ -59,6 +85,24 @@ class behat_editor_atto extends behat_base {
$field->select_text();
}
/**
* Ensure that the Atto editor is used for all tests using the editor_atto, or atto_* tags.
*
* This ensures, whatever the default editor, that the Atto editor is used for these tests.
*
* @BeforeScenario
* @param BeforeScenarioScope $scope The Behat Scope
*/
public function set_default_editor_flag(BeforeScenarioScope $scope): void {
// This only applies to a scenario which matches the editor_atto, or an atto subplugin.
$callback = function (string $tag): bool {
return $tag === 'editor_atto' || substr($tag, 0, 5) === 'atto_';
};
if (!self::scope_tags_match($scope, $callback)) {
return;
}
$this->execute('behat_general::the_default_editor_is_set_to', ['atto']);
}
}
@@ -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/>.
/**
* Moodle textarea editor field.
*
* @package core_form
* @category test
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../../behat/behat_base.php');
require_once(__DIR__ . '/../../../../behat/classes/settable_editor.php');
class behat_editor_textarea extends behat_base implements \core_behat\settable_editor {
/**
* Set the value for the editor.
*
* @param string $editorid
* @param string $value
*/
public function set_editor_value(string $editorid, string $value): void {
$js = <<<EOF
(function() {
const editor = document.getElementById("${editorid}");
if (editor && editor.tagName.toLowerCase() === 'textarea') {
editor.value = "${value}";
}
})();
EOF;
$this->execute_script($js);
}
}
+11
View File
@@ -1657,3 +1657,14 @@ function restrict_php_version_74(&$result) {
function restrict_php_version_80($result) {
return restrict_php_version($result, '8.0');
}
/**
* Check if the current PHP version is greater than or equal to
* PHP version 8.1
*
* @param object $result an environment_results instance
* @return bool result of version check
*/
function restrict_php_version_81($result) {
return restrict_php_version($result, '8.1');
}
+5 -1
View File
@@ -3265,7 +3265,11 @@ class curl {
throw new coding_exception('Curl options should be defined using strings, not constant values.');
}
if (stripos($name, 'CURLOPT_') === false) {
$name = strtoupper('CURLOPT_'.$name);
// Only prefix with CURLOPT_ if the option doesn't contain CURLINFO_,
// which is a valid prefix for at least one option CURLINFO_HEADER_OUT.
if (stripos($name, 'CURLINFO_') === false) {
$name = strtoupper('CURLOPT_'.$name);
}
} else {
$name = strtoupper($name);
}
+13
View File
@@ -64,6 +64,9 @@ class MoodleQuickForm_editor extends HTML_QuickForm_element implements templatab
/** @var array values for editor */
protected $_values = array('text'=>null, 'format'=>null, 'itemid'=>null);
/** @var bool if true label will be hidden */
protected $_hiddenLabel = false;
/**
* Constructor
*
@@ -496,4 +499,14 @@ class MoodleQuickForm_editor extends HTML_QuickForm_element implements templatab
return '';
}
/**
* Sets label to be hidden.
*
* @param bool $hiddenLabel Whether the label should be hidden or not.
* @return void
*/
function setHiddenLabel($hiddenLabel) {
$this->_hiddenLabel = $hiddenLabel;
}
}
@@ -0,0 +1,5 @@
{{< core_form/element-template-inline }}
{{$element}}
{{{element.html}}}
{{/element}}
{{/ core_form/element-template-inline }}
+2
View File
@@ -16,6 +16,8 @@ These changes can be reverted once the following pull requests have been integra
This local changes can be reverted once it's checked that they are present upstream (note the
LTI-Tool-Provider-Library-PHP repo has been archived so it doesn't accept pull requests anymore):
* MDL-67034 php74 compliance fixes
* MDL-71920: Migrated from curl_exec and friends to use our Moodle curl wrapper,
so we can better handle site security settings
It is recommended by upstream to install depdencies via composer - but the composer installation is bundled
with an autoloader so it's better to do it manually.
+28 -20
View File
@@ -2,6 +2,9 @@
namespace IMSGlobal\LTI;
global $CFG;
require_once($CFG->libdir . '/filelib.php');
/**
* Class to represent an HTTP message
*
@@ -110,31 +113,33 @@ class HTTPMessage
{
$this->ok = false;
// Try using curl if available
// Try using curl if available
if (function_exists('curl_init')) {
$resp = '';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->url);
$chResp = '';
$curl = new \curl();
$options = [
'CURLOPT_HEADER' => true,
'CURLINFO_HEADER_OUT' => true,
];
if (!empty($this->requestHeaders)) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->requestHeaders);
$options['CURLOPT_HTTPHEADER'] = $this->requestHeaders;
} else {
curl_setopt($ch, CURLOPT_HEADER, 0);
$options['CURLOPT_HEADER'] = 0;
}
if ($this->method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->request);
$chResp = $curl->post($this->url, $this->request, $options);
} else if ($this->method !== 'GET') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->method);
if (!is_null($this->request)) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->request);
$chResp = $curl->post($this->url, $this->request, $options);
}
} else {
$chResp = $curl->get($this->url, null, $options);
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
curl_setopt($ch, CURLOPT_HEADER, true);
$chResp = curl_exec($ch);
$this->ok = $chResp !== false;
if ($this->ok) {
$info = $curl->get_info();
if (!$curl->get_errno() && !$curl->error) {
$chResp = str_replace("\r\n", "\n", $chResp);
$chRespSplit = explode("\n\n", $chResp, 2);
if ((count($chRespSplit) > 1) && (substr($chRespSplit[1], 0, 5) === 'HTTP/')) {
@@ -142,17 +147,20 @@ class HTTPMessage
}
$this->responseHeaders = $chRespSplit[0];
$resp = $chRespSplit[1];
$this->status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$this->status = $info['http_code'];
$this->ok = $this->status < 400;
if (!$this->ok) {
$this->error = curl_error($ch);
$this->error = $curl->error;
}
} else {
$this->error = $curl->error;
$resp = $chResp;
}
$this->requestHeaders = str_replace("\r\n", "\n", curl_getinfo($ch, CURLINFO_HEADER_OUT));
curl_close($ch);
$this->response = $resp;
$this->requestHeaders = !empty($info['request_header']) ? str_replace("\r\n", "\n", $info['request_header']) : '';
} else {
// Try using fopen if curl was not available
// Try using fopen if curl was not available
$opts = array('method' => $this->method,
'content' => $this->request
);
+2 -1
View File
@@ -1063,8 +1063,9 @@ function clean_param($param, $type) {
} else if (preg_match('/^' . preg_quote($CFG->wwwroot . '/', '/') . '/i', $param)) {
// Absolute, and matches our wwwroot.
} else {
// Relative - let's make sure there are no tricks.
if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?')) {
if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?') && !preg_match('/javascript:/i', $param)) {
// Looks ok.
} else {
$param = '';
+2
View File
@@ -18,3 +18,5 @@ Note: All this changes need to be reviewed on every upgrade and, if they have
been already applied upstream for the release being used, can be removed
from the list. If still not available upstream, they will need to be re-applied.
1) If the relevant pull request has not been accepted yet, apply the following commit, so we are able to disable unnecessary rendering:
https://github.com/bobthecow/mustache.php/pull/402/commits/db771014c7e346438f68077813ebdda3fdae12df#
+53 -26
View File
@@ -26,31 +26,34 @@ class Mustache_Compiler
private $entityFlags;
private $charset;
private $strictCallables;
private $disableLambdaRendering;
/**
* Compile a Mustache token parse tree into PHP source code.
*
* @param string $source Mustache Template source code
* @param string $tree Parse tree of Mustache tokens
* @param string $name Mustache Template class name
* @param bool $customEscape (default: false)
* @param string $charset (default: 'UTF-8')
* @param bool $strictCallables (default: false)
* @param int $entityFlags (default: ENT_COMPAT)
* @param string $source Mustache Template source code
* @param array $tree Parse tree of Mustache tokens
* @param string $name Mustache Template class name
* @param bool $customEscape (default: false)
* @param string $charset (default: 'UTF-8')
* @param bool $strictCallables (default: false)
* @param int $entityFlags (default: ENT_COMPAT)
* @param bool $disableLambdaRendering (default: false)
*
* @return string Generated PHP source code
*/
public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT)
public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT, $disableLambdaRendering = false)
{
$this->pragmas = $this->defaultPragmas;
$this->sections = array();
$this->blocks = array();
$this->source = $source;
$this->indentNextLine = true;
$this->customEscape = $customEscape;
$this->entityFlags = $entityFlags;
$this->charset = $charset;
$this->strictCallables = $strictCallables;
$this->pragmas = $this->defaultPragmas;
$this->sections = array();
$this->blocks = array();
$this->source = $source;
$this->indentNextLine = true;
$this->customEscape = $customEscape;
$this->entityFlags = $entityFlags;
$this->charset = $charset;
$this->strictCallables = $strictCallables;
$this->disableLambdaRendering = $disableLambdaRendering;
return $this->writeCode($tree, $name);
}
@@ -331,14 +334,8 @@ class Mustache_Compiler
if (%s) {
$source = %s;
$result = (string) call_user_func($value, $source, %s);
if (strpos($result, \'{{\') === false) {
$buffer .= $result;
} else {
$buffer .= $this->mustache
->loadLambda($result%s)
->renderInternal($context);
}
$result = (string) call_user_func($value, $source, %s);%s
$buffer .= $result;
} elseif (!empty($value)) {
$values = $this->isIterable($value) ? $value : array($value);
foreach ($values as $value) {
@@ -352,6 +349,36 @@ class Mustache_Compiler
}
';
const SECTION_RENDER_LAMBDA = '
if (strpos($result, \'{{\') !== false) {
$result = $this->mustache
->loadLambda($result%s)
->renderInternal($context);
}
';
/**
* Helper function to compile section with and without lambda rendering.
*
* @param string $key
* @param string $callable
* @param string $source
* @param string $helper
* @param string $delims
* @param string $content
*
* @return string section code
*/
private function getSection($key, $callable, $source, $helper, $delims, $content)
{
$render = '';
if (!$this->disableLambdaRendering) {
$render = sprintf($this->prepare(self::SECTION_RENDER_LAMBDA, 2), $delims);
}
return sprintf($this->prepare(self::SECTION), $key, $callable, $source, $helper, $render, $content);
}
/**
* Generate Mustache Template section PHP source.
*
@@ -383,7 +410,7 @@ class Mustache_Compiler
$key = ucfirst(md5($delims . "\n" . $source));
if (!isset($this->sections[$key])) {
$this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $helper, $delims, $this->walk($nodes, 2));
$this->sections[$key] = $this->getSection($key, $callable, $source, $helper, $delims, $this->walk($nodes, 2));
}
$method = $this->getFindMethod($id);
+21 -10
View File
@@ -23,7 +23,7 @@
*/
class Mustache_Engine
{
const VERSION = '2.14.0';
const VERSION = '2.14.1';
const SPEC_VERSION = '1.2.2';
const PRAGMA_FILTERS = 'FILTERS';
@@ -53,6 +53,7 @@ class Mustache_Engine
private $charset = 'UTF-8';
private $logger;
private $strictCallables = false;
private $disableLambdaRendering = false;
private $pragmas = array();
private $delimiters;
@@ -130,6 +131,11 @@ class Mustache_Engine
* // This currently defaults to false, but will default to true in v3.0.
* 'strict_callables' => true,
*
* // Do not render the output of lambdas. Use this to prevent repeated rendering if the lambda already
* // takes care of rendering its content. This helps protect against mustache code injection when user
* // input is passed directly into the template. Defaults to false.
* 'disable_lambda_rendering' => true,
*
* // Enable pragmas across all templates, regardless of the presence of pragma tags in the individual
* // templates.
* 'pragmas' => [Mustache_Engine::PRAGMA_FILTERS],
@@ -204,6 +210,10 @@ class Mustache_Engine
$this->strictCallables = $options['strict_callables'];
}
if (isset($options['disable_lambda_rendering'])) {
$this->disableLambdaRendering = $options['disable_lambda_rendering'];
}
if (isset($options['delimiters'])) {
$this->delimiters = $options['delimiters'];
}
@@ -624,14 +634,15 @@ class Mustache_Engine
//
// Keep this list in alphabetical order :)
$chunks = array(
'charset' => $this->charset,
'delimiters' => $this->delimiters ? $this->delimiters : '{{ }}',
'entityFlags' => $this->entityFlags,
'escape' => isset($this->escape) ? 'custom' : 'default',
'key' => ($source instanceof Mustache_Source) ? $source->getKey() : 'source',
'pragmas' => $this->getPragmas(),
'strictCallables' => $this->strictCallables,
'version' => self::VERSION,
'charset' => $this->charset,
'delimiters' => $this->delimiters ? $this->delimiters : '{{ }}',
'entityFlags' => $this->entityFlags,
'escape' => isset($this->escape) ? 'custom' : 'default',
'key' => ($source instanceof Mustache_Source) ? $source->getKey() : 'source',
'pragmas' => $this->getPragmas(),
'strictCallables' => $this->strictCallables,
'disableLambdaRendering' => $this->disableLambdaRendering,
'version' => self::VERSION,
);
$key = json_encode($chunks);
@@ -810,7 +821,7 @@ class Mustache_Engine
$compiler = $this->getCompiler();
$compiler->setPragmas($this->getPragmas());
return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags);
return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags, $this->disableLambdaRendering);
}
/**
+4 -1
View File
@@ -129,7 +129,10 @@ class renderer_base {
// Don't allow the JavaScript helper to be executed from within another
// helper. If it's allowed it can be used by users to inject malicious
// JS into the page.
'blacklistednestedhelpers' => ['js']));
'blacklistednestedhelpers' => ['js'],
// Disable lambda rendering - content in helpers is already rendered, no need to render it again.
'disable_lambda_rendering' => true,
));
}
+1 -10
View File
@@ -14,15 +14,6 @@
// 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 core_table\local\filter\filter.
*
* @package core_table
* @category test
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
*/
declare(strict_types=1);
namespace core_table\local\filter;
@@ -33,11 +24,11 @@ use InvalidArgumentException;
/**
* Unit tests for core_table\local\filter\filter.
*
* @coversDefaultClass \core_table\local\filter\filter
* @package core_table
* @category test
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core_table\local\filter\filter
*/
class filter_test extends advanced_testcase {
/**
+116
View File
@@ -1307,4 +1307,120 @@ EOD;
"data generators yet. Class {$classname} not found.");
}
/**
* Create a new category for custom profile fields.
*
* @param array $data Array with 'name' and optionally 'sortorder'
* @return \stdClass New category object
*/
public function create_custom_profile_field_category(array $data): \stdClass {
global $DB;
// Pick next sortorder if not defined.
if (!array_key_exists('sortorder', $data)) {
$data['sortorder'] = (int)$DB->get_field_sql('SELECT MAX(sortorder) FROM {user_info_category}') + 1;
}
$category = (object)[
'name' => $data['name'],
'sortorder' => $data['sortorder']
];
$category->id = $DB->insert_record('user_info_category', $category);
return $category;
}
/**
* Creates a new custom profile field.
*
* Optional fields are:
*
* categoryid (or use 'category' to specify by name). If you don't specify
* either, it will add the field to a 'Testing' category, which will be created for you if
* necessary.
*
* sortorder (if you don't specify this, it will pick the next one in the category).
*
* all the other database fields (if you don't specify this, it will pick sensible defaults
* based on the data type).
*
* @param array $data Array with 'datatype', 'shortname', and 'name'
* @return \stdClass Database object from the user_info_field table
*/
public function create_custom_profile_field(array $data): \stdClass {
global $DB, $CFG;
require_once($CFG->dirroot . '/user/profile/lib.php');
// Set up category if necessary.
if (!array_key_exists('categoryid', $data)) {
if (array_key_exists('category', $data)) {
$data['categoryid'] = $DB->get_field('user_info_category', 'id',
['name' => $data['category']], MUST_EXIST);
} else {
// Make up a 'Testing' category or use existing.
$data['categoryid'] = $DB->get_field('user_info_category', 'id', ['name' => 'Testing']);
if (!$data['categoryid']) {
$created = $this->create_custom_profile_field_category(['name' => 'Testing']);
$data['categoryid'] = $created->id;
}
}
}
// Pick sort order if necessary.
if (!array_key_exists('sortorder', $data)) {
$data['sortorder'] = (int)$DB->get_field_sql(
'SELECT MAX(sortorder) FROM {user_info_field} WHERE categoryid = ?',
[$data['categoryid']]) + 1;
}
// Defaults for other values.
$defaults = [
'description' => '',
'descriptionformat' => 0,
'required' => 0,
'locked' => 0,
'visible' => PROFILE_VISIBLE_ALL,
'forceunique' => 0,
'signup' => 0,
'defaultdata' => '',
'defaultdataformat' => 0,
'param1' => '',
'param2' => '',
'param3' => '',
'param4' => '',
'param5' => ''
];
// Type-specific defaults for other values.
$typedefaults = [
'text' => [
'param1' => 30,
'param2' => 2048
],
'menu' => [
'param1' => "Yes\nNo",
'defaultdata' => 'No'
],
'datetime' => [
'param1' => '2010',
'param2' => '2015',
'param3' => 1
],
'checkbox' => [
'defaultdata' => 0
]
];
foreach ($typedefaults[$data['datatype']] ?? [] as $field => $value) {
$defaults[$field] = $value;
}
foreach ($defaults as $field => $value) {
if (!array_key_exists($field, $data)) {
$data[$field] = $value;
}
}
$data['id'] = $DB->insert_record('user_info_field', $data);
return (object)$data;
}
}
+120 -2
View File
@@ -27,8 +27,8 @@
require_once(__DIR__ . '/../../behat/behat_base.php');
use Behat\Mink\Exception\ExpectationException as ExpectationException;
use Behat\Mink\Exception\DriverException as DriverException;
use Behat\Mink\Element\NodeElement;
use Behat\Mink\Exception\DriverException;
/**
* Steps definitions to open and close action menus.
@@ -78,4 +78,122 @@ class behat_action_menu extends behat_base {
$this->ensure_node_is_visible($node);
$node->click();
}
/**
* Select a specific item in an action menu.
*
* @When /^I choose the "(?P<item_string>(?:[^"]|\\")*)" item in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu$/
* @param string $item The item to choose
* @param string $actionmenu The text used in the description of the action menu
*/
public function i_choose_in_the_named_menu(string $item, string $actionmenu): void {
$menu = $this->find('actionmenu', $actionmenu);
$this->select_item_in_action_menu($item, $menu);
}
/**
* Select a specific item in an action menu within a container.
*
* @When /^I choose the "(?P<item_string>(?:[^"]|\\")*)" item in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu of the "(?P<locator_string>(?:[^"]|\\")*)" "(?P<type_string>(?:[^"]|\\")*)"$/
* @param string $item The item to choose
* @param string $actionmenu The text used in the description of the action menu
* @param string|NodeElement $locator The identifer used for the container
* @param string $selector The type of container to locate
*/
public function i_choose_in_the_named_menu_in_container(string $item, string $actionmenu, $locator, $selector): void {
$container = $this->find($selector, $locator);
$menu = $this->find('actionmenu', $actionmenu, false, $container);
$this->select_item_in_action_menu($item, $menu);
}
/**
* Select an item in the specified menu.
*
* Note: This step does work both with, and without, JavaScript.
*
* @param string $item Item string value
* @param NodeElement $menu The menu NodeElement to select from
*/
protected function select_item_in_action_menu(string $item, NodeElement $menu): void {
if ($this->running_javascript()) {
// Open the menu by clicking on the trigger.
$this->execute(
'behat_general::i_click_on',
[$menu, "NodeElement"]
);
}
// Select the menu item.
$this->execute(
'behat_general::i_click_on_in_the',
[$item, "link", $menu, "NodeElement"]
);
}
/**
* The action menu item should not exist.
*
* @Then /^the "(?P<item_string>(?:[^"]|\\")*)" item should not exist in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu$/
* @param string $item The item to check
* @param string $actionmenu The text used in the description of the action menu
*/
public function item_should_not_exist(string $item, string $actionmenu): void {
$menu = $this->find('actionmenu', $actionmenu);
$this->execute('behat_general::should_not_exist_in_the', [
$item, 'link',
$menu, 'NodeElement'
]);
}
/**
* The action menu item should not exist within a container.
*
* @Then /^the "(?P<item_string>(?:[^"]|\\")*)" item should not exist in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu of the "(?P<locator_string>(?:[^"]|\\")*)" "(?P<type_string>(?:[^"]|\\")*)"$/
* @param string $item The item to check
* @param string $actionmenu The text used in the description of the action menu
* @param string|NodeElement $locator The identifer used for the container
* @param string $selector The type of container to locate
*/
public function item_should_not_exist_in_the(string $item, string $actionmenu, $locator, $selector): void {
$container = $this->find($selector, $locator);
$menu = $this->find('actionmenu', $actionmenu, false, $container);
$this->execute('behat_general::should_not_exist_in_the', [
$item, 'link',
$menu, 'NodeElement'
]);
}
/**
* The action menu item should exist.
*
* @Then /^the "(?P<item_string>(?:[^"]|\\")*)" item should exist in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu$/
* @param string $item The item to check
* @param string $actionmenu The text used in the description of the action menu
*/
public function item_should_exist(string $item, string $actionmenu): void {
$menu = $this->find('actionmenu', $actionmenu);
$this->execute('behat_general::should_exist_in_the', [
$item, 'link',
$menu, 'NodeElement'
]);
}
/**
* The action menu item should exist within a container.
*
* @Then /^the "(?P<item_string>(?:[^"]|\\")*)" item should exist in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu of the "(?P<locator_string>(?:[^"]|\\")*)" "(?P<type_string>(?:[^"]|\\")*)"$/
* @param string $item The item to check
* @param string $actionmenu The text used in the description of the action menu
* @param string|NodeElement $locator The identifer used for the container
* @param string $selector The type of container to locate
*/
public function item_should_exist_in_the(string $item, string $actionmenu, $locator, $selector): void {
$container = $this->find($selector, $locator);
$menu = $this->find('actionmenu', $actionmenu, false, $container);
$this->execute('behat_general::should_exist_in_the', [
$item, 'link',
$menu, 'NodeElement'
]);
}
}
+47
View File
@@ -170,4 +170,51 @@ class behat_data_generators extends behat_base {
$instance = new $componentclass($component);
return $instance;
}
/**
* Get all entities that can be created in all components using the_following_entities_exist()
*
* @return array
* @throws coding_exception
*/
public function get_all_entities(): array {
global $CFG;
// Ensure the generator class is loaded.
require_once($CFG->libdir . '/behat/classes/behat_generator_base.php');
$componenttypes = core_component::get_component_list();
$coregenerator = $this->get_instance_for_component('core');
$pluginswithentities = ['core' => array_keys($coregenerator->get_available_generators())];
foreach ($componenttypes as $components) {
foreach ($components as $component => $componentdir) {
try {
$plugingenerator = $this->get_instance_for_component($component);
$entities = array_keys($plugingenerator->get_available_generators());
if (!empty($entities)) {
$pluginswithentities[$component] = $entities;
}
} catch (Exception $e) {
// The component has no generator, skip it.
continue;
}
}
}
return $pluginswithentities;
}
/**
* Get the required fields for a specific creatable entity.
*
* @param string $entitytype
* @return mixed
* @throws coding_exception
*/
public function get_entity(string $entitytype): array {
[$component, $entity] = $this->parse_entity_type($entitytype);
$generator = $this->get_instance_for_component($component);
$entities = $generator->get_available_generators();
if (!array_key_exists($entity, $entities)) {
throw new coding_exception('No generator for ' . $entity . ' in component ' . $component);
}
return $entities[$entity];
}
}
+1 -1
View File
@@ -718,7 +718,7 @@ class behat_forms extends behat_base {
public function i_open_the_autocomplete_suggestions_list($container = null, $containertype = null) {
$csstarget = ".form-autocomplete-downarrow";
if ($container && $containertype) {
$this->execute('behat_general::i_click_on', [$csstarget, 'css_element', $container, $containertype]);
$this->execute('behat_general::i_click_on_in_the', [$csstarget, 'css_element', $container, $containertype]);
} else {
$this->execute('behat_general::i_click_on', [$csstarget, 'css_element']);
}
+104 -15
View File
@@ -28,6 +28,7 @@
require_once(__DIR__ . '/../../behat/behat_base.php');
use Behat\Gherkin\Node\TableNode;
use Behat\Mink\Element\NodeElement;
use Behat\Mink\Exception\DriverException;
use Behat\Mink\Exception\ElementNotFoundException;
use Behat\Mink\Exception\ExpectationException;
@@ -187,6 +188,22 @@ class behat_general extends behat_base {
);
}
/**
* Switches to a second window.
*
* @Given /^I switch to a second window$/
* @throws DriverException If there aren't exactly 2 windows open.
*/
public function switch_to_second_window() {
$names = $this->getSession()->getWindowNames();
if (count($names) !== 2) {
throw new DriverException('Expected to see 2 windows open, found ' . count($names));
}
$this->getSession()->switchToWindow($names[1]);
}
/**
* Switches to the main Moodle frame.
*
@@ -1146,19 +1163,12 @@ EOF;
* @throws ElementNotFoundException Thrown by behat_base::find
* @param string $element The locator of the specified selector
* @param string $selectortype The selector type
* @param string $containerelement The container selector type
* @param string $containerselectortype The container locator
* @param NodeElement|string $containerelement The locator of the container selector
* @param string $containerselectortype The container selector type
*/
public function should_exist_in_the($element, $selectortype, $containerelement, $containerselectortype) {
// Get the container node.
$containernode = $this->find($containerselectortype, $containerelement);
// Specific exception giving info about where can't we find the element.
$locatorexceptionmsg = "{$element} in the {$containerelement} {$containerselectortype}";
$exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $locatorexceptionmsg);
// Looks for the requested node inside the container node.
$this->find($selectortype, $element, $exception, $containernode);
// Will throw an ElementNotFoundException if it does not exist.
$this->get_node_in_container($selectortype, $element, $containerselectortype, $containerelement);
}
/**
@@ -1170,8 +1180,8 @@ EOF;
* @throws ExpectationException
* @param string $element The locator of the specified selector
* @param string $selectortype The selector type
* @param string $containerelement The container selector type
* @param string $containerselectortype The container locator
* @param NodeElement|string $containerelement The locator of the container selector
* @param string $containerselectortype The container selector type
*/
public function should_not_exist_in_the($element, $selectortype, $containerelement, $containerselectortype) {
// Get the container node.
@@ -1188,8 +1198,10 @@ EOF;
}
// The element was found and should not have been. Throw an exception.
$elementdescription = $this->get_selector_description($selectortype, $element);
$containerdescription = $this->get_selector_description($containerselectortype, $containerelement);
throw new ExpectationException(
"The '{$element}' '{$selectortype}' exists in the '{$containerelement}' '{$containerselectortype}'",
"The {$elementdescription} exists in the {$containerdescription}",
$this->getSession()
);
}
@@ -1713,7 +1725,17 @@ EOF;
* @param string $keys The key, or list of keys, to type
*/
public function i_type(string $keys): void {
behat_base::type_keys($this->getSession(), str_split($keys));
// Certain keys, such as the newline character, must be converted to the appropriate character code.
// Without this, keys will behave differently depending on the browser.
$keylist = array_map(function($key): string {
switch ($key) {
case "\n":
return behat_keys::ENTER;
default:
return $key;
}
}, str_split($keys));
behat_base::type_keys($this->getSession(), $keylist);
}
/**
@@ -2099,4 +2121,71 @@ EOF;
public function i_mark_this_test_as_long_running(int $factor = 2): void {
$this->set_test_timeout_factor($factor);
}
/**
* Set the default text editor to the named text editor.
*
* @Given the default editor is set to :editor
* @param string $editor
* @throws ExpectationException If the specified editor is not available.
*/
public function the_default_editor_is_set_to(string $editor): void {
global $CFG;
// Check if the provided editor is available.
if (!array_key_exists($editor, editors_get_available())) {
throw new ExpectationException(
"Unable to set the editor to {$editor} as it is not installed. The available editors are: " .
implode(', ', array_keys(editors_get_available())),
$this->getSession()
);
}
// Make the provided editor the default one in $CFG->texteditors by
// moving it to the first [editor],atto,tiny,tinymce,textarea on the list.
$list = explode(',', $CFG->texteditors);
array_unshift($list, $editor);
$list = array_unique($list);
// Set the list new list of editors.
set_config('texteditors', implode(',', $list));
}
/**
* Allow to check for minimal Moodle version.
*
* @Given the site is running Moodle version :minversion or higher
* @param string $minversion The minimum version of Moodle required (inclusive).
*/
public function the_site_is_running_moodle_version_or_higher(string $minversion): void {
global $CFG;
require_once($CFG->libdir . '/environmentlib.php');
$currentversion = normalize_version(get_config('', 'release'));
if (version_compare($currentversion, $minversion, '<')) {
throw new Moodle\BehatExtension\Exception\SkippedException(
'Site must be running Moodle version ' . $minversion . ' or higher'
);
}
}
/**
* Allow to check for maximum Moodle version.
*
* @Given the site is running Moodle version :maxversion or lower
* @param string $maxversion The maximum version of Moodle required (inclusive).
*/
public function the_site_is_running_moodle_version_or_lower(string $maxversion): void {
global $CFG;
require_once($CFG->libdir . '/environmentlib.php');
$currentversion = normalize_version(get_config('', 'release'));
if (version_compare($currentversion, $maxversion, '>')) {
throw new Moodle\BehatExtension\Exception\SkippedException(
'Site must be running Moodle version ' . $maxversion . ' or lower'
);
}
}
}
+27
View File
@@ -0,0 +1,27 @@
@core
Feature: Check for minimum or maximimum version of Moodle
In order adapt acceptance tests for different versions of Moodle
As a developer
I should be able to skip tests according to the Moodle version present on a site
Scenario: Minimum version too low
Given the site is running Moodle version 99.0 or higher
# The following steps should not be executed. If they are, the test will fail.
When I log in as "admin"
Then I should not see "Moodle"
Scenario: Maximum version too high
Given the site is running Moodle version 3.0 or lower
# The following steps should not be executed. If they are, the test will fail.
When I log in as "admin"
Then I should not see "Moodle"
Scenario: Minimum version OK
Given the site is running Moodle version 3.0 or higher
When I log in as "admin"
Then I should see "Moodle"
Scenario: Maximum version OK
Given the site is running Moodle version 99.0 or lower
When I log in as "admin"
Then I should see "Moodle"
@@ -90,7 +90,7 @@ class core_renderer_template_exploit_testcase extends advanced_testcase {
'test1' => $norender,
],
'js' => 'some nasty JS',
'expected' => 'core, move, some text',
'expected' => 'core, move, some text {{}}',
'include' => false
],
'js helper not nested' => [
@@ -133,7 +133,7 @@ class core_renderer_template_exploit_testcase extends advanced_testcase {
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move,',
'expected' => 'core, move, {{}}',
'include' => false
],
'js in context double depth with single render' => [
@@ -149,7 +149,7 @@ class core_renderer_template_exploit_testcase extends advanced_testcase {
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move, {{#js}} some nasty JS {{/js}}',
'expected' => 'core, move, {{second}}',
'include' => false
],
'js in context double depth with recursive render' => [
@@ -269,24 +269,7 @@ class core_renderer_template_exploit_testcase extends advanced_testcase {
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move,',
'include' => false
],
'partial double nested with js from context single render' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{foo}}{{/testpix}}',
'test2' => '{{#js}} some nasty JS {{/js}}',
],
'torender' => 'test',
'context' => [
'foo' => '{{{bar}}}',
'bar' => '{{> test2}}'
],
'helpers' => [
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move, {{> test2}}',
'expected' => 'core, move, {{&gt; test2}}',
'include' => false
],
'partial double nested with js from context recursive render' => [
+102
View File
@@ -684,6 +684,108 @@ class core_datalib_testcase extends advanced_testcase {
}
}
/**
* Tests the get_users_listing function.
*/
public function test_get_users_listing(): void {
global $DB;
$this->resetAfterTest();
$generator = $this->getDataGenerator();
// Set up profile field.
$generator->create_custom_profile_field(['datatype' => 'text',
'shortname' => 'specialid', 'name' => 'Special user id']);
// Set up the show user identity option.
set_config('showuseridentity', 'department');
// Get all the existing user ids (we're going to remove these from test results).
$existingids = array_fill_keys($DB->get_fieldset_select('user', 'id', '1 = 1'), true);
// Create some test user accounts.
$userids = [];
foreach (['a', 'b', 'c', 'd'] as $key) {
$record = [
'username' => 'user_' . $key,
'firstname' => $key . '_first',
'lastname' => 'last_' . $key,
'department' => 'department_' . $key,
'lastaccess' => ord($key)
];
$user = $generator->create_user($record);
$userids[] = $user->id;
}
// Check default result with no parameters.
$results = get_users_listing();
$results = array_diff_key($results, $existingids);
// It should return all the results in order.
$this->assertEquals($userids, array_keys($results));
// Results should have some general fields and name fields, check some samples.
$this->assertEquals('user_a', $results[$userids[0]]->username);
$this->assertEquals('user_a@example.com', $results[$userids[0]]->email);
$this->assertEquals(1, $results[$userids[0]]->confirmed);
$this->assertEquals('a_first', $results[$userids[0]]->firstname);
$this->assertObjectHasAttribute('firstnamephonetic', $results[$userids[0]]);
// Should not have department because no context specified.
$this->assertObjectNotHasAttribute('department', $results[$userids[0]]);
// Check sorting.
$results = get_users_listing('username', 'DESC');
$results = array_diff_key($results, $existingids);
$this->assertEquals([$userids[3], $userids[2], $userids[1], $userids[0]], array_keys($results));
// Check default fallback sort field works as expected.
$results = get_users_listing('blah2', 'ASC');
$results = array_diff_key($results, $existingids);
$this->assertEquals([$userids[0], $userids[1], $userids[2], $userids[3]], array_keys($results));
// Check default fallback sort direction works as expected.
$results = get_users_listing('lastaccess', 'blah2');
$results = array_diff_key($results, $existingids);
$this->assertEquals([$userids[0], $userids[1], $userids[2], $userids[3]], array_keys($results));
// Add the options to showuseridentity and check it returns those fields but only if you
// specify a context AND have permissions.
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', '', null,
\context_system::instance());
$this->assertObjectNotHasAttribute('department', $results[$userids[0]]);
$this->setAdminUser();
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', '', null,
\context_system::instance());
$this->assertEquals('department_a', $results[$userids[0]]->department);
// Check search (full name, email, username).
$results = get_users_listing('lastaccess', 'asc', 0, 0, 'b_first last_b');
$this->assertEquals([$userids[1]], array_keys($results));
$results = get_users_listing('lastaccess', 'asc', 0, 0, 'c@example');
$this->assertEquals([$userids[2]], array_keys($results));
$results = get_users_listing('lastaccess', 'asc', 0, 0, 'user_d');
$this->assertEquals([$userids[3]], array_keys($results));
// Check first and last initial restriction (all the test ones have same last initial).
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', 'C');
$this->assertEquals([$userids[2]], array_keys($results));
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', 'L');
$results = array_diff_key($results, $existingids);
$this->assertEquals($userids, array_keys($results));
// Check the extra where clause, either with the 'u.' prefix or not.
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', 'id IN (:x,:y)',
['x' => $userids[1], 'y' => $userids[3]]);
$results = array_diff_key($results, $existingids);
$this->assertEquals([$userids[1], $userids[3]], array_keys($results));
$results = get_users_listing('lastaccess', 'asc', 0, 0, '', '', '', 'id IN (:x,:y)',
['x' => $userids[1], 'y' => $userids[3]]);
$results = array_diff_key($results, $existingids);
$this->assertEquals([$userids[1], $userids[3]], array_keys($results));
}
/**
* Data provider for test_get_safe_orderby().
*
+10 -2
View File
@@ -1679,6 +1679,7 @@ EOF;
}
// Burst up to the capacity and make sure that the bucket allows it.
$burststart = microtime();
for ($i = 0; $i < $capacity; $i++) {
if ($i) {
sleep(1); // A little delay so we have different timemodified value for files.
@@ -1693,9 +1694,16 @@ EOF;
// The bucket should be full after bursting.
$this->assertTrue(file_is_draft_areas_limit_reached($user->id));
// Calculate the time taken to burst up the bucket capacity.
$timetaken = microtime_diff($burststart, microtime());
// The bucket leaks so it shouldn't be full after a certain time.
// Reiterating that this test could have been faster if MDL-37327 was implemented.
sleep(ceil(1 / $leak) - ($capacity - 1));
// Items are added into the bucket at the rate of 1 item per second.
// One item leaks from the bucket every 1/$leak seconds.
// So it takes 1/$leak - ($capacity-1) seconds for the bucket to leak one item and not be full anymore.
$milliseconds = ceil(1000000 * ((1 / $leak) - ($capacity - 1)) - ($timetaken * 1000));
usleep($milliseconds);
$this->assertFalse(file_is_draft_areas_limit_reached($user->id));
// Only one item was leaked from the bucket. So the bucket should become full again if we add a single item to it.
+1 -1
View File
@@ -30,7 +30,7 @@ defined('MOODLE_INTERNAL') || die();
* @package core
* @copyright 2017 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
* @coversDefaultClass \core\oauth2\api
* @covers \core\oauth2\api
*/
class core_oauth2_testcase extends advanced_testcase {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+48 -23
View File
@@ -3479,6 +3479,7 @@
_proto.createEl = function createEl$1(tagName, properties, attributes) {
return createEl(tagName, properties, attributes);
}
/**
* Localize a string given the string in english.
*
@@ -17820,11 +17821,19 @@
_proto.createEl = function createEl(type, props, attrs) {
// The control is textual, not just an icon
this.nonIconControl = true;
return _ClickableComponent.prototype.createEl.call(this, 'li', assign({
var el = _ClickableComponent.prototype.createEl.call(this, 'li', assign({
className: 'vjs-menu-item',
innerHTML: "<span class=\"vjs-menu-item-text\">" + this.localize(this.options_.label) + "</span>",
tabIndex: -1
}, props), attrs);
var iconplaceholder = el.querySelector('.vjs-icon-placeholder');
if (typeof iconplaceholder === 'object' && iconplaceholder !== null) {
el.replaceChild(_ClickableComponent.prototype.createEl.call(this, 'span', {
className: 'vjs-menu-item-text',
textContent: this.localize(this.options_.label)
}), iconplaceholder);
}
return el;
}
/**
* Ignore keys which are used by the menu, but pass any other ones up. See
@@ -18888,18 +18897,18 @@
var _proto = SubsCapsMenuItem.prototype;
_proto.createEl = function createEl(type, props, attrs) {
var innerHTML = "<span class=\"vjs-menu-item-text\">" + this.localize(this.options_.label);
if (this.options_.track.kind === 'captions') {
innerHTML += "\n <span aria-hidden=\"true\" class=\"vjs-icon-placeholder\"></span>\n <span class=\"vjs-control-text\"> " + this.localize('Captions') + "</span>\n ";
}
innerHTML += '</span>';
var el = _TextTrackMenuItem.prototype.createEl.call(this, type, assign({
innerHTML: innerHTML
var el = _TextTrackMenuItem.prototype.createEl.call(this, 'li', assign({
className: 'vjs-menu-item',
tabIndex: -1
}, props), attrs);
const menuitem = el.querySelector('.vjs-menu-item-text');
if (this.options_.track.kind === 'captions' && menuitem) {
const icon = document.createElement("span");
icon.className = 'vjs-icon-placeholder';
menuitem.appendChild(icon);
el.appendChild(menuitem);
}
return el;
};
@@ -19049,18 +19058,19 @@
var _proto = AudioTrackMenuItem.prototype;
_proto.createEl = function createEl(type, props, attrs) {
var innerHTML = "<span class=\"vjs-menu-item-text\">" + this.localize(this.options_.label);
if (this.options_.track.kind === 'main-desc') {
innerHTML += "\n <span aria-hidden=\"true\" class=\"vjs-icon-placeholder\"></span>\n <span class=\"vjs-control-text\"> " + this.localize('Descriptions') + "</span>\n ";
}
innerHTML += '</span>';
var el = _MenuItem.prototype.createEl.call(this, type, assign({
innerHTML: innerHTML
var el = _MenuItem.prototype.createEl.call(this, 'li', assign({
className: 'vjs-menu-item',
tabIndex: -1
}, props), attrs);
const menuitem = el.querySelector('.vjs-menu-item-text');
if (this.options_.track.kind === 'main' && menuitem) {
const icon = document.createElement("span");
icon.className = 'vjs-icon-placeholder';
menuitem.appendChild(icon);
el.appendChild(menuitem);
}
return el;
}
/**
@@ -32546,7 +32556,22 @@
return buf.push(' ', node.name, '="', node.value.replace(/[<&"]/g, _xmlEncoder), '"');
case TEXT_NODE:
return buf.push(node.data.replace(/[<&]/g, _xmlEncoder));
/**
* The ampersand character (&) and the left angle bracket (<) must not appear in their literal form,
* except when used as markup delimiters, or within a comment, a processing instruction, or a CDATA section.
* If they are needed elsewhere, they must be escaped using either numeric character references or the strings
* `&amp;` and `&lt;` respectively.
* The right angle bracket (>) may be represented using the string " &gt; ", and must, for compatibility,
* be escaped using either `&gt;` or a character reference when it appears in the string `]]>` in content,
* when that string is not marking the end of a CDATA section.
*
* In the content of elements, character data is any string of characters
* which does not contain the start-delimiter of any markup
* and does not include the CDATA-section-close delimiter, `]]>`.
*
* @see https://www.w3.org/TR/xml/#NT-CharData
*/
return buf.push(node.data.replace(/[<&]/g, _xmlEncoder).replace(/]]>/g, ']]&gt;'));
case CDATA_SECTION_NODE:
return buf.push('<![CDATA[', node.data, ']]>');
+9 -1
View File
@@ -36,4 +36,12 @@ Import plugins:
define(['media_videojs/video-lazy']
3. Download https://github.com/videojs/video-js-swf/blob/master/dist/video-js.swf
and place it into 'videojs/video-js.swf'
and place it into 'videojs/video-js.swf'
Changes:
In order to improve the validation, a couple of minor changes have been added to the video-lazy.js:
1. Partial upgrade from VideoJS 7.17.0 for 'Subtitles' label check in 17824 - 17833
2. Partial upgrade from VideoJS 7.17.0 for 'Captions' label check from 18897 - 18911
3. Partial upgrade from VideoJS 7.17.0 for 'Description' label check from 19062 - 19075
4. Fixed the video controls not being displayed when playing a video
5. Fixed the menu item not being displayed as expected
+6 -4
View File
@@ -262,8 +262,10 @@ Feature: Delete messages from conversations
And I click on "//button[@data-action='confirm-delete-selected-messages']" "xpath_element"
And I go back in "view-conversation" message drawer
Then I should not see "Student 2" in the "//*[@data-region='message-drawer']//div[@data-region='view-overview-favourites']" "xpath_element"
And I send "Hi!" message to "Student 2" user
And I go back in "view-conversation" message drawer
And I go back in "view-search" message drawer
And the following "private messages" exist:
| user | contact | message |
| student1 | student2 | Hi! |
And I open messaging
And I open the "Starred" conversations list
And I should see "Student 2" in the "//*[@data-region='message-drawer']//div[@data-region='view-overview-favourites']" "xpath_element"
And I select "Student 2" conversation in the "favourites" conversations list
And I should see "Hi!" in the "Student 2" "core_message > Message conversation"
@@ -118,7 +118,8 @@ Feature: Message delete conversations
And I should not see "Hi!" in the "Student 2" "core_message > Message conversation"
And I go back in "view-conversation" message drawer
And I should not see "Student 2" in the "favourites" "core_message > Message list area"
And I send "Hi!" message to "Student 2" user
And I go back in "view-conversation" message drawer
And I go back in "view-search" message drawer
And the following "private messages" exist:
| user | contact | message |
| student1 | student2 | Hi! |
And I open messaging
And I should see "Student 2" in the "favourites" "core_message > Message list area"
@@ -4,7 +4,8 @@ Feature: In an assignment, teachers can edit feedback for a students previous su
As a teacher
I need to be able to edit the feedback for a students previous submission attempt.
@javascript
# This scenario has Atto-specific steps. See MDL-75913 for further details.
@javascript @editor_atto
Scenario: Edit feedback for a students previous attempt.
Given the following "courses" exist:
| fullname | shortname | category | groupmode |
+6 -4
View File
@@ -183,8 +183,10 @@ if (($mode === 'csstemplate') || ($mode === 'jstemplate')) {
$usehtmleditor = data_get_config($data, "editor_{$mode}", true);
}
$datafieldtype = '';
if ($usehtmleditor) {
$format = FORMAT_HTML;
$datafieldtype = ' data-fieldtype="editor" ';
} else {
$format = FORMAT_PLAIN;
}
@@ -226,7 +228,7 @@ if ($mode == 'listtemplate'){
$field = 'listtemplateheader';
$editor->set_text($data->listtemplateheader);
$editor->use_editor($field, $options);
echo '<div><textarea id="'.$field.'" name="'.$field.'" class="form-control" rows="15" cols="80">' .
echo "<div><textarea id='{$field}' {$datafieldtype} name='{$field}' class='form-control' rows='15' cols='80'>" .
s($data->listtemplateheader) . '</textarea></div>';
echo '</td>';
@@ -344,7 +346,7 @@ $field = 'template';
$editor->set_text($data->{$mode});
$editor->use_editor($field, $options);
echo '<div>';
echo '<textarea class="form-control" id="' . $field . '" ' .
echo '<textarea class="form-control" id="' . $field . '" ' . $datafieldtype .
'name="' . $field . '" rows="15" cols="80">' . s($data->{$mode}) . '</textarea>';
echo '</div>';
echo '</td>';
@@ -360,7 +362,7 @@ if ($mode == 'listtemplate'){
$editor->set_text($data->listtemplatefooter);
$editor->use_editor($field, $options);
echo '<div>';
echo '<textarea id="' . $field . '" class="form-control" ' .
echo '<textarea id="' . $field . '" class="form-control" ' . $datafieldtype .
'name="' . $field . '" rows="15" cols="80">' . s($data->listtemplatefooter) . '</textarea>';
echo '</div>';
echo '</td>';
@@ -377,7 +379,7 @@ if ($mode == 'listtemplate'){
$editor->set_text($data->rsstitletemplate);
$editor->use_editor($field, $options);
echo '<div>';
echo '<textarea id="' . $field . '" name="' . $field . '" ' .
echo '<textarea id="' . $field . '" name="' . $field . '" ' . $datafieldtype .
'class="form-control" rows="15" cols="80">' . s($data->rsstitletemplate) . '</textarea>';
echo '</div>';
echo '</td>';
+14 -19
View File
@@ -14,18 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* PHPUnit data generator tests.
*
* @package mod_data
* @category phpunit
* @copyright 2012 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* PHPUnit data generator testcase.
*
@@ -33,8 +21,12 @@ defined('MOODLE_INTERNAL') || die();
* @category phpunit
* @copyright 2012 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \mod_data_generator
*/
class mod_data_generator_testcase extends advanced_testcase {
/**
* @covers ::create_instance
*/
public function test_generator() {
global $DB;
@@ -72,6 +64,9 @@ class mod_data_generator_testcase extends advanced_testcase {
$this->assertEquals(GRADE_TYPE_VALUE, $gitem->gradetype);
}
/**
* @covers ::create_field
*/
public function test_create_field() {
global $DB;
@@ -104,7 +99,6 @@ class mod_data_generator_testcase extends advanced_testcase {
// Creating test Fields with default parameter values.
foreach ($fieldtypes as $fieldtype) {
// Creating variables dynamically.
$fieldname = 'field-' . $count;
$record = new StdClass();
@@ -124,10 +118,15 @@ class mod_data_generator_testcase extends advanced_testcase {
for ($i = 1; $i < $count; $i++) {
$fieldname = 'field-' . $i;
$this->assertTrue(strpos($addtemplate, '[[' . $fieldname . ']]') >= 0);
$position = strpos($addtemplate, '[[' . $fieldname . ']]');
$this->assertIsNumeric($position);
$this->assertGreaterThanOrEqual(0, $position);
}
}
/**
* @covers ::create_entry
*/
public function test_create_entry() {
global $DB;
@@ -166,7 +165,6 @@ class mod_data_generator_testcase extends advanced_testcase {
// Creating test Fields with default parameter values.
foreach ($fieldtypes as $fieldtype) {
// Creating variables dynamically.
$fieldname = 'field-' . $count;
$record = new StdClass();
@@ -174,13 +172,10 @@ class mod_data_generator_testcase extends advanced_testcase {
$record->type = $fieldtype;
$record->required = 1;
${$fieldname} = $this->getDataGenerator()->get_plugin_generator('mod_data')->create_field($record, $data);
$this->assertInstanceOf('data_field_' . $fieldtype, ${$fieldname});
$this->getDataGenerator()->get_plugin_generator('mod_data')->create_field($record, $data);
$count++;
}
$this->assertEquals(count($fieldtypes), $DB->count_records('data_fields', array('dataid' => $data->id)));
$fields = $DB->get_records('data_fields', array('dataid' => $data->id), 'id');
$contents = array();
+2 -2
View File
@@ -649,7 +649,7 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
];
$this->queue_tasks_and_assert($expect);
$tasks = $DB->get_records('task_adhoc');
$tasks = $DB->get_records('task_adhoc', ['component' => 'mod_forum']);
$task = reset($tasks);
$this->assertGreaterThanOrEqual(time(), $task->nextruntime);
}
@@ -689,7 +689,7 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
];
$this->queue_tasks_and_assert($expect);
$tasks = $DB->get_records('task_adhoc');
$tasks = $DB->get_records('task_adhoc', ['component' => 'mod_forum']);
$task = reset($tasks);
$digesttime = usergetmidnight(time(), \core_date::get_server_timezone()) + ($CFG->digestmailtime * 3600);
$this->assertLessThanOrEqual($digesttime, $task->nextruntime);
+25 -5
View File
@@ -332,15 +332,24 @@ class manager {
*
* @since Moodle 3.9.7
* @param bool $allpotentialusers if true, the join will return all active users, not only the ones with attempts.
* @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
* @return sql_join the active users attempts join
*/
public function get_active_users_join(bool $allpotentialusers = false): sql_join {
public function get_active_users_join(bool $allpotentialusers = false, $currentgroup = false): sql_join {
// Only valid users counts. By default, all users with submit capability are considered potential ones.
$context = $this->get_context();
$coursemodule = $this->get_coursemodule();
// Ensure user can view users from all groups.
if ($currentgroup === 0 && $coursemodule->effectivegroupmode == SEPARATEGROUPS
&& !has_capability('moodle/site:accessallgroups', $context)) {
return new sql_join('', '1=2', [], true);
}
// We want to present all potential users.
$capjoin = get_enrolled_with_capabilities_join($context, '', 'mod/h5pactivity:view');
$capjoin = get_enrolled_with_capabilities_join($context, '', 'mod/h5pactivity:view', $currentgroup);
if ($capjoin->cannotmatchanyrows) {
return $capjoin;
@@ -438,10 +447,13 @@ class manager {
*
* @param int $userid an opional userid to show
* @param int $attemptid an optional $attemptid to show
* @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
* @return report|null available report (or null if no report available)
*/
public function get_report(int $userid = null, int $attemptid = null): ?report {
global $USER;
public function get_report(int $userid = null, int $attemptid = null, $currentgroup = false): ?report {
global $USER, $CFG;
require_once("{$CFG->dirroot}/user/lib.php");
// If tracking is disabled, no reports are available.
if (!$this->instance->enabletracking) {
@@ -460,6 +472,14 @@ class manager {
if ($this->can_view_all_attempts()) {
$user = core_user::get_user($userid);
// Ensure user can view the attempt of specific userid, respecting access checks.
if ($user && $user->id != $USER->id) {
$course = get_course($this->coursemodule->course);
if ($this->coursemodule->effectivegroupmode == SEPARATEGROUPS && !user_can_view_profile($user, $course)) {
return null;
}
}
} else if ($this->can_view_own_attempts()) {
$user = core_user::get_user($USER->id);
if ($userid && $user->id != $userid) {
@@ -480,7 +500,7 @@ class manager {
} else if ($user) {
return new attempts($this, $user);
}
return new participants($this);
return new participants($this, $currentgroup);
}
/**
@@ -61,8 +61,9 @@ class participants extends table_sql implements report {
* Create a new participants report.
*
* @param manager $manager h5pactivitymanager object
* @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
*/
public function __construct(manager $manager) {
public function __construct(manager $manager, $currentgroup = false) {
parent::__construct('mod_h5pactivity-participants');
$this->manager = $manager;
$this->scores = $manager->get_users_scaled_score();
@@ -83,7 +84,7 @@ class participants extends table_sql implements report {
$this->no_sorting('attempts');
$this->pageable(true);
$capjoin = $this->manager->get_active_users_join(true);
$capjoin = $this->manager->get_active_users_join(true, $currentgroup);
// Final SQL.
$this->set_sql(
+5 -1
View File
@@ -43,9 +43,11 @@ if (empty($id)) {
require_login($course, true, $cm);
$currentgroup = groups_get_activity_group($cm, true);
$manager = manager::create_from_coursemodule($cm);
$report = $manager->get_report($userid, $attemptid);
$report = $manager->get_report($userid, $attemptid, $currentgroup);
if (!$report) {
print_error('permissiondenied');
}
@@ -125,6 +127,8 @@ $PAGE->set_context($context);
echo $OUTPUT->header();
groups_print_activity_menu($cm, $PAGE->url);
echo $report->print();
echo $OUTPUT->footer();
@@ -22,13 +22,13 @@ Feature: Add H5P activity
@javascript
Scenario: Add a h5pactivity activity to a course
When I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | H5P activity Description |
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Package file" filemanager
And I click on "Save and display" "button"
And I wait until the page is ready
Given the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| intro | H5P activity Description |
| packagefilepath | h5p/tests/fixtures/ipsums.h5p |
When I am on the "Awesome H5P package" "h5pactivity activity" page
Then I should see "H5P activity Description"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
@@ -40,13 +40,13 @@ Feature: Add H5P activity
@javascript
Scenario: Add a h5pactivity activity with download
When I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | Description |
| Allow download | 1 |
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Package file" filemanager
And I click on "Save and display" "button"
Given the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| displayoptions | 12 |
| packagefilepath | h5p/tests/fixtures/ipsums.h5p |
When I am on the "Awesome H5P package" "h5pactivity activity" page
And I wait until the page is ready
Then I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
@@ -57,13 +57,13 @@ Feature: Add H5P activity
@javascript
Scenario: Add a h5pactivity activity with embed
When I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | Description |
| Embed button | 1 |
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Package file" filemanager
And I click on "Save and display" "button"
Given the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| displayoptions | 10 |
| packagefilepath | h5p/tests/fixtures/ipsums.h5p |
When I am on the "Awesome H5P package" "h5pactivity activity" page
And I wait until the page is ready
Then I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
@@ -74,13 +74,13 @@ Feature: Add H5P activity
@javascript
Scenario: Add a h5pactivity activity with copyright
When I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | Description |
| Copyright button | 1 |
And I upload "h5p/tests/fixtures/guess-the-answer.h5p" file to "Package file" filemanager
And I click on "Save and display" "button"
Given the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| displayoptions | 6 |
| packagefilepath | h5p/tests/fixtures/guess-the-answer.h5p |
When I am on the "Awesome H5P package" "h5pactivity activity" page
And I wait until the page is ready
Then I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
@@ -91,13 +91,13 @@ Feature: Add H5P activity
@javascript
Scenario: Add a h5pactivity activity with copyright in a content without copyright
When I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | Description |
| Copyright button | 1 |
And I upload "h5p/tests/fixtures/ipsums.h5p" file to "Package file" filemanager
And I click on "Save and display" "button"
Given the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| displayoptions | 6 |
| packagefilepath | h5p/tests/fixtures/ipsums.h5p |
When I am on the "Awesome H5P package" "h5pactivity activity" page
And I wait until the page is ready
Then I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
@@ -108,15 +108,13 @@ Feature: Add H5P activity
@javascript
Scenario: Add a h5pactivity activity to a course with all display options enabled
When I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | Description |
| Allow download | 1 |
| Embed button | 1 |
| Copyright button | 1 |
And I upload "h5p/tests/fixtures/guess-the-answer.h5p" file to "Package file" filemanager
And I click on "Save and display" "button"
Given the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| displayoptions | 0 |
| packagefilepath | h5p/tests/fixtures/guess-the-answer.h5p |
When I am on the "Awesome H5P package" "h5pactivity activity" page
And I wait until the page is ready
Then I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
@@ -22,6 +22,7 @@ Feature: Content bank link in the activity settings form
And I add a "H5P" to section "1"
Then I should see "Use the content bank (opens in new window) to manage your H5P files"
And I click on "content bank (opens in new window)" "link" in the "General" "fieldset"
And I switch to a second window
And I should see "C1" in the "page-navbar" "region"
And I close all opened windows
@@ -19,36 +19,23 @@ Feature: Change grading options in an H5P activity
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/h5p:updatelibraries | Allow | editingteacher | System | |
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | Description |
And I upload "h5p/tests/fixtures/multiple-choice-2-6.h5p" file to "Package file" filemanager
And I click on "Save and display" "button"
And I log out
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
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"
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
# H5P does not allow to Retry if the user checks the correct answer, we need to refresh the page.
And I reload the page
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
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 log out
And the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| packagefilepath | h5p/tests/fixtures/multiple-choice-2-6.h5p |
And the following "mod_h5pactivity > attempts" exist:
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
| student1 | Awesome H5P package | 1 | choice | 0 | 1 | 4 | 1 | 0 |
| student1 | Awesome H5P package | 2 | choice | 1 | 1 | 4 | 1 | 1 |
| student1 | Awesome H5P package | 3 | choice | 0 | 1 | 4 | 1 | 0 |
@javascript
Scenario: Default grading is max attempt grade
Given I am on the "Awesome H5P package" "h5pactivity activity" page logged in as teacher1
Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1
And I expand all fieldsets
And the field "Grading method" matches value "Highest grade"
And I click on "Save and return to course" "button"
When I navigate to "View > User report" in the course gradebook
And I set the field "Select all or one user" to "Student 1"
Then the following should exist in the "user-grade" table:
@@ -68,7 +55,7 @@ Feature: Change grading options in an H5P activity
| Awesome H5P package | 0.00 | 0.00 % |
@javascript
Scenario: Change setting to first attempt
Scenario: Change setting to last attempt
Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1
When I set the following fields to these values:
| Grading method | Last attempt |
@@ -17,16 +17,12 @@ Feature: View fill the blanks attempt report
And the following config values are set as admin:
# No HTML should appear even with formatstringstriptags disabled.
| formatstringstriptags | 0 |
And I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
And I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | Description |
| Grading method | Average grade |
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Package file" filemanager
And I click on "Save and display" "button"
And I log out
And the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| grademethod | 2 |
| packagefilepath | h5p/tests/fixtures/filltheblanks.h5p |
Scenario: View attempt in a fill the blanks content
Given I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
@@ -17,16 +17,12 @@ Feature: View essay attempt report
And the following config values are set as admin:
# No HTML should appear even with formatstringstriptags disabled.
| formatstringstriptags | 0 |
And I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
And I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | Description |
| Grading method | Average grade |
And I upload "h5p/tests/fixtures/basic_essay.h5p" file to "Package file" filemanager
And I click on "Save and display" "button"
And I log out
And the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| grademethod | 2 |
| packagefilepath | h5p/tests/fixtures/basic_essay.h5p |
Scenario: View attempt essay content
# Do an attempt.
@@ -2,7 +2,7 @@
Feature: Do a H5P attempt
In order to let students do a H5P attempt
As a teacher
I need to list students attempts on the log report
I need to list students attempts on various reports
Background:
Given the following "users" exist:
@@ -19,25 +19,21 @@ Feature: Do a H5P attempt
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/h5p:updatelibraries | Allow | editingteacher | System | |
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | Description |
And I upload "h5p/tests/fixtures/multiple-choice-2-6.h5p" file to "Package file" filemanager
And the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| packagefilepath | h5p/tests/fixtures/multiple-choice-2-6.h5p |
| grademethod | 2 |
@javascript
Scenario: View an H5P as a teacher
When I click on "Save and display" "button"
When I am on the "Awesome H5P package" "h5pactivity activity" page logged in as teacher1
And I wait until the page is ready
Then I should see "This content is displayed in preview mode"
@javascript
Scenario: To an attempts and check on course log report
When I click on "Save and return to course" "button"
And I log out
Given I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
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 wait until the page is ready
And I should not see "This content is displayed in preview mode"
And I switch to "h5p-player" class iframe
@@ -51,3 +47,45 @@ Feature: Do a H5P attempt
And I follow "Student 1"
Then I follow "Today's logs"
And I should see "xAPI statement received"
@javascript
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 wait until the page is ready
And I should not see "This content is displayed in preview mode"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
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"
# We need to wait 1 second here because, in very quick environments, the 2nd
# attempts happen too close to the 1st one and it's not sent properly. See MDL-76010.
And I wait "1" seconds
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"
# 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
# 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"
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"
# Again, the wait between 3rd and 4th attempt, to separate them a little bit.
And I wait "1" seconds
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
When I follow "View my attempts"
And "1" row "Score" column of "table" table should contain "0"
And "2" row "Score" column of "table" table should contain "1"
And "3" row "Score" column of "table" table should contain "0"
And "4" row "Score" column of "table" table should contain "1"
And I am on the "Course 1" course page logged in as teacher1
And I navigate to "View > User report" in the course gradebook
And I set the field "Select all or one user" to "Student 1"
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Percentage |
| Awesome H5P package | 50.00 | 50.00 % |
+9
View File
@@ -55,6 +55,8 @@ class mod_h5pactivity_generator extends testing_module_generator {
// Fill in optional values if not specified.
if (!isset($record->packagefilepath)) {
$record->packagefilepath = $CFG->dirroot.'/h5p/tests/fixtures/h5ptest.zip';
} else if (strpos($record->packagefilepath, $CFG->dirroot) !== 0) {
$record->packagefilepath = "{$CFG->dirroot}/{$record->packagefilepath}";
}
if (!isset($record->grade)) {
$record->grade = 100;
@@ -189,6 +191,7 @@ class mod_h5pactivity_generator extends testing_module_generator {
* @param array $data the attempts data array
*/
public function create_attempt(array $data): void {
global $DB;
if (!isset($data['h5pactivityid'])) {
throw new coding_exception('Must specify h5pactivityid when creating a H5P attempt.');
@@ -216,6 +219,12 @@ class mod_h5pactivity_generator extends testing_module_generator {
}
$this->insert_statement($data, $this->$method($data));
// If the activity has tracking enabled, try to recalculate grades.
$activity = $DB->get_record('h5pactivity', ['id' => $data['h5pactivityid']]);
if ($activity->enabletracking) {
h5pactivity_update_grades($activity, $data['userid']);
}
}
/**
+54 -9
View File
@@ -14,14 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* mod_h5pactivity manager tests
*
* @package mod_h5pactivity
* @category test
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\local;
use context_module;
@@ -31,6 +23,7 @@ use stdClass;
* Manager tests class for mod_h5pactivity.
*
* @package mod_h5pactivity
* @covers \mod_h5pactivity\local\manager
* @category test
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -627,7 +620,7 @@ class manager_testcase extends \advanced_testcase {
}
/**
* Test static count_attempts of all active participants.
* Test static test_get_active_users_join of all active participants.
*
* Most method scenarios are tested in test_count_attempts_all so we only
* need to test the with $allpotentialusers true and false.
@@ -692,6 +685,58 @@ class manager_testcase extends \advanced_testcase {
];
}
/**
* Test active users joins returns appropriate results for groups
*/
public function test_get_active_users_join_groupmode(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course(['groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1]);
// Teacher/user one in group one.
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
$userone = $this->getDataGenerator()->create_and_enrol($course, 'student');
$groupone = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$this->getDataGenerator()->create_group_member(['groupid' => $groupone->id, 'userid' => $teacher->id]);
$this->getDataGenerator()->create_group_member(['groupid' => $groupone->id, 'userid' => $userone->id]);
// User two in group two.
$usertwo = $this->getDataGenerator()->create_and_enrol($course, 'student');
$grouptwo = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$this->getDataGenerator()->create_group_member(['groupid' => $grouptwo->id, 'userid' => $usertwo->id]);
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$manager = manager::create_from_instance($activity);
// Admin user can view all participants.
$usersjoin = $manager->get_active_users_join(true, 0);
$users = $DB->get_fieldset_sql("SELECT u.username FROM {user} u {$usersjoin->joins} WHERE {$usersjoin->wheres}",
$usersjoin->params);
$this->assertEqualsCanonicalizing([$teacher->username, $userone->username, $usertwo->username], $users);
// Switch to teacher, who cannot view all participants.
$this->setUser($teacher);
$usersjoin = $manager->get_active_users_join(true, 0);
$users = $DB->get_fieldset_sql("SELECT u.username FROM {user} u {$usersjoin->joins} WHERE {$usersjoin->wheres}",
$usersjoin->params);
$this->assertEmpty($users);
// Teacher can view participants inside group.
$usersjoin = $manager->get_active_users_join(true, $groupone->id);
$users = $DB->get_fieldset_sql("SELECT u.username FROM {user} u {$usersjoin->joins} WHERE {$usersjoin->wheres}",
$usersjoin->params);
$this->assertEqualsCanonicalizing([$teacher->username, $userone->username], $users);
}
/**
* Test static count_attempts.
*/
+7
View File
@@ -1,6 +1,13 @@
This files describes API changes in /mod/h5pactivity/*,
information provided here is intended especially for developers.
=== 3.9.17 ===
* The following methods now accept an optional `$currentgroup` parameter in order to better support groups:
- `\mod_h5pactivity\local\manager::get_active_users_join`
- `\mod_h5pactivity\local\manager::get_report`
- `\mod_h5pactivity\local\report\participants` constructor
=== 3.9.7 ===
* Added mod_h5pactivity\local\manager::get_active_users_join method to query all active
@@ -1,4 +1,5 @@
@mod @mod_lesson
# This feature has Atto-specific steps. See MDL-75913 for further details.
@mod @mod_lesson @editor_atto
Feature: In a lesson activity, a teacher can duplicate a lesson page
In order to duplicate a lesson page
As a teacher
@@ -3,7 +3,8 @@ Feature: In a lesson activity, teacher can add embedded images in questions answ
As a teacher
I need to add questions with images in answers and responses
@javascript @_file_upload
# This scenario has Atto-specific steps. See MDL-75913 for further details.
@javascript @_file_upload @editor_atto
Scenario: questions with images in answers and responses
Given the following "users" exist:
| username | firstname | lastname | email |
@@ -55,7 +55,8 @@ Feature: Teachers can override the grade for any question
# This time is same as time the window is open. So wait for it to close before proceeding.
And I wait "2" seconds
@javascript @_switch_window @_file_upload @_bug_phantomjs
# This scenario has Atto-specific steps. See MDL-75913 for further details.
@javascript @_switch_window @_file_upload @_bug_phantomjs @editor_atto
Scenario: Comment on a response to an essay question attempt.
When I log in as "teacher1"
And I follow "Manage private files"
@@ -0,0 +1,55 @@
@mod @mod_quiz
Feature: Several attempts in a quiz
As a student
In order to demonstrate what I know
I need to be able to attempt quizzes and sometimes take multiple attempts
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | One | student1@example.com |
| student2 | Student | One | student2@example.com |
| teacher | Teacher | One | teacher@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| student2 | C1 | student |
| teacher | C1 | teacher |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
| Test questions | truefalse | TF2 | Second question |
And the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour | navmethod |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback | free |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| TF1 | 1 | 1 |
| TF2 | 2 | 1 |
# Add some attempts
And user "student1" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | True |
| 2 | False |
And user "student2" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | True |
| 2 | True |
# Add a second attempt by student1
And user "student1" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | False |
| 2 | False |
@javascript
Scenario: The redo question buttons are visible after 2 attempts are preset for student1.
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "student1"
Then "Re-attempt quiz" "button" should exist
And "1" row "Marks / 2.00" column of "quizattemptsummary" table should contain "1.00"
And "2" row "Marks / 2.00" column of "quizattemptsummary" table should contain "0.00"
+15 -7
View File
@@ -99,9 +99,6 @@ class mod_quiz_generator extends testing_module_generator {
/**
* Create a quiz attempt for a particular user at a particular course.
*
* Currently this method can only create a first attempt for each
* user at each quiz. TODO remove this limitation.
*
* @param int $quizid the quiz id (from the mdl_quit table, not cmid).
* @param int $userid the user id.
* @param array $forcedrandomquestions slot => questionid. Optional,
@@ -116,12 +113,18 @@ class mod_quiz_generator extends testing_module_generator {
// Build quiz object and load questions.
$quizobj = quiz::create($quizid, $userid);
if (quiz_get_user_attempts($quizid, $userid, 'all', true)) {
throw new coding_exception('mod_quiz_generator is currently limited to only ' .
'be able to create one attempt for each user. (This should be fixed.)');
$attemptnumber = 1;
$attempt = null;
if ($attempts = quiz_get_user_attempts($quizid, $userid, 'all', true)) {
// There is/are already an attempt/some attempts.
// Take the last attempt.
$attempt = end($attempts);
// Take the attempt number of the last attempt and increase it.
$attemptnumber = $attempt->attempt + 1;
}
return quiz_prepare_and_start_new_attempt($quizobj, 1, null, false,
return quiz_prepare_and_start_new_attempt($quizobj, $attemptnumber, $attempt, false,
$forcedrandomquestions, $forcedvariants);
}
@@ -179,6 +182,7 @@ class mod_quiz_generator extends testing_module_generator {
public function create_override(array $data): void {
global $DB;
// Validate.
if (!isset($data['quiz'])) {
throw new coding_exception('Must specify quiz (id) when creating a quiz override.');
}
@@ -191,6 +195,10 @@ class mod_quiz_generator extends testing_module_generator {
throw new coding_exception('Cannot specify both userid and groupid when creating a quiz override.');
}
// Create the override.
$DB->insert_record('quiz_overrides', (object) $data);
// Update any associated calendar events, if necessary.
quiz_update_events($DB->get_record('quiz', ['id' => $data['quiz']], '*', MUST_EXIST));
}
}
+63 -1
View File
@@ -33,6 +33,7 @@ defined('MOODLE_INTERNAL') || die();
* @category phpunit
* @copyright 2012 Matt Petro
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_quiz_generator
*/
class mod_quiz_generator_testcase extends advanced_testcase {
public function test_generator() {
@@ -42,7 +43,7 @@ class mod_quiz_generator_testcase extends advanced_testcase {
$this->assertEquals(0, $DB->count_records('quiz'));
/** @var mod_quiz_generator $generator */
/** @var \mod_quiz_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$this->assertInstanceOf('mod_quiz_generator', $generator);
$this->assertEquals('quiz', $generator->get_modulename());
@@ -64,4 +65,65 @@ class mod_quiz_generator_testcase extends advanced_testcase {
$this->assertEqualsWithDelta($createtime,
$DB->get_field('quiz', 'timecreated', ['id' => $cm->instance]), 2);
}
public function test_generating_a_user_override() {
$this->resetAfterTest(true);
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$user = $generator->create_user();
$quiz = $generator->create_module('quiz', ['course' => $course->id]);
$generator->enrol_user($user->id, $course->id, 'student');
/** @var \mod_quiz_generator $quizgenerator */
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
$quizgenerator->create_override([
'quiz' => $quiz->id,
'userid' => $user->id,
'timeclose' => strtotime('2022-10-20'),
]);
// Check the corresponding calendar event now exists.
$events = calendar_get_events(strtotime('2022-01-01'),
strtotime('2022-12-31'), $user->id, false, $course->id);
$this->assertCount(1, $events);
$event = reset($events);
$this->assertEquals($user->id, $event->userid);
$this->assertEquals(0, $event->groupid);
$this->assertEquals(0, $event->courseid);
$this->assertEquals('quiz', $event->modulename);
$this->assertEquals($quiz->id, $event->instance);
$this->assertEquals('close', $event->eventtype);
$this->assertEquals(strtotime('2022-10-20'), $event->timestart);
}
public function test_generating_a_group_override() {
$this->resetAfterTest(true);
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$quiz = $generator->create_module('quiz', ['course' => $course->id]);
$group = $generator->create_group(['courseid' => $course->id]);
/** @var \mod_quiz_generator $quizgenerator */
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
$quizgenerator->create_override([
'quiz' => $quiz->id,
'groupid' => $group->id,
'timeclose' => strtotime('2022-10-20'),
]);
// Check the corresponding calendar event now exists.
$events = calendar_get_events(strtotime('2022-01-01'),
strtotime('2022-12-31'), false, $group->id, $course->id);
$this->assertCount(1, $events);
$event = reset($events);
$this->assertEquals(0, $event->userid);
$this->assertEquals($group->id, $event->groupid);
$this->assertEquals($course->id, $event->courseid);
$this->assertEquals('quiz', $event->modulename);
$this->assertEquals($quiz->id, $event->instance);
$this->assertEquals('close', $event->eventtype);
$this->assertEquals(strtotime('2022-10-20'), $event->timestart);
}
}
@@ -20,31 +20,20 @@ Feature: A teacher can use activity completion to track a student progress
And I am on "Course 1" course homepage with editing mode on
Scenario: Require survey view
Given I add a "Survey" to section "1" and I fill the form with:
| Name | Test survey name |
| Survey type | Critical incidents |
| Description | Test survey description |
| Completion tracking | Show activity as complete when conditions are met |
| id_completionview | 1 |
| id_completionsubmit | 0 |
And I log out
When I log in as "student1"
And I am on "Course 1" course homepage
Given the following "activities" exist:
| activity | course | name | template | completion | completionview | completionsubmit |
| survey | C1 | Test survey name | 5 | 2 | 1 | 0 |
When I am on the "Course 1" course page logged in as student1
And the "Test survey name" "survey" activity with "auto" completion should be marked as not complete
And I follow "Test survey name"
And I am on "Course 1" course homepage
Then the "Test survey name" "survey" activity with "auto" completion should be marked as complete
Scenario: Require survey submission
Given I add a "Survey" to section "1" and I fill the form with:
| Name | Test survey name |
| Survey type | Critical incidents |
| Description | Test survey description |
| Completion tracking | Show activity as complete when conditions are met |
| id_completionsubmit | 1 |
And I log out
When I log in as "student1"
And I am on "Course 1" course homepage
Given the following "activities" exist:
| activity | course | name | template | completion | completionsubmit |
| survey | C1 | Test survey name | 5 | 2 | 1 |
When I am on the "Course 1" course page logged in as student1
And the "Test survey name" "survey" activity with "auto" completion should be marked as not complete
And I follow "Test survey name"
And I press "Click here to continue"

Some files were not shown because too many files have changed in this diff Show More