Compare commits

...

94 Commits

Author SHA1 Message Date
Sara Arjona 3828f4b484 Moodle release 3.11.9 2022-08-19 09:50:31 +02:00
Sara Arjona 4c5288b32c weekly release 3.11.8+ 2022-08-18 14:54:20 +02:00
Laurent David 284b201a28 MDL-75210 mod_quiz: fix sequential quiz access
* As a student I should only be able to access the next question or the current question
* The API should not show more than the current question
* Once the quiz is previewed we can see the question in any order (existing behaviour)
* Related to ticket MDL-71728

Co-authored-by: Rajneel Totaram <rjnlfj@yahoo.com>
Co-authored-by: Tim Hunt <t.j.hunt@open.ac.uk>
2022-08-18 08:08:25 +04:00
Andrew Nicols 31c078e183 Merge branch 'MDL-75029-311' of https://github.com/paulholden/moodle into MOODLE_311_STABLE 2022-08-18 11:19:17 +08:00
Shamim Rezaie 6b8961b7ae Merge branch 'MDL-75386-311' of https://github.com/andrewnicols/moodle into MOODLE_311_STABLE 2022-08-17 13:29:53 +10:00
Shamim Rezaie 7ef79076fe Merge branch 'MDL-73876-311' of https://github.com/ilyatregubov/moodle into MOODLE_311_STABLE 2022-08-17 02:32:46 +10:00
Sara Arjona 1bf4ac0c11 Merge branch 'MDL-72178-311' of https://github.com/NoelDeMartin/moodle into MOODLE_311_STABLE 2022-08-16 16:58:43 +02:00
Mark Johnson 2c4cc3caf6 MDL-75388 lib: Update mustache to 2.14.1
MDL-67114 and MDL-73586 local changes removed as they are now available
upstream.
2022-08-12 04:14:19 +02:00
Petr Skoda 3fd34dd999 MDL-75237 core: improve PARAM_SAFEPATH docs 2022-08-12 04:14:19 +02:00
Petr Skoda 4dcb3b244c MDL-75237 qformat_blackboard_six: sanitise blackboard import file paths 2022-08-12 04:14:19 +02:00
Petr Skoda 6272227934 MDL-75237 core: Revert MDL-72029
This reverts commit edc7eb06b7.
2022-08-12 04:14:19 +02:00
Petr Skoda 36e0a49c00 MDL-75237 qformat_blackboard_six: Revert MDL-72029
This reverts commit 46ee4f4062.
2022-08-12 04:14:19 +02:00
Paul Holden 1ee3d6323c MDL-75326 h5p: require sesskey to toggle library enabled state. 2022-08-12 04:14:19 +02:00
Shamim Rezaie 4ed613fbc2 weekly release 3.11.8+ 2022-08-12 11:52:57 +10:00
Noel De Martin 123fc91eea MDL-72178 feedback: Implement behat generators
Based on Andrew's WIP from https://github.com/andrewnicols/moodle/commit/b3d40cb20d1f0edf0bf21077c8f94bce5dec56db

Co-authored-by: Andrew Nicols <andrew@nicols.co.uk>
2022-08-11 10:54:40 +02:00
Jun Pataleta 83c9f41a47 Merge branch 'MDL-75375-311' of https://github.com/junpataleta/moodle into MOODLE_311_STABLE 2022-08-10 00:25:41 +08:00
Brendan Heywood 3bec78df9b MDL-75375 cache: Fix file store path cleaning 2022-08-09 23:54:02 +08:00
Jun Pataleta 6793732790 Merge branch 'MDL-74636_311' of https://github.com/timhunt/moodle into MOODLE_311_STABLE 2022-08-09 11:10:58 +08:00
Jun Pataleta 9ceb538216 Merge branch 'MDL-74211-311' of https://github.com/lameze/moodle into MOODLE_311_STABLE 2022-08-08 11:15:03 +08:00
Ilya Tregubov 5073d3542b Merge branch 'MDL-74475-311' of https://github.com/aanabit/moodle into MOODLE_311_STABLE 2022-08-05 12:27:21 +04:00
Andrew Nicols 1125231af0 MDL-75386 theme: Include CSS from editor subplugins in editor style
Editor stylesheets are kept separately to standard plugin stylesheets so
that they can be provided to style individual iframes where loading the
standard Moodle stylesheets would be incorrect.

Unfortunately the editor stylesheet system does not consider that
subplugins may want to add small individual styling to the editor but
this is required in some situations.
2022-08-05 14:18:49 +08:00
Ilya Tregubov 8a147fee4b weekly release 3.11.8+ 2022-08-05 08:29:20 +04:00
Andrew Lyons 1d4a1bc258 MDL-73876 behat: Show exceptions stack trace 2022-08-04 16:51:37 +04:00
Paul Holden 4119bd99a9 MDL-75029 block_news_items: indicate posts made by deleted users. 2022-08-04 09:43:09 +01:00
Jun Pataleta 6102f8553a Merge branch 'MDL-74895-311' of https://github.com/paulholden/moodle into MOODLE_311_STABLE 2022-08-04 11:35:30 +08:00
Jun Pataleta 982dda9417 Merge branch 'MDL-69251-311' of https://github.com/snake/moodle into MOODLE_311_STABLE 2022-08-04 11:16:03 +08:00
Andrew Nicols e5744719c9 Merge branch 'MDL-56923-311' of https://github.com/rezaies/moodle into MOODLE_311_STABLE 2022-08-04 11:13:11 +08:00
Andrew Nicols 1b513e8c42 Merge branch 'MDL-74502-311' of https://github.com/marinaglancy/moodle into MOODLE_311_STABLE 2022-08-04 11:09:15 +08:00
Jake Dallimore 1aa9a5b11f Merge branch 'MDL-74766-311' of https://github.com/JBThong/moodle into MOODLE_311_STABLE 2022-08-04 11:03:28 +08:00
Andrew Nicols a313e53b54 Merge branch 'MDL-75080-311' of https://github.com/andelacruz/moodle into MOODLE_311_STABLE 2022-08-04 10:45:06 +08:00
Simey Lameze 983a79347b MDL-74211 behat: convert few more steps to use new data generators 2022-08-04 10:38:39 +08:00
Andrew Nicols f6a3cbe339 Merge branch 'MDL-74655-311' of https://github.com/NoelDeMartin/moodle into MOODLE_311_STABLE 2022-08-04 10:09:08 +08:00
Andrew Nicols cccef66f0c MDL-74492 editor_atto: Comment the regex change 2022-08-04 10:05:21 +08:00
Andrew Nicols ad58a4c3c3 Merge branch 'MDL-74492__MOODLE_311_STABLE' of https://github.com/PhilippImhof/moodle into MOODLE_311_STABLE 2022-08-04 10:05:17 +08:00
Jun Pataleta 3583300da5 Merge branch 'MDL-62959-311' of https://github.com/snake/moodle into MOODLE_311_STABLE 2022-08-03 16:50:35 +08:00
Noel De Martin 2d9ee56211 MDL-74655 competency: Implement behat generators 2022-08-03 10:49:14 +02:00
Amaia Anabitarte 5ba22c14a1 MDL-74475 core_badges: Behat test using new step 2022-08-03 10:29:39 +02:00
Amaia Anabitarte b3fab1d80e MDL-74475 behat: Option to jump to course edit page 2022-08-03 10:29:39 +02:00
Andrew Nicols d5cd3c95b0 Merge branch 'MDL-72430-311' of https://github.com/HuongNV13/moodle into MOODLE_311_STABLE 2022-08-03 12:50:03 +08:00
Jun Pataleta 7b841a0f39 Merge branch 'MDL-74947-311' of https://github.com/snake/moodle into MOODLE_311_STABLE 2022-08-02 09:59:19 +08:00
Tim Hunt 25519bc4d8 MDL-74636 behat: step to skip this scenario if a plugin isn't installed 2022-07-29 09:51:11 +01:00
Jun Pataleta 5f6923adf1 Merge branch 'MDL-75244-311' of https://github.com/andrewnicols/moodle into MOODLE_311_STABLE 2022-07-29 15:28:55 +08:00
Philipp Imhof a08fcb8d13 MDL-74492 editor_atto: <line> tags should not be confused with <li> tags 2022-07-29 08:36:38 +02:00
Simey Lameze 9fbbdc8a0a MDL-74211 behat: replace steps to use new categories page type 2022-07-29 13:07:55 +08:00
Simey Lameze 56781dec40 MDL-74211 behat: add course question categories page type 2022-07-29 13:07:55 +08:00
Simey Lameze 15b31bfb39 MDL-74211 quiz: make use of question data generators 2022-07-29 13:07:55 +08:00
Andrew Nicols fc65a2e4a2 Merge branch 'MDL-72349_M311_update_strip_double_headers_function' of https://github.com/christina-roperto/moodle into MOODLE_311_STABLE 2022-07-29 12:12:27 +08:00
Jun Pataleta 3436523f17 weekly release 3.11.8+ 2022-07-29 10:56:47 +08:00
Angelia Dela Cruz 8062e43941 MDL-75080 behat: Chat behat generators use UI
Replace steps that manually add Chat instances via the UI and use
Behat generators. This improves the speed of the Behat test runs.
2022-07-29 10:56:09 +08:00
Andrew Nicols a28e929a74 MDL-75244 form: Change checker should check existence of tinyMCE API
The window.tinyMCE.editors API was present in version 3 of TinyMCE, but
is not present in later versions. As a result, this check tries to loop
over a variable which does not exist and throws an error in the process.

We should check that window.tinyMCE *and* window.tinyMCE.editors both
exist before attempting to loop over them.
2022-07-28 16:09:21 +08:00
Simey Lameze 838f1bfa7b MDL-62959 calendar: add behat to test event type changing 2022-07-27 15:31:10 +08:00
Simey Lameze 88a18df37f MDL-62959 calendar: fix event data mapping for group id 2022-07-27 11:48:16 +08:00
Huong Nguyen cad3403e71 MDL-72430 Calendar: Fix admin cannot edit course event 2022-07-26 11:47:50 +07:00
Andrew Nicols 34b551b4e7 Merge branch 'MDL-73788-311' of https://github.com/andelacruz/moodle into MOODLE_311_STABLE 2022-07-26 11:38:44 +08:00
Jun Pataleta a62e36a9f6 Merge branch 'MDL-74057-311' of https://github.com/andelacruz/moodle into MOODLE_311_STABLE 2022-07-26 11:01:53 +08:00
Paul Holden 7dd9098338 MDL-74895 h5p: more resilient error handling during content fetching.
On new installs, the task for downloading new H5P content types would
throw exceptions and exit when encountering unexpected and/or missing
data.
2022-07-25 20:05:59 +01:00
Ilya Tregubov a69d81beaa Merge branch 'MDL-75083-MOODLE_311_STABLE' of https://github.com/rzhtwberlin/moodle into MOODLE_311_STABLE 2022-07-25 14:56:29 +04:00
Angelia Dela Cruz 4434756fd6 MDL-74057 behat: SCORM behat generators use UI
Replace steps that manually add SCORM instances via the UI and use
Behat generators. This improves the speed of Behat test runs.
2022-07-25 18:22:22 +08:00
Ilya Tregubov 08e64ba626 Merge branch 'MDL-74974__MOODLE_311_STABLE' of https://github.com/PhilippImhof/moodle into MOODLE_311_STABLE 2022-07-25 11:46:42 +04:00
Philipp Imhof caa944907a MDL-74974 quiz override mark: do not hide_all_feedback
(backport of MDL-74495)
2022-07-22 15:34:16 +02:00
Thong Bui 19815c2c2c MDL-74766 html_editor: Fix form is submitted as an empty string 2022-07-22 10:17:45 +07:00
Ilya Tregubov 8de4b14be7 weekly release 3.11.8+ 2022-07-21 21:29:58 +04:00
Ilya Tregubov 2016d64b83 NOBUG: Fixed SVG browser compatibility 2022-07-21 21:29:56 +04:00
Ilya Tregubov 3eedb8a9c9 Merge branch 'install_311_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_311_STABLE 2022-07-21 21:29:56 +04:00
Jun Pataleta 7f8d209ba1 Merge branch 'MDL-75208_311' of https://github.com/stronk7/moodle into MOODLE_311_STABLE 2022-07-21 11:27:14 +08:00
Víctor Déniz 3f2ffcce08 Merge branch 'MDL-73332-311' of https://github.com/martygilbert/moodle into MOODLE_311_STABLE 2022-07-20 19:56:07 +01:00
Christina Thee Roperto 18f46b32a2 MDL-72349 filelib: update strip_double_headers function 2022-07-19 19:14:52 +10:00
Eloy Lafuente (stronk7) ffb762aae0 MDL-75208 oracle: Amend a couple of queries to make Oracle happy
The changes introduced here are completely safe, just we stop
binding SITEID and, instead, embed it in the SQL.

Why? Because Oracle 21 has started to return non-sense results
when SITEID is bound.

After lots of tests, attempts, debugging... we have been unable
to find any logic to the need of this change and also, have been
unable to reproduce the problem with a standalone script that
pretty much runs the same queries that the ones changed here.

I'm sure that there is something, somewhere, but have failed
to find it, grrr.

Please read MDL-75208 and linked issues to find more information
about this problem, that is one of the biggest mysteries I've
seen recently. Maybe at the end there is a tiny detail that
explains it all, but it's really well hidden.
2022-07-19 09:52:59 +02:00
Angelia Dela Cruz 9b6a36114c MDL-73788 behat: URL behat generators use UI
Replace steps that manually add URL instances via the UI and use
Behat generators. This improves the speed of Behat test runs.
2022-07-19 08:53:26 +08:00
Ilya Tregubov 8ef1574c47 Merge branch 'MDL-68843-311' of https://github.com/snake/moodle into MOODLE_311_STABLE 2022-07-18 14:16:37 +04:00
Jake Dallimore 09a93cb6eb MDL-68843 enrol_lti: auto disable orphaned enrolment instances
- pre_course_module_deletion hook to disable on mod delete
- upgrade step dealing with legacy orphaned records
2022-07-18 16:25:08 +08:00
Jake Dallimore b1fa2a9976 MDL-68843 enrol_lti: add deleted activity warning to published tools
Adds a warning to the 'name' column of the tables, letting teachers know
that the activity exposed by the method has been deleted and what their
options are.
2022-07-18 16:25:08 +08:00
AMOS bot 310da5eee7 Automatically generated installer lang files 2022-07-16 00:08:03 +00:00
Denis Brat f9e7aa5820 MDL-69251 enrol_lti: fix sync_members task inconsistent internal state 2022-07-15 12:21:09 +08:00
Marty 91c1bba220 MDL-73332 mustache: Fix shortentext helper
In the docs, it claims you can provide length to
the shortentext template helper as a context variable.
In reality, that is not the case.
2022-07-14 09:36:07 -04:00
Víctor Déniz 8984ffef4f weekly release 3.11.8+ 2022-07-14 12:06:12 +01:00
Víctor Déniz 419e169fa5 Merge branch 'install_311_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_311_STABLE 2022-07-14 12:06:10 +01:00
Víctor Déniz 73003c7c96 Merge branch 'MDL-74681-written_time_in_query_end_311' of https://github.com/catalyst/moodle into MOODLE_311_STABLE 2022-07-12 11:54:39 +01:00
Srdjan 4619b5128d MDL-74681 lib/dml: moodle_read_slave_trait: written table timestamping
Moved written table timestamping from query_start() to query_end():
We are adjusting table last written times at the end of transaction.
That does not apply to immediate database writes that are not performed
within transaction. This change is to set last written time after the query
has finished for such writes, rather than before it started. That way
long write operations cannot spill over the latency parameter.
2022-07-12 14:39:04 +10:00
Jun Pataleta 9a5389830e Merge branch 'MDL-71747_311' of https://github.com/stronk7/moodle into MOODLE_311_STABLE 2022-07-12 10:50:51 +08:00
AMOS bot 44cd084562 Automatically generated installer lang files 2022-07-12 00:07:59 +00:00
Jun Pataleta ca6f1479ad Merge branch 'MDL-74282-311' of https://github.com/paulholden/moodle into MOODLE_311_STABLE 2022-07-11 20:35:40 +08:00
Jun Pataleta 2e592749aa Merge branch 'MDL-73215-undef-plagiarismlinks-311' of https://github.com/leonstr/moodle into MOODLE_311_STABLE 2022-07-11 17:01:50 +08:00
Jun Pataleta e261c7ed25 Merge branch 'MDL-74784-311' of https://github.com/sarjona/moodle into MOODLE_311_STABLE 2022-07-11 16:53:41 +08:00
Jake Dallimore 57c492a6a5 MDL-74947 mod_lti: remove text validation from password field
This is a password field so should not be cleaned.
2022-07-11 13:37:31 +08:00
Johannes Burk 442e1f33c9 MDL-75083 login: prevent double submission
Apply core_form/submit JavaScript code to prevent double submission of login form and guest login button.
Add ID to guest login button to make it addressable.
2022-07-08 16:28:13 +02:00
Paul Holden 9cde5a75bb MDL-74282 grade: omit outcomes report if outcomes aren't enabled. 2022-07-08 10:43:31 +01:00
Eloy Lafuente (stronk7) 191351d2a3 MDL-71747 environment: Ensure that check_xmlrpc_usage is in >=39
This is a partial backport of MDL-73270 to ensure that the
check_xmlrpc_usage custom check is present in all the supported
branches and it's applied to all branches able to run php80:

- MOODLE_311_STABLE
- MOODLE_400_STABLE
- master (aka, 4.1 and up)

Note that the whole patch has not been backported, only the
environmental check (xml file and check implementation).
2022-07-07 23:44:57 +02:00
Eloy Lafuente (stronk7) fec4afdbd3 MDL-71747 environment: Apply agreed changes
- PHP 7.4 required.
- Moodle 3.9 required.
- MariaDB 10.4 required.
- PostgreSQL 12 required.
- Oracle 19c required.
2022-07-07 19:19:53 +02:00
Eloy Lafuente (stronk7) 02daea22d5 MDL-71747 environment: 4.1 base information
This is a direct copy of the 4.0 information, to
have the changes under control in the next commit.
2022-07-07 19:19:53 +02:00
Shamim Rezaie df61870abc MDL-56923 gradingform_guide: Fix icon positioning in RTL 2022-07-07 16:03:34 +10:00
Marina Glancy cb42a0ad6a MDL-74502 behat: when setting a field value it may not be visible
for example in case of editor field, the textarea element is hidden by wysiwyg widget but we still need to be able to set it
2022-07-01 10:31:28 +02:00
Leon Stringer 9b3be75879 MDL-73215 assign: Undefined var: plagiarismlinks
Fix: $plagiarismlinks is concatenated in return but may not have been
initialised.
2022-05-27 08:53:57 +01:00
Sara Arjona 1b9df59e3c MDL-74784 core_badges: Fix exception with related badges
System badges with related badges were displaying an error before this patch,
because the is_guest($context) function expects a context_course.
2022-05-18 09:18:05 +02:00
129 changed files with 2377 additions and 521 deletions
+196
View File
@@ -3539,6 +3539,8 @@
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_max_input_vars" level="optional">
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_xmlrpc_usage" level="optional">
</CUSTOM_CHECK>
</CUSTOM_CHECKS>
</MOODLE>
<MOODLE version="4.0" requires="3.6">
@@ -3729,6 +3731,200 @@
<ON_CHECK message="iscustomadminwarnings" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_xmlrpc_usage" level="optional">
</CUSTOM_CHECK>
</CUSTOM_CHECKS>
</MOODLE>
<MOODLE version="4.1" requires="3.9">
<UNICODE level="required">
<FEEDBACK>
<ON_ERROR message="unicoderequired" />
</FEEDBACK>
</UNICODE>
<DATABASE level="required">
<VENDOR name="mariadb" version="10.4" />
<VENDOR name="mysql" version="5.7" />
<VENDOR name="postgres" version="12" />
<VENDOR name="mssql" version="14.0" />
<VENDOR name="oracle" version="19" />
</DATABASE>
<PHP version="7.4.0" level="required">
</PHP>
<PCREUNICODE level="optional">
<FEEDBACK>
<ON_CHECK message="pcreunicodewarning" />
</FEEDBACK>
</PCREUNICODE>
<PHP_EXTENSIONS>
<PHP_EXTENSION name="iconv" level="required">
<FEEDBACK>
<ON_ERROR message="iconvrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="mbstring" level="required">
<FEEDBACK>
<ON_ERROR message="mbstringrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="curl" level="required">
<FEEDBACK>
<ON_ERROR message="curlrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="openssl" level="required">
<FEEDBACK>
<ON_ERROR message="opensslrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="tokenizer" level="optional">
<FEEDBACK>
<ON_CHECK message="tokenizerrecommended" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="xmlrpc" level="optional">
<FEEDBACK>
<ON_CHECK message="xmlrpcrecommended" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="soap" level="optional">
<FEEDBACK>
<ON_CHECK message="soaprecommended" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="ctype" level="required">
<FEEDBACK>
<ON_ERROR message="ctyperequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="zip" level="required">
<FEEDBACK>
<ON_ERROR message="ziprequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="zlib" level="required">
</PHP_EXTENSION>
<PHP_EXTENSION name="gd" level="required">
<FEEDBACK>
<ON_ERROR message="gdrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="simplexml" level="required">
<FEEDBACK>
<ON_ERROR message="simplexmlrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="spl" level="required">
<FEEDBACK>
<ON_ERROR message="splrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="pcre" level="required">
</PHP_EXTENSION>
<PHP_EXTENSION name="dom" level="required">
</PHP_EXTENSION>
<PHP_EXTENSION name="xml" level="required">
</PHP_EXTENSION>
<PHP_EXTENSION name="xmlreader" level="required">
</PHP_EXTENSION>
<PHP_EXTENSION name="intl" level="required">
<FEEDBACK>
<ON_ERROR message="intlrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="json" level="required">
</PHP_EXTENSION>
<PHP_EXTENSION name="hash" level="required"/>
<PHP_EXTENSION name="fileinfo" level="required"/>
<PHP_EXTENSION name="sodium" level="optional"/>
<PHP_EXTENSION name="exif" level="optional"/>
</PHP_EXTENSIONS>
<PHP_SETTINGS>
<PHP_SETTING name="memory_limit" value="96M" level="required">
<FEEDBACK>
<ON_ERROR message="settingmemorylimit" />
</FEEDBACK>
</PHP_SETTING>
<PHP_SETTING name="file_uploads" value="1" level="optional">
<FEEDBACK>
<ON_CHECK message="settingfileuploads" />
</FEEDBACK>
</PHP_SETTING>
<PHP_SETTING name="opcache.enable" value="1" level="optional">
<FEEDBACK>
<ON_CHECK message="opcacherecommended" />
</FEEDBACK>
</PHP_SETTING>
</PHP_SETTINGS>
<CUSTOM_CHECKS>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
<FEEDBACK>
<ON_ERROR message="unsupporteddbstorageengine" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
<FEEDBACK>
<ON_ERROR message="quizattemptsupgradedmessage" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
<FEEDBACK>
<ON_CHECK message="slashargumentswarning" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
<FEEDBACK>
<ON_CHECK message="unsupporteddbtablerowformat" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
<FEEDBACK>
<ON_CHECK message="unoconvwarning" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
<FEEDBACK>
<ON_CHECK message="libcurlwarning" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
<FEEDBACK>
<ON_ERROR message="unsupporteddbfileformat" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
<FEEDBACK>
<ON_ERROR message="unsupporteddbfilepertable" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
<FEEDBACK>
<ON_ERROR message="unsupporteddblargeprefix" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
<FEEDBACK>
<ON_CHECK message="ishttpswarning" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
<FEEDBACK>
<ON_CHECK message="incompleteunicodesupport" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="optional">
<FEEDBACK>
<ON_CHECK message="sixtyfourbitswarning" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_max_input_vars" level="optional">
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_admin_dir_usage" level="optional">
<FEEDBACK>
<ON_CHECK message="iscustomadminwarnings" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_xmlrpc_usage" level="optional">
</CUSTOM_CHECK>
</CUSTOM_CHECKS>
</MOODLE>
</COMPATIBILITY_MATRIX>
@@ -7,8 +7,8 @@ Feature: Behat steps for interacting with form work
@javascript
Scenario: Test fields in containers
Given the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
| fullname | shortname | summary | summaryformat |
| Course 1 | C1 | Red | 1 |
When I log in as "admin"
And I am on "Course 1" course homepage
# Just get to any form.
@@ -17,11 +17,15 @@ Feature: Behat steps for interacting with form work
And I set the following fields in the "Appearance" "fieldset" to these values:
| Show activity reports | Yes |
| Number of announcements | 1 |
And I set the following fields in the "Description" "fieldset" to these values:
| Course summary | Green |
Then the field "Show activity reports" in the "Appearance" "fieldset" matches value "Yes"
And the field "Show activity reports" in the "Appearance" "fieldset" does not match value "No"
And the following fields in the "region-main" "region" match these values:
| Course full name | Frog |
| Number of announcements | 1 |
| Course summary | Green |
And the following fields in the "region-main" "region" do not match these values:
| Course full name | Course 1 |
| Number of announcements | 5 |
| Course summary | Red |
@@ -63,4 +63,67 @@ class behat_tool_lp extends behat_base {
$this->execute('behat_general::i_click_on', [$xpathtarget, 'xpath_element']);
}
/**
* Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'.
*
* Recognised page names are:
* | pagetype | name meaning | description |
* | Course competencies | Course name | The course competencies page |
*
* @param string $page identifies which type of page this is, e.g. 'Course competencies'.
* @param string $identifier identifies the particular page, e.g. 'C1'.
* @return moodle_url the corresponding URL.
* @throws Exception with a meaningful error message if the specified page cannot be found.
*/
protected function resolve_page_instance_url(string $page, string $identifier): moodle_url {
switch (strtolower($page)) {
case 'course competencies':
$courseid = $this->get_course_id($identifier);
return new moodle_url('/admin/tool/lp/coursecompetencies.php', [
'courseid' => $courseid,
]);
default:
throw new Exception("Unrecognised page type '{$page}'");
}
}
/**
* Return a list of the exact named selectors for the component.
*
* @return behat_component_named_selector[]
*/
public static function get_exact_named_selectors(): array {
return [
new behat_component_named_selector('competency', [
"//*[@data-region='coursecompetencies']//table[contains(@class,'managecompetencies')]".
"//tr[contains(., //a[@title='View details'][contains(., %locator%)])]",
]),
new behat_component_named_selector('learning plan', [
"//*[@data-region='plan-competencies']//table[contains(@class,'managecompetencies')]".
"//tr[@data-node='user-competency'][contains(., //a[@data-usercompetency='true'][contains(., %locator%)])]",
]),
new behat_component_named_selector('competency description', [
"//td/p[contains(., %locator%)]",
]),
new behat_component_named_selector('competency grade', [
"//span[contains(concat(' ', normalize-space(@class), ' '), ' badge ')][contains(., %locator%)]",
]),
new behat_component_named_selector('learning plan rating', [
"//td[position()=2][contains(., %locator%)]",
]),
new behat_component_named_selector('learning plan proficiency', [
"//td[position()=3][contains(., %locator%)]",
]),
new behat_component_named_selector('competency page proficiency', [
"//dt[contains(., 'Proficient')]/following-sibling::dd[1][contains(., %locator%)]",
]),
new behat_component_named_selector('competency page rating', [
"//dt[contains(., 'Rating')]/following-sibling::dd[1][contains(., %locator%)]",
]),
new behat_component_named_selector('competency page related competency', [
"//*[@data-region='relatedcompetencies']//a[contains(., %locator%)]",
]),
];
}
}
@@ -0,0 +1,187 @@
@tool @tool_lp @javascript
Feature: View competencies
In order to access competency information
As a user
I need to view user competencies
Background:
Given the following "users" exist:
| username | firstname | lastname |
| student1 | Student | first |
| teacher1 | Teacher | first |
And the following "system role assigns" exist:
| user | role | contextlevel |
| teacher1 | editingteacher | System |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/competency:planview | Allow | editingteacher | System | |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| teacher1 | C1 | editingteacher |
And the following "scales" exist:
| name | scale |
| Test Scale | Bad, Good, Great |
And the following "core_competency > frameworks" exist:
| shortname | idnumber | scale |
| Cookery | cookery | Test Scale |
| Literacy | literacy | Test Scale |
And the following "core_competency > competencies" exist:
| shortname | idnumber | description | competencyframework |
| Salads | salads | Salads are important | cookery |
| Desserts | desserts | Desserts are important | cookery |
| Cakes | cakes | Cakes are important | cookery |
| Reading | reading | Reading is important | literacy |
| Writing | writing | Writing is important | literacy |
And the following "core_competency > related_competencies" exist:
| competency | relatedcompetency |
| desserts | cakes |
And the following "core_competency > plans" exist:
| name | description | competencies | user |
| Cookery | Cookery is important | salads, desserts, cakes | student1 |
| Literacy | Literacy is important | reading, writing | student1 |
And the following "core_competency > course_competencies" exist:
| course | competency |
| C1 | salads |
| C1 | desserts |
| C1 | cakes |
| C1 | reading |
| C1 | writing |
And the following "core_competency > user_competency" exist:
| competency | user | grade |
| salads | student1 | Good |
| desserts | student1 | Great |
| cakes | student1 | Great |
And the following "core_competency > user_competency_courses" exist:
| course | competency | user | grade |
| C1 | salads | student1 | Good |
| C1 | desserts | student1 | Great |
| C1 | cakes | student1 | Great |
Scenario: Student view
# Course competencies
Given I am on the "C1" "tool_lp > course competencies" page logged in as "student1"
Then I should see "You are proficient in 3 out of 5 competencies in this course"
And "Salads are important" "tool_lp > competency description" should exist in the "Salads" "tool_lp > competency"
And "Good" "tool_lp > competency grade" should exist in the "Salads" "tool_lp > competency"
And "Desserts are important" "tool_lp > competency description" should exist in the "Desserts" "tool_lp > competency"
And "Great" "tool_lp > competency grade" should exist in the "Desserts" "tool_lp > competency"
And "Cakes are important" "tool_lp > competency description" should exist in the "Cakes" "tool_lp > competency"
And "Great" "tool_lp > competency grade" should exist in the "Cakes" "tool_lp > competency"
And "Reading is important" "tool_lp > competency description" should exist in the "Reading" "tool_lp > competency"
And "Good" "tool_lp > competency grade" should not exist in the "Reading" "tool_lp > competency"
And "Great" "tool_lp > competency grade" should not exist in the "Reading" "tool_lp > competency"
And "Bad" "tool_lp > competency grade" should not exist in the "Reading" "tool_lp > competency"
And "Writing is important" "tool_lp > competency description" should exist in the "Writing" "tool_lp > competency"
And "Good" "tool_lp > competency grade" should not exist in the "Writing" "tool_lp > competency"
And "Great" "tool_lp > competency grade" should not exist in the "Writing" "tool_lp > competency"
And "Bad" "tool_lp > competency grade" should not exist in the "Writing" "tool_lp > competency"
# Course competencies details
And I click on "Desserts" "link" in the "Desserts" "tool_lp > competency"
And I should see "Desserts are important"
And "Yes" "tool_lp > competency page proficiency" should exist
And "Great" "tool_lp > competency page rating" should exist
# Course competencies summary
And I click on "Cakes" "link"
And I should see "Cakes are important"
# Learning plans
And I click on "Close" "button" in the "Cakes" "dialogue"
And I follow "Profile" in the user menu
And I click on "Learning plans" "link"
And I should see "Cookery"
And I should see "Literacy"
# Learning plans details
And I click on "Cookery" "link"
And I should see "Cookery is important"
And I should see "3 out of 3 competencies are proficient"
And "Good" "tool_lp > learning plan rating" should exist in the "Salads" "tool_lp > learning plan"
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Salads" "tool_lp > learning plan"
And "Great" "tool_lp > learning plan rating" should exist in the "Desserts" "tool_lp > learning plan"
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Desserts" "tool_lp > learning plan"
And "Great" "tool_lp > learning plan rating" should exist in the "Cakes" "tool_lp > learning plan"
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Cakes" "tool_lp > learning plan"
And I should not see "Literacy"
And I should not see "Reading"
And I should not see "Writing"
# Learning plans competency details
And I click on "Desserts" "link" in the "Desserts" "tool_lp > learning plan"
And I should see "Desserts are important"
And "Yes" "tool_lp > competency page proficiency" should exist
And "Great" "tool_lp > competency page rating" should exist
# Learning plans competency summary
And I click on "Cakes cakes" "link"
And I should see "Cakes are important"
Scenario: Teacher view
# Participant competencies
Given I am on the "C1" "report_competency > breakdown" page logged in as "teacher1"
Then I should see "Student first"
And "Good" "report_competency > breakdown rating" should exist in the "Salads" "report_competency > breakdown"
And "Great" "report_competency > breakdown rating" should exist in the "Desserts" "report_competency > breakdown"
And "Great" "report_competency > breakdown rating" should exist in the "Cakes" "report_competency > breakdown"
And "Not rated" "report_competency > breakdown rating" should exist in the "Reading" "report_competency > breakdown"
And "Not rated" "report_competency > breakdown rating" should exist in the "Writing" "report_competency > breakdown"
# Participant competencies details
And I click on "Great" "report_competency > breakdown rating" in the "Desserts" "report_competency > breakdown"
And "Yes" "tool_lp > competency page proficiency" should exist
And "Great" "tool_lp > competency page rating" should exist
# Participant competencies summary
And I click on "Cakes" "tool_lp > competency page related competency"
And I should see "Cakes are important"
# Participant learning plans
And I click on "Close" "button" in the "Cakes" "dialogue"
And I click on "Close" "button" in the "User competency summary" "dialogue"
And I navigate to course participants
And I click on "Student first" "link"
And I click on "Learning plans" "link"
And I should see "Cookery"
And I should see "Literacy"
# Participant learning plans details
And I click on "Cookery" "link"
And I should see "Cookery is important"
And I should see "3 out of 3 competencies are proficient"
And "Good" "tool_lp > learning plan rating" should exist in the "Salads" "tool_lp > learning plan"
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Salads" "tool_lp > learning plan"
And "Great" "tool_lp > learning plan rating" should exist in the "Desserts" "tool_lp > learning plan"
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Desserts" "tool_lp > learning plan"
And "Great" "tool_lp > learning plan rating" should exist in the "Cakes" "tool_lp > learning plan"
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Cakes" "tool_lp > learning plan"
And I should not see "Literacy"
And I should not see "Reading"
And I should not see "Writing"
# Learning plans competency details
And I click on "Desserts" "link"
And I should see "Desserts are important"
And "Yes" "tool_lp > competency page proficiency" should exist
And "Great" "tool_lp > competency page rating" should exist
# Learning plans competency summary
And I click on "Cakes" "tool_lp > competency page related competency"
And I should see "Cakes are important"
@@ -12,9 +12,10 @@ Feature: Restore Moodle 2 course backups
| Course 3 | C3 | 0 | topics | 2 | 0 |
| Course 4 | C4 | 0 | topics | 20 | 0 |
And the following "activities" exist:
| activity | course | idnumber | name | intro | section |
| assign | C3 | assign1 | Test assign name | Assign description | 1 |
| data | C3 | data1 | Test database name | Database description | 2 |
| activity | course | idnumber | name | intro | section | externalurl |
| assign | C3 | assign1 | Test assign name | Assign description | 1 | |
| data | C3 | data1 | Test database name | Database description | 2 | |
| url | C1 | url1 | Test URL name | Test URL description | 3 | http://www.moodle.org |
And I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
And I add a "Forum" to section "1" and I fill the form with:
@@ -108,11 +109,7 @@ Feature: Restore Moodle 2 course backups
@javascript
Scenario: Restore a backup in an existing course retaining the backup course settings
Given I add a "URL" to section "3" and I fill the form with:
| Name | Test URL name |
| Description | Test URL description |
| id_externalurl | http://www.moodle.org |
And I hide section "3"
Given I hide section "3"
And I hide section "7"
When I backup "Course 1" course using this options:
| Confirmation | Filename | test_backup.mbz |
@@ -134,11 +131,7 @@ Feature: Restore Moodle 2 course backups
@javascript
Scenario: Restore a backup in an existing course keeping the target course settings
Given I add a "URL" to section "3" and I fill the form with:
| Name | Test URL name |
| Description | Test URL description |
| id_externalurl | http://www.moodle.org |
And I hide section "3"
Given I hide section "3"
And I hide section "7"
When I backup "Course 1" course using this options:
| Confirmation | Filename | test_backup.mbz |
@@ -160,11 +153,7 @@ Feature: Restore Moodle 2 course backups
@javascript
Scenario: Restore a backup in an existing course deleting contents and retaining the backup course settings
Given I add a "URL" to section "3" and I fill the form with:
| Name | Test URL name |
| Description | Test URL description |
| id_externalurl | http://www.moodle.org |
And I hide section "3"
Given I hide section "3"
And I hide section "7"
When I backup "Course 1" course using this options:
| Initial | Include enrolled users | 0 |
@@ -189,11 +178,7 @@ Feature: Restore Moodle 2 course backups
@javascript
Scenario: Restore a backup in an existing course deleting contents and keeping the current course settings
Given I add a "URL" to section "3" and I fill the form with:
| Name | Test URL name |
| Description | Test URL description |
| id_externalurl | http://www.moodle.org |
And I hide section "3"
Given I hide section "3"
And I hide section "7"
When I backup "Course 1" course using this options:
| Initial | Include enrolled users | 0 |
@@ -218,11 +203,7 @@ Feature: Restore Moodle 2 course backups
@javascript
Scenario: Restore a backup in an existing course deleting contents decreasing the number of sections
Given I add a "URL" to section "3" and I fill the form with:
| Name | Test URL name |
| Description | Test URL description |
| id_externalurl | http://www.moodle.org |
And I hide section "3"
Given I hide section "3"
And I hide section "7"
When I backup "Course 1" course using this options:
| Initial | Include enrolled users | 0 |
+1 -1
View File
@@ -207,7 +207,7 @@ class issued_badge implements renderable {
$data->hasrelatedbadges = true;
$data->relatedbadges = [];
foreach ($relatedbadges as $related) {
if (isloggedin() && !is_guest($context)) {
if (isloggedin() && ($context instanceof context_course && !is_guest($context))) {
$related->url = (new moodle_url('/badges/overview.php', ['id' => $related->id]))->out(false);
}
$data->relatedbadges[] = (array)$related;
@@ -30,9 +30,7 @@ Feature: Award badges with separate groups
| teacher1 | CB |
| student2 | CA |
| teacher2 | CA |
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Edit settings" in current page administration
And I am on the "Course 1" "course editing" page logged in as "teacher1"
And I expand all fieldsets
And I set the field "Group mode" to "Separate groups"
And I press "Save and display"
+10 -1
View File
@@ -119,10 +119,19 @@ class block_news_items extends block_base {
if (!empty($CFG->forum_enabletimedposts) && ($discussion->timestart > $posttime)) {
$posttime = $discussion->timestart;
}
// If the user who created the discussion post has been deleted, indicate so.
if ($discussion->userdeleted) {
$userfullname = get_string('deleteduser', 'mod_forum');
} else {
$userfullname = fullname($discussion, has_capability('moodle/site:viewfullnames', $context));
}
$text .= '<li class="post">'.
'<div class="head clearfix">'.
'<div class="date">'.userdate($posttime, $strftimerecent).'</div>'.
'<div class="name">'.fullname($discussion).'</div></div>'.
'<div class="name">'.$userfullname.'</div>'.
'</div>'.
'<div class="info"><a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->discussion.'">'.$discussion->subject.'</a></div>'.
"</li>\n";
}
@@ -10,11 +10,18 @@ Feature: Add URL to main menu block
And I am on site homepage
And I turn editing mode on
And I add the "Main menu" block
When I add a "URL" to section "0" and I fill the form with:
| Name | reference link |
| Description | mooooooooodle |
| External URL | http://www.moodle.com |
| id_display | In pop-up |
And the following "activity" exists:
| activity | url |
| course | Acceptance test site |
| name | reference link |
| intro | mooooooooodle |
| externalurl | http://www.moodle.com |
| section | 0 |
When I am on the "reference link" "url activity editing" page
And I expand all fieldsets
And I set the following fields to these values:
| id_display | In pop-up |
And I press "Save and return to course"
Then "reference link" "link" should exist in the "Main menu" "block"
And "Add an activity or resource" "button" should exist in the "Main menu" "block"
@@ -33,12 +40,18 @@ Feature: Add URL to main menu block
And I set the following fields to these values:
| Page contexts | Display throughout the entire site |
And I press "Save changes"
When I am on the "C1" Course page
And I add a "URL" to section "0" and I fill the form with:
| Name | reference link |
| Description | mooooooooodle |
| External URL | http://www.moodle.com |
| Display description on course page | 1 |
| id_display | In pop-up |
When the following "activity" exists:
| activity | url |
| course | C1 |
| name | reference link |
| intro | mooooooooodle |
| externalurl | http://www.moodle.com |
| section | 0 |
| showdescription | 1 |
And I am on the "reference link" "url activity editing" page
And I expand all fieldsets
And I set the following fields to these values:
| id_display | In pop-up |
And I press "Save and return to course"
Then "reference link" "link" should not exist in the "Main menu" "block"
And I should see "mooooooooodle" in the "region-main" "region"
+1 -1
View File
@@ -43,7 +43,7 @@ class cachestore_file_addinstance_form extends cachestore_addinstance_form {
$form = $this->_form;
$form->addElement('text', 'path', get_string('path', 'cachestore_file'));
$form->setType('path', PARAM_SAFEPATH);
$form->setType('path', PARAM_RAW);
$form->addHelpButton('path', 'path', 'cachestore_file');
$form->addElement('checkbox', 'autocreate', get_string('autocreate', 'cachestore_file'));
+1 -1
View File
@@ -5,6 +5,6 @@
* @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define("core_calendar/crud",["jquery","core/str","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","core/modal_factory","core/modal_events","core_calendar/modal_event_form","core_calendar/repository","core_calendar/events","core_calendar/modal_delete","core_calendar/selectors","core/pending"],(function($,Str,Notification,CustomEvents,Modal,ModalRegistry,ModalFactory,ModalEvents,ModalEventForm,CalendarRepository,CalendarEvents,ModalDelete,CalendarSelectors,Pending){return{registerRemove:function(root){root.on("click",CalendarSelectors.actions.remove,(function(e){var eventSource=$(this).closest(CalendarSelectors.eventItem);!function(eventId,eventTitle,eventCount){var deletePromise,pendingPromise=new Pending("core_calendar/crud:confirmDeletion"),deleteStrings=[{key:"deleteevent",component:"calendar"}],isRepeatedEvent=(eventCount=parseInt(eventCount,10))>1;isRepeatedEvent?(deleteStrings.push({key:"confirmeventseriesdelete",component:"calendar",param:{name:eventTitle,count:eventCount}}),deletePromise=ModalFactory.create({type:ModalDelete.TYPE})):(deleteStrings.push({key:"confirmeventdelete",component:"calendar",param:eventTitle}),deletePromise=ModalFactory.create({type:ModalFactory.types.SAVE_CANCEL}));var stringsPromise=Str.get_strings(deleteStrings);$.when(stringsPromise,deletePromise).then((function(strings,deleteModal){return deleteModal.setRemoveOnClose(!0),deleteModal.setTitle(strings[0]),deleteModal.setBody(strings[1]),isRepeatedEvent||deleteModal.setSaveButtonText(strings[0]),deleteModal.show(),deleteModal.getRoot().on(ModalEvents.save,(function(){var pendingPromise=new Pending("calendar/crud:initModal:deletedevent");CalendarRepository.deleteEvent(eventId,!1).then((function(){$("body").trigger(CalendarEvents.deleted,[eventId,!1])})).then(pendingPromise.resolve).catch(Notification.exception)})),deleteModal.getRoot().on(CalendarEvents.deleteAll,(function(){var pendingPromise=new Pending("calendar/crud:initModal:deletedallevent");CalendarRepository.deleteEvent(eventId,!0).then((function(){$("body").trigger(CalendarEvents.deleted,[eventId,!0])})).then(pendingPromise.resolve).catch(Notification.exception)})),deleteModal})).then((function(modal){return pendingPromise.resolve(),modal})).catch(Notification.exception)}(eventSource.data("eventId"),eventSource.data("eventTitle"),eventSource.data("eventCount")),e.preventDefault()}))},registerEditListeners:function(root,eventFormModalPromise){var pendingPromise=new Pending("core_calendar/crud:registerEditListeners");return eventFormModalPromise.then((function(modal){return $("body").on(CalendarEvents.editEvent,(function(e,eventId){var calendarWrapper=root.find(CalendarSelectors.wrapper);modal.setEventId(eventId),modal.setContextId(calendarWrapper.data("contextId")),modal.show(),e.stopImmediatePropagation()})),modal})).then((function(modal){return pendingPromise.resolve(),modal})).catch(Notification.exception)},registerEventFormModal:function(root){var eventFormPromise=ModalFactory.create({type:ModalEventForm.TYPE,large:!0});return root.on("click",CalendarSelectors.actions.create,(function(e){eventFormPromise.then((function(modal){var wrapper=root.find(CalendarSelectors.wrapper),categoryId=wrapper.data("categoryid");void 0!==categoryId&&modal.setCategoryId(categoryId);var today=root.find(CalendarSelectors.today),firstDay=root.find(CalendarSelectors.day);!today.length&&firstDay.length&&modal.setStartTime(firstDay.data("newEventTimestamp")),modal.setContextId(wrapper.data("contextId")),modal.setCourseId(wrapper.data("courseid")),modal.show()})).fail(Notification.exception),e.preventDefault()})),root.on("click",CalendarSelectors.actions.edit,(function(e){e.preventDefault();var target=$(e.currentTarget),calendarWrapper=target.closest(CalendarSelectors.wrapper),eventWrapper=target.closest(CalendarSelectors.eventItem);eventFormPromise.then((function(modal){modal.setEventId(eventWrapper.data("eventId")),modal.setContextId(calendarWrapper.data("contextId")),modal.show(),e.stopImmediatePropagation()})).fail(Notification.exception)})),eventFormPromise}}}));
define("core_calendar/crud",["jquery","core/str","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","core/modal_factory","core/modal_events","core_calendar/modal_event_form","core_calendar/repository","core_calendar/events","core_calendar/modal_delete","core_calendar/selectors","core/pending"],(function($,Str,Notification,CustomEvents,Modal,ModalRegistry,ModalFactory,ModalEvents,ModalEventForm,CalendarRepository,CalendarEvents,ModalDelete,CalendarSelectors,Pending){return{registerRemove:function(root){root.on("click",CalendarSelectors.actions.remove,(function(e){var eventSource=$(this).closest(CalendarSelectors.eventItem);!function(eventId,eventTitle,eventCount){var deletePromise,pendingPromise=new Pending("core_calendar/crud:confirmDeletion"),deleteStrings=[{key:"deleteevent",component:"calendar"}],isRepeatedEvent=(eventCount=parseInt(eventCount,10))>1;isRepeatedEvent?(deleteStrings.push({key:"confirmeventseriesdelete",component:"calendar",param:{name:eventTitle,count:eventCount}}),deletePromise=ModalFactory.create({type:ModalDelete.TYPE})):(deleteStrings.push({key:"confirmeventdelete",component:"calendar",param:eventTitle}),deletePromise=ModalFactory.create({type:ModalFactory.types.SAVE_CANCEL}));var stringsPromise=Str.get_strings(deleteStrings);$.when(stringsPromise,deletePromise).then((function(strings,deleteModal){return deleteModal.setRemoveOnClose(!0),deleteModal.setTitle(strings[0]),deleteModal.setBody(strings[1]),isRepeatedEvent||deleteModal.setSaveButtonText(strings[0]),deleteModal.show(),deleteModal.getRoot().on(ModalEvents.save,(function(){var pendingPromise=new Pending("calendar/crud:initModal:deletedevent");CalendarRepository.deleteEvent(eventId,!1).then((function(){$("body").trigger(CalendarEvents.deleted,[eventId,!1])})).then(pendingPromise.resolve).catch(Notification.exception)})),deleteModal.getRoot().on(CalendarEvents.deleteAll,(function(){var pendingPromise=new Pending("calendar/crud:initModal:deletedallevent");CalendarRepository.deleteEvent(eventId,!0).then((function(){$("body").trigger(CalendarEvents.deleted,[eventId,!0])})).then(pendingPromise.resolve).catch(Notification.exception)})),deleteModal})).then((function(modal){return pendingPromise.resolve(),modal})).catch(Notification.exception)}(eventSource.data("eventId"),eventSource.data("eventTitle"),eventSource.data("eventCount")),e.preventDefault()}))},registerEditListeners:function(root,eventFormModalPromise){var pendingPromise=new Pending("core_calendar/crud:registerEditListeners");return eventFormModalPromise.then((function(modal){return $("body").on(CalendarEvents.editEvent,(function(e,eventId){var calendarWrapper=root.find(CalendarSelectors.wrapper);modal.setEventId(eventId),modal.setContextId(calendarWrapper.data("contextId")),modal.show(),e.stopImmediatePropagation()})),modal})).then((function(modal){return pendingPromise.resolve(),modal})).catch(Notification.exception)},registerEventFormModal:function(root){var eventFormPromise=ModalFactory.create({type:ModalEventForm.TYPE,large:!0});return root.on("click",CalendarSelectors.actions.create,(function(e){eventFormPromise.then((function(modal){var wrapper=root.find(CalendarSelectors.wrapper),categoryId=wrapper.data("categoryid");void 0!==categoryId&&modal.setCategoryId(categoryId);var today=root.find(CalendarSelectors.today),firstDay=root.find(CalendarSelectors.day);!today.length&&firstDay.length&&modal.setStartTime(firstDay.data("newEventTimestamp")),modal.setContextId(wrapper.data("contextId")),modal.setCourseId(wrapper.data("courseid")),modal.show()})).fail(Notification.exception),e.preventDefault()})),root.on("click",CalendarSelectors.actions.edit,(function(e){e.preventDefault();var target=$(e.currentTarget),calendarWrapper=target.closest(CalendarSelectors.wrapper),eventWrapper=target.closest(CalendarSelectors.eventItem);eventFormPromise.then((function(modal){modal.setEventId(eventWrapper.data("eventId")),modal.setContextId(calendarWrapper.data("contextId")),modal.setCourseId(eventWrapper.data("courseId")),modal.show(),e.stopImmediatePropagation()})).fail(Notification.exception)})),eventFormPromise}}}));
//# sourceMappingURL=crud.min.js.map
File diff suppressed because one or more lines are too long
+1
View File
@@ -202,6 +202,7 @@ function(
modal.setEventId(eventWrapper.data('eventId'));
modal.setContextId(calendarWrapper.data('contextId'));
modal.setCourseId(eventWrapper.data('courseId'));
modal.show();
e.stopImmediatePropagation();
@@ -109,6 +109,9 @@ class create_update_form_mapper implements create_update_form_mapper_interface {
} else {
$properties->courseid = $data->courseid;
}
if (empty($properties->groupid)) {
$properties->groupid = 0;
}
}
// Decode the form fields back into valid event property.
+68 -1
View File
@@ -16,11 +16,13 @@ Feature: Perform basic calendar functionality
| Course 1 | C1 | topics |
| Course 2 | C2 | topics |
| Course 3 | C3 | topics |
| Course 4 | C4 | topics |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| student3 | C1 | student |
| teacher1 | C1 | teacher |
| admin | C1 | editingteacher |
And the following "groups" exist:
| name | course | idnumber |
| Group 1 | C1 | G1 |
@@ -195,7 +197,6 @@ Feature: Perform basic calendar functionality
Scenario: Admin can only see all courses if calendar_adminseesall setting is enabled.
Given I log in as "admin"
And I am on "Course 1" course homepage
And I enrol "admin" user as "Teacher"
And I am viewing site calendar
And I click on "New event" "button"
And I set the field "Type of event" to "Course"
@@ -247,3 +248,69 @@ Feature: Perform basic calendar functionality
Then the page should meet accessibility standards
And the page should meet "wcag131, wcag143, wcag412" accessibility standards
And the page should meet accessibility standards with "wcag131, wcag143, wcag412" extra tests
@javascript
Scenario: Admin can create and edit course events if calendar_adminseesall setting is disabled
Given I log in as "admin"
And the following config values are set as admin:
| calendar_adminseesall | 0 |
And I am on "Course 4" course homepage with editing mode on
And I add the "Upcoming events" block
And I click on "Go to calendar..." "link" in the "Upcoming events" "block"
And I click on "New event" "button"
And I should see "Course" in the "Type of event" "select"
And "Course 4" "autocomplete_selection" should exist
And I set the field "Event title" to "Test course event"
And I click on "Save" "button"
And I should see "Test course event"
When I click on "Edit" "link" in the "region-main" "region"
Then the field "Event title" matches value "Test course event"
And the field "Type of event" matches value "Course"
And "Course 4" "autocomplete_selection" should exist
@javascript
Scenario: Changing the event type should clear previous data
Given I log in as "admin"
And I am on "Course 1" course homepage
And I follow "This month"
And I press "New event"
And I set the following fields to these values:
| Event title | Group 1 event |
| Type of event | Group |
And I press "Save"
And I am on "Course 1" course homepage
And I follow "This month"
And I click on "Group 1 event" "link"
And I should see "Group event"
And I should see "Group 1"
When I click on "Edit" "button"
And I set the following fields to these values:
| Event title | My own user event |
| Type of event | user |
And I press "Save"
And I click on "My own user event" "link"
Then I should see "User event"
And I should not see "Group 1"
And I click on "Edit" "button"
And I set the following fields to these values:
| Event title | Site event |
| Type of event | site |
And I press "Save"
And I click on "Site event" "link"
And I should see "Site event"
And I click on "Edit" "button"
And I set the following fields to these values:
| Event title | Course 1 event |
| Type of event | course |
And I expand the "Course" autocomplete
And I click on "Course 1" item in the autocomplete list
And I press "Save"
And I click on "Course 1 event" "link"
And I should see "Course event"
And I click on "Edit" "button"
And I set the following fields to these values:
| Event title | Category event |
| Type of event | category |
And I press "Save"
And I click on "Category event" "link"
And I should see "Category event"
@@ -0,0 +1,261 @@
<?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/>.
use core_competency\competency;
use core_competency\competency_framework;
use core_competency\plan;
/**
* Behat data generator for core_competency.
*
* @package core_competency
* @category test
* @copyright 2022 Noel De Martin
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_core_competency_generator extends behat_generator_base {
/**
* Get a list of the entities that Behat can create using the generator step.
*
* @return array
*/
protected function get_creatable_entities(): array {
return [
'competencies' => [
'singular' => 'competency',
'datagenerator' => 'competency',
'required' => ['shortname', 'competencyframework'],
'switchids' => ['competencyframework' => 'competencyframeworkid'],
],
'course_competencies' => [
'singular' => 'course_competency',
'datagenerator' => 'course_competency',
'required' => ['course', 'competency'],
'switchids' => ['course' => 'courseid', 'competency' => 'competencyid'],
],
'frameworks' => [
'singular' => 'framework',
'datagenerator' => 'framework',
'required' => ['shortname'],
'switchids' => ['scale' => 'scaleid'],
],
'plans' => [
'singular' => 'plan',
'datagenerator' => 'plan',
'required' => ['name'],
'switchids' => ['user' => 'userid'],
],
'related_competencies' => [
'singular' => 'related_competency',
'datagenerator' => 'related_competency',
'required' => ['competency', 'relatedcompetency'],
'switchids' => ['competency' => 'competencyid', 'relatedcompetency' => 'relatedcompetencyid'],
],
'user_competency' => [
'singular' => 'user_competency',
'datagenerator' => 'user_competency',
'required' => ['competency', 'user'],
'switchids' => ['competency' => 'competencyid', 'user' => 'userid'],
],
'user_competency_courses' => [
'singular' => 'user_competency_course',
'datagenerator' => 'user_competency_course',
'required' => ['course', 'competency', 'user'],
'switchids' => ['course' => 'courseid', 'competency' => 'competencyid', 'user' => 'userid'],
],
'user_competency_plans' => [
'singular' => 'user_competency_plan',
'datagenerator' => 'user_competency_plan',
'required' => ['plan', 'competency', 'user'],
'switchids' => ['plan' => 'planid', 'competency' => 'competencyid', 'user' => 'userid'],
],
];
}
/**
* Get the competecy framework id using an idnumber.
*
* @param string $idnumber
* @return int The competecy framework id
*/
protected function get_competencyframework_id(string $idnumber): int {
global $DB;
if (!$id = $DB->get_field('competency_framework', 'id', ['idnumber' => $idnumber])) {
throw new Exception('The specified competency framework with idnumber "' . $idnumber . '" could not be found.');
}
return $id;
}
/**
* Get the competecy id using an idnumber.
*
* @param string $idnumber
* @return int The competecy id
*/
protected function get_competency_id(string $idnumber): int {
global $DB;
if (!$id = $DB->get_field('competency', 'id', ['idnumber' => $idnumber])) {
throw new Exception('The specified competency with idnumber "' . $idnumber . '" could not be found.');
}
return $id;
}
/**
* Get the learning plan id using a name.
*
* @param string $name
* @return int The learning plan id
*/
protected function get_plan_id(string $name): int {
global $DB;
if (!$id = $DB->get_field('competency_plan', 'id', ['name' => $name])) {
throw new Exception('The specified learning plan with name "' . $name . '" could not be found.');
}
return $id;
}
/**
* Get the related competecy id using an idnumber.
*
* @param string $idnumber
* @return int The related competecy id
*/
protected function get_relatedcompetency_id(string $idnumber): int {
return $this->get_competency_id($idnumber);
}
/**
* Add a plan.
*
* @param array $data Plan data.
*/
public function process_plan(array $data): void {
$generator = $this->get_data_generator();
$competencyids = $data['competencyids'] ?? [];
unset($data['competencyids']);
$plan = $generator->create_plan($data);
foreach ($competencyids as $competencyid) {
$generator->create_plan_competency([
'planid' => $plan->get('id'),
'competencyid' => $competencyid,
]);
}
}
/**
* Preprocess user competency data.
*
* @param array $data Raw data.
* @return array Processed data.
*/
protected function preprocess_user_competency(array $data): array {
$this->prepare_grading($data);
return $data;
}
/**
* Preprocess user course competency data.
*
* @param array $data Raw data.
* @return array Processed data.
*/
protected function preprocess_user_competency_course(array $data): array {
$this->prepare_grading($data);
return $data;
}
/**
* Preprocess user learning plan competency data.
*
* @param array $data Raw data.
* @return array Processed data.
*/
protected function preprocess_user_competency_plan(array $data): array {
$this->prepare_grading($data);
return $data;
}
/**
* Preprocess plan data.
*
* @param array $data Raw data.
* @return array Processed data.
*/
protected function preprocess_plan(array $data): array {
if (isset($data['competencies'])) {
$competencies = array_map('trim', str_getcsv($data['competencies']));
$data['competencyids'] = array_map([$this, 'get_competency_id'], $competencies);
unset($data['competencies']);
}
global $USER;
return $data + [
'userid' => $USER->id,
'status' => plan::STATUS_ACTIVE,
];
}
/**
* Prepare grading attributes for record data.
*
* @param array $data Record data.
*/
protected function prepare_grading(array &$data): void {
if (!isset($data['grade'])) {
return;
}
global $DB;
$competency = competency::get_record(['id' => $data['competencyid']]);
$competencyframework = competency_framework::get_record(['id' => $competency->get('competencyframeworkid')]);
$scale = $DB->get_field('scale', 'scale', ['id' => $competencyframework->get('scaleid')]);
$grades = array_map('trim', explode(',', $scale));
$grade = array_search($data['grade'], $grades);
if ($grade === false) {
throw new Exception('The grade "'.$data['grade'].'" was not found in the "'.
$competencyframework->get('shortname').'" competency framework.');
}
$data['proficiency'] = true;
$data['grade'] = $grade + 1;
}
/**
* Get the module data generator.
*
* @return core_competency_generator Competency data generator.
*/
protected function get_data_generator(): core_competency_generator {
return $this->componentdatagenerator;
}
}
+5 -2
View File
@@ -136,7 +136,11 @@ class core_competency_generator extends component_generator_base {
* @return competency_framework
*/
public function create_framework($record = null) {
$generator = phpunit_util::get_data_generator();
if (defined('BEHAT_TEST') && BEHAT_TEST) {
$generator = behat_util::get_data_generator();
} else {
$generator = phpunit_util::get_data_generator();
}
$this->frameworkcount++;
$i = $this->frameworkcount;
$record = (object) $record;
@@ -584,4 +588,3 @@ class core_competency_generator extends component_generator_base {
}
}
+2 -5
View File
@@ -2477,9 +2477,7 @@ function update_course($data, $editoroptions = NULL) {
function average_number_of_participants(bool $onlyactive = false, int $lastloginsince = null): float {
global $DB;
$params = [
'siteid' => SITEID,
];
$params = [];
$sql = "SELECT DISTINCT ue.userid, e.courseid
FROM {user_enrolments} ue
@@ -2490,8 +2488,7 @@ function average_number_of_participants(bool $onlyactive = false, int $lastlogin
$sql .= "JOIN {user} u ON u.id = ue.userid ";
}
$sql .= "WHERE e.courseid <> :siteid
AND c.visible = 1 ";
$sql .= "WHERE e.courseid <> " . SITEID . " AND c.visible = 1 ";
if ($onlyactive) {
$sql .= "AND ue.status = :active
+3 -3
View File
@@ -14,11 +14,11 @@ Feature: Force group mode in a course
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | course | name |
| chat | C1 | Chat room |
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I add a "Chat" to section "1" and I fill the form with:
| Name of this chat room | Chat room |
| Description | Chat description |
And I navigate to "Edit settings" in current page administration
@javascript
@@ -43,15 +43,15 @@ Feature: Course paged mode
Given the following "courses" exist:
| fullname | shortname | category | format | coursedisplay | numsections | startdate |
| Course 1 | C1 | 0 | <courseformat> | 1 | 3 | 0 |
And I log in as "admin"
And the following "activities" exist:
| activity | course | name |
| chat | C1 | Chat room |
When I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
And I click on <section1> "link" in the <section1> "section"
And I should see <section1> in the "div.single-section" "css_element"
And I should see <section2> in the ".single-section span.mdl-right" "css_element"
And I should not see <prevunexistingsection> in the ".single-section" "css_element"
When I add a "Chat" to section "1" and I fill the form with:
| Name of this chat room | Chat room |
| Description | Chat description |
Then I should see <section1> in the "div.single-section" "css_element"
And I should see <section2> in the ".single-section span.mdl-right" "css_element"
And I should not see <prevunexistingsection> in the ".single-section" "css_element"
@@ -14,6 +14,9 @@ Feature: Restrict activities availability
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | course | name |
| chat | C1 | Test chat name |
@javascript
Scenario: Activities can be added with the default permissions
@@ -22,9 +25,6 @@ Feature: Restrict activities availability
When I add a "Glossary" to section "1" and I fill the form with:
| Name | Test glossary name |
| Description | Test glossary description |
And I add a "Chat" to section "1" and I fill the form with:
| Name of this chat room | Test chat name |
| Description | Test chat description |
Then I should see "Test glossary name"
And I should see "Test chat name"
+33 -2
View File
@@ -101,7 +101,8 @@ class manage_table extends \table_sql {
* @return string
*/
public function col_name($tool) {
$name = helper::get_name($tool);
$toolcontext = \context::instance_by_id($tool->contextid, IGNORE_MISSING);
$name = $toolcontext ? helper::get_name($tool) : $this->get_deleted_activity_name_html($tool);
return $this->get_display_text($tool, $name);
}
@@ -243,9 +244,39 @@ class manage_table extends \table_sql {
*/
protected function get_display_text($tool, $text) {
if ($tool->status != ENROL_INSTANCE_ENABLED) {
return \html_writer::tag('span', $text, array('class' => 'dimmed_text'));
return \html_writer::tag('div', $text, array('class' => 'dimmed_text'));
}
return $text;
}
/**
* Get a warning icon, with tooltip, describing enrolment instances sharing activities which have been deleted.
*
* @param \stdClass $tool the tool instance record.
* @return string the HTML for the name column.
*/
protected function get_deleted_activity_name_html(\stdClass $tool): string {
global $OUTPUT;
$icon = \html_writer::tag(
'a',
$OUTPUT->pix_icon('enrolinstancewarning', get_string('deletedactivityalt' , 'enrol_lti'), 'enrol_lti'), [
"class" => "btn btn-link p-0",
"role" => "button",
"data-container" => "body",
"data-toggle" => "popover",
"data-placement" => right_to_left() ? "left" : "right",
"data-content" => get_string('deletedactivitydescription', 'enrol_lti'),
"data-html" => "true",
"tabindex" => "0",
"data-trigger" => "focus"
]
);
$name = \html_writer::span($icon . get_string('deletedactivity', 'enrol_lti'));
if ($tool->name) {
$name .= \html_writer::empty_tag('br') . \html_writer::empty_tag('br') . $tool->name;
}
return $name;
}
}
+18 -21
View File
@@ -50,9 +50,6 @@ class sync_members extends scheduled_task {
/** @var array Array of user photos. */
protected $userphotos = [];
/** @var array Array of current LTI users. */
protected $currentusers = [];
/** @var data_connector $dataconnector A data_connector instance. */
protected $dataconnector;
@@ -111,15 +108,15 @@ class sync_members extends scheduled_task {
// Fetched members count.
$membercount = count($members);
$usercount += $membercount;
mtrace("$membercount members received.\n");
// Process member information.
list($usercount, $enrolcount) = $this->sync_member_information($tool, $consumer, $members);
}
list($users, $enrolledcount) = $this->sync_member_information($tool, $consumer, $members);
$enrolcount += $enrolledcount;
// Now we check if we have to unenrol users who were not listed.
if ($this->should_sync_unenrol($tool->membersyncmode)) {
$unenrolcount = $this->sync_unenrol($tool);
// Now sync unenrolments for the consumer.
$unenrolcount += $this->sync_unenrol($tool, $consumer->getKey(), $users);
}
mtrace("Completed - Synced members for tool '$tool->id' in the course '$tool->courseid'. " .
@@ -216,17 +213,15 @@ class sync_members extends scheduled_task {
* @param stdClass $tool
* @param ToolConsumer $consumer
* @param User[] $members
* @return array An array containing the number of members that were processed and the number of members that were enrolled.
* @return array An array of users from processed members and the number that were enrolled.
*/
protected function sync_member_information(stdClass $tool, ToolConsumer $consumer, $members) {
global $DB;
$usercount = 0;
$users = [];
$enrolcount = 0;
// Process member information.
foreach ($members as $member) {
$usercount++;
// Set the user data.
$user = new stdClass();
$user->username = helper::create_username($consumer->getKey(), $member->ltiUserId);
@@ -248,9 +243,7 @@ class sync_members extends scheduled_task {
user_update_user($user);
// Add the information to the necessary arrays.
if (!in_array($user->id, $this->currentusers)) {
$this->currentusers[] = $user->id;
}
$users[$user->id] = $user;
$this->userphotos[$user->id] = $member->image;
} else {
if ($this->should_sync_enrol($tool->membersyncmode)) {
@@ -264,7 +257,7 @@ class sync_members extends scheduled_task {
$user->id = user_create_user($user);
// Add the information to the necessary arrays.
$this->currentusers[] = $user->id;
$users[$user->id] = $user;
$this->userphotos[$user->id] = $member->image;
}
}
@@ -290,16 +283,18 @@ class sync_members extends scheduled_task {
}
}
return [$usercount, $enrolcount];
return [$users, $enrolcount];
}
/**
* Performs unenrolment of users that are no longer enrolled in the consumer side.
*
* @param stdClass $tool The tool record object.
* @param string $consumerkey ensure we only unenrol users from this tool consumer.
* @param array $currentusers The list of current users.
* @return int The number of users that have been unenrolled.
*/
protected function sync_unenrol(stdClass $tool) {
protected function sync_unenrol(stdClass $tool, string $consumerkey, array $currentusers) {
global $DB;
$ltiplugin = enrol_get_plugin('lti');
@@ -308,16 +303,18 @@ class sync_members extends scheduled_task {
return 0;
}
if (empty($this->currentusers)) {
if (empty($currentusers)) {
return 0;
}
$unenrolcount = 0;
$ltiusersrs = $DB->get_recordset('enrol_lti_users', array('toolid' => $tool->id), 'lastaccess DESC', 'userid');
$select = "toolid = :toolid AND " . $DB->sql_compare_text('consumerkey', 255) . " = :consumerkey";
$ltiusersrs = $DB->get_recordset_select('enrol_lti_users', $select, ['toolid' => $tool->id, 'consumerkey' => $consumerkey],
'lastaccess DESC', 'userid');
// Go through the users and check if any were never listed, if so, remove them.
foreach ($ltiusersrs as $ltiuser) {
if (!in_array($ltiuser->userid, $this->currentusers)) {
if (!array_key_exists($ltiuser->userid, $currentusers)) {
$instance = new stdClass();
$instance->id = $tool->enrolid;
$instance->courseid = $tool->courseid;
+13 -1
View File
@@ -37,7 +37,7 @@
* @return boolean
*/
function xmldb_enrol_lti_upgrade($oldversion) {
global $CFG;
global $CFG, $DB;
// Automatically generated Moodle v3.6.0 release upgrade line.
// Put any upgrade step following this.
@@ -57,5 +57,17 @@ function xmldb_enrol_lti_upgrade($oldversion) {
// Automatically generated Moodle v3.11.0 release upgrade line.
// Put any upgrade step following this.
if ($oldversion < 2021051701) {
// Disable all orphaned enrolment method instances.
$sql = "id IN (SELECT t.enrolid
FROM {enrol_lti_tools} t
LEFT JOIN {context} c ON (t.contextid = c.id)
WHERE c.id IS NULL)";
$DB->set_field_select('enrol', 'status', 1, $sql);
// Lti savepoint reached.
upgrade_plugin_savepoint(true, 2021051701, 'enrol', 'lti');
}
return true;
}
+3
View File
@@ -25,6 +25,9 @@
$string['allowframeembedding'] = 'Note: It is recommended that the site administration setting \'Allow frame embedding\' is enabled, so that tools are displayed within a frame rather than in a new window.';
$string['authltimustbeenabled'] = 'Note: This plugin requires the LTI authentication plugin to be enabled too.';
$string['couldnotestablishproxy'] = 'Could not establish proxy with consumer.';
$string['deletedactivity'] = 'Deleted activity';
$string['deletedactivityalt'] = 'The activity shared by this instance has been deleted';
$string['deletedactivitydescription'] = 'The activity shared by this instance has been deleted. You can select another activity to share by editing the instance or, if no longer required, you can simply delete the instance. Deleting the instance will remove any associated user enrolments.';
$string['enrolenddate'] = 'End date';
$string['enrolenddate_help'] = 'If enabled, users can access until this date only.';
$string['enrolenddateerror'] = 'Enrolment end date cannot be earlier than start date';
+24
View File
@@ -415,3 +415,27 @@ function enrol_lti_extend_navigation_course($navigation, $course, $context) {
}
}
}
/**
* Get icon mapping for font-awesome.
*/
function enrol_lti_get_fontawesome_icon_map() {
return [
'enrol_lti:enrolinstancewarning' => 'fa-exclamation-circle text-danger',
];
}
/**
* Pre-delete course module hook which disables any methods referring to the deleted module, preventing launches and allowing remap.
*
* @param stdClass $cm The deleted course module record.
*/
function enrol_lti_pre_course_module_delete(stdClass $cm) {
global $DB;
$sql = "id IN (SELECT t.enrolid
FROM {enrol_lti_tools} t
JOIN {context} c ON (t.contextid = c.id)
WHERE c.contextlevel = :contextlevel
AND c.instanceid = :cmid)";
$DB->set_field_select('enrol', 'status', ENROL_INSTANCE_DISABLED, $sql, ['contextlevel' => CONTEXT_MODULE, 'cmid' => $cm->id]);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet"><path d="M896 128q209 0 385.5 103t279.5 279.5 103 385.5-103 385.5-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103zm128 1247v-190q0-14-9-23.5t-22-9.5h-192q-13 0-23 10t-10 23v190q0 13 10 23t23 10h192q13 0 22-9.5t9-23.5zm-2-344l18-621q0-12-10-18-10-8-24-8h-220q-14 0-24 8-10 6-10 18l17 621q0 10 10 17.5t24 7.5h185q14 0 23.5-7.5t10.5-17.5z" fill="#999"/></svg>

After

Width:  |  Height:  |  Size: 560 B

+32
View File
@@ -169,4 +169,36 @@ class lib_test extends \advanced_testcase {
// LTI enrolment has 1 enrol actions for active users -- unenrol.
$this->assertCount(1, $actions);
}
/**
* Test the behaviour of an enrolment method when the activity to which it provides access is deleted.
*
* @covers \enrol_lti_pre_course_module_delete
*/
public function test_course_module_deletion() {
// Create two modules and publish them.
$course = $this->getDataGenerator()->create_course();
$mod = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]);
$mod2 = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]);
$tooldata = [
'cmid' => $mod->cmid,
'courseid' => $course->id,
];
$tool = $this->getDataGenerator()->create_lti_tool((object)$tooldata);
$tooldata['cmid'] = $mod2->cmid;
$tool2 = $this->getDataGenerator()->create_lti_tool((object)$tooldata);
// Verify the instances are both enabled.
$modinstance = helper::get_lti_tool($tool->id);
$mod2instance = helper::get_lti_tool($tool2->id);
$this->assertEquals(ENROL_INSTANCE_ENABLED, $modinstance->status);
$this->assertEquals(ENROL_INSTANCE_ENABLED, $mod2instance->status);
// Delete a module and verify the associated instance is disabled.
course_delete_module($mod->cmid);
$modinstance = helper::get_lti_tool($tool->id);
$mod2instance = helper::get_lti_tool($tool2->id);
$this->assertEquals(ENROL_INSTANCE_DISABLED, $modinstance->status);
$this->assertEquals(ENROL_INSTANCE_ENABLED, $mod2instance->status);
}
}
+11 -16
View File
@@ -213,10 +213,10 @@ class sync_members_test extends \advanced_testcase {
* Test for sync_members::sync_member_information().
*/
public function test_sync_member_information() {
list($totalcount, $enrolledcount) = $this->task->sync_member_information($this->tool, $this->consumer, $this->members);
list($users, $enrolledcount) = $this->task->sync_member_information($this->tool, $this->consumer, $this->members);
$membercount = count($this->members);
$this->assertCount(10, $this->members);
$this->assertEquals($membercount, $totalcount);
$this->assertCount($membercount, $users);
$this->assertEquals($membercount, $enrolledcount);
}
@@ -225,10 +225,10 @@ class sync_members_test extends \advanced_testcase {
*/
public function test_sync_profile_images() {
$task = $this->task;
list($totalcount, $enrolledcount) = $task->sync_member_information($this->tool, $this->consumer, $this->members);
list($users, $enrolledcount) = $task->sync_member_information($this->tool, $this->consumer, $this->members);
$membercount = count($this->members);
$this->assertCount(10, $this->members);
$this->assertEquals($membercount, $totalcount);
$this->assertCount($membercount, $users);
$this->assertEquals($membercount, $enrolledcount);
// Suppress output.
@@ -244,14 +244,14 @@ class sync_members_test extends \advanced_testcase {
$tool = $this->tool;
$task = $this->task;
$task->sync_member_information($tool, $this->consumer, $this->members);
list($users) = $task->sync_member_information($tool, $this->consumer, $this->members);
// Simulate that the fetched list of current users has been reduced by 3.
$unenrolcount = 3;
for ($i = 0; $i < $unenrolcount; $i++) {
$task->pop_current_users();
array_pop($users);
}
$this->assertEquals($unenrolcount, $task->sync_unenrol($tool));
$this->assertEquals($unenrolcount, $task->sync_unenrol($tool, 'Consumer1Key', $users));
}
/**
@@ -297,13 +297,6 @@ class dummy_sync_members_task extends sync_members {
return $this->dataconnector;
}
/**
* Helper method that removes an element in the array of current users.
*/
public function pop_current_users() {
array_pop($this->currentusers);
}
/**
* Exposes sync_members::do_context_membership_request()
*
@@ -390,10 +383,12 @@ class dummy_sync_members_task extends sync_members {
* Exposes sync_members::sync_unenrol()
*
* @param \stdClass $tool
* @param string $consumerkey
* @param array $currentusers
* @return int
*/
public function sync_unenrol(\stdClass $tool) {
$count = parent::sync_unenrol($tool);
public function sync_unenrol(\stdClass $tool, string $consumerkey, array $currentusers) {
$count = parent::sync_unenrol($tool, $consumerkey, $currentusers);
return $count;
}
}
+1 -1
View File
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2021051700; // The current plugin version (Date: YYYYMMDDXX).
$plugin->version = 2021051701; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2021051100; // Requires this Moodle version.
$plugin->component = 'enrol_lti'; // Full name of the plugin (used for diagnostics).
+1 -1
View File
@@ -135,7 +135,7 @@
.gradingform_guide.editor .addcriterion input,
.gradingform_guide.editor .addcomment input {
background-position: 5px 8px;
background-position: left 5px top 8px;
height: 30px;
line-height: 29px;
margin-bottom: 14px;
+6 -1
View File
@@ -3050,7 +3050,7 @@ abstract class grade_helper {
* @return array
*/
public static function get_plugins_reports($courseid) {
global $SITE;
global $SITE, $CFG;
if (self::$gradereports !== null) {
return self::$gradereports;
@@ -3064,6 +3064,11 @@ abstract class grade_helper {
continue;
}
// Remove outcomes report if outcomes not enabled.
if ($plugin === 'outcomes' && empty($CFG->enableoutcomes)) {
continue;
}
// Remove ones we can't see
if (!has_capability('gradereport/'.$plugin.':view', $context)) {
continue;
+4
View File
@@ -39,6 +39,10 @@ $context = context_course::instance($course->id);
require_capability('gradereport/outcomes:view', $context);
if (empty($CFG->enableoutcomes)) {
redirect(course_get_url($course), get_string('outcomesdisabled', 'core_grades'));
}
// First make sure we have proper final grades.
grade_regrade_final_grades_if_required($course);
-1
View File
@@ -71,7 +71,6 @@ Feature: We can enter in grades and view reports from the gradebook
Scenario: Grade a grade item and ensure the results display correctly in the gradebook
When I navigate to "View > User report" in the course gradebook
And "Grader report" "link" should exist
And "Outcomes report" "link" should exist
And the "Select all or one user" select box should contain "All users (1)"
And I log out
And I log in as "student1"
+7 -1
View File
@@ -277,7 +277,13 @@ class core extends H5PCore {
$file->delete();
$librarykey = static::libraryToString($library);
$libraryid = $factory->get_storage()->h5pC->librariesJsonData[$librarykey]["libraryId"];
$libraryjson = $factory->get_storage()->h5pC->librariesJsonData[$librarykey];
if (!array_key_exists('libraryId', $libraryjson)) {
return null;
}
$libraryid = $libraryjson['libraryId'];
// Update example and tutorial (if any of them are defined in $library).
$params = ['id' => $libraryid];
+2
View File
@@ -93,11 +93,13 @@ class libraries implements renderable, templatable {
$version->toggleenabledurl = new moodle_url('/h5p/libraries.php', [
'id' => $version->id,
'action' => 'disable',
'sesskey' => sesskey(),
]);
} else {
$version->toggleenabledurl = new moodle_url('/h5p/libraries.php', [
'id' => $version->id,
'action' => 'enable',
'sesskey' => sesskey(),
]);
}
$installed[] = $version;
+2
View File
@@ -66,6 +66,8 @@ if ($deletelibrary) {
}
if (!is_null($action)) {
require_sesskey();
if ($action == 'enable' || $action == 'disable') {
// If action is enable or disable, library id is required too.
$libraryid = required_param('id', PARAM_INT);
+1 -1
View File
@@ -31,4 +31,4 @@ defined('MOODLE_INTERNAL') || die();
$string['parentlanguage'] = 'de';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Deutsch (Du)';
$string['thislanguage'] = 'Deutsch (du)';
+32
View File
@@ -0,0 +1,32 @@
<?php
// This file is part of Moodle - https://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
/**
* Automatically generated strings for Moodle installer
*
* Do not edit this file manually! It contains just a subset of strings
* needed during the very first steps of installation. This file was
* generated automatically by export-installer.php (which is part of AMOS
* {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
* list of strings defined in /install/stringnames.txt.
*
* @package installer
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['thislanguage'] = 'isiXhosa';
+3
View File
@@ -1506,6 +1506,9 @@ $string['warningiconvbuggy'] = 'Your version of the iconv library does not suppo
$string['webproxy'] = 'Web proxy';
$string['webproxyinfo'] = 'Fill in the following options if your Moodle server cannot access the internet directly. Internet access is required for the download of environment data, language packs, RSS feeds, timezones, etc.<br /><em>The PHP cURL extension is highly recommended.</em>';
$string['xmlrpcrecommended'] = 'The XMLRPC extension is useful for web services and Moodle networking.';
$string['xmlrpcmaharaenabled'] = 'It has been detected that the Mahara ePortfolio is enabled on your site. This feature relies on the PHP XML-RPC extension which is no longer maintained by PHP.';
$string['xmlrpcmnetenabled'] = 'It has been detected that the Moodle Networking is enabled on your site. This feature relies on the PHP XML-RPC extension which is no longer maintained by PHP.';
$string['xmlrpcwebserviceenabled'] = 'It has been detected that the XML-RPC Web Service protocol is enabled on your site. This feature relies on the PHP XML-RPC extension which is no longer maintained by PHP.';
$string['yuicomboloading'] = 'YUI combo loading';
$string['ziprequired'] = 'The Zip PHP extension is now required by Moodle, info-ZIP binaries or PclZip library are not used anymore.';
+1
View File
@@ -599,6 +599,7 @@ $string['outcomescoursecustom'] = 'Custom used (no remove)';
$string['outcomescoursenotused'] = 'Standard not used';
$string['outcomescourseused'] = 'Standard used (no remove)';
$string['outcomescustom'] = 'Custom outcomes';
$string['outcomesdisabled'] = 'Outcomes are not enabled';
$string['outcomeshortname'] = 'Short name';
$string['outcomesstandard'] = 'Standard outcomes';
$string['outcomesstandardavailable'] = 'Available standard outcomes';
+5 -2
View File
@@ -944,8 +944,11 @@ EOF;
}
} else {
$errorinfo = $this->get_debug_text($errorinfoboxes[0]->getHtml()) . "\n" .
$this->get_debug_text($errorinfoboxes[1]->getHtml());
$errorinfo = implode("\n", [
$this->get_debug_text($errorinfoboxes[0]->getHtml()),
$this->get_debug_text($errorinfoboxes[1]->getHtml()),
html_to_text($errorinfoboxes[2]->find('css', 'ul')->getHtml()),
]);
}
$msg = "Moodle exception: " . $errormsg->getText() . "\n" . $errorinfo;
@@ -55,8 +55,9 @@ class mustache_shorten_text_helper {
$length = trim($length);
$text = trim($text);
// Allow mustache tags in the text.
// Allow mustache tags in the length and text.
$text = $helper->render($text);
$length = $helper->render($length);
return shorten_text($text, $length);
}
+22 -11
View File
@@ -274,6 +274,25 @@ trait moodle_read_slave_trait {
$this->select_db_handle($type, $sql);
}
/**
* This should be called immediately after each db query. It does a clean up of resources.
*
* @param mixed $result The db specific result obtained from running a query.
* @return void
*/
protected function query_end($result) {
if ($this->written) {
// Adjust the written time.
array_walk($this->written, function (&$val) {
if ($val === true) {
$val = microtime(true);
}
});
}
parent::query_end($result);
}
/**
* Select appropriate db handle - readwrite or readonly
* @param int $type type of query
@@ -339,10 +358,8 @@ trait moodle_read_slave_trait {
return true;
case SQL_QUERY_INSERT:
case SQL_QUERY_UPDATE:
// If we are in transaction we cannot set the written time yet.
$now = $this->slavelatency && !$this->transactions ? microtime(true) : true;
foreach ($this->table_names($sql) as $tablename) {
$this->written[$tablename] = $now;
$this->written[$tablename] = true;
}
return false;
case SQL_QUERY_STRUCTURE:
@@ -370,16 +387,10 @@ trait moodle_read_slave_trait {
return;
}
if (!$this->slavelatency) {
return;
}
$now = null;
foreach ($this->written as $tablename => $when) {
if ($when === true) {
$now = $now ?: microtime(true);
$this->written[$tablename] = $now;
}
$now = $now ?: microtime(true);
$this->written[$tablename] = $now;
}
}
+15 -6
View File
@@ -41,6 +41,7 @@ class pgsql_native_moodle_database extends moodle_database {
select_db_handle as read_slave_select_db_handle;
can_use_readonly as read_slave_can_use_readonly;
query_start as read_slave_query_start;
query_end as read_slave_query_end;
}
/** @var array $dbhcursor keep track of open cursors */
@@ -184,12 +185,20 @@ class pgsql_native_moodle_database extends moodle_database {
}
ob_start();
if (empty($this->dboptions['dbpersist'])) {
$this->pgsql = pg_connect($connection, PGSQL_CONNECT_FORCE_NEW);
} else {
$this->pgsql = pg_pconnect($connection, PGSQL_CONNECT_FORCE_NEW);
// It seems that pg_connect() handles some errors differently.
// For example, name resolution error will raise an exception, and non-existing
// database or wrong credentials will just return false.
// We need to cater for both.
try {
if (empty($this->dboptions['dbpersist'])) {
$this->pgsql = pg_connect($connection, PGSQL_CONNECT_FORCE_NEW);
} else {
$this->pgsql = pg_pconnect($connection, PGSQL_CONNECT_FORCE_NEW);
}
$dberr = ob_get_contents();
} catch (\Exception $e) {
$dberr = $e->getMessage();
}
$dberr = ob_get_contents();
ob_end_clean();
$status = $this->pgsql ? pg_connection_status($this->pgsql) : false;
@@ -326,7 +335,7 @@ class pgsql_native_moodle_database extends moodle_database {
// reset original debug level
error_reporting($this->last_error_reporting);
try {
parent::query_end($result);
$this->read_slave_query_end($result);
if ($this->savepointpresent and $this->last_type != SQL_QUERY_AUX and $this->last_type != SQL_QUERY_SELECT) {
$res = @pg_query($this->pgsql, "RELEASE SAVEPOINT moodle_pg_savepoint; SAVEPOINT moodle_pg_savepoint");
if ($res) {
+2 -1
View File
@@ -34,8 +34,9 @@ require_once(__DIR__.'/fixtures/read_slave_moodle_database_mock_mysqli.php');
* @category dml
* @copyright 2018 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mysqli_native_moodle_database
*/
class core_dml_mysqli_read_slave_testcase extends base_testcase {
class dml_mysqli_read_slave_test extends base_testcase {
/**
* Test readonly handle is not used for reading from special pg_*() call queries,
* pg_try_advisory_lock and pg_advisory_unlock.
+2 -1
View File
@@ -34,8 +34,9 @@ require_once(__DIR__.'/fixtures/read_slave_moodle_database_mock_pgsql.php');
* @category dml
* @copyright 2018 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \pgsql_native_moodle_database
*/
class core_dml_pgsql_read_slave_testcase extends base_testcase {
class dml_pgsql_read_slave_test extends base_testcase {
/**
* Test correct database handles are used for cursors
*
+46 -1
View File
@@ -35,8 +35,9 @@ require_once(__DIR__.'/fixtures/read_slave_moodle_database_special.php');
* @category dml
* @copyright 2018 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \moodle_read_slave_trait
*/
class core_dml_read_slave_testcase extends base_testcase {
class dml_read_slave_test extends base_testcase {
/** @var float */
static private $dbreadonlylatency = 0.8;
@@ -311,6 +312,50 @@ class core_dml_read_slave_testcase extends base_testcase {
$this->assert_readonly_handle($handle);
}
/**
* Test readonly handle is not used immediately after update
* Test last written time is adjusted post-write,
* so the latency parameter is applied properly.
*
* @return void
* @covers ::can_use_readonly
* @covers ::query_end
*/
public function test_long_update(): void {
$DB = $this->new_db(true);
$this->assertNull($DB->get_dbhwrite());
$skip = false;
list($sql, $params, $ptype) = $DB->fix_sql_params("UPDATE {table} SET a = 1 WHERE id = 1");
$DB->with_query_start_end($sql, $params, SQL_QUERY_UPDATE, function ($dbh) use (&$now) {
sleep(1);
$now = microtime(true);
});
// This condition should always evaluate true, however we need to
// safeguard from an unaccounted delay that can break this test.
if (microtime(true) - $now < self::$dbreadonlylatency) {
// Not enough time passed, use rw handle.
$handle = $DB->get_records_sql("SELECT * FROM {table}");
$this->assertEquals('test_rw::test:test', $handle);
// Make sure enough time passes.
sleep(1);
} else {
$skip = true;
}
// Exceeded latency time, use ro handle.
$handle = $DB->get_records_sql("SELECT * FROM {table}");
$this->assert_readonly_handle($handle);
if ($skip) {
$this->markTestSkipped("Delay too long to test write handle immediately after transaction");
}
}
/**
* Test failed readonly connection falls back to write connection.
*
+5 -1
View File
@@ -91,11 +91,15 @@ class read_slave_moodle_database extends test_moodle_database {
* @param string $sql
* @param array $params
* @param int $querytype
* @param ?callable $callback
* @return string $handle handle property
*/
private function with_query_start_end($sql, array $params = null, $querytype) {
public function with_query_start_end($sql, array $params = null, $querytype, $callback = null) {
$this->query_start($sql, $params, $querytype);
$ret = $this->handle;
if ($callback) {
call_user_func($callback, $ret);
}
$this->query_end(null);
return $ret;
}
@@ -50,6 +50,7 @@ trait test_moodle_read_slave_trait {
$ro = fopen("php://memory", 'r+');
fputs($ro, 'ro');
$this->prefix = 'test_'; // Default, not to leave empty.
$this->wantreadslave = true;
$this->dbhwrite = $rw;
$this->dbhreadonly = $ro;
@@ -109,6 +110,7 @@ trait test_moodle_read_slave_trait {
* @param mixed $result
*/
public function query_end($result) {
parent::query_end($result);
$this->set_db_handle($this->dbhwrite);
}
@@ -10,3 +10,22 @@ Feature: Atto edit HTML
And I click on "Show more buttons" "button"
And I click on "HTML" "button"
Then the field "Description" matches value "<p style=\"color: blue;\">Smurf</p>"
@javascript
Scenario: Validation of empty string when the form is submitted with HTML source mode.
Given the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | weeks |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | essay | Essay 01 new | Write about whatever you want |
And I am on the "Essay 01 new" "core_question > edit" page logged in as admin
And I click on "Show more buttons" "button" in the "Question text" "form_row"
And I click on "HTML" "button" in the "Question text" "form_row"
And I press the shift + end key
And I press the delete key
When I press "id_submitbutton"
Then I should see "You must supply a value here." in the "Question text" "form_row"
@@ -132,6 +132,10 @@ Y.namespace('M.atto_html').Button = Y.Base.create('button', Y.M.editor_atto.Edit
_hideCodeMirror: function() {
var host = this.get('host');
// Disable placeholder for empty content.
// This will prevent the editor to copy and submit the empty placeholder.
host.disablePlaceholderForEmptyContent();
// Enable all plugins.
host.enablePlugins();
@@ -164,6 +168,9 @@ Y.namespace('M.atto_html').Button = Y.Base.create('button', Y.M.editor_atto.Edit
// And re-mark everything as updated.
this.markUpdated();
// Enable placeholder for empty content.
host.enablePlaceholderForEmptyContent();
}
}, {
ATTRS: {
@@ -1 +1 @@
YUI.add("moodle-atto_html-button",function(o,t){o.namespace("M.atto_html").Button=o.Base.create("button",o.M.editor_atto.EditorPlugin,[],{_codeMirror:null,initializer:function(){this.addButton({icon:"e/source_code",callback:this._toggleHTML});var t=this.get("host").textarea.ancestor("form");t&&t.on("submit",function(){this.get("isHTML")&&this._hideCodeMirror()},this)},_toggleHTML:function(){this.set("isHTML",!this.get("isHTML")),this.get("isHTML")?this._showCodeMirror():this._hideCodeMirror()},_showCodeMirror:function(){var t,e,i=this.get("host");i.disablePlugins(),i.enablePlugins(this.name),i.updateOriginal(),t={width:this.editor.getComputedStyle("width"),height:this.editor.getComputedStyle("height")},e=o.M.atto_html.beautify.html_beautify(i.textarea.get("value"),{indent_size:4,indent_inner_html:!0}),i.textarea.set("value",e),this._codeMirror=o.M.atto_html.CodeMirror.fromTextArea(i.textarea.getDOMNode(),{lineNumbers:!0,mode:"htmlmixed",tabSize:2,lineWrapping:!0}),this._codeMirror.on("change",function(t){t.save()}),this.editor.hide(),this._codeMirror.setSize(t.width,t.height),this._codeMirror.focus()},_hideCodeMirror:function(){var t=this.get("host");t.enablePlugins(),t.updateFromTextArea(),this._codeMirror&&(o.one(this._codeMirror.getWrapperElement()).hide(),this._codeMirror.toTextArea(),o.Object.each(this._codeMirror._handlers,function(t,e){o.Array.each(t,function(t){this.off(e,t)},this)},this._codeMirror),delete this._codeMirror,this._codeMirror=null),t.textarea.hide(),this.editor.show(),t.focus(),this.markUpdated()}},{ATTRS:{isHTML:{value:!1}}})},"@VERSION@",{requires:["promise","moodle-editor_atto-plugin","moodle-atto_html-beautify","moodle-atto_html-codemirror","event-valuechange"]});
YUI.add("moodle-atto_html-button",function(o,t){o.namespace("M.atto_html").Button=o.Base.create("button",o.M.editor_atto.EditorPlugin,[],{_codeMirror:null,initializer:function(){this.addButton({icon:"e/source_code",callback:this._toggleHTML});var t=this.get("host").textarea.ancestor("form");t&&t.on("submit",function(){this.get("isHTML")&&this._hideCodeMirror()},this)},_toggleHTML:function(){this.set("isHTML",!this.get("isHTML")),this.get("isHTML")?this._showCodeMirror():this._hideCodeMirror()},_showCodeMirror:function(){var t,e,i=this.get("host");i.disablePlugins(),i.enablePlugins(this.name),i.updateOriginal(),t={width:this.editor.getComputedStyle("width"),height:this.editor.getComputedStyle("height")},e=o.M.atto_html.beautify.html_beautify(i.textarea.get("value"),{indent_size:4,indent_inner_html:!0}),i.textarea.set("value",e),this._codeMirror=o.M.atto_html.CodeMirror.fromTextArea(i.textarea.getDOMNode(),{lineNumbers:!0,mode:"htmlmixed",tabSize:2,lineWrapping:!0}),this._codeMirror.on("change",function(t){t.save()}),this.editor.hide(),this._codeMirror.setSize(t.width,t.height),this._codeMirror.focus()},_hideCodeMirror:function(){var t=this.get("host");t.disablePlaceholderForEmptyContent(),t.enablePlugins(),t.updateFromTextArea(),this._codeMirror&&(o.one(this._codeMirror.getWrapperElement()).hide(),this._codeMirror.toTextArea(),o.Object.each(this._codeMirror._handlers,function(t,e){o.Array.each(t,function(t){this.off(e,t)},this)},this._codeMirror),delete this._codeMirror,this._codeMirror=null),t.textarea.hide(),this.editor.show(),t.focus(),this.markUpdated(),t.enablePlaceholderForEmptyContent()}},{ATTRS:{isHTML:{value:!1}}})},"@VERSION@",{requires:["promise","moodle-editor_atto-plugin","moodle-atto_html-beautify","moodle-atto_html-codemirror","event-valuechange"]});
@@ -132,6 +132,10 @@ Y.namespace('M.atto_html').Button = Y.Base.create('button', Y.M.editor_atto.Edit
_hideCodeMirror: function() {
var host = this.get('host');
// Disable placeholder for empty content.
// This will prevent the editor to copy and submit the empty placeholder.
host.disablePlaceholderForEmptyContent();
// Enable all plugins.
host.enablePlugins();
@@ -164,6 +168,9 @@ Y.namespace('M.atto_html').Button = Y.Base.create('button', Y.M.editor_atto.Edit
// And re-mark everything as updated.
this.markUpdated();
// Enable placeholder for empty content.
host.enablePlaceholderForEmptyContent();
}
}, {
ATTRS: {
@@ -130,6 +130,10 @@ Y.namespace('M.atto_html').Button = Y.Base.create('button', Y.M.editor_atto.Edit
_hideCodeMirror: function() {
var host = this.get('host');
// Disable placeholder for empty content.
// This will prevent the editor to copy and submit the empty placeholder.
host.disablePlaceholderForEmptyContent();
// Enable all plugins.
host.enablePlugins();
@@ -162,6 +166,9 @@ Y.namespace('M.atto_html').Button = Y.Base.create('button', Y.M.editor_atto.Edit
// And re-mark everything as updated.
this.markUpdated();
// Enable placeholder for empty content.
host.enablePlaceholderForEmptyContent();
}
}, {
ATTRS: {
+10
View File
@@ -1,5 +1,15 @@
This files describes API changes in the editor_atto code.
=== 3.11 ===
* Two new helper functions have been added to lib/editor/atto
to handle enable/disable the empty placeholder for empty content in the HTML source view:
- enablePlaceholderForEmptyContent() - Handle to enable the empty placeholder for empty content
- disablePlaceholderForEmptyContent() - Handle to disable the empty placeholder for empty content.
* One property have been added to lib/editor/atto to handle the state of the empty placeholder content.
- enableAppropriateEmptyContent - Enable/disable the empty placeholder content.
=== 3.4 ===
* The following functions, previously used (exclusively) by upgrade steps are not available
@@ -158,6 +158,14 @@ Y.extend(Editor, Y.Base, {
*/
coreDirection: null,
/**
* Enable/disable the empty placeholder content.
*
* @property enableAppropriateEmptyContent
* @type Boolean
*/
enableAppropriateEmptyContent: null,
/**
* Event Handles to clear on editor destruction.
*
@@ -207,6 +215,9 @@ Y.extend(Editor, Y.Base, {
// Set diretcion according to current page language.
this.coreDirection = Y.one('body').hasClass('dir-rtl') ? 'rtl' : 'ltr';
// Enable the placeholder for empty content.
this.enablePlaceholderForEmptyContent();
// Add everything to the wrapper.
this.setupToolbar();
@@ -420,6 +431,20 @@ Y.extend(Editor, Y.Base, {
}
},
/**
* Enable the empty placeholder for empty content.
*/
enablePlaceholderForEmptyContent: function() {
this.enableAppropriateEmptyContent = true;
},
/**
* Disable the empty placeholder for empty content.
*/
disablePlaceholderForEmptyContent: function() {
this.enableAppropriateEmptyContent = false;
},
/**
* Register an event handle for disposal in the destructor.
*
@@ -692,6 +717,10 @@ EditorTextArea.prototype = {
* @private
*/
_getEmptyContent: function() {
if (!this.enableAppropriateEmptyContent) {
// Return the empty string if we do not enable the empty placeholder. Ex: HTML mode.
return '';
}
var alignment;
if (this.coreDirection === 'rtl') {
alignment = 'style="text-align: right;"';
@@ -1793,7 +1822,8 @@ EditorClean.prototype = {
// Use a regular expression to find the next open or close li, ul, or ol tag.
// Keep going until there are no more matching tags left.
while ((match = toProcess.match(/<(\/?)(li|ul|ol)[^>]*>/i))) {
// This expression looks for whole words by employing the word boundary (\b) metacharacter.
while ((match = toProcess.match(/<(\/?)(li|ul|ol)\b[^>]*>/i))) {
currentTag = {
tag: match[2],
tagLowerCase: match[2].toLowerCase(),
File diff suppressed because one or more lines are too long
@@ -158,6 +158,14 @@ Y.extend(Editor, Y.Base, {
*/
coreDirection: null,
/**
* Enable/disable the empty placeholder content.
*
* @property enableAppropriateEmptyContent
* @type Boolean
*/
enableAppropriateEmptyContent: null,
/**
* Event Handles to clear on editor destruction.
*
@@ -205,6 +213,9 @@ Y.extend(Editor, Y.Base, {
// Set diretcion according to current page language.
this.coreDirection = Y.one('body').hasClass('dir-rtl') ? 'rtl' : 'ltr';
// Enable the placeholder for empty content.
this.enablePlaceholderForEmptyContent();
// Add everything to the wrapper.
this.setupToolbar();
@@ -417,6 +428,20 @@ Y.extend(Editor, Y.Base, {
}
},
/**
* Enable the empty placeholder for empty content.
*/
enablePlaceholderForEmptyContent: function() {
this.enableAppropriateEmptyContent = true;
},
/**
* Disable the empty placeholder for empty content.
*/
disablePlaceholderForEmptyContent: function() {
this.enableAppropriateEmptyContent = false;
},
/**
* Register an event handle for disposal in the destructor.
*
@@ -687,6 +712,10 @@ EditorTextArea.prototype = {
* @private
*/
_getEmptyContent: function() {
if (!this.enableAppropriateEmptyContent) {
// Return the empty string if we do not enable the empty placeholder. Ex: HTML mode.
return '';
}
var alignment;
if (this.coreDirection === 'rtl') {
alignment = 'style="text-align: right;"';
@@ -1780,7 +1809,8 @@ EditorClean.prototype = {
// Use a regular expression to find the next open or close li, ul, or ol tag.
// Keep going until there are no more matching tags left.
while ((match = toProcess.match(/<(\/?)(li|ul|ol)[^>]*>/i))) {
// This expression looks for whole words by employing the word boundary (\b) metacharacter.
while ((match = toProcess.match(/<(\/?)(li|ul|ol)\b[^>]*>/i))) {
currentTag = {
tag: match[2],
tagLowerCase: match[2].toLowerCase(),
+2 -1
View File
@@ -498,7 +498,8 @@ EditorClean.prototype = {
// Use a regular expression to find the next open or close li, ul, or ol tag.
// Keep going until there are no more matching tags left.
while ((match = toProcess.match(/<(\/?)(li|ul|ol)[^>]*>/i))) {
// This expression looks for whole words by employing the word boundary (\b) metacharacter.
while ((match = toProcess.match(/<(\/?)(li|ul|ol)\b[^>]*>/i))) {
currentTag = {
tag: match[2],
tagLowerCase: match[2].toLowerCase(),
+25
View File
@@ -156,6 +156,14 @@ Y.extend(Editor, Y.Base, {
*/
coreDirection: null,
/**
* Enable/disable the empty placeholder content.
*
* @property enableAppropriateEmptyContent
* @type Boolean
*/
enableAppropriateEmptyContent: null,
/**
* Event Handles to clear on editor destruction.
*
@@ -205,6 +213,9 @@ Y.extend(Editor, Y.Base, {
// Set diretcion according to current page language.
this.coreDirection = Y.one('body').hasClass('dir-rtl') ? 'rtl' : 'ltr';
// Enable the placeholder for empty content.
this.enablePlaceholderForEmptyContent();
// Add everything to the wrapper.
this.setupToolbar();
@@ -418,6 +429,20 @@ Y.extend(Editor, Y.Base, {
}
},
/**
* Enable the empty placeholder for empty content.
*/
enablePlaceholderForEmptyContent: function() {
this.enableAppropriateEmptyContent = true;
},
/**
* Disable the empty placeholder for empty content.
*/
disablePlaceholderForEmptyContent: function() {
this.enableAppropriateEmptyContent = false;
},
/**
* Register an event handle for disposal in the destructor.
*
+4
View File
@@ -45,6 +45,10 @@ EditorTextArea.prototype = {
* @private
*/
_getEmptyContent: function() {
if (!this.enableAppropriateEmptyContent) {
// Return the empty string if we do not enable the empty placeholder. Ex: HTML mode.
return '';
}
var alignment;
if (this.coreDirection === 'rtl') {
alignment = 'style="text-align: right;"';
+4 -4
View File
@@ -670,8 +670,8 @@ function enrol_get_my_courses($fields = null, $sort = null, $limit = 0, $coursei
$orderby = "ORDER BY $sort";
}
$wheres = array("c.id <> :siteid");
$params = array('siteid'=>SITEID);
$wheres = ['c.id <> ' . SITEID];
$params = [];
if (isset($USER->loginascontext) and $USER->loginascontext->contextlevel == CONTEXT_COURSE) {
// list _only_ this course - anything else is asking for trouble...
@@ -1075,7 +1075,7 @@ function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = nul
$orderby = "ORDER BY $sort";
}
$params = array('siteid'=>SITEID);
$params = [];
if ($onlyactive) {
$subwhere = "WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
@@ -1101,7 +1101,7 @@ function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = nul
$subwhere
) en ON (en.courseid = c.id)
$ccjoin
WHERE c.id <> :siteid
WHERE c.id <> " . SITEID . "
$orderby";
$params['userid'] = $userid;
+2 -2
View File
@@ -4162,14 +4162,14 @@ class curl {
$crlf = "\r\n";
return preg_replace(
// HTTP version and status code (ignore value of code).
'~^HTTP/1\..*' . $crlf .
'~^HTTP/[1-9](\.[0-9])?.*' . $crlf .
// Header name: character between 33 and 126 decimal, except colon.
// Colon. Header value: any character except \r and \n. CRLF.
'(?:[\x21-\x39\x3b-\x7e]+:[^' . $crlf . ']+' . $crlf . ')*' .
// Headers are terminated by another CRLF (blank line).
$crlf .
// Second HTTP status code, this time must be 200.
'(HTTP/1.[01] 200 )~', '$1', $input);
'(HTTP/[1-9](\.[0-9])? 200)~', '$2', $input);
}
}
+6 -13
View File
@@ -206,7 +206,10 @@ define('PARAM_RAW_TRIMMED', 'raw_trimmed');
define('PARAM_SAFEDIR', 'safedir');
/**
* PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths, etc.
* PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths
* and other references to Moodle code files.
*
* This is NOT intended to be used for absolute paths or any user uploaded files.
*/
define('PARAM_SAFEPATH', 'safepath');
@@ -983,18 +986,8 @@ function clean_param($param, $type) {
return preg_replace('/[^a-zA-Z0-9_-]/i', '', $param);
case PARAM_SAFEPATH:
// Replace MS \ separators.
$param = str_replace('\\', '/', $param);
// Remove any number of ../ to prevent path traversal.
$param = preg_replace('/\.\.+\//', '', $param);
// Remove everything not a-zA-Z0-9/:_- .
$param = preg_replace('/[^a-zA-Z0-9\/:_-]/i', '', $param);
// Remove leading slash.
$param = ltrim($param, '/');
if ($param === '.') {
$param = '';
}
return $param;
// Remove everything not a-zA-Z0-9/_- .
return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', $param);
case PARAM_FILE:
// Strip all suspicious characters from filename.
-6
View File
@@ -17,9 +17,3 @@ Local changes:
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.
- MDL-67114: PHP 7.4 compatibility. Array operations on scalar value.
This corresponds to upstream https://github.com/bobthecow/mustache.php/pull/352
- MDL-73586: PHP 8.0 compatibility. Removed 'mbstring.func_overload' init setting.
This corresponds to upstream commit https://github.com/bobthecow/mustache.php/commit/e7165a33b282ab4d20b3863825caadb46313d62b
that is availbale for the library versions 2.14.1 and up
+7
View File
@@ -33,4 +33,11 @@ interface Mustache_Cache
* @param string $value
*/
public function cache($key, $value);
/**
* Set a logger instance.
*
* @param Mustache_Logger|Psr\Log\LoggerInterface $logger
*/
public function setLogger($logger = null);
}
@@ -47,7 +47,7 @@ abstract class Mustache_Cache_AbstractCache implements Mustache_Cache
/**
* Add a log record if logging is enabled.
*
* @param int $level The logging level
* @param string $level The logging level
* @param string $message The log message
* @param array $context The log context
*/
+5 -8
View File
@@ -286,7 +286,6 @@ class Mustache_Compiler
private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level)
{
$key = $this->block($nodes);
$keystr = var_export($key, true);
$id = var_export($id, true);
return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key);
@@ -321,7 +320,6 @@ class Mustache_Compiler
}
const SECTION_CALL = '
// %s section
$value = $context->%s(%s);%s
$buffer .= $this->section%s($context, $indent, $value);
';
@@ -333,12 +331,12 @@ class Mustache_Compiler
if (%s) {
$source = %s;
$result = call_user_func($value, $source, %s);
$result = (string) call_user_func($value, $source, %s);
if (strpos($result, \'{{\') === false) {
$buffer .= $result;
} else {
$buffer .= $this->mustache
->loadLambda((string) $result%s)
->loadLambda($result%s)
->renderInternal($context);
}
} elseif (!empty($value)) {
@@ -392,11 +390,10 @@ class Mustache_Compiler
$id = var_export($id, true);
$filters = $this->getFilters($filters, $level);
return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $method, $id, $filters, $key);
return sprintf($this->prepare(self::SECTION_CALL, $level), $method, $id, $filters, $key);
}
const INVERTED_SECTION = '
// %s inverted section
$value = $context->%s(%s);%s
if (empty($value)) {
%s
@@ -419,7 +416,7 @@ class Mustache_Compiler
$id = var_export($id, true);
$filters = $this->getFilters($filters, $level);
return sprintf($this->prepare(self::INVERTED_SECTION, $level), $id, $method, $id, $filters, $this->walk($nodes, $level));
return sprintf($this->prepare(self::INVERTED_SECTION, $level), $method, $id, $filters, $this->walk($nodes, $level));
}
const PARTIAL_INDENT = ', $indent . %s';
@@ -507,7 +504,7 @@ class Mustache_Compiler
const VARIABLE = '
$value = $this->resolveValue($context->%s(%s), $context);%s
$buffer .= %s%s;
$buffer .= %s($value === null ? \'\' : %s);
';
/**
+2 -2
View File
@@ -23,8 +23,8 @@
*/
class Mustache_Engine
{
const VERSION = '2.13.0';
const SPEC_VERSION = '1.1.2';
const VERSION = '2.14.0';
const SPEC_VERSION = '1.2.2';
const PRAGMA_FILTERS = 'FILTERS';
const PRAGMA_BLOCKS = 'BLOCKS';
@@ -38,7 +38,7 @@ class Mustache_Source_FilesystemSource implements Mustache_Source
/**
* Get the Source key (used to generate the compiled class name).
*
* @throws RuntimeException when a source file cannot be read
* @throws Mustache_Exception_RuntimeException when a source file cannot be read
*
* @return string
*/
@@ -54,7 +54,7 @@ class Mustache_Source_FilesystemSource implements Mustache_Source
}
if ($this->stat === false) {
throw new RuntimeException(sprintf('Failed to read source file "%s".', $this->fileName));
throw new Mustache_Exception_RuntimeException(sprintf('Failed to read source file "%s".', $this->fileName));
}
foreach ($this->statProps as $prop) {
+3 -3
View File
@@ -88,11 +88,11 @@ class Mustache_Tokenizer
* @throws Mustache_Exception_InvalidArgumentException when $delimiters string is invalid
*
* @param string $text Mustache template source to tokenize
* @param string $delimiters Optionally, pass initial opening and closing delimiters (default: null)
* @param string $delimiters Optionally, pass initial opening and closing delimiters (default: empty string)
*
* @return array Set of Mustache tokens
*/
public function scan($text, $delimiters = null)
public function scan($text, $delimiters = '')
{
// Setting mbstring.func_overload makes things *really* slow.
// Let's do everyone a favor and scan this string as ASCII instead.
@@ -112,7 +112,7 @@ class Mustache_Tokenizer
$this->reset();
if ($delimiters = trim($delimiters)) {
if (is_string($delimiters) && $delimiters = trim($delimiters)) {
$this->setDelimiters($delimiters);
}
+14 -1
View File
@@ -943,12 +943,25 @@ class theme_config {
// First editor plugins.
$plugins = core_component::get_plugin_list('editor');
foreach ($plugins as $plugin=>$fulldir) {
foreach ($plugins as $plugin => $fulldir) {
$sheetfile = "$fulldir/editor_styles.css";
if (is_readable($sheetfile)) {
$files['plugin_'.$plugin] = $sheetfile;
}
$subplugintypes = core_component::get_subplugins("editor_{$plugin}");
// Fetch sheets for any editor subplugins.
foreach ($subplugintypes as $plugintype => $subplugins) {
foreach ($subplugins as $subplugin) {
$plugindir = core_component::get_plugin_directory($plugintype, $subplugin);
$sheetfile = "{$plugindir}/editor_styles.css";
if (is_readable($sheetfile)) {
$files["{$plugintype}_{$subplugin}"] = $sheetfile;
}
}
}
}
// Then parent themes - base first, the immediate parent last.
foreach (array_reverse($this->parent_configs) as $parent_config) {
if (empty($parent_config->editor_sheets)) {
+5 -1
View File
@@ -178,7 +178,7 @@
<input type="hidden" name="logintoken" value="{{logintoken}}">
<input type="hidden" name="username" value="guest" />
<input type="hidden" name="password" value="guest" />
<button class="btn btn-secondary btn-block" type="submit">{{#str}}loginguest{{/str}}</button>
<button class="btn btn-secondary btn-block" type="submit" id="loginguestbtn">{{#str}}loginguest{{/str}}</button>
</form>
</div>
{{/canloginasguest}}
@@ -262,4 +262,8 @@
});
{{/autofocusform}}
{{/error}}
require(['core_form/submit'], function(Submit) {
Submit.init("loginbtn");
Submit.init("loginguestbtn");
});
{{/js}}
-1
View File
@@ -651,7 +651,6 @@ class behat_forms extends behat_base {
* @param string $value
*/
public function set_field_node_value(NodeElement $fieldnode, string $value): void {
$this->ensure_node_is_visible($fieldnode);
$field = behat_field_manager::get_form_field($fieldnode, $this->getSession());
$field->set_value($value);
}
+15
View File
@@ -1922,6 +1922,21 @@ EOF;
throw new \Moodle\BehatExtension\Exception\SkippedException();
}
/**
* Checks if given plugin is installed, and skips the current scenario if not.
*
* @Given the :plugin plugin is installed
* @param string $plugin frankenstyle plugin name, e.g. 'filter_embedquestion'.
* @throws \Moodle\BehatExtension\Exception\SkippedException
*/
public function plugin_is_installed(string $plugin): void {
$path = core_component::get_component_directory($plugin);
if (!is_readable($path . '/version.php')) {
throw new \Moodle\BehatExtension\Exception\SkippedException(
'Skipping this scenario because the ' . $plugin . ' is not installed.');
}
}
/**
* Checks focus is with the given element.
*
+9
View File
@@ -718,6 +718,7 @@ class behat_navigation extends behat_base {
* | Page type | Identifier meaning | description |
* | Category | category idnumber | List of courses in that category. |
* | Course | course shortname | Main course home pag |
* | Course editing | course shortname | Edit settings page for the course |
* | Activity | activity idnumber | Start page for that activity |
* | Activity editing | activity idnumber | Edit settings page for that activity |
* | [modname] Activity | activity name or idnumber | Start page for that activity |
@@ -745,6 +746,14 @@ class behat_navigation extends behat_base {
}
return new moodle_url('/course/index.php', ['categoryid' => $categoryid]);
case 'course editing':
$courseid = $this->get_course_id($identifier);
if (!$courseid) {
throw new Exception('The specified course with shortname, fullname, or idnumber "' .
$identifier . '" does not exist');
}
return new moodle_url('/course/edit.php', ['id' => $courseid]);
case 'course':
$courseid = $this->get_course_id($identifier);
if (!$courseid) {
+149
View File
@@ -954,6 +954,155 @@ EOF;
$this->assertSame($httpsexpected, curl::strip_double_headers($httpsexample));
// Test it does nothing to the 'plain' data.
$this->assertSame($httpsexpected, curl::strip_double_headers($httpsexpected));
$httpsexample = <<<EOF
HTTP/1.0 200 Connection established
HTTP/2 200 OK
Date: Fri, 22 Feb 2013 17:14:23 GMT
Server: Apache/2
X-Powered-By: PHP/5.3.3-7+squeeze14
Content-Type: text/xml
Connection: close
Content-Encoding: gzip
Transfer-Encoding: chunked
<?xml version="1.0" encoding="ISO-8859-1" ?>
<rss version="2.0">...
EOF;
$httpsexpected = <<<EOF
HTTP/2 200 OK
Date: Fri, 22 Feb 2013 17:14:23 GMT
Server: Apache/2
X-Powered-By: PHP/5.3.3-7+squeeze14
Content-Type: text/xml
Connection: close
Content-Encoding: gzip
Transfer-Encoding: chunked
<?xml version="1.0" encoding="ISO-8859-1" ?>
<rss version="2.0">...
EOF;
// For HTTP, replace the \n with \r\n.
$httpsexample = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexample);
$httpsexpected = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexpected);
// Test stripping works OK.
$this->assertSame($httpsexpected, curl::strip_double_headers($httpsexample));
// Test it does nothing to the 'plain' data.
$this->assertSame($httpsexpected, curl::strip_double_headers($httpsexpected));
$httpsexample = <<<EOF
HTTP/1.0 200 Connection established
HTTP/2.1 200 OK
Date: Fri, 22 Feb 2013 17:14:23 GMT
Server: Apache/2
X-Powered-By: PHP/5.3.3-7+squeeze14
Content-Type: text/xml
Connection: close
Content-Encoding: gzip
Transfer-Encoding: chunked
<?xml version="1.0" encoding="ISO-8859-1" ?>
<rss version="2.0">...
EOF;
$httpsexpected = <<<EOF
HTTP/2.1 200 OK
Date: Fri, 22 Feb 2013 17:14:23 GMT
Server: Apache/2
X-Powered-By: PHP/5.3.3-7+squeeze14
Content-Type: text/xml
Connection: close
Content-Encoding: gzip
Transfer-Encoding: chunked
<?xml version="1.0" encoding="ISO-8859-1" ?>
<rss version="2.0">...
EOF;
// For HTTP, replace the \n with \r\n.
$httpsexample = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexample);
$httpsexpected = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexpected);
// Test stripping works OK.
$this->assertSame($httpsexpected, curl::strip_double_headers($httpsexample));
// Test it does nothing to the 'plain' data.
$this->assertSame($httpsexpected, curl::strip_double_headers($httpsexpected));
$httpsexample = <<<EOF
HTTP/1.1 200 Connection established
HTTP/3 200 OK
Date: Fri, 22 Feb 2013 17:14:23 GMT
Server: Apache/2
X-Powered-By: PHP/5.3.3-7+squeeze14
Content-Type: text/xml
Connection: close
Content-Encoding: gzip
Transfer-Encoding: chunked
<?xml version="1.0" encoding="ISO-8859-1" ?>
<rss version="2.0">...
EOF;
$httpsexpected = <<<EOF
HTTP/3 200 OK
Date: Fri, 22 Feb 2013 17:14:23 GMT
Server: Apache/2
X-Powered-By: PHP/5.3.3-7+squeeze14
Content-Type: text/xml
Connection: close
Content-Encoding: gzip
Transfer-Encoding: chunked
<?xml version="1.0" encoding="ISO-8859-1" ?>
<rss version="2.0">...
EOF;
// For HTTP, replace the \n with \r\n.
$httpsexample = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexample);
$httpsexpected = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexpected);
// Test stripping works OK.
$this->assertSame($httpsexpected, curl::strip_double_headers($httpsexample));
// Test it does nothing to the 'plain' data.
$this->assertSame($httpsexpected, curl::strip_double_headers($httpsexpected));
$httpsexample = <<<EOF
HTTP/2 200 Connection established
HTTP/4 200 OK
Date: Fri, 22 Feb 2013 17:14:23 GMT
Server: Apache/2
X-Powered-By: PHP/5.3.3-7+squeeze14
Content-Type: text/xml
Connection: close
Content-Encoding: gzip
Transfer-Encoding: chunked
<?xml version="1.0" encoding="ISO-8859-1" ?>
<rss version="2.0">...
EOF;
$httpsexpected = <<<EOF
HTTP/4 200 OK
Date: Fri, 22 Feb 2013 17:14:23 GMT
Server: Apache/2
X-Powered-By: PHP/5.3.3-7+squeeze14
Content-Type: text/xml
Connection: close
Content-Encoding: gzip
Transfer-Encoding: chunked
<?xml version="1.0" encoding="ISO-8859-1" ?>
<rss version="2.0">...
EOF;
// For HTTP, replace the \n with \r\n.
$httpsexample = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexample);
$httpsexpected = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexpected);
// Test stripping works OK.
$this->assertSame($httpsexpected, curl::strip_double_headers($httpsexample));
// Test it does nothing to the 'plain' data.
$this->assertSame($httpsexpected, curl::strip_double_headers($httpsexpected));
}
/**
-34
View File
@@ -837,40 +837,6 @@ class core_moodlelib_testcase extends advanced_testcase {
}
}
/**
* Provide some tested base url and expected results.
*
* @return array Array of tested base url and expected results.
*/
public function clean_param_safepath_provider(): array {
return [
// MS separator test.
['c:\temp', 'c:/temp'],
// Leading slash test.
['/tmp/', 'tmp/'],
// Path traversal test.
['../../../../../etc/', 'etc/'],
['../', ''],
['.../...//', ''],
['.', '']
];
}
/**
* Test clean_param() method with PARAM_SAFEPATH type.
*
* @dataProvider clean_param_safepath_provider
* @covers ::clean_param
* @param string $path
* @param string $expected
*/
public function test_clean_param_safepath(string $path, string $expected) {
$result = clean_param($path, PARAM_SAFEPATH);
$this->assertSame($expected, $result);
}
public function test_validate_param() {
try {
$param = validate_param('11a', PARAM_INT);
+1 -1
View File
@@ -199,7 +199,7 @@
<location>mustache</location>
<name>Mustache</name>
<license>MIT</license>
<version>2.13.0</version>
<version>2.14.1</version>
</library>
<library>
<location>amd/src/mustache.js</location>
+52
View File
@@ -2726,3 +2726,55 @@ function check_admin_dir_usage(environment_results $result): ?environment_result
return $result;
}
/**
* Check whether the XML-RPC protocol is enabled and warn if so.
*
* The XML-RPC protocol will be removed in a future version (4.1) as it is no longer supported by PHP.
*
* See MDL-70889 for further information.
*
* @param environment_results $result
* @return null|environment_results
*/
function check_xmlrpc_usage(environment_results $result): ?environment_results {
global $CFG;
// Checking Web Service protocols.
if (!empty($CFG->webserviceprotocols)) {
$plugins = array_flip(explode(',', $CFG->webserviceprotocols));
if (array_key_exists('xmlrpc', $plugins)) {
$result->setInfo('xmlrpc_webservice_usage');
$result->setFeedbackStr('xmlrpcwebserviceenabled');
return $result;
}
}
if (isset($CFG->mnet_dispatcher_mode) && $CFG->mnet_dispatcher_mode == 'strict') {
// Checking Mnet hosts.
$mnethosts = mnet_get_hosts();
if ($mnethosts) {
$actualhost = 0;
foreach ($mnethosts as $mnethost) {
if ($mnethost->id != $CFG->mnet_all_hosts_id) {
$actualhost++;
}
}
if ($actualhost > 0) {
$result->setInfo('xmlrpc_mnet_usage');
$result->setFeedbackStr('xmlrpcmnetenabled');
return $result;
}
}
// Checking Mahara.
$portfolios = \core\plugininfo\portfolio::get_enabled_plugins();
if (array_key_exists('mahara', $portfolios)) {
$result->setInfo('xmlrpc_mahara_usage');
$result->setFeedbackStr('xmlrpcmaharaenabled');
return $result;
}
}
return null;
}
@@ -205,7 +205,7 @@ M.core_formchangechecker.get_form_dirty_state = function() {
// Handle TinyMCE editor instances
// We can't add a listener in the initializer as the editors may not have been created by that point
// so we do so here instead
if (typeof window.tinyMCE !== 'undefined') {
if (typeof window.tinyMCE !== 'undefined' && window.tinyMCE.editors) {
for (editor in window.tinyMCE.editors) {
if (window.tinyMCE.editors[editor].isDirty()) {
return 1;
@@ -1 +1 @@
YUI.add("moodle-core-formchangechecker",function(r,e){var t=function(){t.superclass.constructor.apply(this,arguments)};r.extend(t,r.Base,{initialvaluelisteners:[],initializer:function(){var e="form#"+this.get("formid"),t=r.one(e);t&&(M.core_formchangechecker.stateinformation.formchanged||(M.core_formchangechecker.stateinformation.formchanged=this.get("initialdirtystate")),r.on(M.core.event.EDITOR_CONTENT_RESTORED,M.core_formchangechecker.reset_form_dirty_state,this),t.delegate("change",M.core_formchangechecker.set_form_changed,"input",this),t.delegate("change",M.core_formchangechecker.set_form_changed,"textarea",this),t.delegate("change",M.core_formchangechecker.set_form_changed,"select",this),this.initialvaluelisteners.push(t.delegate("focus",this.store_initial_value,"input",this)),this.initialvaluelisteners.push(t.delegate("focus",this.store_initial_value,"textarea",this)),this.initialvaluelisteners.push(t.delegate("focus",this.store_initial_value,"select",this)),t.delegate("click",function(){t.setData("ignoreSubmission",!0)},"[data-formchangechecker-ignore-submit]"),r.one(e).on("submit",function(){t.getData("ignoreSubmission")?t.clearData("ignoreSubmission"):M.core_formchangechecker.set_form_submitted()},this),window.onbeforeunload=M.core_formchangechecker.report_form_dirty_state)},store_initial_value:function(e){if(!e.target.ancestor(".ignoredirty",!0)&&!e.target.ancestor("[data-formchangechecker-ignore-dirty]",!0))if(M.core_formchangechecker.get_form_dirty_state())for(;this.initialvaluelisteners.length;)this.initialvaluelisteners.shift().detach();else M.core_formchangechecker.stateinformation.focused_element={element:e.target,initial_value:e.target.get("value")}}},{NAME:"core-formchangechecker",ATTRS:{formid:{value:""},initialdirtystate:{value:!1}}}),M.core_formchangechecker=M.core_formchangechecker||{},M.core_formchangechecker.instances=M.core_formchangechecker.instances||[],M.core_formchangechecker.init=function(e){e=new t(e);return M.core_formchangechecker.instances.push(e),e},M.core_formchangechecker.stateinformation=[],M.core_formchangechecker.set_form_changed=function(e){e&&e.target&&(e.target.ancestor(".ignoredirty",!0)||e.target.ancestor("[data-formchangechecker-ignore-dirty]",!0))||(M.core_formchangechecker.stateinformation.formchanged=1,delete M.core_formchangechecker.stateinformation.focused_element)},M.core_formchangechecker.set_form_submitted=function(){M.core_formchangechecker.stateinformation.formsubmitted=1},M.core_formchangechecker.get_form_dirty_state=function(){var e,t=M.core_formchangechecker.stateinformation;if(t.formsubmitted)return 0;if(t.formchanged)return 1;if(t.focused_element&&t.focused_element.element.get("value")!==t.focused_element.initial_value)return 1;if("undefined"!=typeof window.tinyMCE)for(e in window.tinyMCE.editors)if(window.tinyMCE.editors[e].isDirty())return 1;return 0},M.core_formchangechecker.reset_form_dirty_state=function(){M.core_formchangechecker.stateinformation.formsubmitted=!1,M.core_formchangechecker.stateinformation.formchanged=!1},M.core_formchangechecker.report_form_dirty_state=function(e){if(M.core_formchangechecker.get_form_dirty_state()){var t=M.util.get_string("changesmadereallygoaway","moodle");if(!M.cfg.behatsiterunning)return e&&(e.returnValue=t),t}}},"@VERSION@",{requires:["base","event-focus","moodle-core-event"]});
YUI.add("moodle-core-formchangechecker",function(r,e){var t=function(){t.superclass.constructor.apply(this,arguments)};r.extend(t,r.Base,{initialvaluelisteners:[],initializer:function(){var e="form#"+this.get("formid"),t=r.one(e);t&&(M.core_formchangechecker.stateinformation.formchanged||(M.core_formchangechecker.stateinformation.formchanged=this.get("initialdirtystate")),r.on(M.core.event.EDITOR_CONTENT_RESTORED,M.core_formchangechecker.reset_form_dirty_state,this),t.delegate("change",M.core_formchangechecker.set_form_changed,"input",this),t.delegate("change",M.core_formchangechecker.set_form_changed,"textarea",this),t.delegate("change",M.core_formchangechecker.set_form_changed,"select",this),this.initialvaluelisteners.push(t.delegate("focus",this.store_initial_value,"input",this)),this.initialvaluelisteners.push(t.delegate("focus",this.store_initial_value,"textarea",this)),this.initialvaluelisteners.push(t.delegate("focus",this.store_initial_value,"select",this)),t.delegate("click",function(){t.setData("ignoreSubmission",!0)},"[data-formchangechecker-ignore-submit]"),r.one(e).on("submit",function(){t.getData("ignoreSubmission")?t.clearData("ignoreSubmission"):M.core_formchangechecker.set_form_submitted()},this),window.onbeforeunload=M.core_formchangechecker.report_form_dirty_state)},store_initial_value:function(e){if(!e.target.ancestor(".ignoredirty",!0)&&!e.target.ancestor("[data-formchangechecker-ignore-dirty]",!0))if(M.core_formchangechecker.get_form_dirty_state())for(;this.initialvaluelisteners.length;)this.initialvaluelisteners.shift().detach();else M.core_formchangechecker.stateinformation.focused_element={element:e.target,initial_value:e.target.get("value")}}},{NAME:"core-formchangechecker",ATTRS:{formid:{value:""},initialdirtystate:{value:!1}}}),M.core_formchangechecker=M.core_formchangechecker||{},M.core_formchangechecker.instances=M.core_formchangechecker.instances||[],M.core_formchangechecker.init=function(e){e=new t(e);return M.core_formchangechecker.instances.push(e),e},M.core_formchangechecker.stateinformation=[],M.core_formchangechecker.set_form_changed=function(e){e&&e.target&&(e.target.ancestor(".ignoredirty",!0)||e.target.ancestor("[data-formchangechecker-ignore-dirty]",!0))||(M.core_formchangechecker.stateinformation.formchanged=1,delete M.core_formchangechecker.stateinformation.focused_element)},M.core_formchangechecker.set_form_submitted=function(){M.core_formchangechecker.stateinformation.formsubmitted=1},M.core_formchangechecker.get_form_dirty_state=function(){var e,t=M.core_formchangechecker.stateinformation;if(t.formsubmitted)return 0;if(t.formchanged)return 1;if(t.focused_element&&t.focused_element.element.get("value")!==t.focused_element.initial_value)return 1;if("undefined"!=typeof window.tinyMCE&&window.tinyMCE.editors)for(e in window.tinyMCE.editors)if(window.tinyMCE.editors[e].isDirty())return 1;return 0},M.core_formchangechecker.reset_form_dirty_state=function(){M.core_formchangechecker.stateinformation.formsubmitted=!1,M.core_formchangechecker.stateinformation.formchanged=!1},M.core_formchangechecker.report_form_dirty_state=function(e){if(M.core_formchangechecker.get_form_dirty_state()){var t=M.util.get_string("changesmadereallygoaway","moodle");if(!M.cfg.behatsiterunning)return e&&(e.returnValue=t),t}}},"@VERSION@",{requires:["base","event-focus","moodle-core-event"]});
@@ -205,7 +205,7 @@ M.core_formchangechecker.get_form_dirty_state = function() {
// Handle TinyMCE editor instances
// We can't add a listener in the initializer as the editors may not have been created by that point
// so we do so here instead
if (typeof window.tinyMCE !== 'undefined') {
if (typeof window.tinyMCE !== 'undefined' && window.tinyMCE.editors) {
for (editor in window.tinyMCE.editors) {
if (window.tinyMCE.editors[editor].isDirty()) {
return 1;
+1 -1
View File
@@ -203,7 +203,7 @@ M.core_formchangechecker.get_form_dirty_state = function() {
// Handle TinyMCE editor instances
// We can't add a listener in the initializer as the editors may not have been created by that point
// so we do so here instead
if (typeof window.tinyMCE !== 'undefined') {
if (typeof window.tinyMCE !== 'undefined' && window.tinyMCE.editors) {
for (editor in window.tinyMCE.editors) {
if (window.tinyMCE.editors[editor].isDirty()) {
return 1;
@@ -11,11 +11,14 @@ Feature: Embed videos without the media filter
@javascript
Scenario: Add a video in a URL resource. Make sure media filters work
When I add a "URL" to section "1" and I fill the form with:
| Name | Video URL |
| Description | Example of a video url |
| External URL | http://download.moodle.org/mediatest/quicktime_320_180.mov |
And I follow "Video URL"
Given the following "activity" exists:
| activity | url |
| course | Acceptance test site |
| name | Video URL |
| intro | Example of a video url |
| externalurl | http://download.moodle.org/mediatest/quicktime_320_180.mov |
| section | 1 |
When I am on the "Video URL" "url activity" page
Then ".video-js" "css_element" should exist
And I am on site homepage
@@ -455,6 +455,7 @@ class assign_submission_onlinetext extends assign_submission_plugin {
public function view(stdClass $submission) {
global $CFG;
$result = '';
$plagiarismlinks = '';
$onlinetextsubmission = $this->get_onlinetext_submission($submission->id);
@@ -467,8 +468,6 @@ class assign_submission_onlinetext extends assign_submission_plugin {
'onlinetext',
'assignsubmission_onlinetext');
$plagiarismlinks = '';
if (!empty($CFG->enableplagiarism)) {
require_once($CFG->libdir . '/plagiarismlib.php');
+1 -1
View File
@@ -154,7 +154,7 @@ abstract class feedback_item_base {
* Prepares the value for exporting to Excel
*
* @param object $item the db-object from feedback_item
* @param string $value a item-related value from feedback_values
* @param object $value object with item-related value from feedback_values in the 'value' property
* @return string
*/
abstract public function get_printval($item, $value);
+53 -61
View File
@@ -50,43 +50,28 @@ Feature: Feedbacks in courses with groups
| feedback | Site feedback | Acceptance test site | feedback0 | 2 | 1 | 1 | 1 |
| feedback | Course feedback | C1 | feedback1 | 2 | 1 | 1 | 0 |
| feedback | Course anon feedback | C1 | feedback2 | 1 | 1 | 1 | 0 |
And I am on the "Site feedback" "feedback activity" page logged in as manager
And I click on "Edit questions" "link" in the "[role=main]" "css_element"
And I add a "Multiple choice" question to the feedback with:
| Question | Do you like our site? |
| Label | multichoice2 |
| Multiple choice type | Multiple choice - single answer |
| Hide the "Not selected" option | Yes |
| Multiple choice values | Yes of course\nNot at all\nI don't know |
And I log out
@javascript
Scenario: Non anonymous feedback with groups in a course
Given I am on the "Course feedback" "feedback activity" page logged in as teacher
And I click on "Edit questions" "link" in the "[role=main]" "css_element"
And I add a "Multiple choice" question to the feedback with:
| Question | Do you like this course? |
| Label | multichoice1 |
| Multiple choice type | Multiple choice - single answer |
| Hide the "Not selected" option | Yes |
| Multiple choice values | Yes of course\nNot at all\nI don't know |
And I log out
And I log in as "user1" and complete feedback "Course feedback" in course "Course 1" with:
| Not at all | 1 |
And I log in as "user2" and complete feedback "Course feedback" in course "Course 1" with:
| I don't know | 1 |
And I log in as "user3" and complete feedback "Course feedback" in course "Course 1" with:
| Not at all | 1 |
And I log in as "user4" and complete feedback "Course feedback" in course "Course 1" with:
| Yes of course | 1 |
And I log in as "user5" and complete feedback "Course feedback" in course "Course 1" with:
| Yes of course | 1 |
And I log in as "user6" and complete feedback "Course feedback" in course "Course 1" with:
| Not at all | 1 |
And I log in as "user7" and complete feedback "Course feedback" in course "Course 1" with:
| I don't know | 1 |
Given the following "mod_feedback > question" exists:
| activity | feedback1 |
| name | Do you like this course? |
| questiontype | multichoice |
| label | multichoice1 |
| subtype | r |
| hidenoselect | 1 |
| values | Yes of course\nNot at all\nI don't know |
And the following "mod_feedback > responses" exist:
| activity | user | Do you like this course? |
| feedback1 | user1 | Not at all |
| feedback1 | user2 | I don't know |
| feedback1 | user3 | Not at all |
| feedback1 | user4 | Yes of course |
| feedback1 | user5 | Yes of course |
| feedback1 | user6 | Not at all |
| feedback1 | user7 | I don't know |
# View analysis, user1 should only see one group - group 1
And I am on the "Course feedback" "feedback activity" page logged in as user1
When I am on the "Course feedback" "feedback activity" page logged in as user1
And I follow "Submitted answers"
And I should see "Separate groups: Group 1"
And I show chart data for the "multichoice1" feedback
@@ -151,37 +136,44 @@ Feature: Feedbacks in courses with groups
And I should see "Username 3"
@javascript
Scenario: Anonymous feedback with groups in a course
Given I am on the "Course anon feedback" "feedback activity" page logged in as teacher
And I click on "Edit questions" "link" in the "[role=main]" "css_element"
And I add a "Multiple choice" question to the feedback with:
| Question | Do you like this course? |
| Label | multichoice1 |
| Multiple choice type | Multiple choice - single answer |
| Hide the "Not selected" option | Yes |
| Multiple choice values | Yes of course\nNot at all\nI don't know |
And I log out
And I log in as "user1" and complete feedback "Course anon feedback" in course "Course 1" with:
| Not at all | 1 |
And I am on the "Course anon feedback" "feedback activity" page logged in as user1
Scenario: Anonymous feedback with groups in a course - insufficient responses
Given the following "mod_feedback > question" exists:
| activity | feedback2 |
| name | Do you like this course? |
| questiontype | multichoice |
| label | multichoice1 |
| subtype | r |
| hidenoselect | 1 |
| values | Yes of course\nNot at all\nI don't know |
And the following "mod_feedback > responses" exist:
| activity | user | Do you like this course? |
| feedback2 | user1 | Not at all |
When I am on the "Course anon feedback" "feedback activity" page logged in as user1
And I follow "Submitted answers"
Then I should not see "Yes of course"
And I should see "There are insufficient responses for this group"
And I should not see "Yes of course"
And I log out
And I log in as "user2" and complete feedback "Course anon feedback" in course "Course 1" with:
| I don't know | 1 |
And I log in as "user3" and complete feedback "Course anon feedback" in course "Course 1" with:
| Not at all | 1 |
And I log in as "user4" and complete feedback "Course anon feedback" in course "Course 1" with:
| Yes of course | 1 |
And I log in as "user5" and complete feedback "Course anon feedback" in course "Course 1" with:
| Yes of course | 1 |
And I log in as "user6" and complete feedback "Course anon feedback" in course "Course 1" with:
| Not at all | 1 |
And I log in as "user7" and complete feedback "Course anon feedback" in course "Course 1" with:
| I don't know | 1 |
@javascript
Scenario: Anonymous feedback with groups in a course
Given the following "mod_feedback > question" exists:
| activity | feedback2 |
| name | Do you like this course? |
| questiontype | multichoice |
| label | multichoice1 |
| subtype | r |
| hidenoselect | 1 |
| values | Yes of course\nNot at all\nI don't know |
And the following "mod_feedback > responses" exist:
| activity | user | Do you like this course? |
| feedback2 | user1 | Not at all |
| feedback2 | user2 | I don't know |
| feedback2 | user3 | Not at all |
| feedback2 | user4 | Yes of course |
| feedback2 | user5 | Yes of course |
| feedback2 | user6 | Not at all |
| feedback2 | user7 | I don't know |
# View analysis, user1 should only see one group - group 1
And I am on the "Course anon feedback" "feedback activity" page logged in as user1
When I am on the "Course anon feedback" "feedback activity" page logged in as user1
And I follow "Submitted answers"
And I should see "Separate groups: Group 1"
And I show chart data for the "multichoice1" feedback
@@ -0,0 +1,49 @@
<?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/>.
/**
* Behat data generator for mod_feedback.
*
* @package mod_feedback
* @category test
* @copyright 2022 Noel De Martin
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_mod_feedback_generator extends behat_generator_base {
/**
* Get a list of the entities that Behat can create using the generator step.
*
* @return array
*/
protected function get_creatable_entities(): array {
return [
'questions' => [
'singular' => 'question',
'datagenerator' => 'question',
'required' => ['activity'],
'switchids' => ['activity' => 'cmid'],
],
'responses' => [
'singular' => 'response',
'datagenerator' => 'response',
'required' => ['activity', 'user'],
'switchids' => ['activity' => 'cmid', 'user' => 'userid'],
],
];
}
}
+158 -1
View File
@@ -83,6 +83,97 @@ class mod_feedback_generator extends testing_module_generator {
return parent::create_instance($record, (array)$options);
}
/**
* Create question.
*
* @param array $data Question data
* @return mixed Question instance
*/
public function create_question(array $data) {
global $DB;
$questiontype = $data['questiontype'] ?? 'textfield';
$cm = get_coursemodule_from_id('feedback', $data['cmid']);
$feedback = $DB->get_record('feedback', ['id' => $cm->instance]);
unset($data['questiontype']);
unset($data['cmid']);
if (isset($data['values'])) {
$data['values'] = $this->format_item_values($questiontype, $data['values']);
}
return call_user_func([$this, "create_item_{$questiontype}"], $feedback, $data);
}
/**
* Create response.
*
* @param array $data Response data.
* @return stdClass feedback_completed response instance.
*/
public function create_response(array $data): stdClass {
global $DB;
$userid = $data['userid'];
$responsenumber = null;
$cm = get_coursemodule_from_id('feedback', $data['cmid']);
$feedback = $DB->get_record('feedback', ['id' => $cm->instance]);
$answers = [];
if (isset($data['responsenumber']) && trim($data['responsenumber']) !== '') {
$responsenumber = $data['responsenumber'];
}
if (isset($data['anonymous']) && trim($data['anonymous']) !== '') {
$anonymous = filter_var(trim($data['anonymous']), FILTER_VALIDATE_BOOLEAN);
$feedback->anonymous = $anonymous ? FEEDBACK_ANONYMOUS_YES : FEEDBACK_ANONYMOUS_NO;
}
unset($data['cmid']);
unset($data['userid']);
unset($data['anonymous']);
unset($data['responsenumber']);
foreach ($data as $question => $response) {
$item = $DB->get_record('feedback_item', ['name' => trim($question)], '*', MUST_EXIST);
$answers["{$item->typ}_{$item->id}"] = $this->get_item_response_value($item, $response);
}
$feedbackcompletion = new mod_feedback_completion(
$feedback,
$cm,
$cm->course,
false,
null,
$feedback->anonymous === FEEDBACK_ANONYMOUS_YES ? null : $userid,
$userid
);
if (!$feedbackcompletion->can_complete()) {
throw new coding_exception("User {$userid} cannot complete this feedback activity.");
}
if (!$feedbackcompletion->is_open()) {
throw new coding_exception("This activity is not open.");
}
$feedbackcompletion->set_module_viewed();
$feedbackcompletion->save_response_tmp((object) $answers);
$feedbackcompletion->save_response();
$completed = $feedbackcompletion->get_completed();
if (!is_null($responsenumber)) {
$DB->update_record('feedback_completed', [
'id' => $completed->id,
'random_response' => $responsenumber,
]);
}
return $completed;
}
/**
* Create info question item.
*
@@ -392,5 +483,71 @@ class mod_feedback_generator extends testing_module_generator {
return feedback_create_pagebreak($feedback->id);
}
}
/**
* Format feedback item values.
*
* This method will replace newline characters with the proper line separator for each question type.
*
* @param string $questiontype Question types
* @param string $values Values
* @return string Formatted values
*/
protected function format_item_values(string $questiontype, string $values): string {
global $CFG;
if (!file_exists($CFG->dirroot.'/mod/feedback/item/'.$questiontype.'/lib.php')) {
throw new coding_exception("Question type '$questiontype' not found");
}
require_once($CFG->dirroot.'/mod/feedback/item/'.$questiontype.'/lib.php');
$questiontype = strtoupper($questiontype);
if (defined("FEEDBACK_{$questiontype}_LINE_SEP")) {
return implode(constant("FEEDBACK_{$questiontype}_LINE_SEP"), explode('\n', $values));
}
return $values;
}
/**
* Given a response to a feedback item, return its corresponding value.
*
* @param mixed $record Item record
* @param string $response Response name
* @return int|string Response value
*/
protected function get_item_response_value($record, string $response) {
if (strpos($record->typ, 'multichoice') === 0) {
$item = feedback_get_item_class($record->typ);
return $this->get_choice_item_response_value($item, $record, $response);
}
return $response;
}
/**
* Given a response to a feedback choice item, return its corresponding value.
*
* @param feedback_item_base $item Feedback item
* @param mixed $record Item record
* @param string $response Response
* @param int $offset Choice to start looking from
* @return int Response choice index
*/
protected function get_choice_item_response_value(feedback_item_base $item, $record, string $response, int $offset = 1): int {
$printval = $item->get_printval($record, (object) ['value' => $offset]);
if (empty($printval)) {
throw new coding_exception("Value '$offset' not found");
}
if ($printval === $response) {
return $offset;
}
return $this->get_choice_item_response_value($item, $record, $response, $offset + 1);
}
}
+1 -1
View File
@@ -1722,7 +1722,7 @@ function forum_get_discussions($cm, $forumsort="", $fullpost=true, $unused=-1, $
$allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
$sql = "SELECT $postdata, $discussionfields,
$allnames, u.email, u.picture, u.imagealt $umfields
$allnames, u.email, u.picture, u.imagealt, u.deleted AS userdeleted $umfields
FROM {forum_discussions} d
JOIN {forum_posts} p ON p.discussion = d.id
JOIN {user} u ON p.userid = u.id
+1 -1
View File
@@ -114,7 +114,7 @@ class mod_lti_edit_types_form extends moodleform {
$mform->setForceLtr('lti_resourcekey');
$mform->addElement('passwordunmask', 'lti_password', get_string('password_admin', 'lti'));
$mform->setType('lti_password', PARAM_TEXT);
$mform->setType('lti_password', PARAM_RAW);
$mform->addHelpButton('lti_password', 'password_admin', 'lti');
$mform->hideIf('lti_password', 'lti_ltiversion', 'eq', LTI_VERSION_1P3);
+16 -10
View File
@@ -1837,7 +1837,7 @@ class quiz_attempt {
*/
public function render_question_for_commenting($slot) {
$options = $this->get_display_options(true);
$options->hide_all_feedback();
$options->generalfeedback = question_display_options::HIDDEN;
$options->manualcomment = question_display_options::EDITABLE;
return $this->quba->render_question($slot, $options,
$this->get_question_number($slot));
@@ -2494,19 +2494,25 @@ class quiz_attempt {
}
/**
* Check a page access to see if is an out of sequence access.
* Check a page read access to see if is an out of sequence access.
*
* @param int $page page number.
* @return boolean false is is an out of sequence access, true otherwise.
* If allownext is set then we also check whether access to the page
* after the current one should be permitted.
*
* @param int $page page number.
* @param bool $allownext in case of a sequential navigation, can we go to next page ?
* @return boolean false is an out of sequence access, true otherwise.
* @since Moodle 3.1
*/
public function check_page_access($page) {
if ($this->get_currentpage() != $page) {
if ($this->get_navigation_method() == QUIZ_NAVMETHOD_SEQ && $this->get_currentpage() > $page) {
return false;
}
public function check_page_access($page, $allownext = true) {
if ($this->get_navigation_method() != QUIZ_NAVMETHOD_SEQ) {
return true;
}
return true;
// Sequential access: allow access to the summary, current page or next page.
// Or if the user review his/her attempt, see MDLQA-1523.
return $page == -1
|| $page == $this->get_currentpage()
|| $allownext && ($page == $this->get_currentpage() + 1);
}
/**
+3 -2
View File
@@ -1021,8 +1021,9 @@ class mod_quiz_external extends external_api {
if ($displayoptions->marks >= question_display_options::MARK_AND_MAX) {
$question['mark'] = $attemptobj->get_question_mark($slot);
}
$questions[] = $question;
if ($attemptobj->check_page_access($attemptobj->get_question_page($slot), false)) {
$questions[] = $question;
}
}
return $questions;
}
+57 -2
View File
@@ -40,9 +40,10 @@ class mod_quiz_attempt_testcase extends advanced_testcase {
* Create quiz and attempt data with layout.
*
* @param string $layout layout to set. Like quiz attempt.layout. E.g. '1,2,0,3,4,0,'.
* @param string $navmethod quiz navigation method (defaults to free)
* @return quiz_attempt the new quiz_attempt object
*/
protected function create_quiz_and_attempt_with_layout($layout) {
protected function create_quiz_and_attempt_with_layout($layout, $navmethod = QUIZ_NAVMETHOD_FREE) {
$this->resetAfterTest(true);
// Make a user to do the quiz.
@@ -51,7 +52,7 @@ class mod_quiz_attempt_testcase extends advanced_testcase {
// Make a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $course->id,
'grade' => 100.0, 'sumgrades' => 2, 'layout' => $layout]);
'grade' => 100.0, 'sumgrades' => 2, 'layout' => $layout, 'navmethod' => $navmethod]);
$quizobj = quiz::create($quiz->id, $user->id);
@@ -368,4 +369,58 @@ class mod_quiz_attempt_testcase extends advanced_testcase {
$step = $quba->get_question_attempt(1)->get_step(0);
$this->assertEquals($student1->id, $step->get_user_id());
}
/**
* Test check_page_access function
* @covers \quiz_attempt::check_page_access
*/
public function test_check_page_access() {
$timenow = time();
// Free navigation.
$attempt = $this->create_quiz_and_attempt_with_layout('1,0,2,0,3,0,4,0,5,0', QUIZ_NAVMETHOD_FREE);
// Check access.
$this->assertTrue($attempt->check_page_access(4));
$this->assertTrue($attempt->check_page_access(3));
$this->assertTrue($attempt->check_page_access(2));
$this->assertTrue($attempt->check_page_access(1));
$this->assertTrue($attempt->check_page_access(0));
$this->assertTrue($attempt->check_page_access(2));
// Access page 2.
$attempt->set_currentpage(2);
$attempt = quiz_attempt::create($attempt->get_attempt()->id);
// Check access.
$this->assertTrue($attempt->check_page_access(0));
$this->assertTrue($attempt->check_page_access(1));
$this->assertTrue($attempt->check_page_access(2));
$this->assertTrue($attempt->check_page_access(3));
$this->assertTrue($attempt->check_page_access(4));
// Sequential navigation.
$attempt = $this->create_quiz_and_attempt_with_layout('1,0,2,0,3,0,4,0,5,0', QUIZ_NAVMETHOD_SEQ);
// Check access.
$this->assertTrue($attempt->check_page_access(0));
$this->assertTrue($attempt->check_page_access(1));
$this->assertFalse($attempt->check_page_access(2));
$this->assertFalse($attempt->check_page_access(3));
$this->assertFalse($attempt->check_page_access(4));
// Access page 1.
$attempt->set_currentpage(1);
$attempt = quiz_attempt::create($attempt->get_attempt()->id);
$this->assertTrue($attempt->check_page_access(1));
// Access page 2.
$attempt->set_currentpage(2);
$attempt = quiz_attempt::create($attempt->get_attempt()->id);
$this->assertTrue($attempt->check_page_access(2));
$this->assertTrue($attempt->check_page_access(3));
$this->assertFalse($attempt->check_page_access(4));
$this->assertFalse($attempt->check_page_access(1));
}
}
@@ -0,0 +1,94 @@
@mod @mod_quiz
Feature: Attempt a quiz in a sequential mode
As a student I should not be able to see the previous questions
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student | Student | One | student@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 |
| student | 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 |
| Test questions | truefalse | TF3 | Third question |
| Test questions | truefalse | TF4 | Fourth question |
And the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour | navmethod |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback | sequential |
And quiz "Quiz 1" contains the following questions:
| question | page | requireprevious |
| TF1 | 1 | 1 |
| TF2 | 2 | 1 |
| TF3 | 3 | 1 |
| TF4 | 4 | 1 |
@javascript
Scenario Outline: As a student I should not be able to navigate out of sequence if sequential navigation is on.
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
And I should see "First question"
When I am on the "Quiz 1 > student > Attempt 1 > <pagenumber>" "mod_quiz > Attempt view" page
And I should see "<canseequestion>"
Then I should not see "<cannotseequestion>"
Examples:
| pagenumber | canseequestion | cannotseequestion |
| 1 | First question | Second question |
| 2 | Second question | First question |
| 4 | First question | Fourth question |
@javascript
Scenario: As a student I should not be able to navigate out of sequence by opening new windows on the same quiz.
Given the following config values are set as admin:
| config | value | plugin |
| autosaveperiod | 60 | quiz |
And I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
And I should see "First question"
And I click on "True" "radio" in the "First question" "question"
And I click on "Next page" "button"
When I am on the "Quiz 1 > student > Attempt 1 > 3" "mod_quiz > Attempt view" page
And I click on "True" "radio" in the "Third question" "question"
And I should see "Third question"
And I click on "Next page" "button"
And I am on the "Quiz 1 > student > Attempt 1 > 1" "mod_quiz > Attempt view" page
Then I should see "Fourth question"
@javascript
Scenario: As a student I should not be able to save my data by opening a given page out of sequence.
Given the following config values are set as admin:
| config | value | plugin |
| autosaveperiod | 1 | quiz |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I press "Attempt quiz"
And I am on the "Quiz 1 > student > Attempt 1 > 2" "mod_quiz > Attempt view" page
And I should see "Second question"
And I click on "True" "radio" in the "Second question" "question"
And I wait "2" seconds
And I am on the "Quiz 1 > student > Attempt 1 > 1" "mod_quiz > Attempt view" page
Then I should see "Second question"
@javascript
Scenario: As a student I can review question I have finished in any order
Given user "student" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | True |
| 2 | False |
| 3 | False |
| 4 | False |
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
And I follow "Review"
And I am on the "Quiz 1 > student > Attempt 1 > 3" "mod_quiz > Attempt view" page
And I should see "Third question"
And I am on the "Quiz 1 > student > Attempt 1 > 2" "mod_quiz > Attempt view" page
Then I should see "Second question"
+15 -1
View File
@@ -111,7 +111,21 @@ class behat_mod_quiz extends behat_question_base {
case 'manual grading report':
return new moodle_url('/mod/quiz/report.php',
['id' => $this->get_cm_by_quiz_name($identifier)->id, 'mode' => 'grading']);
case 'attempt view':
list($quizname, $username, $attemptno, $pageno) = explode(' > ', $identifier);
$pageno = intval($pageno);
$pageno = $pageno > 0 ? $pageno - 1 : 0;
$attemptno = (int) trim(str_replace ('Attempt', '', $attemptno));
$quiz = $this->get_quiz_by_name($quizname);
$quizcm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course);
$user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
$attempt = $DB->get_record('quiz_attempts',
['quiz' => $quiz->id, 'userid' => $user->id, 'attempt' => $attemptno], '*', MUST_EXIST);
return new moodle_url('/mod/quiz/attempt.php', [
'attempt' => $attempt->id,
'cmid' => $quizcm->id,
'page' => $pageno
]);
case 'attempt review':
if (substr_count($identifier, ' > ') !== 2) {
throw new coding_exception('For "attempt review", name must be ' .
+1 -2
View File
@@ -105,8 +105,7 @@ Feature: Edit quiz page - adding things
in various categories and add them to the question bank.
# Create a couple of sub categories.
When I am on "Course 1" course homepage
And I navigate to "Question bank > Categories" in current page administration
When I am on the "Course 1" "core_question > course question categories" page
Then I should see "Add category"
Then I set the field "Parent category" to "Default for C1"
And I set the field "Name" to "Subcat 1"

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