Compare commits

..

176 Commits

Author SHA1 Message Date
moodler b5cfd0e85f Like some horror movie, the second hand on the clock ticks over
in slow motion to the next mark
2004-08-31 13:43:44 +00:00
moodler fd772c442e More robust with bad themes 2004-08-31 13:36:47 +00:00
defacer 2ebaaa5e68 Mike noticed that PHP_SELF was being used here. Replaced all instances with $ME. 2004-08-31 13:31:00 +00:00
moodler e5bd075113 Fixing my recent typo 2004-08-31 12:56:11 +00:00
moodler 88289032b6 FIx install.php for PostgreSQL. I *think* ... this is untested 2004-08-31 12:48:36 +00:00
defacer 9c0458b046 One typo and a glitch
The developer fixes
Lo! Bug-free Moodle!
2004-08-31 12:45:05 +00:00
defacer 4ea496519e Fix for bug 1800:
Adding this harmless column makes the get_recent_enrolments() query legal
in Postgres. Patch submitted by Petri.
2004-08-31 12:26:04 +00:00
moodler 36d0082229 FIxed up recent problems with external authentication
http://moodle.org/mod/forum/discuss.php?d=11723#57001
2004-08-31 12:23:24 +00:00
moodler e0a456a3b3 SLightly reworded the enrollfirst string ... no difference to translators probably 2004-08-31 10:16:56 +00:00
moodler 88a70c8436 FIxed the link in the change I just made 2004-08-31 10:03:54 +00:00
moodler a5b9e4d94e Prevent restricted users from using "login as" 2004-08-31 09:59:59 +00:00
moodler 8913dce495 Fixed some english errors 2004-08-31 09:17:49 +00:00
moodler de3e4de88d Some extra safety when using variables - doesn't appear to be an exploit
in the general case but thanks, Fernando Quintero
2004-08-31 08:52:12 +00:00
moodler d0d1008498 Moved loginhttps into main config screen. Some security has been built
in to prevent accidental enabling on sites without https.
2004-08-31 08:39:22 +00:00
moodler 46808f4afc Added shortcut button to edit course settings (bug 1850) 2004-08-31 07:24:33 +00:00
defacer 7eda7f6cbd Another one bites the dust, this one wasn't needed and noone had any
objections about the array-based notation. RIP.
2004-08-31 06:56:44 +00:00
gustav_delius 21c500f755 my second attempt at getting this block right. 2004-08-30 23:24:03 +00:00
stronk7 dfb203cad6 Release info for 1.4. 2004-08-30 22:57:38 +00:00
gustav_delius 8ec882c22f Access to site participants list is now controlled through $CFG->showsiteparticipantslist 2004-08-30 20:57:26 +00:00
gustav_delius d31cdc0ee9 isteacher() with no arguments now does the expected 2004-08-30 20:54:42 +00:00
moodler f547ee52e7 Merged latest language files from HEAD to STABLE 2004-08-30 19:36:07 +00:00
gustav_delius 129af65665 fixed the isstudent function 2004-08-30 19:09:36 +00:00
moodler b2794b6554 removed junk 2004-08-30 18:48:48 +00:00
gustav_delius 04fd313892 Site participants list now shows correct list of site students 2004-08-30 18:43:13 +00:00
moodler be037b8039 Updated index 2004-08-30 18:19:57 +00:00
moodler f01c72ee73 Added help buttons to the new help file for window settings 2004-08-30 18:17:52 +00:00
moodler 2eebac9129 New help file, based on one from Marc Dastous - thanks Marc! 2004-08-30 18:16:36 +00:00
moodler a8db8bdc3e Fixed an unquoted string 2004-08-30 17:45:51 +00:00
gustav_delius bee864c912 Just tidying up. 2004-08-30 17:28:47 +00:00
moodler 4a51bb7cca No need to put MS up there :-) 2004-08-30 16:57:01 +00:00
moodler 4cbb5d6b4b Default editor fonts now match default Moodle themes 2004-08-30 16:53:21 +00:00
gustav_delius 30bc2fa671 This now implements the new rule on who is a site student, see http://moodle.org/mod/forum/discuss.php?d=8452#56743 2004-08-30 16:42:32 +00:00
moodler 4e65d23fd1 OK, this should finally fix the block upgrading problem.
The problem was that site_admin_menu had not been added to the
database yet when we tried to update the blockinfo for the site.
2004-08-30 15:57:09 +00:00
moodler 327f0cdf22 Er, this will work better ;-) 2004-08-30 14:58:13 +00:00
moodler 5e1eaab52f Checking in a fix for the blocks upgrade on the home page so I can test
it on upgrading a real site.
2004-08-30 14:53:43 +00:00
stronk7 431c81eefa Bug 1831
(http://moodle.org/bugs/bug.php?op=show&bugid=1831)
2004-08-30 14:40:23 +00:00
moodler 6908074777 New configuration variable allusersaresitestudents for Gustav to use
in modules ...
2004-08-30 13:21:16 +00:00
moodler 7f446f68b6 Colored text now works.
Allow CSS "color" styles (which look like a protocol to the security system)
to get through the net.  Luckily anything with a hyphen is let through by
default to everything else the editor produces is OK.
2004-08-30 12:09:14 +00:00
bobopinna 6de08a7a4d Updated to Moodle 1.4 stable 2004-08-30 12:06:43 +00:00
defacer 4172b51f81 Some improvements to the code that assigns sideblocks to new courses.
Moved all "magic strings" to nice and tidy constants.
2004-08-30 11:55:36 +00:00
defacer 52f42b41fc Bug 1848:
As per Gustav's recommendation, event names in Upcoming Events are now hyperlinked.
This required an internal API change, but as far as I can see only block_upcoming
used it the relevant functions so it's ok.
Also, substituded SITEID for the magic number 1 in calendar/lib.php as an aside.
2004-08-30 11:42:07 +00:00
bobopinna 63d9ac4893 Fixed a bug in mounth view 2004-08-30 11:38:09 +00:00
gustav_delius cc4a7edf6f course teachers are also site students. This implements the rule described in http://moodle.org/mod/forum/discuss.php?d=8452#56678.
This commit is probably pointless because the rules are likely to change again soon. I won't merge it onto HEAD.
2004-08-30 08:49:01 +00:00
moodler 1afe51e630 Disabled HTML editor when editing HTML files, for now. See bug 1815 2004-08-30 08:42:21 +00:00
gustav_delius eb4f1a380a course teachers are also site students. 2004-08-30 08:13:14 +00:00
moodler 3977f41672 MOved the Editor on/off to the editor settings page, and tidied up a bit 2004-08-30 07:45:03 +00:00
moodler bce227c2bd Fixed some notices ... bug 1846 .. Thanks Bruno! 2004-08-30 06:28:41 +00:00
moodler 862d076232 Fix from Bruno for 1843 2004-08-30 04:31:45 +00:00
moodler 7ac53b8334 Added chooseparamter ... bug 1824 2004-08-30 04:21:56 +00:00
moodler b42451d5c7 "Choose parameter" is now translatable. 2004-08-30 04:21:24 +00:00
stronk7 e706e6bf35 Check that glossary is correct before edit categories.
Bug 1838
(http://moodle.org/bugs/bug.php?op=show&bugid=1838)
2004-08-29 23:26:04 +00:00
stronk7 4e3cfbb4d8 strip_tags() to some parameters. Thanks skodak.
Bug 1836
(http://moodle.org/bugs/bug.php?op=show&bugid=1836)
2004-08-29 22:46:31 +00:00
stronk7 7f322bf4f0 Typo. Thanks to skodak.
Bug 1839
(http://moodle.org/bugs/bug.php?op=show&bugid=1839)
2004-08-29 22:03:01 +00:00
moodler be50231d4a Upgraded to Markdown 1.0 release 2004-08-29 15:45:22 +00:00
gustav_delius 19425053af added constant SITEID=1 2004-08-29 14:00:49 +00:00
moodler c0c2ae7637 Some small changes .. nothing new just re-phrasing 2004-08-29 13:11:43 +00:00
gustav_delius aef7d2ef0b replacing get_site() in add_to_log() for efficiency. Moodle 1.5 will have this done more thoroughly. 2004-08-29 12:54:39 +00:00
moodler f4fbd4d664 Fixed some uninitialised variables bug 1830 2004-08-29 11:59:19 +00:00
gustav_delius d5fe509f44 add_to_log now updates lastaccess if used on site. See bug 1821 2004-08-29 11:35:08 +00:00
stronk7 420b3d092c Backup is ready for 1.4! Bump. 2004-08-29 10:14:56 +00:00
moodler 19625783b5 By default turn off validation for Moodle 1.4 to make things a bit easier
for some packages.  It can be enabled manually in the SCORM module settings.
2004-08-29 09:03:22 +00:00
moodler c5e704f41b FIxed heading and helpbutton link 2004-08-29 07:37:52 +00:00
moodler fda51b8d69 Fixes to authentication logic to help upgraders (ie admin and guest accounts) 2004-08-29 05:42:11 +00:00
moodler 79424afeb2 Slight rewording, not enough to warrant a new file but translators may
want to make similar changes.
2004-08-29 04:46:02 +00:00
moodler bcabc1aac6 Merged change from HEAD (Display -> Window) 2004-08-29 04:34:46 +00:00
moodler ad13b33c01 Added some stripping of data when displaying logs to help prevent some
possible XSS.  Thanks for highlighting this, Petr Skoda!
2004-08-29 03:54:51 +00:00
stronk7 b50ab1c6ac Define a bit better the 'automatic link' feature at glossary and entry levels.
Bug 1745
(http://moodle.org/bugs/bug.php?op=show&bugid=1745)
2004-08-29 00:06:28 +00:00
stronk7 486e156924 translated to es 2004-08-28 23:44:25 +00:00
stronk7 62368bd2a2 Slighty change
Merged from HEAD
2004-08-28 23:41:26 +00:00
stronk7 bbbfb3193b If we are updating an entry, compare current session user with
existing entry user to avoid some potential problems if secureforms=off
Perhaps too much security? Anyway thanks to skodak

(Bug 1823)
(http://moodle.org/bugs/bug.php?op=show&bugid=1823)
2004-08-28 23:20:39 +00:00
stronk7 c2716331a4 Avoid one notice 2004-08-28 19:58:55 +00:00
stronk7 a2ce999684 HTML editor (format popup) display wrong in glossary comments
Bug 1826
(http://moodle.org/bugs/bug.php?op=show&bugid=1826)
2004-08-28 19:45:35 +00:00
gustav_delius 8c8be6622e added explanation about non-editing teachers to help file 2004-08-28 19:27:46 +00:00
stronk7 ac75511c40 Now students cannot add/edit/delete entries if glossary->studentcanpost = 0
using manual URLs.
2004-08-28 19:10:47 +00:00
stronk7 c14abb9990 - students cannot add/edit/delete comments if it isn't allowed
- students can view comments if they exist
- teachers bypass the commentsallowed field always (this can be useful to create glossaries with public comments from the teacher).
- buttons are showed correctly.

Bug 1814
(http://moodle.org/bugs/bug.php?op=show&bugid=1814)
2004-08-28 18:30:39 +00:00
gustav_delius d97903b9ca improvement of online_users block on front page, suggested by Bruno, see bug # 1821 2004-08-28 18:29:55 +00:00
stronk7 356f6d69d2 Added compatibility in user->auth from 1.3 backups...$CFG->auth is used 2004-08-28 17:28:18 +00:00
gustav_delius d0437b3eab fixed name order in headings on log.php 2004-08-28 17:17:25 +00:00
moodler 799ce03dff Fixed a rarely-occurring problem when deleting a module sometimes. 2004-08-28 16:30:01 +00:00
moodler b4ad8b3d24 Added two new site-wide settings for the resource module so that the
admin can deternine the default state of the collapsible panes on the
resource creation screen.
2004-08-28 16:29:20 +00:00
moodler e8ef939008 Aded user preferences to the collapsing resources. Panel settings are
now remembered per user from one edit to the next.
2004-08-28 15:56:29 +00:00
mchurch ebea2dd039 Fixed some SQL statements so they work with PostGreSQL as well as MySQL.
Fix provided by paca.
2004-08-28 15:10:16 +00:00
defacer be37cf962d Candidate fix for bug 1808:
When upgrading from versions < 1.4, put blocks in the site front page by
default. This was already taken care of when doing a fresh install.

Please check the validity of the fix, if possible. It needs to be extended
to Postgres, Oracle and also merged to HEAD if everything's OK.
2004-08-28 12:33:25 +00:00
stronk7 c75a34409f changed timemodified to timecreated 2004-08-28 11:23:25 +00:00
stronk7 eb4409277e Now action is checked and defaults to edit.
Thanks skodak!

Bug 1813
(http://moodle.org/bugs/bug.php?op=show&bugid=1813)
2004-08-28 11:11:27 +00:00
stronk7 f7311bbe14 Now maxeditingtime for entries work againt timecreated instead of timemodified.
Thanks skodak!

Bug 1822
(http://moodle.org/bugs/bug.php?op=show&bugid=1822)
2004-08-28 10:52:54 +00:00
stronk7 04f1308619 Now user->auth is in backup & restore
Bug 1789
(http://moodle.org/bugs/bug.php?op=show&bugid=1789)
2004-08-28 10:19:34 +00:00
moodler 48143e5a51 Fix from a while back 2004-08-28 08:04:10 +00:00
moodler 223b5ee619 Merged changes from trunk 2004-08-28 07:57:57 +00:00
moodler 56fdffcfeb Fixed bug 1787 - clean_text doesn't work as expected on text with added slashes 2004-08-28 07:55:22 +00:00
moodler eb363f4d99 Added ellipses to buttons "Show settings..." 2004-08-28 05:15:57 +00:00
stronk7 1a625fd590 Some important changes arrive:
- 'editalways' applied to comments.
   - some minor notices out.
   - security bug solved:
      + users cannot edit any comment now.
      + users cannot delete any comment now.
      + users cannot edit any entry now.
      + users cannot delete any antry now.

Thanks to skodak, discovering the bug!!!
2004-08-28 00:23:09 +00:00
stronk7 7e71e010a7 Init 2 arrays 2004-08-27 17:20:02 +00:00
stronk7 bc213d6e8a Now, terms being defined in URLs aren't broken by the <nolink> tag.
Bug 1790
(http://moodle.org/bugs/bug.php?op=show&bugid=1790)
2004-08-27 16:46:22 +00:00
moodler 19dd3171b1 Merged Howard's changes from HEAD 2004-08-27 15:50:18 +00:00
moodler aff55f17a2 It's quick but it will do for now. 2004-08-27 11:47:15 +00:00
moodler 42f291c945 Tidied up form sections with javascript 2004-08-27 09:44:13 +00:00
stronk7 64c548ef7a Error when sending a delete-entry action to logs... 2004-08-27 09:12:03 +00:00
moodler 1404a13a33 Merged from trunk. Changes to file resource. 2004-08-27 09:06:36 +00:00
bobopinna 3e93cbe128 Updated to Moodle 1.4 Stable 2004-08-27 08:06:45 +00:00
julmis dafecdb3d9 Fixing Bug #1792 in Student view :-) 2004-08-27 06:37:54 +00:00
moodler 56e55d22ec Merged from HEAD 2004-08-27 04:42:30 +00:00
moodler ae2b896821 Merged Penny's fix from HEAD 2004-08-27 03:39:47 +00:00
stronk7 68467fbf94 Another duplicated tag....now it's really well-formed :-) 2004-08-27 00:57:35 +00:00
stronk7 b9f5885b4d Found it!!!!
A missing </select> was the cause to the wrong display under Safari!!!
2004-08-27 00:50:22 +00:00
stronk7 9f46966132 Solved some inconsistencies when using the HTML editor and the
format menu!!

One Curiosity: I've discovered a bug in Safari Browser. Edit any
multi-line definition entry and you will see it without line
breaks (not using the HTML editor). I've tried moving that textarea
to other positions in the page and then it works.
2004-08-27 00:00:28 +00:00
julmis 6d388d8e29 Added string editorresettodefaults. 2004-08-26 23:06:36 +00:00
stronk7 b6162f17e2 Strange typo.
Bug 1804
(http://moodle.org/bugs/bug.php?op=show&bugid=1804)
2004-08-26 22:36:17 +00:00
gustav_delius 9b1f47c6bd fixed my own typo, bug 1798 2004-08-26 22:17:07 +00:00
stronk7 ba1b26a543 One notice 2004-08-26 21:37:44 +00:00
julmis 53542fb830 Fixing Bug #1791 for real this time ;-) 2004-08-26 19:58:48 +00:00
julmis d390acad28 Added reset to defaults 2004-08-26 18:30:56 +00:00
julmis c27e555d40 Fix for Bug #1793 2004-08-26 17:27:33 +00:00
julmis 1fed924a16 Spell-button added to images directory 2004-08-26 17:24:58 +00:00
julmis 8009ff257a Spell-button to second row 2004-08-26 17:18:41 +00:00
julmis b7eccbf37f Spell-button to second row
Bug #1791 fix
Better debug mode compatibility
2004-08-26 17:10:36 +00:00
moodler 8428897528 OK, I've been having bad luck with this one. NOW it's fixed properly! 2004-08-26 15:08:11 +00:00
moodler 3a5cc3936d Dammit, I accidentally deleted a line. 2004-08-26 14:46:53 +00:00
bobopinna c0ee89d2dc Updated to Moodle 1.4 stable 2004-08-26 14:41:35 +00:00
moodler 6365268b3b 1. Delete is no longer shown on posts after maxeditingtime has expired
2.  Prune strings are cached (my that's an odd sentence)
3.  Last post links from discussion listing now work when there are no replies
2004-08-26 14:19:50 +00:00
moodler 1e27382ed1 Added Password and Phone 2
Frame is now default again
2004-08-26 12:58:59 +00:00
moodler e75647df91 FIxed bug 1799 - thanks Eloy! 2004-08-26 12:24:20 +00:00
stronk7 19b1bd0bc8 Recode page->userid at restore time. 2004-08-26 10:56:02 +00:00
defacer 72d8255f7e Fix for WHERE clauses in Postgres, submitted by Petri Asikainen.
Apparently, Postgres doesn't like "SELECT * FROM table WHERE 0"
2004-08-26 10:48:51 +00:00
defacer ca27c7f734 Small fixes and improvements. 2004-08-26 09:57:59 +00:00
defacer ebc9be85c7 1. Code restructuring
2. Small logic fixes (e.g. only delete chat_user records WHERE type='sockets')
3. More informative messages when rejecting connections for some reason
4. Better handling of updating chat_user records (lastping updated correctly)
5. Custom error handling, so that PHP errors show up in the trace()s
2004-08-26 08:48:15 +00:00
gustav_delius d1dec2d112 fixed bug in online user list, see http://moodle.org/mod/forum/discuss.php?d=11554 2004-08-26 04:11:46 +00:00
stronk7 f54e807d59 Notice out 2004-08-26 00:10:51 +00:00
mchurch ab37ddd5d2 Updated diff functions to handle HTML formatting a little better. 2004-08-25 20:31:30 +00:00
moodler 6e2c52bad7 This hack doesn't work, and it doesn't matter, but I'm leaving it in
in case someone can fix it later
2004-08-25 16:03:39 +00:00
moodler 7b4bb520ba Course passed properly to messages
Old chat_users all deleted on startup
2004-08-25 14:20:54 +00:00
mchurch ab55cb4107 Reorganized the attachment details page a bit, and added author info. 2004-08-25 13:39:18 +00:00
moodler 785d24f0f6 FIx for recent activity when sockets being used 2004-08-25 13:37:38 +00:00
mchurch 5d2f527449 Changed attachment section on content page to only display if there are
attachments, and then only as a link to the attachments control page.
2004-08-25 13:18:46 +00:00
mchurch b36a469ffc Changed title for attachments on content page. 2004-08-25 13:17:45 +00:00
moodler 1820e4e3f2 Fixed a style, just for Przemyslaw 2004-08-25 12:37:37 +00:00
defacer e55474005b *** TRANSLATOR ALERT ***
This commit contains changes and additions to the chat language file.

Better display and explanations for the chat config screen. Final tweaks to
chatd now that we know which settings we 'll be using and how.
2004-08-25 12:13:06 +00:00
defacer 6e82cd4cd3 Second part of changes. Doing this slowly to keep integrity between this branch and HEAD. 2004-08-25 10:57:43 +00:00
defacer 0b0d2241e3 Lots and lots of improvements! Foremost changes are:
1. The users window is now provided with BOTH JavaScript and HTTP refresh mechanisms
2. The daemon now reliably detects disconnected users even in the absence of chat
   activity. Coupled with the fact that the "users in chats" lists are now manipulated
   only by chatd, they have reached 100% accuracy. Even better than the "normal"
   version, since you can leave your chat open (and idle) for as long as you want
   _without_ being disconnected.
3. Better OOP.
2004-08-25 10:45:13 +00:00
moodler 7d9137d05a Merged fixes from HEAD 2004-08-25 09:21:44 +00:00
moodler 97a8cdafd6 FIxed international display of timezones using string 'strftimedaytime'
http://moodle.org/mod/forum/discuss.php?d=11510
2004-08-25 08:27:53 +00:00
moodler cc876bc667 I was trying to make the glossary link open in the parent window, but
I could not get it to work all the time.
2004-08-25 08:15:15 +00:00
moodler 5b5302faa8 Fixed tabs links when glossary called without id 2004-08-25 07:39:26 +00:00
moodler afa051933d Fixed showentry for POstgreSQLo
Moved SQL to its own function in lib.php
Added links to glossary for each entry
2004-08-25 07:36:29 +00:00
moodler 1281b0a7df Redundant cleaning removed 2004-08-25 05:09:31 +00:00
moodler 092f22eb7f Removing some cleaning see bug 1787 2004-08-25 05:07:30 +00:00
moodler e0f0b7bc2d Added USERID field to restore/backup bug 1788 2004-08-25 04:48:19 +00:00
mchurch e8df6a8946 Restoring summary view to attachments view. 2004-08-25 04:19:48 +00:00
moodler d00f113675 Info page -> History 2004-08-25 03:22:15 +00:00
moodler efe91196f3 Hiding attachment listing altogether 2004-08-25 03:19:50 +00:00
moodler b4d9eecbd0 Fixed my bug with the missing Attachments tab 2004-08-25 03:00:36 +00:00
mchurch d2049a8b7b Cleaned up attachment section of view page. 2004-08-25 02:51:15 +00:00
stronk7 cbac49ac50 Don't clean posted text on input (will be cleaned on output). 2004-08-24 22:56:24 +00:00
stronk7 ea4dec3080 Bump to beta!
I suppose that I cannot update the version number because I'm in a branch,
isn't it?
2004-08-24 22:34:06 +00:00
stronk7 e73ae3f14e Deleted to use the English version until it can be updated. 2004-08-24 21:44:56 +00:00
stronk7 4ec1c2a0bd Release info updated to 1.3.4 2004-08-24 21:42:13 +00:00
stronk7 834fc9fd91 one more warning out 2004-08-24 20:55:40 +00:00
stronk7 f3d2d06518 Avoid some warnings
Bug 1729
(http://moodle.org/bugs/bug.php?op=show&bugid=1729)
2004-08-24 20:35:43 +00:00
gustav_delius 2e32990a63 reduced $module->requires to 052505 after Martin put my new functions into that version. 2004-08-24 15:14:32 +00:00
defacer 4b31fcf608 Still better handling of console output, and starting to experiment with the
new idea of "externally" refreshing the users window for reliability.
2004-08-24 14:29:00 +00:00
moodler dd23e499f3 A default I never checked in auth_pop3mailbox => 'INBOX' 2004-08-24 14:24:14 +00:00
moodler 72fe9309f3 Fixed an echo -> mtrace 2004-08-24 14:23:01 +00:00
defacer 3b905f1aab Adding ID attributes to the frames. Better than NAMEs, for one, and also I
have a devious idea to vanquish user window dropouts once and for all...
2004-08-24 14:02:40 +00:00
defacer f11d5ccb4f Improvements in the way that STDOUT output is handled (turns out the constant
exists in PHP >= 4.3 and it's not defined() at all).

Don't let delete_old_users() kill sockets-type clients, we can do that on
our own, thank you.

Comment some trace()s.
2004-08-24 13:58:01 +00:00
mchurch 75c180d2e3 Set Moodle filtering for wiki text only. 2004-08-24 13:39:03 +00:00
gustav_delius ae7fb33a55 disabled email_protect plugin, see Bug #1775 2004-08-24 12:50:57 +00:00
defacer 7a89b30fac I may not be as productive a translator as Koen, but the ? lines in my CVS
updates were approaching the third page boundary! ;-)
2004-08-24 12:27:20 +00:00
defacer d9bdf8cb96 Brand new spiffy shining bright!
1. REAL support for command-line parameters! Pretty good too!
   (actually don't tell Jon, it gets to his head)
2. Can you show the "users in chat" normally now? Please?
2004-08-24 12:22:13 +00:00
stronk7 1a2e6cfb12 Now some CFG->glossary_XXXX are checked in lib.php.
Bug 1764
(http://moodle.org/bugs/bug.php?op=show&bugid=1764)
2004-08-24 11:20:01 +00:00
gustav_delius df85e55750 All the modules in which I made changes require the latest version of datalib.php. 2004-08-24 10:13:41 +00:00
moodler dba8c2802e Merged some changes from HEAD 2004-08-24 06:48:42 +00:00
moodler 67f2fae171 FIxed Upgrade feedback from SQL 2004-08-24 04:57:00 +00:00
gustav_delius bc1ed83f43 fixed small typo 2004-08-23 21:27:52 +00:00
gustav_delius 2559db73e6 fixed bug 1748 2004-08-23 21:26:03 +00:00
gustav_delius 956ad3b8a8 removed ancient german release notes 2004-08-23 20:46:01 +00:00
gustav_delius 0420e8715d removed empty bullet point 2004-08-23 20:34:52 +00:00
70229 changed files with 615878 additions and 9221572 deletions
-185
View File
@@ -1,185 +0,0 @@
// 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/>.
/**
* Generates tsconfig.aliases.json for cross-component TypeScript imports.
*
* Moodle ESM source files can import from other components using the alias
* scheme @moodle/lms/<component>/* (e.g. @moodle/lms/core/SomeUtil). This
* file reads the Moodle component registry (.grunt/components.js), finds every
* component that has a js/esm/src/ directory, and writes the corresponding
* TypeScript path mappings to tsconfig.aliases.json.
*
* tsconfig.json extends tsconfig.aliases.json, so TypeScript picks up the
* mappings automatically. The file is skipped if its content is unchanged, to
* avoid unnecessary rebuilds.
*
* This must run before esbuild compilation. The Grunt `jsconfig` task calls
* generateAliases() so that both jsconfig.json and tsconfig.aliases.json are
* kept in sync. The `react` task depends on `jsconfig`, so aliases are always
* up to date before a React build.
*
* @copyright 2026 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import fs from "fs";
import path from "path";
import { createRequire } from "module";
const rootDir = process.cwd();
const tsconfigOut = path.join(rootDir, "tsconfig.aliases.json");
const TS_ALIASES_WARNING =
"// WARNING: Do not edit this file manually. It is generated by `grunt jsconfig`.\n";
/**
* Load Moodle component paths from `.grunt/components.js`.
*
* @returns {Record<string, string>} Map of component paths to component names.
*/
function loadComponentPathMap() {
const require = createRequire(import.meta.url);
// Load Moodle's components data from .grunt/components.js.
const { fetchComponentData } = require(
path.join(process.cwd(), ".grunt", "components.js")
);
// Format: `{ components: { "public/lib": "core", ... } }`.
const componentsData = fetchComponentData().components;
// Expected:
// `{ "public/lib": "core", "public/ai": "core_ai", ... }`.
return componentsData;
}
/**
* Compare two TypeScript `paths` maps for exact equality.
*
* @param {Record<string, string[]>} a First paths map.
* @param {Record<string, string[]>} b Second paths map.
* @returns {boolean} True when keys and values match in order.
*/
function pathsEqual(a, b) {
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) {
return false;
}
for (const key of aKeys) {
if (!Object.prototype.hasOwnProperty.call(b, key)) {
return false;
}
const aArr = a[key] ?? [];
const bArr = b[key] ?? [];
if (aArr.length !== bArr.length) {
return false;
}
for (let i = 0; i < aArr.length; i++) {
if (aArr[i] !== bArr[i]) {
return false;
}
}
}
return true;
}
/**
* Parse JSON content and tolerate line comments.
*
* @param {string} content Raw JSON/JSONC file content.
* @returns {unknown}
*/
function parseJsonWithComments(content) {
const stripped = content
.replace(/^\uFEFF/, "")
.replace(/^\s*\/\/.*$/gm, "");
return JSON.parse(stripped);
}
/**
* Convert JSON output so single-value arrays are rendered on one line.
*
* @param {unknown} obj Data to stringify.
* @returns {string} JSON string with flattened single-value arrays.
*/
function stringifyFlatArrays(obj) {
const json = JSON.stringify(obj, null, 2);
return TS_ALIASES_WARNING + json.replace(/\[\s+\"(.*?)\"\s+\]/g, '["$1"]') + "\n";
}
/**
* Generate `tsconfig.aliases.json` from Moodle component metadata.
*
* Includes core aliases, discovers plugin React source aliases, and skips
* rewriting when aliases have not changed.
*
* @returns {void}
*/
export function generateAliases() {
const componentPathMap = loadComponentPathMap();
// Build TS paths for tsconfig.aliases.json
/** @type {Record<string, string[]>} */
const tsPaths = {
// Always include core alias.
"@moodle/lms/core/*": ["./public/lib/js/esm/src/*"],
};
for (const [componentPath, componentName] of Object.entries(componentPathMap)) {
const runtimeAliasKey = `@moodle/lms/${componentName}/*`;
const targetPattern = `./${path
.join(componentPath, "js", "esm", "src", "*")
.replace(/\\/g, "/")}`;
tsPaths[runtimeAliasKey] = [targetPattern];
}
const tsconfig = {
compilerOptions: { paths: tsPaths },
};
// If tsconfig paths didn't change, skip regeneration
let previousPaths = null;
let hasWarningHeader = false;
if (fs.existsSync(tsconfigOut)) {
try {
const previousContent = fs.readFileSync(tsconfigOut, "utf8");
hasWarningHeader = previousContent.startsWith(TS_ALIASES_WARNING);
const previousTsconfig = /** @type {{compilerOptions?: {paths?: Record<string, string[]>}}} */ (
parseJsonWithComments(previousContent)
);
previousPaths = previousTsconfig.compilerOptions?.paths ?? {};
} catch {
// ignore parse errors and treat as "no previous cache"
previousPaths = null;
}
}
if (previousPaths && hasWarningHeader && pathsEqual(previousPaths, tsPaths)) {
console.log("✓ Generating tsconfig.aliases.json was skipped. No alias modifications detected.");
return;
}
// Write tsconfig.aliases.json
fs.writeFileSync(tsconfigOut, stringifyFlatArrays(tsconfig));
console.log("✓ Generating tsconfig.aliases.json");
}
-324
View File
@@ -1,324 +0,0 @@
// 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/>.
/**
* Core esbuild library for Moodle ESM components.
*
* This is the single source of all build logic. The Grunt tasks
* (.grunt/tasks/react.js) import from here rather than duplicating
* configuration or build steps.
*
* Source layout convention:
* <component>/js/esm/src/**\/*.{ts,tsx} → <component>/js/esm/build/**\/*.js
*
* Exports:
* createBuildConfig(isDev) esbuild config object; pass isDev=true
* to disable minification / add sourcemaps.
* buildPluginComponents(isDev) Glob for every js/esm/src tree across
* core and plugins, compile all in parallel.
* buildSingleFile(filePath, isDev) Compile one source file; used by the
* Grunt watch task on incremental changes.
* watchComponents(isDev) Start esbuild's native watch mode so the
* compiler rebuilds affected files on save.
* resolveComponentPaths(entry) Map an absolute source path to its
* relative input path and output path.
*
* @copyright 2026 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import esbuild from "esbuild";
import { glob } from "glob";
import chalk from "chalk";
import path from "path";
import fs from "fs";
const projectRoot = process.cwd();
/**
* Resolve a path from the current project root.
*
* @param {...string} segments Path segments to resolve.
* @returns {string} Absolute path from project root.
*/
const fromRoot = (...segments) => path.resolve(projectRoot, ...segments);
/**
* Create an incremental dot progress indicator.
* Prints one dot per completed build and wraps lines for readability.
*
* @param {number} total Total number of items to build.
* @returns {{ start: () => void, tick: () => void, succeed: (msg: string) => void, fail: (msg: string) => void }}
*/
function createDotProgress(total) {
const dotsPerLine = 60;
let printed = 0;
return {
start: () => {
process.stdout.write(`${chalk.cyan("Building")} `);
},
tick: () => {
printed++;
process.stdout.write(chalk.cyan("."));
if (printed % dotsPerLine === 0 && printed < total) {
process.stdout.write(` ${chalk.dim(`[${printed}/${total}]`)}\n`);
}
},
succeed: (msg) => {
process.stdout.write(` ${chalk.dim(`[${printed}/${total}]`)}\n`);
process.stdout.write(`${chalk.green("✓")} ${msg}\n`);
},
fail: (msg) => {
process.stdout.write(` ${chalk.dim(`[${printed}/${total}]`)}\n`);
process.stdout.write(`${chalk.red("✗")} ${msg}\n`);
},
};
}
/**
* Build a single React component entry file.
*
* @param {string} entry Absolute entry file path.
* @param {import('esbuild').BuildOptions} buildConfig Shared esbuild configuration.
* @returns {Promise<{file: string, output?: string, error: Error|null}>}
*/
async function buildComponent(entry, buildConfig) {
const resolved = resolveComponentPaths(entry);
if (!resolved) {
return { file: entry, error: new Error(`Unknown path pattern: ${entry}`) };
}
const { file, output } = resolved;
fs.mkdirSync(path.dirname(output), { recursive: true });
try {
await esbuild.build({
...buildConfig,
entryPoints: [entry],
outfile: output,
});
return { file, output, error: null };
} catch (error) {
return { file, error: error instanceof Error ? error : new Error(String(error)) };
}
}
/**
* Resolve source and output paths for a component entry.
*
* @param {string} entry Absolute component source path.
* @returns {{file: string, output: string} | null} Relative file info and output path, or null for unsupported paths.
*/
export function resolveComponentPaths(entry) {
const rel = path.relative(projectRoot, entry);
if (rel.includes(path.join('esm', 'src'))) {
const [part, rawFile] = rel.split(path.join('esm', 'src'));
const file = rawFile.replace(/^[\/\\]/, '');
return {
file,
output: fromRoot(part, 'esm', 'build', file.replace(/\.(ts|tsx)$/, '.js')),
};
}
return null;
}
/**
* Run all builds in parallel, showing incremental dot progress.
*
* @param {string[]} entryPoints Absolute entry file paths.
* @param {import('esbuild').BuildOptions} buildConfig Shared esbuild configuration.
* @returns {Promise<{errors: {file: string, output?: string, error: Error|null}[]}>}
*/
async function runParallelBuilds(entryPoints, buildConfig) {
const total = entryPoints.length;
/** @type {{file: string, output?: string, error: Error|null}[]} */
const errors = [];
const startTime = Date.now();
const progress = createDotProgress(total);
progress.start();
await Promise.all(entryPoints.map(async (entry) => {
const result = await buildComponent(entry, buildConfig);
if (result.error) {
errors.push(result);
}
progress.tick();
}));
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
const succeeded = total - errors.length;
if (errors.length > 0) {
progress.fail(`${succeeded}/${total} built · ${elapsed}s`);
for (const e of errors) {
console.error(chalk.red(`${e.file}: ${e.error?.message ?? ''}`));
}
} else {
progress.succeed(chalk.bold(`${total} components built`) + chalk.dim(` · ${elapsed}s`));
}
return { errors };
}
/**
* Create the shared esbuild build configuration.
*
* @param {boolean} isDev Whether development mode is enabled.
* @returns {import('esbuild').BuildOptions} esbuild configuration object.
*/
export function createBuildConfig(isDev) {
return {
bundle: true,
format: "esm",
external: ["react", "react/*", "react-dom", "react-dom/*", "@moodlehq/design-system", "@moodlehq/design-system/*", "@moodle/lms", "@moodle/lms/*"],
jsx: "automatic",
minify: !isDev,
sourcemap: isDev ? 'inline' : false,
jsxDev: isDev,
keepNames: isDev,
treeShaking: !isDev,
define: { 'process.env.NODE_ENV': isDev ? '"development"' : '"production"' },
};
}
/**
* Build all plugin and core React components.
*
* @param {boolean} isDev Whether development mode is enabled.
* @returns {Promise<void>}
*/
export async function buildPluginComponents(isDev) {
console.log(chalk.green('> Building components...'));
const entryPoints = glob.sync("**/js/esm/src/**/*.{ts,tsx}", {
cwd: projectRoot,
absolute: true,
ignore: [
`${process.cwd()}/node_modules/**`,
`${process.cwd()}/vendor/**`,
],
});
const buildConfig = createBuildConfig(isDev);
const { errors } = await runParallelBuilds(entryPoints, buildConfig);
if (errors.length > 0) {
throw new Error(`React build failed: ${errors.length} component(s) could not be built`);
}
}
/**
* Start esbuild in native watch mode over all React components.
*
* Creates a single incremental build context for all entry points so that
* esbuild can reuse its internal graph between rebuilds instead of starting
* from scratch on every file change.
*
* @param {boolean} isDev Whether to build in development mode.
* @param {((srcFiles: string[]) => void) | undefined} [onRebuild] Called with the rebuilt entry source
* files (relative to project root) after each non-initial successful rebuild. Use this to run
* follow-up tasks such as linting without coupling them to this module.
* @returns {Promise<import('esbuild').BuildContext|null>} The active context, or null if no source files exist.
*/
export async function watchComponents(isDev, onRebuild) {
const entryPoints = glob.sync("**/js/esm/src/**/*.{ts,tsx}", {
cwd: projectRoot,
absolute: true,
ignore: [
`${process.cwd()}/node_modules/**`,
`${process.cwd()}/vendor/**`,
],
});
if (entryPoints.length === 0) {
return null;
}
const buildConfig = createBuildConfig(isDev);
// Map each source file to an {in, out} pair so esbuild can write each
// component to its custom output directory while sharing a single context.
// The 'out' path is relative to outdir (projectRoot) and has no extension —
// esbuild appends the appropriate extension automatically.
// Build entry pairs and ensure output directories exist in a single pass.
const entryPairs = entryPoints.flatMap(entry => {
const resolved = resolveComponentPaths(entry);
if (!resolved) {
return [];
}
fs.mkdirSync(path.dirname(resolved.output), { recursive: true });
return [{ in: entry, out: path.relative(projectRoot, resolved.output).replace(/\.js$/, '') }];
});
// Report build results to the terminal after every build (initial and on each change).
// metafile: true populates result.metafile.outputs so we know which files were written.
// On a rebuild only the affected outputs appear, so it effectively names the changed file.
/** @type {import('esbuild').Plugin} */
const watchReporter = {
name: 'watch-reporter',
setup(build) {
let isInitial = true;
let startTime = 0;
build.onStart(() => {
startTime = Date.now();
});
build.onEnd(result => {
const now = new Date().toLocaleTimeString();
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
if (result.errors.length > 0) {
console.error(chalk.red(`[${now}] ✗ Build failed (${result.errors.length} error(s))`) + chalk.dim(` · ${elapsed}s`));
return;
}
const outputs = Object.keys(result.metafile?.outputs ?? {});
console.log(chalk.green(`[${now}] ✓ ${outputs.length} component(s) built`) + chalk.dim(` · ${elapsed}s`));
if (isInitial) {
isInitial = false;
} else if (onRebuild) {
// entryPoint is the source file (relative to projectRoot) that triggered
// this rebuild. Pass it to the caller so they can run follow-up tasks
// (e.g. linting) without this module needing to know about them.
const srcFiles = Object.values(result.metafile?.outputs ?? {})
.map(output => output.entryPoint)
.filter(/** @param {string|undefined} f */ f => !!f);
onRebuild(/** @type {string[]} */ (srcFiles));
}
});
},
};
const ctx = await esbuild.context({
...buildConfig,
entryPoints: entryPairs,
outdir: projectRoot,
metafile: true,
plugins: [watchReporter],
});
await ctx.watch();
return ctx;
}
-266
View File
@@ -1,266 +0,0 @@
{
'plugins': [
'@babel',
'promise',
'jsdoc'
],
'extends': [
'eslint:recommended',
'plugin:promise/recommended'
],
'env': {
'browser': true,
// Enable ES6+ features by default.
// See http://eslint.org/docs/user-guide/configuring#specifying-environments
// Note: The YUI override must exactly match this when disabling the ES6+ version because those features are not supported by Shifter.
'es2020': true,
'amd': true
},
'globals': {
'M': true,
'Y': true
},
// Warn about unused eslint-disable statements.
'reportUnusedDisableDirectives': true,
'rules': {
// See http://eslint.org/docs/rules/ for all rules and explanations of all
// rules.
// === Possible Errors ===
'comma-dangle': 'off',
'no-console': 'error',
'no-empty': 'warn',
'no-extra-parens': 'off',
'no-prototype-builtins': 'off',
'no-unreachable': 'warn',
// === Best Practices ===
// (these mostly match our jshint config)
'array-callback-return': 'warn',
'block-scoped-var': 'warn',
'complexity': 'warn',
'consistent-return': 'warn',
'curly': 'error',
'dot-notation': 'warn',
'no-alert': 'warn',
'no-caller': 'error',
'no-div-regex': 'error',
'no-empty-function': 'warn',
'no-eq-null': 'error',
'no-eval': 'error',
'no-extend-native': 'error',
'no-extra-bind': 'warn',
'no-floating-decimal': 'warn',
'no-global-assign': 'warn',
'no-implied-eval': 'error',
'no-invalid-this': 'error',
'no-iterator': 'error',
'no-labels': 'error',
'no-loop-func': 'error',
'no-multi-spaces': 'warn',
'no-multi-str': 'error',
'no-new-func': 'error',
'no-new-wrappers': 'error',
'no-octal-escape': 'error',
'no-proto': 'error',
'no-redeclare': 'warn',
'no-restricted-globals': ['error', { 'name': 'Notification' }],
'no-return-assign': 'error',
'no-script-url': 'error',
'no-self-compare': 'error',
'no-sequences': 'warn',
'no-throw-literal': 'warn',
'no-unmodified-loop-condition': 'error',
'no-unused-expressions': 'error',
'no-useless-call': 'warn',
'no-useless-escape': 'warn',
'wrap-iife': ['error', 'any'],
// === Variables ===
'no-undef-init': 'error',
'no-unused-vars': ['error', { 'caughtErrors': 'none' }],
// === Stylistic Issues ===
'array-bracket-spacing': 'warn',
'block-spacing': 'warn',
'brace-style': ['warn', '1tbs'],
'camelcase': 'warn',
'capitalized-comments': ['warn', 'always', { 'ignoreConsecutiveComments': true }],
'comma-spacing': ['warn', { 'before': false, 'after': true }],
'comma-style': ['warn', 'last'],
'computed-property-spacing': 'error',
'consistent-this': 'off',
'eol-last': 'off',
'func-call-spacing': ['warn', 'never'],
'func-names': 'off',
'func-style': 'off',
// indent currently not doing well with our wrapping style, so disabled.
'indent': ['off', 4, { 'SwitchCase': 1 }],
'key-spacing': ['warn', { 'beforeColon': false, 'afterColon': true, 'mode': minimum }],
'keyword-spacing': 'warn',
'linebreak-style': ['error', 'unix'],
'lines-around-comment': 'off',
'max-len': ['error', 132],
'max-lines': 'off',
'max-depth': 'warn',
'max-nested-callbacks': ['warn', 5],
'max-params': 'off',
'max-statements': 'off',
'max-statements-per-line': ['warn', { max: 2 }],
'new-cap': ['warn', { 'properties': false }],
'new-parens': 'warn',
'newline-per-chained-call': 'off',
'no-array-constructor': 'off',
'no-bitwise': 'error',
'no-continue': 'off',
'no-inline-comments': 'off',
'no-lonely-if': 'off',
'no-mixed-operators': 'off',
'no-multiple-empty-lines': 'warn',
'no-negated-condition': 'off',
'no-nested-ternary': 'warn',
'no-new-object': 'off',
'no-plusplus': 'off',
'no-tabs': 'error',
'no-ternary': 'off',
'no-trailing-spaces': 'error',
'no-underscore-dangle': 'off',
'no-unneeded-ternary': 'off',
'no-whitespace-before-property': 'warn',
'object-curly-newline': 'off',
'object-curly-spacing': 'warn',
'object-property-newline': 'off',
'one-var': 'off',
'one-var-declaration-per-line': ['warn', 'initializations'],
'operator-assignment': 'off',
'operator-linebreak': 'off',
'padded-blocks': 'off',
'quote-props': ['warn', 'as-needed', {'unnecessary': false, 'keywords': true, 'numbers': true}],
'quotes': 'off',
'semi': 'error',
'semi-spacing': ['warn', {'before': false, 'after': true}],
'sort-vars': 'off',
'space-before-blocks': 'warn',
'space-before-function-paren': ['warn', 'never'],
'space-in-parens': 'warn',
'space-infix-ops': 'warn',
'space-unary-ops': 'warn',
'spaced-comment': 'warn',
'unicode-bom': 'error',
'wrap-regex': 'off',
// === Promises (override plugin:promise/recommended) ===
'promise/always-return': 'warn',
'promise/no-return-wrap': 'warn',
'promise/param-names': 'warn',
'promise/catch-or-return': ['warn', {terminationMethod: ['catch', 'fail', 'always'], allowFinally: true}],
'promise/no-native': 'warn',
'promise/avoid-new': 'warn',
// === Deprecations ===
"no-restricted-properties": ['warn', {
'object': 'M',
'property': 'str',
'message': 'Use "core/str" module or M.util.get_string()'
}],
},
overrides: [
{
files: ["**/yui/src/**/*.js"],
'env': {
// Disable ES6+ for YUI files.
'es2020': false,
},
// Disable some rules which we can't safely define for YUI rollups.
rules: {
'no-undef': 'off',
'no-unused-vars': 'off',
'no-unused-expressions': 'off',
// === JSDocs ===
"jsdoc/check-access": 'off',
"jsdoc/check-alignment": 'off',
"jsdoc/check-param-names": 'off',
"jsdoc/check-property-names": 'off',
"jsdoc/empty-tags": 'off',
"jsdoc/implements-on-classes": 'off',
"jsdoc/multiline-blocks": 'off',
"jsdoc/require-jsdoc": 'off',
"jsdoc/require-param": 'off',
"jsdoc/require-param-name": 'off',
"jsdoc/require-param-type": 'off',
"jsdoc/require-property": 'off',
"jsdoc/require-property-name": 'off',
"jsdoc/require-property-type": 'off',
}
},
{
files: ["**/amd/src/*.js", "**/amd/src/**/*.js", "Gruntfile.js", ".grunt/*.js", ".grunt/tasks/*.js", "jsdoc.conf.js"],
// We're using babel transpiling so use their parser
// for linting.
parser: '@babel/eslint-parser',
// Check AMD with some slightly stricter rules.
rules: {
'no-implicit-globals': 'error',
// Disable all of the rules that have babel versions.
'new-cap': 'off',
// Not using this rule for the time being because it isn't
// compatible with jQuery and ES6.
'no-invalid-this': 'off',
'object-curly-spacing': 'off',
'quotes': 'off',
'semi': 'off',
'no-unused-expressions': 'off',
// Enable all of the babel version of these rules.
'@babel/new-cap': ['warn', { 'properties': false }],
// Not using this rule for the time being because it isn't
// compatible with jQuery and ES6.
'@babel/no-invalid-this': 'off',
'@babel/object-curly-spacing': 'warn',
'@babel/semi': 'error',
'@babel/no-unused-expressions': 'error',
// === Promises ===
// We have Promise now that we're using ES6.
'promise/no-native': 'off',
'promise/avoid-new': 'off',
// === JSDocs ===
'jsdoc/check-access': 'error',
'jsdoc/check-alignment': 1, // Recommended.
'jsdoc/check-param-names': 'error',
'jsdoc/check-property-names': 'error',
'jsdoc/empty-tags': 'error',
'jsdoc/implements-on-classes': 'error',
'jsdoc/multiline-blocks': 'error',
'jsdoc/require-jsdoc': 'error',
'jsdoc/require-param': 'error',
'jsdoc/require-param-name': 'error',
'jsdoc/require-param-type': 'error',
'jsdoc/require-property': 'error',
'jsdoc/require-property-name': 'error',
'jsdoc/require-property-type': 'error'
},
parserOptions: {
'sourceType': 'module',
'requireConfigFile': false
}
},
{
files: ["**/*.ts", "**/*.tsx"],
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
ecmaFeatures: {
jsx: true
},
allowSyntheticDefaultImports: true
},
rules: {
// promise/no-native crashes on TypeScript ASTs (same reason it is
// disabled for AMD files above). Native Promises are standard in TS.
'promise/no-native': 'off',
'promise/avoid-new': 'off',
}
}
]
}
-24
View File
@@ -1,24 +0,0 @@
{
"indentation": ["on",{
"Feature": 0,
"Background": 2,
"Scenario": 2,
"Examples": 4,
"example": 6,
"Step": 4,
"given": 4,
"and": 4
}],
"no-dupe-feature-names": "on",
"no-dupe-scenario-names": "off",
"no-empty-file": "on",
"no-files-without-scenarios": "on",
"no-multiple-empty-lines": "on",
"no-partially-commented-tag-lines": "on",
"no-trailing-spaces": "on",
"no-unnamed-features": "on",
"no-unnamed-scenarios": "on",
"no-scenario-outlines-without-examples": "on",
"no-examples-in-scenarios": "on",
"new-line-at-eof": ["on", "yes"]
}
-9
View File
@@ -1,9 +0,0 @@
**/yui/build/** -diff
**/amd/build/** -diff
**.js.map -diff
**-min.js -diff
**.min.js -diff
# Remove composer lock file changes from standard diff views.
/composer.lock -diff
/lib/js/bundles/*/*.js -diff
-2
View File
@@ -1,2 +0,0 @@
# Primary donations pages.
custom: ["https://moodle.com/donations/"]
-7
View File
@@ -1,7 +0,0 @@
*** PLEASE DO NOT OPEN PULL REQUESTS VIA GITHUB ***
The moodle.git repository at Github is just a mirror of the official repository. We do not accept pull requests at Github.
See CONTRIBUTING.md guidelines for how to contribute patches for Moodle. Thank you.
--
-6
View File
@@ -1,6 +0,0 @@
> [!IMPORTANT]
> PLEASE DO NOT PUBLISH SECURITY FINDINGS PUBLICLY.
If you discover a potential security issue, please report it via our [Security Submission Form](https://moodle.org/security/report).
For more information about our security processes and responsible disclosure policy, see the [Security Procedures documentation](https://moodledev.io/general/development/process/security).
-1
View File
@@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1231.24 314.44" preserveAspectRatio="xMinYMid meet"><defs><style>.cls-1{fill:#f48020;}.cls-2,.cls-3{fill:#383738;stroke:#4a4a4c;}.cls-2{stroke-width:2.37px;}.cls-3{stroke-width:2.38px;}.cls-4{fill:#585858;}</style></defs><title>Moodle_Logo_UseonBlack_RGB</title><path class="cls-1" d="M1086.3,235.34a53.42,53.42,0,0,0,16.71,29q13.26,12.27,29.48,12.27,11.29,0,24.56-5.89,16.23-6.88,16.22-16.22l58-.49q-12.29,29-41.76,45.2-27.52,15.23-60.43,15.24-42.25,0-70-26.29t-27.75-68.54q0-43.73,27.75-69.51t72-25.8q48.15,0,75.17,31.94,25.07,29.47,25.06,79.1Zm89.91-33.91q-3.43-17.69-14.73-29-12.8-12.28-29.49-12.29-17.21,0-28.74,11.79t-15.47,29.48Z"/><path class="cls-1" d="M965.45,308.54V0h55V308.54Z"/><path class="cls-1" d="M892.69,308.54V290.36q-7.38,9.83-25.06,15.72a92.31,92.31,0,0,1-30,5.41q-39.31,0-63.13-27T750.7,217.65q0-39.3,23.34-65.84t62.15-26.53q43.71,0,56.5,23.58V0h53.55V308.54Zm0-102.68q0-14.75-14-28.26t-28.74-13.51q-21.12,0-33.41,17.2-10.8,15.24-10.8,37.33,0,21.63,10.8,36.85,12.29,17.71,33.41,17.69,12.78,0,27.76-12t15-24.81Z"/><path class="cls-1" d="M647,313.45q-41.77,0-69-26.53t-27.27-68.3q0-41.73,27.27-68.28t69-26.53q41.75,0,69.27,26.53t27.52,68.28q0,41.79-27.52,68.3T647,313.45ZM647,168q-19.89,0-30.58,15.07t-10.68,35.79q0,20.73,9.94,35.05,11.43,16.31,31.32,16.28t31.32-16.28q10.44-14.31,10.44-35.05t-9.94-35Q667.42,168,647,168Z"/><path class="cls-1" d="M448.07,313.45q-41.76,0-69-26.53t-27.27-68.3q0-41.73,27.27-68.28t69-26.53q41.74,0,69.28,26.53t27.5,68.28q0,41.79-27.5,68.3T448.07,313.45Zm0-145.43q-19.89,0-30.59,15.07t-10.69,35.79q0,20.73,9.94,35.05,11.43,16.31,31.34,16.28t31.32-16.28q10.42-14.31,10.44-35.05t-9.94-35Q468.45,168,448.07,168Z"/><path class="cls-1" d="M288.89,309.11V201q0-33.9-28-33.9t-28,33.9V309.11h-55V201q0-33.9-27.52-33.9-28,0-28,33.9V309.11h-55V194.65q0-35.37,24.56-53.54,21.61-16.22,58.46-16.22,37.35,0,55,19.16,15.23-19.16,55.51-19.16,36.86,0,58.45,16.22,24.57,18.16,24.57,53.54V309.11Z"/><path class="cls-2" d="M.76,145.76"/><path class="cls-3" d="M1.73,145.32"/><path class="cls-4" d="M173.86,116.85l55.08-40.1-.7-2.44C135,83.92,80.42,96.44,0,144.13l.73,2.38,21-.67c15.17.52,30.85,1.26,46,1.32-1,24.09.83,32.65.83,32.65,51.92,17.58,101.22,1.53,127.14-43.9A248.72,248.72,0,0,0,173.86,116.85Z"/><path class="cls-4" d="M113.78,110c-.3-1.08-2.23-1.48-4.28-.9a4.73,4.73,0,0,0-3.14,2.19c-18.53,5-49.16,13.72-70.73,21l-1.33.45c-16.68,5.61-24.73,8.49-25.22,12.55-2.29,18.44-.87,42.18-.86,42.48l.19,6.4c-9.74,27.56.06,49.6,8.38,69.77.06-24.92,1.07-45.22-6-71-.2-3.82-1.49-30.36.61-47.34.34-2.75,11.65-6.56,23.63-10.59l1.33-.44c22-7.42,53.57-16.39,71.9-21.29a6.75,6.75,0,0,0,2.29-.25C112.64,112.43,114.08,111.09,113.78,110Z"/></svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

-31
View File
@@ -1,31 +0,0 @@
name: 'Close Pull Requests'
on:
pull_request_target:
types: opened
workflow_dispatch:
jobs:
close:
# Avoid forks to run this job
if: github.repository_owner == 'moodle'
name: Close Pull Requests
runs-on: ubuntu-latest
steps:
- uses: dessant/repo-lockdown@v4
with:
process-only: prs
close-pr: true
skip-closed-pr-comment: true
pr-comment: >
**Please, don't open pull requests via GitHub.**
This repository (moodle.git) at GitHub is just a mirror of the official Moodle repository.
For how to contribute patches for Moodle see
[CONTRIBUTING.md](https://github.com/moodle/moodle/blob/main/CONTRIBUTING.md).
Closing this pull request.
-118
View File
@@ -1,118 +0,0 @@
name: Run tests in a Composed Moodle Setup
on:
push:
branches-ignore:
- main
- MOODLE_[0-9]+_STABLE
tags-ignore:
- v[0-9]+.[0-9]+.[0-9]+*
workflow_dispatch:
inputs:
phpunit_extra_options:
description: Additional options to apply to PHPUnit
required: false
default: ''
env:
php: 8.4
jobs:
PHPUnit:
runs-on: ${{ matrix.os }}
services:
exttests:
image: moodlehq/moodle-exttests
ports:
- 8080:80
redis:
image: redis
ports:
- 6379:6379
postgres:
image: ${{ matrix.db == 'pgsql' && 'postgres:16' || '' }}
env:
POSTGRES_DB: test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- 5432:5432
strategy:
fail-fast: false
matrix:
include:
# MySQL builds always run with the lowest PHP supported version.
- os: ubuntu-24.04
php: 8.3
extensions:
db: mysqli
# PostgreSQL builds always run with the highest PHP supported version.
- os: ubuntu-24.04
php: 8.4
db: pgsql
steps:
- name: Run MySQL Server
if: ${{ matrix.db == 'mysqli' }}
run: |
docker run --rm \
-e MYSQL_DATABASE=test \
-e MYSQL_USER=test \
-e MYSQL_PASSWORD=test \
-e MYSQL_ROOT_PASSWORD=test \
-p 3306:3306 \
-d \
--tmpfs /var/lib/mysql:rw,noexec,nosuid,size=1024M \
mysql:8.4 \
--skip-log-bin \
--collation-server=utf8mb4_bin
- name: Configuring git vars
uses: rlespinasse/github-slug-action@v5
- name: Setting up PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: ${{ matrix.extensions }}
ini-values: max_input_vars=5000
coverage: none
tools: composer
- name: Checking out code from ${{ env.GITHUB_REF_SLUG }}
uses: actions/checkout@v6
with:
path: 'moodle'
- name: 'Preparing the environment'
run: |
echo "pathtophp=$(which php)" >> $GITHUB_ENV # Inject installed pathtophp to env. The template config needs it.
sudo locale-gen en_AU.UTF-8
- name: Create the new Composer Project
run: |
mkdir testsite
cp moodle/.github/workflows/composed/composer.json testsite/composer.json
cp moodle/.github/workflows/composed/config.php testsite/config.php
mkdir -p testsite/data/moodledata
mkdir -p testsite/data/phpunitdata
- name: Install the Moodle test site
working-directory: testsite
run: |
composer install --no-interaction
- name: Setting up PHPUnit
working-directory: testsite
env:
dbtype: ${{ matrix.db }}
run: |
php moodle/public/admin/tool/phpunit/cli/init.php --no-composer-self-update
- name: Running PHPUnit tests
working-directory: testsite
env:
dbtype: ${{ matrix.db }}
phpunit_options: ${{ secrets.phpunit_options }}
run: |
vendor/bin/phpunit $phpunit_options ${{ inputs.phpunit_extra_options }}
-25
View File
@@ -1,25 +0,0 @@
{
"type": "project",
"require": {
"moodle/composer-installer": "^1",
"moodle/moodle-composer-scaffold": "^1",
"moodle/moodle": "@dev",
"moodle/moodle-testing": "^1.0"
},
"repositories": [
{
"name": "moodleundertest",
"type": "path",
"url": "../moodle"
}
],
"config": {
"audit": {
"block-insecure": false
},
"allow-plugins": {
"moodle/composer-installer": true,
"moodle/moodle-composer-scaffold": true
}
}
}
-67
View File
@@ -1,67 +0,0 @@
<?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/>.
/**
* Template configuraton file for GitHub Actions CI/CD using Composer.
*
* @package core
* @copyright 2020 onwards Eloy Lafuente (stronk7) {@link https://stronk7.com}
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// This cannot be used out from a GitHub Actions workflow, so just exit.
getenv('GITHUB_WORKFLOW') || die; // phpcs:ignore moodle.Files.MoodleInternal.MoodleInternalGlobalState
unset($CFG);
global $CFG;
$CFG = new stdClass();
$CFG->dbtype = getenv('dbtype');
$CFG->dblibrary = 'native';
$CFG->dbhost = '127.0.0.1';
$CFG->dbname = 'test';
$CFG->dbuser = 'test';
$CFG->dbpass = 'test';
$CFG->prefix = 'm_';
$CFG->dboptions = ['dbcollation' => 'utf8mb4_bin'];
$host = 'localhost';
$CFG->wwwroot = "http://{$host}";
$CFG->dataroot = __DIR__ . '/data/moodledata';
$CFG->admin = 'admin';
$CFG->directorypermissions = 0777;
// Debug options - possible to be controlled by flag in future.
$CFG->debug = (E_ALL); // DEBUG_DEVELOPER.
$CFG->debugdisplay = 1;
$CFG->debugstringids = 1; // Add strings=1 to url to get string ids.
$CFG->perfdebug = 15;
$CFG->debugpageinfo = 1;
$CFG->allowthemechangeonurl = 1;
$CFG->passwordpolicy = 0;
$CFG->cronclionly = 0;
$CFG->pathtophp = getenv('pathtophp');
$CFG->phpunit_dataroot = __DIR__ . '/data/phpunitdata';
$CFG->phpunit_prefix = 't_';
$CFG->routerconfigured = true;
define('TEST_EXTERNAL_FILES_HTTP_URL', 'http://localhost:8080');
define('TEST_EXTERNAL_FILES_HTTPS_URL', 'http://localhost:8080');
define('TEST_SESSION_REDIS_HOST', 'localhost');
define('TEST_CACHESTORE_REDIS_TESTSERVERS', 'localhost');
-73
View File
@@ -1,73 +0,0 @@
<?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/>.
/**
* Template configuraton file for github actions CI/CD.
*
* @package core
* @copyright 2020 onwards Eloy Lafuente (stronk7) {@link https://stronk7.com}
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// This cannot be used out from a github actions workflow, so just exit.
getenv('GITHUB_WORKFLOW') || die; // phpcs:ignore moodle.Files.MoodleInternal.MoodleInternalGlobalState
unset($CFG);
global $CFG;
$CFG = new stdClass();
$CFG->dbtype = getenv('dbtype');
$CFG->dblibrary = 'native';
$CFG->dbhost = '127.0.0.1';
$CFG->dbname = 'test';
$CFG->dbuser = 'test';
$CFG->dbpass = 'test';
$CFG->prefix = 'm_';
$CFG->dboptions = ['dbcollation' => 'utf8mb4_bin'];
$host = 'localhost';
$CFG->wwwroot = "http://{$host}";
$CFG->dataroot = realpath(dirname(__DIR__)) . '/moodledata';
$CFG->admin = 'admin';
$CFG->directorypermissions = 0777;
// Debug options - possible to be controlled by flag in future.
$CFG->debug = (E_ALL); // DEBUG_DEVELOPER.
$CFG->debugdisplay = 1;
$CFG->debugstringids = 1; // Add strings=1 to url to get string ids.
$CFG->perfdebug = 15;
$CFG->debugpageinfo = 1;
$CFG->allowthemechangeonurl = 1;
$CFG->passwordpolicy = 0;
$CFG->cronclionly = 0;
$CFG->pathtophp = getenv('pathtophp');
$CFG->phpunit_dataroot = realpath(dirname(__DIR__)) . '/phpunitdata';
$CFG->phpunit_prefix = 't_';
$CFG->routerconfigured = true;
define('TEST_EXTERNAL_FILES_HTTP_URL', 'http://localhost:8080');
define('TEST_EXTERNAL_FILES_HTTPS_URL', 'http://localhost:8080');
define('TEST_SESSION_REDIS_HOST', 'localhost');
define('TEST_CACHESTORE_REDIS_TESTSERVERS', 'localhost');
// TODO: add others (solr, mongodb, memcached, ldap...).
// Too much for now: define('PHPUNIT_LONGTEST', true); // Only leaves a few tests out and they are run later by CI.
require_once(__DIR__ . '/lib/setup.php');
-143
View File
@@ -1,143 +0,0 @@
name: One by One Testing
# Run all the individual unit tests one by one, with
# fully independent PHPUnit executions. Useful to
# detect issues with some tests that are using stuff
# that has been made available by others, but is not
# available when running individually.
#
# Note that we aren't using PHPUnit's own isolation
# here but completely separated runs, one for each
# test.
#
# The workflow will fail reporting all the tests
# that have failed (and will pass if no failure is
# detected, of course).
#
# It's only executed via workflow dispatch (automated
# or manual), not by push/tag. And acceptd configuration
# of phpunit, specially useful to run it with PHPUnit's
# own isolation or any other option.
on:
workflow_dispatch:
inputs:
phpunit_extra_options:
description: Additional options to apply to PHPUnit
required: false
default: ''
env:
chunks: 7
jobs:
collect:
name: Collect individual unit tests
runs-on: ubuntu-latest
outputs:
matrix: ${{steps.individual-tests.outputs.matrix }}
steps:
- name: Checking out code
uses: actions/checkout@v4
- name: Looking for all individual tests
id: individual-tests
run: |
count=0 # Number of individual tests found.
while read -r testfile; do # For each test file.
while read -r testname; do # For each unit test in a file.
count=$((count + 1))
# Sent it to the correct chunk file.
chunk=$(((($count % $chunks)) + 1))
echo "$testname $testfile" >> ./chunk_$chunk.txt
done < <(grep "function test_" "${testfile}" | sed -r "s/^.*function (test_[a-zA-Z0-9_]+).*/\1/")
done < <(find . -name "*_test.php" -not -path "*/fixtures/*")
# Generate the matrix to run tests.
echo "matrix=$(ls -1 chunk_*.txt | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT
echo "$count individual tests collected in $chunks files"
- name: Upload individual tests files
uses: actions/upload-artifact@v4
with:
name: individual_tests
path: chunk_*.txt
retention-days: 1
test:
name: Run tests
needs: collect
runs-on: ubuntu-latest
services:
exttests:
image: moodlehq/moodle-exttests
ports:
- 8080:80
redis:
image: redis
ports:
- 6379:6379
strategy:
fail-fast: false
matrix:
file: ${{ fromJson(needs.collect.outputs.matrix) }}
steps:
- name: Setting up DB pgsql
uses: m4nu56/postgresql-action@v1
with:
postgresql version: 16
postgresql db: test
postgresql user: test
postgresql password: test
- name: Setting up PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.4
ini-values: max_input_vars=5000
coverage: none
- name: Checking out code
uses: actions/checkout@v4
- name: Download individual test files
uses: actions/download-artifact@v4
with:
name: individual_tests # Make all the chunk files available for the next steps.
- name: Setting up PHPUnit
env:
dbtype: pgsql
run: |
echo "pathtophp=$(which php)" >> $GITHUB_ENV
cp .github/workflows/config-template.php config.php
mkdir ../moodledata
sudo locale-gen en_AU.UTF-8
php public/admin/tool/phpunit/cli/init.php --no-composer-self-update
- name: Run PHPUnit test (one by one)
env:
dbtype: pgsql
run: |
status=0
count=0
while read -r line; do # For each line in the chunk file
count=$((count + 1))
filter="${line% *}"
file="${line#* }"
# Run the individual unit test and report problems if needed to.
if ! php vendor/bin/phpunit \
--fail-on-empty-test-suite \
--fail-on-warning \
--fail-on-risky \
--filter "$filter" ${{ inputs.phpunit_extra_options }} \
"$file" >/dev/null 2>&1; then
if [ $status -eq 0 ]; then
echo "Problems found, list of PHPUnit commands failing:"
fi
echo "vendor/bin/phpunit --filter '${filter}' ${{ inputs.phpunit_extra_options }} $file"
status=$((status + 1))
fi
done < ${{ matrix.file }}
echo "Finished: $count individual tests executed, $status tests failed"
exit $status
-128
View File
@@ -1,128 +0,0 @@
name: Core
on:
push:
branches-ignore:
- main
- MOODLE_[0-9]+_STABLE
tags-ignore:
- v[0-9]+.[0-9]+.[0-9]+*
workflow_dispatch:
inputs:
phpunit_extra_options:
description: Additional options to apply to PHPUnit
required: false
default: ''
env:
php: 8.4
jobs:
Grunt:
runs-on: ubuntu-24.04
steps:
- name: Checking out code
uses: actions/checkout@v4
- name: Configuring node & npm
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Installing node stuff
run: npm install
- name: Running grunt
run: npx grunt
- name: Looking for uncommitted changes
# Add all files to the git index and then run diff --cached to see all changes.
# This ensures that we get the status of all files, including new files.
# We ignore npm-shrinkwrap.json to make the tasks immune to npm changes.
run: |
git add .
git reset -- npm-shrinkwrap.json
git diff --cached --exit-code
PHPUnit:
runs-on: ${{ matrix.os }}
services:
exttests:
image: moodlehq/moodle-exttests
ports:
- 8080:80
redis:
image: redis
ports:
- 6379:6379
postgres:
image: ${{ matrix.db == 'pgsql' && 'postgres:16' || '' }}
env:
POSTGRES_DB: test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- 5432:5432
strategy:
fail-fast: false
matrix:
include:
# MySQL builds always run with the lowest PHP supported version.
- os: ubuntu-24.04
php: 8.3
extensions:
db: mysqli
# PostgreSQL builds always run with the highest PHP supported version.
- os: ubuntu-24.04
php: 8.4
db: pgsql
steps:
- name: Run MySQL Server
if: ${{ matrix.db == 'mysqli' }}
run: |
docker run --rm \
-e MYSQL_DATABASE=test \
-e MYSQL_USER=test \
-e MYSQL_PASSWORD=test \
-e MYSQL_ROOT_PASSWORD=test \
-p 3306:3306 \
-d \
--tmpfs /var/lib/mysql:rw,noexec,nosuid,size=1024M \
mysql:8.4 \
--skip-log-bin \
--collation-server=utf8mb4_bin
- name: Configuring git vars
uses: rlespinasse/github-slug-action@v4
- name: Setting up PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: ${{ matrix.extensions }}
ini-values: max_input_vars=5000
coverage: none
- name: Checking out code from ${{ env.GITHUB_REF_SLUG }}
uses: actions/checkout@v4
- name: Setting up PHPUnit
env:
dbtype: ${{ matrix.db }}
run: |
echo "pathtophp=$(which php)" >> $GITHUB_ENV # Inject installed pathtophp to env. The template config needs it.
cp .github/workflows/config-template.php config.php
mkdir ../moodledata
sudo locale-gen en_AU.UTF-8
php public/admin/tool/phpunit/cli/init.php --no-composer-self-update
- name: Running PHPUnit tests
env:
dbtype: ${{ matrix.db }}
phpunit_options: ${{ secrets.phpunit_options }}
run: vendor/bin/phpunit $phpunit_options ${{ inputs.phpunit_extra_options }}
- name: Git status
run: git status
-23
View File
@@ -1,23 +0,0 @@
name: Web Installer Testing
on:
workflow_dispatch:
jobs:
InstallationTest:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
php: [8.3, 8.4]
steps:
- name: Construct repository URL
run: echo "REPOSITORY_URL=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" >> $GITHUB_ENV
shell: bash
- uses: moodlehq/moodle-webinstaller-test@main
with:
repository: ${{ env.REPOSITORY_URL }}
branch: ${{ github.ref_name }}
php: ${{ matrix.php }}
-149
View File
@@ -1,149 +0,0 @@
name: Windows Testing
on:
workflow_dispatch:
inputs:
phpunit_extra_options:
description: Additional options to apply to PHPUnit
required: false
default: ''
env:
php: 8.4
jobs:
Grunt:
runs-on: windows-latest
steps:
- name: Set git to use LF
run: |
git config --global core.autocrlf false
git config --global core.eol lf
- name: Checking out code
uses: actions/checkout@v4
- name: Configuring node & npm
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Installing node stuff
run: npm ci
- name: Running grunt
run: npx grunt
- name: Looking for uncommitted changes
# Add all files to the git index and then run diff --cached to see all changes.
# This ensures that we get the status of all files, including new files.
# We ignore npm-shrinkwrap.json to make the tasks immune to npm changes.
run: |
git add .
git reset -- npm-shrinkwrap.json
git diff --cached --exit-code
PHPUnit:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
php: 8.4
# Ideally we should use mysql/mariadb, but they are 4x slower without tweaks and configuration
# so let's run only postgres (1.5h vs 6h) only, If some day we want to improve the mysql runs,
# this is the place to enable them.
# db: mysqli
db: pgsql
extensions: exif, fileinfo, gd, intl, pgsql, mysql, redis, soap, sodium, zip
- os: windows-latest
php: 8.3
db: pgsql
extensions: exif, fileinfo, gd, intl, pgsql, mysql, redis, soap, sodium, zip
steps:
- name: Setting up DB mysql
if: ${{ matrix.db == 'mysqli' }}
uses: shogo82148/actions-setup-mysql@v1
with:
mysql-version: 8.4
user: test
password: test
- name: Creating DB mysql
if: ${{ matrix.db == 'mysqli' }}
run: mysql --host 127.0.0.1 -utest -ptest -e 'CREATE DATABASE IF NOT EXISTS test COLLATE = utf8mb4_bin;';
- name: Setting up DB pgsql
if: ${{ matrix.db == 'pgsql' }}
run: |
# TODO: Remove these conf. modifications when php74 or php80 are lowest.
# Change to old md5 auth, because php73 does not support it.
# #password_encryption = scram-sha-256
(Get-Content "$env:PGDATA\postgresql.conf"). `
replace('#password_encryption = scram-sha-256', 'password_encryption = md5') | `
Set-Content "$env:PGDATA\postgresql.conf"
(Get-Content "$env:PGDATA\pg_hba.conf"). `
replace('scram-sha-256', 'md5') | `
Set-Content "$env:PGDATA\pg_hba.conf"
$pgService = Get-Service -Name postgresql*
Set-Service -InputObject $pgService -Status running -StartupType automatic
Start-Process -FilePath "$env:PGBIN\pg_isready" -Wait -PassThru
& $env:PGBIN\psql --command="CREATE USER test PASSWORD 'test'" --command="\du"
- name: Creating DB pgsql
if: ${{ matrix.db == 'pgsql' }}
run: |
& $env:PGBIN\createdb --owner=test test
$env:PGPASSWORD = 'test'
& $env:PGBIN\psql --username=test --host=localhost --list test
- name: Configuring git vars
uses: rlespinasse/github-slug-action@v4
- name: Setting up PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: ${{ matrix.extensions }}
ini-values: max_input_vars=5000
coverage: none
- name: Set git to use LF
run: |
git config --global core.autocrlf false
git config --global core.eol lf
- name: Checking out code from ${{ env.GITHUB_REF_SLUG }}
uses: actions/checkout@v4
# Needs to be done after php is available, git configured and Moodle checkout has happened.
- name: Setting up moodle-exttests service
run: |
git clone https://github.com/moodlehq/moodle-exttests.git
nssm install php-built-in C:\tools\php\php.exe -S localhost:8080 -t D:\a\moodle\moodle\moodle-exttests
nssm start php-built-in
- name: Setting up redis service
run: |
choco install redis --version 5.0.7 --no-progress
nssm install redis redis-server
nssm start redis
- name: Setting up PHPUnit
env:
dbtype: ${{ matrix.db }}
shell: bash
run: |
echo "pathtophp=$(which php)" >> $GITHUB_ENV # Inject installed pathtophp to env. The template config needs it.
cp .github/workflows/config-template.php config.php
mkdir ../moodledata
php public/admin/tool/phpunit/cli/init.php --no-composer-self-update
- name: Running PHPUnit tests
env:
dbtype: ${{ matrix.db }}
phpunit_options: ${{ secrets.phpunit_options }}
run: vendor/bin/phpunit $phpunit_options ${{ inputs.phpunit_extra_options }}
-63
View File
@@ -1,63 +0,0 @@
# This file specifies intentionally untracked files that all Moodle git
# repositories should ignore. It is recommended not to modify this file in your
# local clone. Instead, use .git/info/exclude and add new records there as
# needed.
#
# Example: if you deploy a contributed plugin mod/foobar into your site, put
# the following line into .git/info/exclude file in your Moodle clone:
# /mod/foobar/
#
# See gitignore(5) man page for more details
#
# Swap files (vim)
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
# Temporary files including undo
*~
#
/config.php
/tags
/TAGS
/cscope.*
/.patches/
/.idea/
.phpstorm.*
!/.phpstorm.meta.php/
/.phpstorm.meta.php/*
!/.phpstorm.meta.php/di.php
/nbproject/
CVS
.DS_Store
/.settings/
/.project
/.buildpath
/.cache
.phpunit.result.cache
phpunit.xml
# Composer support. Do not ignore composer.json, or composer.lock. These should be shipped by us.
composer.phar
/vendor/
/behat.yml
*/yui/build/*/*-coverage.js
/public/lib/yuilib/*/build/*/*-coverage.js
# lib/yuilib/version/module/module-coverage.js
/public/lib/yuilib/*/*/*-coverage.js
atlassian-ide-plugin.xml
/node_modules/
/.vscode/
moodle-plugin-ci.phar
.eslintignore
.stylelintignore
/jsdoc
/public/admin/tool/componentlibrary/docs
/public/admin/tool/componentlibrary/hugo/site/data/my-index.json
.hugo_build.lock
phpcs.xml
jsconfig.json
UPGRADING-CURRENT.md
.phpunit.cache
tsconfig.aliases.json
-169
View File
@@ -1,169 +0,0 @@
// 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/>.
/**
* This is a babel plugin to add the Moodle module names to the AMD modules
* as part of the transpiling process.
*
* In addition it will also add a return statement for the default export if the
* module is using default exports. This is a highly specific Moodle thing because
* we're transpiling to AMD and none of the existing Babel 7 plugins work correctly.
*
* This will fix the issue where an ES6 module using "export default Foo" will be
* transpiled into an AMD module that returns {default: Foo}; Instead it will now
* just simply return Foo.
*
* Note: This means all other named exports in that module are ignored and won't be
* exported.
*
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
"use strict";
/* eslint-env node */
module.exports = ({template, types}) => {
const fs = require('fs');
const path = require('path');
const cwd = process.cwd();
const ComponentList = require(path.join(process.cwd(), '.grunt', 'components.js'));
/**
* Search the list of components that match the given file name
* and return the Moodle component for that file, if found.
*
* Throw an exception if no matching component is found.
*
* @throws {Error}
* @param {string} searchFileName The file name to look for.
* @return {string} Moodle component
*/
function getModuleNameFromFileName(searchFileName) {
searchFileName = fs.realpathSync(searchFileName);
const relativeFileName = searchFileName.replace(`${cwd}${path.sep}`, '').replace(/\\/g, '/');
const [componentPath, file] = relativeFileName.split('/amd/src/');
const fileName = file.replace('.js', '');
// Check subsystems first which require an exact match.
const componentName = ComponentList.getComponentFromPath(componentPath);
if (componentName) {
return `${componentName}/${fileName}`;
}
// This matches the previous PHP behaviour that would throw an exception
// if it couldn't parse an AMD file.
throw new Error(`Unable to find module name for ${searchFileName} (${componentPath}::${file}}`);
}
/**
* This is heavily inspired by the babel-plugin-add-module-exports plugin.
* See: https://github.com/59naga/babel-plugin-add-module-exports
*
* This is used when we detect a module using "export default Foo;" to make
* sure the transpiled code just returns Foo directly rather than an object
* with the default property (i.e. {default: Foo}).
*
* Note: This means that we can't support modules that combine named exports
* with a default export.
*
* @param {String} path
* @param {String} exportObjectName
*/
function addModuleExportsDefaults(path, exportObjectName) {
const rootPath = path.findParent(path => {
return path.key === 'body' || !path.parentPath;
});
// HACK: `path.node.body.push` instead of path.pushContainer(due doesn't work in Plugin.post).
// This is hardcoded to work specifically with AMD.
rootPath.node.body.push(template(`return ${exportObjectName}.default`)());
}
return {
pre() {
this.seenDefine = false;
this.addedReturnForDefaultExport = false;
},
visitor: {
// Plugin ordering is only respected if we visit the "Program" node.
// See: https://babeljs.io/docs/en/plugins.html#plugin-preset-ordering
//
// We require this to run after the other AMD module transformation so
// let's visit the "Program" node.
Program: {
exit(path) {
path.traverse({
CallExpression(path) {
// If we find a "define" function call.
if (!this.seenDefine && path.get('callee').isIdentifier({name: 'define'})) {
// We only want to modify the first instance of define that we find.
this.seenDefine = true;
// Get the Moodle component for the file being processed.
var moduleName = getModuleNameFromFileName(this.file.opts.filename);
// The function signature of `define()` is:
// define = function (name, deps, callback) {...}
// Ensure that if the moduel supplied its own name that it is replaced.
if (path.node.arguments.length > 0) {
// Ensure that there is only one name.
if (path.node.arguments[0].type === 'StringLiteral') {
// eslint-disable-next-line
console.log(`Replacing module name '${path.node.arguments[0].extra.rawValue}' with ${moduleName}`);
path.node.arguments.shift();
}
}
// Add the module name as the first argument to the define function.
path.node.arguments.unshift(types.stringLiteral(moduleName));
// Add a space after the define function in the built file so that previous versions
// of Moodle will not try to add the module name to the file when it's being served
// by PHP. This forces the regex in PHP to not match for this file.
path.node.callee.name = 'define ';
}
// Check for any Object.defineProperty('exports', 'default') calls.
if (!this.addedReturnForDefaultExport && path.get('callee').matchesPattern('Object.defineProperty')) {
const [identifier, prop] = path.get('arguments');
const objectName = identifier.get('name').node;
const propertyName = prop.get('value').node;
if ((objectName === 'exports' || objectName === '_exports') && propertyName === 'default') {
addModuleExportsDefaults(path, objectName);
this.addedReturnForDefaultExport = true;
}
}
},
AssignmentExpression(path) {
// Check for an exports.default assignments.
if (
!this.addedReturnForDefaultExport &&
(
path.get('left').matchesPattern('exports.default') ||
path.get('left').matchesPattern('_exports.default')
)
) {
const objectName = path.get('left.object.name').node;
addModuleExportsDefaults(path, objectName);
this.addedReturnForDefaultExport = true;
}
}
}, this);
}
}
}
};
};
-455
View File
@@ -1,455 +0,0 @@
// 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/>.
/**
* Helper functions for working with Moodle component names, directories, and sources.
*
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
"use strict";
/* eslint-env node */
/** @var {Object} A list of subsystems in Moodle */
const componentData = {};
/**
* Load details of all moodle modules.
*
* @returns {object}
*/
const fetchComponentData = () => {
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const gruntFilePath = process.cwd();
if (!Object.entries(componentData).length) {
componentData.subsystems = {};
componentData.pathList = [];
componentData.components = {};
componentData.standardComponents = {};
// Fetch the component definiitions from the distributed JSON file.
const components = JSON.parse(fs.readFileSync(`${gruntFilePath}/lib/components.json`));
const pluginData = JSON.parse(fs.readFileSync(`${gruntFilePath}/lib/plugins.json`));
componentData.pluginTypes = components.plugintypes
const standardPlugins = Object.entries(pluginData.standard).map(
([pluginType, pluginNames]) => {
return pluginNames.map(pluginName => `${pluginType}_${pluginName}`);
}
).reduce((acc, val) => acc.concat(val), []);
// Build the list of moodle subsystems.
componentData.subsystems['public/lib'] = 'core';
componentData.pathList.push(`${process.cwd()}/public/lib`);
for (const [component, thisPath] of Object.entries(components.subsystems)) {
if (thisPath) {
// Prefix "core_" to the front of the subsystems.
componentData.subsystems[thisPath] = `core_${component}`;
componentData.pathList.push(`${process.cwd()}/${thisPath}`);
}
}
// The list of components includes the list of subsystems.
componentData.components = Object.fromEntries(
Object.entries(componentData.subsystems)
.map(([path, name]) => ([path, name]))
);
const subpluginAdder = (subpluginType, subpluginTypePath) => {
glob.sync(`${subpluginTypePath}/*/version.php`).forEach(versionPath => {
const componentPath = fs.realpathSync(path.dirname(versionPath));
const componentName = path.basename(componentPath);
const frankenstyleName = `${subpluginType}_${componentName}`;
componentData.components[`${subpluginTypePath}/${componentName}`] = frankenstyleName;
componentData.pathList.push(componentPath);
});
};
// Go through each of the plugintypes.
Object.entries(components.plugintypes).forEach(([pluginType, pluginTypePath]) => {
// We don't allow any code in this place..?
glob.sync(`${pluginTypePath}/*/version.php`).forEach(versionPath => {
const componentPath = fs.realpathSync(path.dirname(versionPath));
const componentName = path.basename(componentPath);
const frankenstyleName = `${pluginType}_${componentName}`;
componentData.components[`${pluginTypePath}/${componentName}`] = frankenstyleName;
componentData.pathList.push(componentPath);
// Look for any subplugins.
const subPluginConfigurationFile = `${componentPath}/db/subplugins.json`;
if (fs.existsSync(subPluginConfigurationFile)) {
const subpluginList = JSON.parse(fs.readFileSync(fs.realpathSync(subPluginConfigurationFile)));
if (subpluginList.subplugintypes) {
Object.entries(subpluginList.subplugintypes).forEach(([subpluginType, subpluginTypePath]) => {
subpluginAdder(
subpluginType,
`${pluginTypePath}/${componentName}/${subpluginTypePath}`
);
});
} else if (subpluginList.plugintypes) {
Object.entries(subpluginList.plugintypes).forEach(([subpluginType, subpluginTypePath]) => {
subpluginAdder(subpluginType, `public/${subpluginTypePath}`);
});
}
}
});
});
// Create a list of the standard subsystem and plugins.
componentData.standardComponents = Object.fromEntries(
Object.entries(componentData.components).filter(([, name]) => {
if (name === 'core' || name.startsWith('core_')) {
return true;
}
return standardPlugins.indexOf(name) !== -1;
})
);
componentData.componentMapping = Object.fromEntries(
Object.entries(componentData.components).map(([path, name]) => [name, path])
);
}
return componentData;
};
/**
* Get the list of component paths.
*
* @param {string} relativeTo
* @returns {array}
*/
const getComponentPaths = (relativeTo = '') => fetchComponentData().pathList.map(componentPath => {
return componentPath.replace(relativeTo, '');
});
/**
* Get the list of paths to build AMD sources.
*
* @returns {Array}
*/
const getAmdSrcGlobList = () => {
const globList = [];
fetchComponentData().pathList.forEach(componentPath => {
globList.push(`${componentPath}/amd/src/*.js`);
globList.push(`${componentPath}/amd/src/**/*.js`);
});
return globList;
};
/**
* Get the list of paths to build YUI sources.
*
* @param {String} relativeTo
* @returns {Array}
*/
const getYuiSrcGlobList = relativeTo => {
const globList = [];
fetchComponentData().pathList.forEach(componentPath => {
const relativeComponentPath = componentPath.replace(relativeTo, '');
globList.push(`${relativeComponentPath}/yui/src/**/*.js`);
});
return globList;
};
/**
* Get the list of paths to thirdpartylibs.xml.
*
* @param {String} relativeTo
* @returns {Array}
*/
const getThirdPartyLibsList = relativeTo => {
const fs = require('fs');
const path = require('path');
const pathList = fetchComponentData().pathList
.map(componentPath => path.relative(relativeTo, componentPath) + '/thirdpartylibs.xml')
.map(componentPath => componentPath.replace(/\\/g, '/'))
.filter(path => fs.existsSync(path));
pathList.push('lib/thirdpartylibs.xml');
return pathList.sort();
};
/**
* Get the list of thirdparty library paths.
*
* @returns {array}
*/
const getThirdPartyPaths = () => {
const DOMParser = require('@xmldom/xmldom').DOMParser;
const fs = require('fs');
const path = require('path');
const xpath = require('xpath');
const thirdpartyfiles = getThirdPartyLibsList(fs.realpathSync('./'));
const libs = ['node_modules/', 'vendor/'];
const addLibToList = lib => {
if (!lib.match('\\*') && fs.statSync(lib).isDirectory()) {
// Ensure trailing slash on dirs.
lib = lib.replace(/\/?$/, '/');
}
// Look for duplicate paths before adding to array.
if (libs.indexOf(lib) === -1) {
libs.push(lib);
}
};
thirdpartyfiles.forEach(function(file) {
const dirname = path.dirname(file);
const xmlContent = fs.readFileSync(file, 'utf8');
const doc = new DOMParser().parseFromString(xmlContent);
const nodes = xpath.select("/libraries/library/location/text()", doc);
nodes.forEach(function(node) {
let lib = path.posix.join(dirname, node.toString());
addLibToList(lib);
});
});
return libs;
};
/**
* Find the name of the component matching the specified path.
*
* @param {String} path
* @returns {String|null} Name of matching component.
*/
const getComponentFromPath = path => {
const componentList = fetchComponentData().components;
if (componentList.hasOwnProperty(path)) {
return componentList[path];
}
return null;
};
/**
* Check whether the supplied path, relative to the Gruntfile.js, is in a known component.
*
* @param {String} checkPath The path to check. This can be with either Windows, or Linux directory separators.
* @returns {String|null}
*/
const getOwningComponentDirectory = checkPath => {
const path = require('path');
// Fetch all components into a reverse sorted array.
// This ensures that components which are within the directory of another component match first.
const pathList = Object.keys(fetchComponentData().components).sort().reverse();
for (const componentPath of pathList) {
// If the componentPath is the directory being checked, it will be empty.
// If the componentPath is a parent of the directory being checked, the relative directory will not start with ..
if (!path.relative(componentPath, checkPath).startsWith('..')) {
return componentPath;
}
}
return null;
};
/**
* Get the latest tag in a remote GitHub repository.
*
* @param {string} url The remote repository.
* @returns {Array}
*/
const getRepositoryTags = async(url) => {
const gtr = require('git-tags-remote');
try {
const tags = await gtr.get(url);
if (tags !== undefined) {
return tags;
}
} catch (error) {
return [];
}
return [];
};
/**
* Get the list of thirdparty libraries that could be upgraded.
*
* @returns {Array}
*/
const getThirdPartyLibsUpgradable = async() => {
const libraries = getThirdPartyLibsData().filter((library) => !!library.repository);
const upgradableLibraries = [];
const versionCompare = (a, b) => {
if (a === b) {
return 0;
}
const aParts = a.split('.');
const bParts = b.split('.');
for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) {
const aPart = parseInt(aParts[i], 10);
const bPart = parseInt(bParts[i], 10);
if (aPart > bPart) {
// 1.1.0 > 1.0.9
return 1;
} else if (aPart < bPart) {
// 1.0.9 < 1.1.0
return -1;
} else {
// Same version.
continue;
}
}
if (aParts.length > bParts.length) {
// 1.0.1 > 1.0
return 1;
}
// 1.0 < 1.0.1
return -1;
};
for (let library of libraries) {
upgradableLibraries.push(
getRepositoryTags(library.repository).then((tagMap) => {
library.version = library.version.replace(/^v/, '');
const currentVersion = library.version.replace(/moodle-/, '');
const currentMajorVersion = library.version.split('.')[0];
const tags = [...tagMap]
.map((tagData) => tagData[0])
.filter((tag) => !tag.match(/(alpha|beta|rc)/))
.map((tag) => tag.replace(/^v/, ''))
.sort((a, b) => versionCompare(b, a));
if (!tags.length) {
library.warning = "Unable to find any comparable tags.";
return library;
}
library.latestVersion = tags[0];
tags.some((tag) => {
if (!tag) {
return false;
}
// See if the version part matches.
const majorVersion = tag.split('.')[0];
if (majorVersion === currentMajorVersion) {
library.latestSameMajorVersion = tag;
return true;
}
return false;
});
if (versionCompare(currentVersion, library.latestVersion) > 0) {
// Moodle somehow has a newer version than the latest version.
library.warning = `Newer version found: ${currentVersion} > ${library.latestVersion} for ${library.name}`;
return library;
}
if (library.version !== library.latestVersion) {
// Delete version and add it again at the end of the array. That way, current and new will stay closer.
delete library.version;
library.version = currentVersion;
return library;
}
return null;
})
);
}
return (await Promise.all(upgradableLibraries)).filter((library) => !!library);
};
/**
* Get the list of paths to build react sources.
*
* @param {string} relativeTo
* @returns {string[]}
*/
const getReactTsSrcGlobList = (relativeTo = '') => {
const globList = [];
fetchComponentData().pathList.forEach(componentPath => {
const relativeComponentPath = componentPath.replace(relativeTo, '');
globList.push(`${relativeComponentPath}/js/esm/src/**/*.ts`);
globList.push(`${relativeComponentPath}/js/esm/src/**/*.tsx`);
});
return globList.map(componentPath => componentPath.replace(/\\/g, '/'));
};
/**
* Get the list of thirdparty libraries.
*
* @returns {Array}
*/
const getThirdPartyLibsData = () => {
const DOMParser = require('@xmldom/xmldom').DOMParser;
const fs = require('fs');
const xpath = require('xpath');
const path = require('path');
const libraryList = [];
const libraryFields = [
'location',
'name',
'version',
'repository',
];
const thirdpartyfiles = getThirdPartyLibsList(fs.realpathSync('./'));
thirdpartyfiles.forEach(function(libraryPath) {
const xmlContent = fs.readFileSync(libraryPath, 'utf8');
const doc = new DOMParser().parseFromString(xmlContent);
const libraries = xpath.select("/libraries/library", doc);
for (const library of libraries) {
const libraryData = [];
for (const field of libraryFields) {
libraryData[field] = xpath.select(`${field}/text()`, library)?.toString();
}
libraryData.location = path.join(path.dirname(libraryPath), libraryData.location);
libraryList.push(libraryData);
}
});
return libraryList.sort((a, b) => a.location.localeCompare(b.location));
};
module.exports = {
fetchComponentData,
getAmdSrcGlobList,
getComponentFromPath,
getComponentPaths,
getOwningComponentDirectory,
getYuiSrcGlobList,
getThirdPartyLibsList,
getThirdPartyPaths,
getThirdPartyLibsUpgradable,
getReactTsSrcGlobList,
};
-19
View File
@@ -1,19 +0,0 @@
# Moodle JavaScript Documentation
```
.-..-.
_____ | || |
/____/-.---_ .---. .---. .-.| || | .---.
| | _ _ |/ _ \/ _ \/ _ || |/ __ \
* | | | | | || |_| || |_| || |_| || || |___/
|_| |_| |_|\_____/\_____/\_____||_|\_____)
Moodle - the world's open source learning platform
```
## About
This generated documentation includes API documentation for JavaScript written in the AMD and ES2015 module formats within Moodle.
## Related information
See [https://moodledev.io](https://moodledev.io) for other related Developer Documentation.
-131
View File
@@ -1,131 +0,0 @@
// 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/>.
/**
* Helper functions for working with Moodle component names, directories, and sources.
*
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
"use strict";
/* eslint-env node */
// Do not include any plugins as stanard.
const plugins = [];
plugins.push('plugins/markdown');
/**
* Get the source configuration.
*
* @return {Object}
*/
const getSource = () => {
const glob = require('glob');
const path = require('path');
const ComponentList = require(path.resolve('.grunt/components.js'));
const thirdPartyPaths = ComponentList.getThirdPartyPaths();
const source = {
include: [],
includePattern: ".+\\.js$",
};
let includeList = [];
ComponentList.getAmdSrcGlobList().forEach(async pattern => {
includeList.push(...glob.sync(pattern));
});
const cwdLength = process.cwd().length + 1;
includeList.forEach(path => {
if (source.include.indexOf(path) !== -1) {
// Ensure no duplicates.
return;
}
const relPath = path.substring(cwdLength);
if (thirdPartyPaths.indexOf(relPath) !== -1) {
return;
}
source.include.push(path);
});
source.include.push('.grunt/jsdoc/README.md');
return source;
};
const tags = {
// Allow the use of unknown tags.
// We have a lot of legacy uses of these.
allowUnknownTags: true,
// We make use of jsdoc and closure dictionaries as standard.
dictionaries: [
'jsdoc',
'closure',
],
};
// Template configuraiton.
const templates = {
cleverLinks: false,
monospaceLinks: false,
};
module.exports = {
opts: {
destination: "./public/jsdoc/",
template: "node_modules/docdash",
},
plugins,
recurseDepth: 10,
source: getSource(),
sourceType: 'module',
tags,
templates,
docdash: {
collapse: true,
search: true,
sort: true,
sectionOrder: [
"Namespaces",
"Modules",
"Events",
"Classes",
"Externals",
"Mixins",
"Tutorials",
"Interfaces"
],
"menu": {
"Developer Docs": {
href: "https://moodledev.io",
target: "_blank",
"class": "menu-item",
id: "devdocs"
},
"MDN Docs": {
href: "https://developer.mozilla.org/en-US/docs/Web/JavaScript",
target: "_blank",
"class": "menu-item",
id: "mdndocs",
},
},
typedefs: true,
},
};
-138
View File
@@ -1,138 +0,0 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
import * as Components from '../../components.js';
const componentData = Components.fetchComponentData();
/**
* The standard components shipped with core Moodle.
*
* @type {Object}
*/
export const standardComponents = componentData.standardComponents;
/**
* All components of the current Moodle instance.
*
* @type {Object}
*/
export const allComponents = componentData.components;
/**
* Get all components of the current Moodle instance.
*
* @returns {Array}
*/
export const getAllComponents = () => {
let components = new Map(Object.entries(componentData.pluginTypes).map(([value, path]) => ([path,{
path,
value,
name: `${value} (plugin type)`,
}])));
Object
.entries(componentData.components)
.filter(([path, value]) => Object.values(componentData.standardComponents).includes(value))
.forEach(([path, value]) => {
const entry = {
path,
value,
name: value,
};
if (Object.values(componentData.subsystems).includes(value)) {
if (components.has(path)) {
entry.name = `${value} (subsystem / plugintype)`;
} else {
entry.name = `${value} (subsystem)`;
}
}
components.set(path, entry);
});
return Array.from(components.values());
};
/**
* Whether the specified component is a standard component shipped with core Moodle.
*
* @param {string} componentName
* @returns {boolean}
*/
export const isStandardComponent = (componentName) => {
if (Object.values(componentData.standardComponents).includes(componentName)) {
return true;
}
if (Object.keys(componentData.pluginTypes).includes(componentName)) {
return true;
}
return false;
};
export const rewritePlugintypeAsSubsystem = (componentName) => {
if (Object.keys(componentData.pluginTypes).includes(componentName)) {
const pluginTypePath = componentData.pluginTypes[componentName];
if (Object.keys(componentData.subsystems).includes(pluginTypePath)) {
return true;
}
}
return false;
};
/**
* Whether the specified component is a community component.
*
* @param {string} componentName
* @returns {boolean}
*/
export const isCommunityComponent = (componentName) => {
if (isStandardComponent(componentName)) {
return false;
}
return Object.values(componentData.components).indexOf(componentName) !== -1;
};
/**
* Sort method for components.
*
* This method sorts components putting `core` first, followed by core subsystems, then everything else.
*
* @param {String} a
* @param {String} b
* @returns {Number}
*/
export const sortComponents = (a, b) => {
// Always put 'core' first.
if (a === 'core') {
return -1;
} else if (b === 'core') {
return 1;
}
// Put core subsystems next.
if (a.startsWith('core_') && !b.startsWith('core_')) {
return -1;
} else if (b.startsWith('core_') && !a.startsWith('core_')) {
return 1;
}
// Sort alphabetically for everything else.
return a.localeCompare(b);
};
-85
View File
@@ -1,85 +0,0 @@
#!/usr/bin/env node
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
import inquirer from 'inquirer';
import chalk from 'chalk';
import { createNote } from './note.mjs';
import { getInitialValues, getIssueNumberFromBranchName } from './helpers.mjs';
import * as Prompts from './prompts.mjs';
import logger from './logger.mjs';
export default async (options) => {
// Processs the initial values.
const initialValues = getInitialValues(options);
// Fetch information.
const messages = [];
const { issueNumber } = await inquirer.prompt([
Prompts.getIssuePrompt(getIssueNumberFromBranchName()),
], initialValues);
let selection = {};
let notePath;
do {
selection = {};
selection = await inquirer.prompt([
Prompts.getComponentsPrompt(),
Prompts.getTypePrompt(),
Prompts.getMessagePromptInput(),
], initialValues);
if (selection.message === '') {
selection = Object.assign(
selection,
await inquirer.prompt([
Prompts.getMessagePromptEditor(),
]),
);
}
logger.info(`
Creating upgrade note with the following options:
- Issue: ${chalk.bold(issueNumber)}
- Component: ${chalk.bold(selection.components)}
- Type: ${chalk.bold(selection.type)}
- Message:
${chalk.bold(selection.message)}
`);
messages.push({
components: [selection.components],
type: selection.type,
message: selection.message,
});
// Save the note so far.
if (notePath) {
await createNote(issueNumber, messages, notePath);
logger.info(`Updated note at: ${chalk.underline(chalk.bold(notePath))}`);
} else {
notePath = await createNote(issueNumber, messages);
logger.info(`Note created at: ${chalk.underline(chalk.bold(notePath))}`);
}
selection = Object.assign(
selection,
await inquirer.prompt([
Prompts.getAddAnotherPrompt(),
], initialValues),
);
} while (selection.addAnother);
};
-357
View File
@@ -1,357 +0,0 @@
#!/usr/bin/env node
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
import chalk from 'chalk';
import { getAllComponents } from './components.mjs';
import { getCombinedNotesByComponent, deleteAllNotes } from './note.mjs';
import { getNoteName } from './noteTypes.mjs';
import { writeFile, readFile, unlink } from 'fs/promises';
import { join as joinPath } from 'path';
import logger from './logger.mjs';
import { getCurrentVersion } from './helpers.mjs';
/**
* Helper to fetch the current notes from a file.
*
* @param {string} file
* @returns {Promise<string>}
*/
const getCurrentNotes = async (file) => {
try {
return await readFile(file, 'utf8');
} catch (error) {
return null;
}
}
/**
* Update the UPGRADING.md file.
*
* @param {string} upgradeNotes
* @param {Object} options
* @param {boolean} options.deleteNotes
* @returns {Promise<void>}
*/
const updateUpgradeNotes = async (upgradeNotes, options) => {
const fileName = 'UPGRADING.md';
// Write the notes to a file.
logger.info(`Writing notes to ${chalk.underline(chalk.bold(fileName))}`);
// Prepend to the existing file.
const existingContent = await getCurrentNotes(fileName);
if (existingContent) {
await writeFile(fileName, getUpdatedNotes(existingContent, upgradeNotes));
} else {
// This should not normally happen.
await writeFile(fileName, upgradeNotes);
}
if (options.deleteNotes) {
logger.warn(`>>> Deleting all notes <<<`)
// Delete the notes.
deleteAllNotes();
}
};
/**
* Create the current summary notes.
*
* @param {string} upgradeNotes
* @returns {Promise<void>}
*/
const createCurrentSummary = async (upgradeNotes) => {
const fileName = 'UPGRADING-CURRENT.md';
const notes = `# Moodle upgrade notes\n\n${upgradeNotes}`;
await writeFile(fileName, notes);
logger.info(`Running upgrade notes written to ${chalk.underline(chalk.bold(fileName))}`);
};
/**
* Get the indexes of the lines that contain the version headings.
*
* @param {array<string>} lines
* @returns {array<object>}
*/
const getVersionLineIndexes = (lines) => {
const h2Indexes = [];
lines.forEach((line, index) => {
const matches = line.match(/^##\s(?<version>.*)$/);
if (matches) {
h2Indexes.push({
index,
line,
version: matches.groups.version,
});
}
});
return h2Indexes;
};
/**
* Find the index of the Unreleased heading.
*
* @param {array<object>} versionHeadings
* @returns {number}
*/
const findUnreleasedHeadingIndex = (versionHeadings) => versionHeadings.findIndex((heading) => {
if (heading.version === 'Unreleased') {
// Used if version cannot be guessed.
return true;
}
if (heading.version.endsWith('+')) {
// Weekly release for a stable branch.
return true;
}
if (heading.version.match(/beta|rc\d/)) {
// Beta and RC rolls are treated as weeklies.
return true;
}
if (heading.version.endsWith('dev')) {
// Development version.
return true;
}
return false;
});
/**
* Get the before and after content, to facilitate replacing any existing Unreleased notes.
*
* @param {array<string>} lines
* @returns {Object} {beforeContent: string, afterContent: string}
*/
const getBeforeAndAfterContent = (lines) => {
const existingLines = lines.split('\n');
const versionHeadings = getVersionLineIndexes(existingLines);
if (versionHeadings.length > 0) {
const unreleasedHeadingIndex = findUnreleasedHeadingIndex(versionHeadings);
if (unreleasedHeadingIndex !== -1) {
const beforeContent = existingLines.slice(0, versionHeadings[unreleasedHeadingIndex].index).join('\n');
if (versionHeadings.length > unreleasedHeadingIndex + 1) {
const afterContent = existingLines.slice(versionHeadings[unreleasedHeadingIndex + 1].index).join('\n');
return {
beforeContent,
afterContent,
};
}
return {
beforeContent,
afterContent: '',
};
}
return {
beforeContent: existingLines.slice(0, versionHeadings[0].index).join('\n'),
afterContent: existingLines.slice(versionHeadings[0].index).join('\n'),
};
}
return {
beforeContent: existingLines.join('\n'),
afterContent: '',
}
};
/**
* Get the notes for the component.
*
* @param {string} types
* @param {Number} headingLevel
* @returns {string}
*/
const getNotesForComponent = (types, headingLevel) => {
let upgradeNotes = '';
Object.entries(types).forEach(([type, notes]) => {
upgradeNotes += '#'.repeat(headingLevel);
upgradeNotes += ` ${getNoteName(type)}\n\n`;
notes.forEach(({ message, issueNumber }) => {
// Split the message into lines, removing empty lines.
const messageLines = message
.split('\n')
// Remove empty lines between tables, and list entries, but not after lists.
.filter((line, index, lines) => {
if (line.trim().length === 0) {
// This line is empty.
// If it's the first line in the file, remove it.
if (index === 0) {
return false;
}
// This is the last line in the file, remove it.
if (index === lines.length - 1) {
return false;
}
// If the previous line relates to a table, remove this line.
if (lines[index - 1].match(/^\s*\|/)) {
return false;
}
// If the next line is also empty, do not remove this line.
if (lines[index + 1].trim().length === 0) {
return true;
}
// Do not remove the line if the previous line was a list item.
if (lines[index - 1].match(/^\s*[-*]\s/)) {
return true;
}
if (lines[index - 1].match(/^\s*\d+\.\s/)) {
return true;
}
// Preserve all other empty lines by default.
return true;
}
// Keep any line which has content.
return true;
});
const firstLine = messageLines.shift().trim();
upgradeNotes += `- ${firstLine}\n`;
messageLines
.forEach((line) => {
upgradeNotes += ` ${line}`.trimEnd() + `\n`;
});
upgradeNotes += `\n For more information see [${issueNumber}](https://tracker.moodle.org/browse/${issueNumber})\n`;
});
upgradeNotes += '\n';
});
return upgradeNotes;
};
/**
* Get the updated notes mixed with existing content.
*
* @param {string} existingContent
* @param {string} upgradeNotes
*/
const getUpdatedNotes = (existingContent, upgradeNotes) => {
const { beforeContent, afterContent } = getBeforeAndAfterContent(existingContent);
const newContent = `${beforeContent}\n${upgradeNotes}\n${afterContent}`
.split('\n')
.filter((line, index, lines) => {
if (line === '' && lines[index - 1] === '') {
// Remove multiple consecutive empty lines.
return false;
}
return true;
})
.join('\n');
return newContent;
};
/**
* Update the notes for each component.
*/
const updateComponentNotes = (
notes,
version,
notesFileName = 'UPGRADING.md',
removeEmpty = false,
) => {
return getAllComponents().map(async (component) => {
logger.verbose(`Updating notes for ${component.name} into ${component.path}`);
const fileName = joinPath(component.path, notesFileName);
const existingContent = await getCurrentNotes(fileName);
if (!existingContent) {
if (!notes[component.value]) {
// No existing notes, and no new notes to add.
return;
}
} else {
if (!notes[component.value]) {
// There is existing content, but nothing to add.
if (removeEmpty) {
logger.verbose(`Removing empty notes file ${fileName}`);
await unlink(fileName);
}
return;
}
}
const componentNotes = notes[component.value];
let upgradeNotes = `## ${version}\n\n`;
upgradeNotes += getNotesForComponent(componentNotes, 3);
if (existingContent) {
await writeFile(fileName, getUpdatedNotes(existingContent, upgradeNotes));
} else {
await writeFile(
fileName,
`# ${component.name} Upgrade notes\n\n${upgradeNotes}`,
);
}
});
}
/**
* Generate the upgrade notes for a new release.
*
* @param {string|undefined} version
* @param {Object} options
* @param {boolean} options.generateUpgradeNotes
* @param {boolean} options.deleteNotes
* @returns {Promise<void>}
*/
export default async (version, options = {}) => {
const notes = await getCombinedNotesByComponent();
if (Object.keys(notes).length === 0) {
logger.warn('No notes to generate');
return;
}
if (!version) {
version = await getCurrentVersion();
}
// Generate the upgrade notes for this release.
// We have
// - a title with the release name
// - the change types
// - which contain the components
// - which document each change
let upgradeNotes = `## ${version}\n\n`;
Object.entries(notes).forEach(([component, types]) => {
upgradeNotes += `### ${component}\n\n`;
upgradeNotes += getNotesForComponent(types, 4);
});
await Promise.all([
createCurrentSummary(upgradeNotes),
...updateComponentNotes(notes, version, 'UPGRADING-CURRENT.md', true),
]);
if (options.generateUpgradeNotes) {
await Promise.all(updateComponentNotes(notes, version));
await updateUpgradeNotes(upgradeNotes, options);
}
};
-244
View File
@@ -1,244 +0,0 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
import chalk from 'chalk';
import branchName from 'current-git-branch';
import { isStandardComponent, isCommunityComponent, rewritePlugintypeAsSubsystem } from './components.mjs';
import { isValidNoteName } from './noteTypes.mjs';
import logger from './logger.mjs';
import { readFile } from 'fs/promises';
/**
* Validate an issue number input
*
* @param {string} input
* @returns {string|boolean}
*/
export const validateIssueNumber = (input) => {
if (!input) {
return 'You must provide a tracker issue number';
}
if (input.match(/^[a-zA-Z]*-\d+$/)) {
return true;
}
if (input.match(/^\d+$/)) {
return true;
}
return 'The issue number was not recognised as a valid issue number';
};
/**
* Format an issue number input.
*
* @param {string} input
* @returns {string}
*/
export const formatIssueNumber = (input) => {
if (input.match(/^[a-zA-Z]*-\d+$/)) {
return input;
}
if (input.match(/^\d+$/)) {
return `MDL-${input}`;
}
return input;
};
/**
* Get the issue number, based off current git branch name
*
* @returns {string|null}
*/
export const getIssueNumberFromBranchName = () => {
const issueNumber = String(branchName()).match(/^[a-zA-Z]*-\d+/);
if (issueNumber) {
return formatIssueNumber(issueNumber[0]);
}
return null;
};
/**
* Validate a component.
*
* @param {string} input
* @returns {string|boolean}
*/
export const validateComponent = (input) => {
if (isStandardComponent(input)) {
return true;
}
if (isCommunityComponent(input)) {
return 'Currently only core plugins are supported.';
}
return 'The component was not recognised as a standard component';
};
export const formatComponent = (input) => {
if (rewritePlugintypeAsSubsystem(input)) {
return `core_${input}`;
}
return input;
}
/**
* Get the initial values from the options.
*
* @param {object} options
* @returns {object}
*/
export const getInitialValues = (options) => {
const initialValues = {};
const type = getInitialTypeValue(options);
if (type) {
initialValues.type = type;
}
const issueNumber = getInitialIssueValue(options);
if (issueNumber) {
initialValues.issueNumber = issueNumber;
}
const component = getInitialComponentValue(options);
if (component) {
initialValues.components = component;
}
const message = getInitialMessageValue(options);
if (message) {
initialValues.message = message
initialValues.addAnother = false;
}
return initialValues;
};
/**
* Get the initial type value.
*
* @param {Object} options
* @returns {string|undefined}
*/
const getInitialTypeValue = (options) => {
if (!options.type) {
return;
}
options.type = options.type.trim().toLowerCase();
if (isValidNoteName(options.type)) {
return options.type;
}
logger.warn(`Note type "${chalk.underline(chalk.red(options.type))}" is not valid.`);
};
/**
* Get the initial issue number value.
*
* @param {Object} options
* @returns {string|undefined}
*/
const getInitialIssueValue = (options) => {
if (!options.issue) {
return;
}
options.issue = options.issue.trim().toUpperCase();
const issueNumberValidated = validateIssueNumber(options.issue);
if (issueNumberValidated === true) {
const issueNumber = formatIssueNumber(options.issue);
if (issueNumber !== options.issue) {
logger.warn(
`Issue number "${chalk.underline(chalk.red(options.issue))}" was updated to ` +
`"${chalk.underline(chalk.green(issueNumber))}"`
);
}
return issueNumber;
} else {
logger.warn(`Issue number "${chalk.underline(chalk.red(options.issue))}" is not valid: ${issueNumberValidated}`);
}
};
/**
* Get the initial component value.
*
* @param {Object} options
* @returns {string|undefined}
*/
const getInitialComponentValue = (options) => {
if (!options.component) {
return;
}
options.component = options.component.trim().toLowerCase();
const componentValidated = validateComponent(options.component);
if (componentValidated === true) {
const component = formatComponent(options.component);
if (component !== options.component) {
logger.warn(
`Component "${chalk.underline(chalk.red(options.component))}" was updated to ` +
`"${chalk.underline(chalk.green(component))}"`
);
}
return component;
} else {
logger.warn(`Component "${chalk.underline(chalk.red(options.component))}" is not valid: ${componentValidated}`);
}
};
/**
* Get the initial message value.
*
* @param {Object} options
* @returns {string|undefined}
*/
const getInitialMessageValue = (options) => {
if (!options.message) {
return;
}
return options.message.trim();
};
/**
* Get the current version from the project /version.php file.
*
* @returns {Promise<string>}
*/
export const getCurrentVersion = async () => {
const versionRegex = new RegExp(/^ *\$release *= *['\"](?<release>[^ \+]+\+?) *\(Build:.*/m);
try {
const versionFile = await readFile('public/version.php', 'utf8');
const match = versionFile.match(versionRegex);
if (match) {
return match.groups.release;
}
} catch(error) {
logger.error('Unable to read the version file');
}
return "Unreleased";
}
-30
View File
@@ -1,30 +0,0 @@
#!/usr/bin/env node
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
transports: [
new winston.transports.Console(),
],
});
export default logger;
-194
View File
@@ -1,194 +0,0 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
import yaml from 'js-yaml';
import path from 'path';
import { writeFile, mkdir, readdir, readFile, unlink } from 'fs/promises';
import { isValidNoteName, sortNoteTypes } from './noteTypes.mjs';
import { sortComponents } from './components.mjs';
const unreleasedPath = path.resolve('.upgradenotes');
/**
* Get the filename for the note.
*
* @param {string} issueNumnber The issue number
* @returns {string}
*/
const getFilename = (issueNumber) => {
const dateTimeFormat = new Intl.DateTimeFormat(undefined, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 2,
timeZone: 'UTC',
});
const date = Object.fromEntries(
dateTimeFormat.formatToParts(new Date())
.filter((p) => p.type !== 'literal')
.map((p) => ([p.type, p.value]))
);
const dateString = [
date.year,
date.month,
date.day,
date.hour,
date.minute,
date.second,
date.fractionalSecond,
].join('');
return `${issueNumber}-${dateString}.yml`;
};
/**
* Create a new note.
*
* @param {string} issueNumber
* @param {[type: string]: {message: string}} messages
* @returns {string} The path to the note on disk
*/
export const createNote = async (
issueNumber,
messages,
notePath,
) => {
const note = {
issueNumber,
notes: {},
};
messages.forEach(({components, type, message}) => {
note.notes[components] = note.notes[components] || [];
note.notes[components].push({message, type});
});
if (!notePath) {
notePath = path.resolve(unreleasedPath, getFilename(issueNumber));
}
const noteContent = yaml.dump(note);
await mkdir(unreleasedPath, {recursive: true});
await writeFile(notePath, noteContent);
return notePath;
};
/**
* Get all unreleased notes.
*
* @returns {Promise<{issueNumber: string, components: string[], types: {[type: string]: {message: string}[]}}[]>
*/
export const getAllNotes = async () => {
const files = await readdir(unreleasedPath);
const notes = files
.filter((file) => file.endsWith('.yml'))
.map(async (file) => {
const filePath = path.resolve(unreleasedPath, file);
const fileContent = await readFile(filePath, 'utf8');
return {
...yaml.load(fileContent),
filePath,
};
});
return Promise.all(notes);
};
/**
* Get the list of notes, grouped by note type, then component.
*
* @returns {Promise<{[type: string]: {[components: string]: {message: string, issueNumber: string}[]}}>}
*/
export const getCombinedNotes = async () => {
const notes = await getAllNotes();
const combinedNotes = {};
notes.forEach((note) => {
Object.entries(note.notes).forEach(([components, data]) => {
data.forEach((entry) => {
if (!isValidNoteName(entry.type)) {
throw new Error(`Invalid note type: "${entry.type}" in file ${note.filePath}`);
}
combinedNotes[entry.type] = combinedNotes[entry.type] || {};
combinedNotes[entry.type][components] = combinedNotes[entry.type][components] || [];
combinedNotes[entry.type][components].push({message: entry.message, issueNumber: note.issueNumber});
});
});
});
return combinedNotes;
};
/**
* Get the list of notes, grouped by component, then by note type.
*
* @returns {Promise<{[component: string]: {[type: string]: {message: string, issueNumber: string}[]}}>}
*/
export const getCombinedNotesByComponent = async () => {
const notes = await getAllNotes();
const combinedNotes = {};
notes.forEach((note) => {
Object.entries(note.notes).forEach(([component, data]) => {
combinedNotes[component] = combinedNotes[component] || {};
data.forEach((entry) => {
if (!isValidNoteName(entry.type)) {
throw new Error(`Invalid note type: "${entry.type}" in file ${note.filePath}`);
}
combinedNotes[component][entry.type] = combinedNotes[component][entry.type] || [];
combinedNotes[component][entry.type].push({
component,
message: entry.message,
issueNumber: note.issueNumber,
type: entry.type,
});
});
});
});
// Sort notes by note type.
Object.entries(combinedNotes).forEach(([component, types]) => {
combinedNotes[component] = Object.fromEntries(
Object.entries(types).sort(([a], [b]) => sortNoteTypes(a, b))
);
});
// Sort components.
return Object.fromEntries(
Object.entries(combinedNotes).sort(([a], [b]) => sortComponents(a, b))
);
};
/**
* Delete all unreleased notes.
*
* @returns {Promise<void>}
*/
export const deleteAllNotes = async () => {
const files = await readdir(unreleasedPath);
return Promise.all(
files
.filter((item, index) => files.indexOf(item) === index)
.filter((file) => file.endsWith('.yml'))
.map((file) => unlink(`${unreleasedPath}/${file}`))
);
};
-80
View File
@@ -1,80 +0,0 @@
// 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/>.
/**
* The note types.
*
* Matches the suggested order in https://keepachangelog.com/en/1.1.0/.
* @type {Object<string, string>}
*/
const noteTypes = {
'improved': 'Added',
'changed': 'Changed',
'deprecated': 'Deprecated',
'removed': 'Removed',
'fixed': 'Fixed',
};
/**
* The preferred order of note types.
*
* @type {string[]}
*/
const preferredOrder = Object.keys(noteTypes);
/**
* Comparison method to sort note types.
*
* @param {String} a
* @param {String} b
* @returns {Number}
*/
export const sortNoteTypes = (a, b) => {
const aIndex = preferredOrder.indexOf(a);
const bIndex = preferredOrder.indexOf(b);
if (aIndex === -1) {
return 1;
}
if (bIndex === -1) {
return -1;
}
return aIndex - bIndex;
};
/**
* Get the note names.
*
* @returns {string[]}
*/
export const getNoteNames = () => Object.keys(noteTypes);
/**
* Get the human-readable note name.
*
* @param {string} type
* @returns {string}
*/
export const getNoteName = (type) => noteTypes[type];
/**
* Whether the note name is valid.
*
* @param {string} type
* @returns {boolean}
*/
export const isValidNoteName = (type) => noteTypes[type] !== undefined;
-153
View File
@@ -1,153 +0,0 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
import inquirer from 'inquirer';
import SearchList from 'inquirer-search-list';
import { getNoteNames } from './noteTypes.mjs';
import { getAllComponents } from './components.mjs';
import {
formatComponent,
formatIssueNumber,
validateComponent,
validateIssueNumber,
} from './helpers.mjs';
/**
* A Search List which accepts an initial value.
*/
class SearchListWithInitialValue extends SearchList {
constructor(options, ...args) {
super(options, ...args);
if (options.default) {
const pointer = this.filterList.findIndex((item) => {
return item.value === options.default;
});
if (pointer > -1) {
this.pointer = pointer;
}
}
}
}
inquirer.registerPrompt('search-list', SearchListWithInitialValue);
/**
* Get the issue type prompt.
*
* @param {string} defaultData The initially selected value
* @returns {Object}
*/
export const getTypePrompt = (defaultData) => ({
default: defaultData,
type: 'search-list',
message: 'Type of change',
name: 'type',
choices: getNoteNames(),
validate: (selection) => {
if (selection.length < 1) {
return 'You must select at least one type of change';
}
return true;
},
});
/**
* Get the component prompt.
*
* @param {string} [defaultValue='core'] The initally selected value.
* @returns
*/
export const getComponentsPrompt = (defaultValue) => {
if (!defaultValue ) {
defaultValue = 'core';
}
return {
choices: getAllComponents(),
default: defaultValue,
type: 'search-list',
message: 'Component',
name: 'components',
validate: validateComponent,
filter: formatComponent,
};
};
/**
* Get the issue number prompt as an inline input.
*
* @param {string} defaultData
* @returns {object}
*/
export const getIssuePrompt = (defaultData) => ({
default: defaultData,
type: 'input',
message: 'Tracker issue number',
name: 'issueNumber',
validate: validateIssueNumber,
filter: formatIssueNumber,
});
/**
* Get a message prompt.
*
* @param {string} defaultData
* @returns
*/
export const getMessagePromptEditor = (defaultData) => ({
default: defaultData,
type: process.stdin.isTTY ? 'editor' : 'input',
postfix: '.md',
message: 'Message',
name: 'message',
waitUserInput: false,
validate: (input) => {
if (!input) {
return 'You must provide a message';
}
return true;
},
// Remove any trailing whitespace.
filter: (input) => input.split('\n').map((line) => line.trimEnd()).join('\n'),
});
/**
* Get a message prompt.
*
* @param {string} defaultData
* @returns
*/
export const getMessagePromptInput = (defaultData) => ({
default: defaultData,
type: 'input',
message: 'Message (leave empty to use editor)',
name: 'message',
filter: (input) => input.trim(),
});
/**
* Get a prompt to ask the user if they wish to add another entry.
*
* @returns {Object}
*/
export const getAddAnotherPrompt = () => ({
type: 'confirm',
message: 'Do you want to add another note?',
default: false,
name: 'addAnother',
});
-58
View File
@@ -1,58 +0,0 @@
#!/usr/bin/env node
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
import { getAllComponents } from './components.mjs';
import { getCombinedNotesByComponent } from './note.mjs';
import logger from './logger.mjs';
import { existsSync } from 'fs';
/**
* Generate links to component-specific upgrade notes for each component.
*
* @param {string|undefined} tag The git tag to link to
* @returns {Promise<void>}
*/
export default async (tag) => {
const notes = await getCombinedNotesByComponent();
if (Object.keys(notes).length === 0) {
logger.warn('No notes to generate');
return;
}
if (!tag) {
logger.error('No tag provided');
return;
}
// Generate the links for the components which have upgrade notes in this release.
let upgradeNotes = `### Component API updates\n<!--cspell: disable -->\n\n`;
const componentList = Object.fromEntries(
Object.values(getAllComponents()).map(({path, value}) => [value, path]),
);
Object.entries(notes)
.map(([component]) => ({
component,
componentPath: componentList[component],
}))
.filter(({ componentPath }) => existsSync(`./${componentPath}/UPGRADING.md`))
.forEach(({component, componentPath}) => {
upgradeNotes += `- [${component}](https://github.com/moodle/moodle/blob/${tag}/${componentPath}/UPGRADING.md)\n`;
});
console.log(upgradeNotes);
};
-142
View File
@@ -1,142 +0,0 @@
// 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/>.
/* jshint node: true, browser: false */
/* eslint-env node */
/**
* Component Library build tasks.
*
* @copyright 2021 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
module.exports = grunt => {
/**
* Get a child path of the component library.
*
* @param {string} path
* @returns {string}
*/
const getCLPath = path => `public/admin/tool/componentlibrary/${path}`;
/**
* Get a spawn handler.
*
* This is a generic function to write the spawn output, and then to exit if required and mark the async task as
* complete.
*
* @param {Promise} done
* @returns {function}
*/
const getSpawnHandler = done => (error, result, code) => {
grunt.log.write(result);
if (error) {
grunt.log.error(result.stdout);
process.exit(code);
}
done();
};
/**
* Spawn a function against Node with the provided args.
*
* @param {array} args
* @returns {object}
*/
const spawnNodeCall = (args) => grunt.util.spawn({
cmd: 'node',
args,
}, getSpawnHandler(grunt.task.current.async()));
/**
* Build the docs using Hugo.
*
* @returns {Object} Reference to the spawned task
*/
const docsBuild = () => spawnNodeCall([
'node_modules/hugo-bin/cli.js',
'--config', getCLPath('config.yml'),
'--cleanDestinationDir',
]);
/**
* Build the docs index using the hugo-lunr-indexer.
*
* @returns {Object} Reference to the spawned task
*/
const indexBuild = () => spawnNodeCall([
'node_modules/hugo-lunr-indexer/bin/hli.js',
'-i', getCLPath('content/**'),
'-o', getCLPath('hugo/site/data/my-index.json'),
'-l', 'yaml',
'-d', '---',
]);
/**
* Build the hugo CSS.
*
* @returns {Object} Reference to the spawned task
*/
const cssBuild = () => spawnNodeCall([
'node_modules/sass/sass.js',
'--style', 'expanded',
'--source-map',
'--embed-sources',
'--precision', 6,
'--load-path', `${process.cwd()}/public`,
getCLPath('hugo/scss/docs.scss'),
getCLPath('hugo/dist/css/docs.css'),
]);
// Register the various component library tasks.
grunt.registerTask('componentlibrary:docsBuild', 'Build the component library', docsBuild);
grunt.registerTask('componentlibrary:cssBuild', 'Build the component library', cssBuild);
grunt.registerTask('componentlibrary:indexBuild', 'Build the component library', indexBuild);
grunt.registerTask('componentlibrary', 'Build the component library', [
'componentlibrary:docsBuild',
'componentlibrary:cssBuild',
'componentlibrary:indexBuild',
]);
grunt.config.merge({
watch: {
componentLibraryDocs: {
files: [
getCLPath('content/**/*.md'),
getCLPath('hugo'),
],
tasks: ['componentlibrary:docsBuild', 'componentlibrary:indexBuild'],
},
componentLibraryCSS: {
files: [
getCLPath('hugo/scss/**/*.scss'),
'hugo',
],
tasks: ['componentlibrary:cssBuild'],
},
},
});
// Add the 'componentlibrary' task as a startup task.
grunt.moodleEnv.startupTasks.push('componentlibrary');
return {
docsBuild,
cssBuild,
indexBuild,
};
};
-72
View File
@@ -1,72 +0,0 @@
// 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/>.
/* jshint node: true, browser: false */
/* eslint-env node */
/**
* @copyright 2021 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
module.exports = grunt => {
const files = grunt.moodleEnv.files;
// Project configuration.
grunt.config.merge({
eslint: {
// Even though warnings dont stop the build we don't display warnings by default because
// at this moment we've got too many core warnings.
// To display warnings call: grunt eslint --show-lint-warnings
// To fail on warnings call: grunt eslint --max-lint-warnings=0
// Also --max-lint-warnings=-1 can be used to display warnings but not fail.
options: {
quiet: (!grunt.option('show-lint-warnings')) && (typeof grunt.option('max-lint-warnings') === 'undefined'),
maxWarnings: ((typeof grunt.option('max-lint-warnings') !== 'undefined') ? grunt.option('max-lint-warnings') : -1)
},
// Check AMD src files.
amd: {src: files ? files : grunt.moodleEnv.amdSrc},
// Check YUI module source files.
yui: {src: files ? files : grunt.moodleEnv.yuiSrc},
// Check React source files.
'react': {
options: {
fix: true,
},
src: files ? files : grunt.moodleEnv.reactSrc,
},
},
});
grunt.loadNpmTasks('grunt-eslint');
// On watch, we dynamically modify config to build only affected files. This
// method is slightly complicated to deal with multiple changed files at once (copied
// from the grunt-contrib-watch readme).
let changedFiles = Object.create(null);
const onChange = grunt.util._.debounce(function() {
const files = Object.keys(changedFiles);
grunt.config('eslint.amd.src', files);
grunt.config('eslint.yui.src', files);
changedFiles = Object.create(null);
}, 200);
grunt.event.on('watch', (action, filepath) => {
changedFiles[filepath] = action;
onChange();
});
};
-92
View File
@@ -1,92 +0,0 @@
// 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/>.
/* jshint node: true, browser: false */
/* eslint-env node */
/**
* @copyright 2021 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
module.exports = grunt => {
/**
* Get the list of feature files to pass to the gherkin linter.
*
* @returns {Array}
*/
const getGherkinLintTargets = () => {
if (grunt.moodleEnv.files) {
// Specific files were requested. Only check these.
return grunt.moodleEnv.files;
}
if (grunt.moodleEnv.inComponent) {
return [`${grunt.moodleEnv.runDir}/tests/behat/*.feature`];
}
return ['**/tests/behat/*.feature'];
};
const handler = function() {
const done = this.async();
const options = grunt.config('gherkinlint.options');
// Grab the gherkin-lint linter and required scaffolding.
const linter = require('gherkin-lint/dist/linter.js');
const featureFinder = require('gherkin-lint/dist/feature-finder.js');
const configParser = require('gherkin-lint/dist/config-parser.js');
const formatter = require('gherkin-lint/dist/formatters/stylish.js');
// Run the linter.
return linter.lint(
featureFinder.getFeatureFiles(grunt.file.expand(options.files)),
configParser.getConfiguration(configParser.defaultConfigFileName)
)
.then(results => {
// Print the results out uncondtionally.
formatter.printResults(results);
return results;
})
.then(results => {
// Report on the results.
// The done function takes a bool whereby a falsey statement causes the task to fail.
return results.every(result => result.errors.length === 0);
})
.then(done); // eslint-disable-line promise/no-callback-in-promise
};
grunt.registerTask('gherkinlint', 'Run gherkinlint against the current directory', handler);
grunt.config.set('gherkinlint', {
options: {
files: getGherkinLintTargets(),
}
});
grunt.config.merge({
watch: {
gherkinlint: {
files: [grunt.moodleEnv.inComponent ? 'tests/behat/*.feature' : '**/tests/behat/*.feature'],
tasks: ['gherkinlint'],
},
},
});
// Add the 'gherkinlint' task as a startup task.
grunt.moodleEnv.startupTasks.push('gherkinlint');
return handler;
};
-139
View File
@@ -1,139 +0,0 @@
// 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/>.
/* jshint node: true, browser: false */
/* eslint-env node */
/**
* @copyright 2021 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
module.exports = grunt => {
/**
* Generate the PHPCS configuration.
*
* @param {Object} thirdPartyPaths
*/
const phpcsIgnore = (thirdPartyPaths) => {
const {toXML} = require('jstoxml');
const config = {
_name: 'ruleset',
_attrs: {
name: "MoodleCore",
},
_content: [
{
rule: {
_attrs: {
ref: './phpcs.xml.dist',
},
},
},
],
};
thirdPartyPaths.forEach(library => {
config._content.push({
'exclude-pattern': library,
});
});
grunt.file.write('phpcs.xml', toXML(config, {
header: true,
indent: ' ',
}) + "\n");
};
/**
* Extracts ignore entries from a local ignore file.
*
* @param {string} componentPath the file path to the component, relative to the code base directory
* @param {string} ignoreFilePath the path to the ignore file
* @return {array} array of ignore paths to be included in the global ignore files
*/
const getEntriesFromLocalIgnoreFile = (componentPath, ignoreFilePath) => {
const ignorePaths = [];
if (grunt.file.exists(ignoreFilePath)) {
const ignoreFile = grunt.file.read(ignoreFilePath);
const entries = ignoreFile.split('\n');
entries.forEach(entry => {
entry = entry.trim();
if (entry.length > 0 && !entry.startsWith('#') && !entry.startsWith('!')) {
while (entry.startsWith('/')) {
entry = entry.substring(1);
}
ignorePaths.push(componentPath + '/' + entry);
}
});
}
return ignorePaths;
};
/**
* Generate ignore files (utilising thirdpartylibs.xml data)
*/
const handler = function() {
const path = require('path');
const ComponentList = require(path.join(process.cwd(), '.grunt', 'components.js'));
// An array of paths to third party directories.
const thirdPartyPaths = ComponentList.getThirdPartyPaths();
const localStylelintIgnorePaths = [];
const localEslintIgnorePaths = [];
ComponentList.getComponentPaths(process.cwd() + '/').forEach(componentPath => {
const localEslintIgnorePath = process.cwd() + '/' + componentPath + '/.eslintignore';
const localEslintIgnoreEntries = getEntriesFromLocalIgnoreFile(componentPath, localEslintIgnorePath);
if (localEslintIgnoreEntries.length > 0) {
localEslintIgnorePaths.push(...localEslintIgnoreEntries);
}
const localStylelintIgnorePath = process.cwd() + '/' + componentPath + '/.stylelintignore';
const localStylelintIgnoreEntries = getEntriesFromLocalIgnoreFile(componentPath, localStylelintIgnorePath);
if (localStylelintIgnoreEntries.length > 0) {
localStylelintIgnorePaths.push(...localStylelintIgnoreEntries);
}
});
// Generate .eslintignore.
const eslintIgnores = [
'# Generated by "grunt ignorefiles"',
// Do not ignore the .grunt directory.
'!/.grunt',
// Ignore all yui/src meta directories and build directories.
'*/**/yui/src/*/meta/',
'*/**/build/',
].concat(thirdPartyPaths).concat(localEslintIgnorePaths);
grunt.file.write('.eslintignore', eslintIgnores.join('\n') + '\n');
// Generate .stylelintignore.
const stylelintIgnores = [
'# Generated by "grunt ignorefiles"',
'**/yui/build/*',
'public/theme/boost/style/moodle.css',
'public/theme/classic/style/moodle.css',
'jsdoc/styles/*.css',
'public/admin/tool/componentlibrary/hugo/dist/css/docs.css',
].concat(thirdPartyPaths).concat(localStylelintIgnorePaths);
grunt.file.write('.stylelintignore', stylelintIgnores.join('\n') + '\n');
phpcsIgnore(thirdPartyPaths);
};
grunt.registerTask('ignorefiles', 'Generate ignore files for linters', handler);
return handler;
};
-218
View File
@@ -1,218 +0,0 @@
// 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/>.
/* jshint node: true, browser: false */
/* eslint-env node */
/**
* @copyright 2021 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Function to generate the destination for the minification task
* (e.g. build/file.min.js). This function will be passed to
* the rename property of files array when building dynamically:
* http://gruntjs.com/configuring-tasks#building-the-files-object-dynamically
*
* @param {String} destPath the current destination
* @param {String} srcPath the matched src path
* @return {String} The rewritten destination path.
*/
const babelRename = function(destPath, srcPath) {
destPath = srcPath.replace(`amd/src`, `amd/build`);
destPath = destPath.replace(/\.js$/, '.min.js');
return destPath;
};
module.exports = grunt => {
// Load the React build tasks.
require('./react')(grunt);
// Load the Ignorefiles tasks.
require('./ignorefiles')(grunt);
// Load the Shifter tasks.
require('./shifter')(grunt);
// Load ESLint.
require('./eslint')(grunt);
// Load jsconfig.
require('./jsconfig')(grunt);
// Load JSDoc.
require('./jsdoc')(grunt);
const path = require('path');
// Register JS tasks.
grunt.registerTask('yui', ['eslint:yui', 'shifter']);
grunt.registerTask('amd', ['ignorefiles', 'eslint:amd', 'rollup']);
grunt.registerTask('js', ['amd', 'yui', 'react']);
// Register NPM tasks.
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-rollup');
const babelTransform = require('@babel/core').transform;
const babel = (options = {}) => {
return {
name: 'babel',
transform: (code, id) => {
grunt.log.debug(`Transforming ${id}`);
options.filename = id;
const transformed = babelTransform(code, options);
return {
code: transformed.code,
map: transformed.map
};
}
};
};
// Note: We have to use a rate limit plugin here because rollup runs all tasks asynchronously and in parallel.
// When we kick off a full run, if we kick off a rollup of every file this will fork-bomb the machine.
// To work around this we use a concurrent Promise queue based on the number of available processors.
const rateLimit = () => {
const queue = [];
let queueRunner;
const startQueue = () => {
if (queueRunner) {
return;
}
queueRunner = setTimeout(() => {
const limit = Math.max(1, require('os').cpus().length / 2);
grunt.log.debug(`Starting rollup with queue size of ${limit}`);
runQueue(limit);
}, 100);
};
// The queue runner will run the next `size` items in the queue.
const runQueue = (size = 1) => {
queue.splice(0, size).forEach(resolve => {
grunt.log.debug(`Item resolved. Kicking off next one.`);
resolve();
});
};
return {
name: 'ratelimit',
// The options hook is run in parallel.
// We can return an unresolved Promise which is queued for later resolution.
options: async(options) => {
return new Promise(resolve => {
queue.push(resolve);
startQueue();
return options;
});
},
// When an item in the queue completes, start the next item in the queue.
generateBundle: (options, bundle) => {
grunt.log.debug(`Finished output phase for ${Object.keys(bundle).join(', ')}`);
runQueue();
},
};
};
const terser = require('rollup-plugin-terser').terser;
grunt.config.merge({
rollup: {
options: {
format: 'esm',
dir: 'output',
sourcemap: true,
treeshake: false,
context: 'window',
plugins: [
rateLimit(),
babel({
sourceMaps: true,
comments: false,
compact: false,
plugins: [
'transform-es2015-modules-amd-lazy',
'system-import-transformer',
// This plugin modifies the Babel transpiling for "export default"
// so that if it's used then only the exported value is returned
// by the generated AMD module.
//
// It also adds the Moodle plugin name to the AMD module definition
// so that it can be imported as expected in other modules.
path.resolve('.grunt/babel-plugin-add-module-to-define.js')
],
presets: [
['@babel/preset-env', {
modules: false,
useBuiltIns: false
}]
]
}),
terser({
// Do not mangle variables.
// Makes debugging easier.
mangle: false,
}),
],
},
dist: {
files: [{
expand: true,
src: grunt.moodleEnv.files ? grunt.moodleEnv.files : grunt.moodleEnv.amdSrc,
rename: babelRename
}],
},
},
});
grunt.config.merge({
watch: {
amd: {
files: grunt.moodleEnv.inComponent
? ['amd/src/*.js', 'amd/src/**/*.js']
: ['**/amd/src/**/*.js'],
tasks: ['amd']
},
},
});
// Add the 'js' task as a startup task.
grunt.moodleEnv.startupTasks.push('js');
// On watch, we dynamically modify config to build only affected files. This
// method is slightly complicated to deal with multiple changed files at once (copied
// from the grunt-contrib-watch readme).
let changedFiles = Object.create(null);
const onChange = grunt.util._.debounce(function() {
const files = Object.keys(changedFiles);
grunt.config('rollup.dist.files', [{expand: true, src: files, rename: babelRename}]);
changedFiles = Object.create(null);
}, 200);
grunt.event.on('watch', function(action, filepath) {
changedFiles[filepath] = action;
onChange();
});
return {
babelRename,
};
};
-83
View File
@@ -1,83 +0,0 @@
// 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/>.
/* jshint node: true, browser: false */
/* eslint-env node */
// @ts-nocheck
/**
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const configuration = {
compilerOptions: {
baseUrl: ".",
paths: {
},
target: "es2015",
allowSyntheticDefaultImports: false,
},
exclude: [
"node_modules",
],
include: [],
};
/**
* Generate jsconfig.json with AMD module path aliases for all components.
*
* @param {object} grunt The Grunt instance.
*/
const generateJsconfig = (grunt) => {
const jsconfigData = Object.assign({}, configuration);
const path = require('path');
const {fetchComponentData} = require(path.join(process.cwd(), '.grunt', 'components.js'));
const componentData = fetchComponentData().components;
for (const [thisPath, component] of Object.entries(componentData)) {
jsconfigData.compilerOptions.paths[`${component}/*`] = [`${thisPath}/amd/src/*`];
jsconfigData.include.push(`${thisPath}/amd/src/**/*`);
}
grunt.file.write('jsconfig.json', JSON.stringify(jsconfigData, null, " ") + "\n");
grunt.log.write('✓ Generating jsconfig.json\n');
};
/**
* Generate tsconfig.aliases.json with TypeScript path aliases for all ESM components.
*
* @returns {Promise<void>}
*/
const generateTsAliases = async() => {
const {generateAliases} = await import('../../.esbuild/generate-aliases.mjs');
generateAliases();
};
module.exports = (grunt) => {
const handler = async function() {
const done = this.async();
try {
generateJsconfig(grunt);
await generateTsAliases();
done();
} catch (err) {
grunt.log.error(err.message);
done(false);
}
};
grunt.registerTask('jsconfig', 'Generate a jsconfig configuration compatible with the LSP', handler);
};
-52
View File
@@ -1,52 +0,0 @@
// 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/>.
/* jshint node: true, browser: false */
/* eslint-env node */
/**
* @copyright 2021 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
module.exports = (grunt) => {
const path = require('path');
grunt.registerTask('jsdoc', 'Generate JavaScript documentation using jsdoc', function() {
const done = this.async();
const configuration = path.resolve('.grunt/jsdoc/jsdoc.conf.js');
grunt.util.spawn({
cmd: 'jsdoc',
args: [
'--configure',
configuration,
]
}, function(error, result, code) {
if (result.stdout) {
grunt.log.write(result.stdout);
}
if (result.stderr) {
grunt.log.error(result.stderr);
}
if (error) {
grunt.fail.fatal(`JSDoc failed with error code ${code}`);
} else {
grunt.log.write('JSDoc completed successfully'.green);
}
done();
});
});
};
-111
View File
@@ -1,111 +0,0 @@
// 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/>.
/* jshint node: true, browser: false */
/* eslint-env node */
// @ts-nocheck
/**
* Grunt tasks for building React components.
*
* @copyright Meirza <meirza.arson@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
module.exports = grunt => {
/**
* Register react task — build or watch React components.
*
* Modes:
* grunt react — production build
* grunt react:dev — development build (sourcemaps, no minification)
* grunt react:watch — esbuild native watch (dev mode, incremental context)
*
* Note: react:watch uses esbuild's own context.watch() and is intentionally
* separate from grunt-contrib-watch. This keeps esbuild's incremental build
* graph alive between rebuilds rather than restarting from scratch on each change.
*/
grunt.registerTask('react', 'Build all React components', function(mode) {
const done = this.async();
const isWatch = mode === 'watch';
const isDev = isWatch || mode === 'dev';
if (isWatch) {
const path = require('path');
const {spawn} = require('child_process');
// Run ESLint on the rebuilt source files in check-only mode (no --fix)
// to avoid writing changes that would re-trigger esbuild.
const eslintBin = path.join(grunt.moodleEnv.gruntFilePath, 'node_modules', '.bin', 'eslint');
const onRebuild = (srcFiles) => {
if (srcFiles.length === 0) {
return;
}
const absSrcFiles = srcFiles.map(f => path.join(grunt.moodleEnv.gruntFilePath, f));
spawn(eslintBin, absSrcFiles, {stdio: 'inherit'})
.on('error', err => grunt.log.error(`ESLint: ${err.message}`));
};
(async() => {
try {
const {watchComponents} = await import('../../.esbuild/plugin/plugincomponents.mjs');
const { generateAliases } = await import('../../.esbuild/generate-aliases.mjs');
const { buildPluginComponents } = await import('../../.esbuild/plugin/plugincomponents.mjs');
generateAliases();
await buildPluginComponents(isDev);
const ctx = await watchComponents(true, onRebuild);
if (!ctx) {
grunt.log.warn('No React source files found. Nothing to watch.');
done();
return;
}
grunt.log.ok('esbuild is watching for React changes. Press Ctrl+C to stop.');
// Keep the process alive until the user interrupts. done() is intentionally
// not called here — grunt's async mechanism holds the process open.
process.on('SIGINT', async() => {
await ctx.dispose();
done();
});
} catch (err) {
grunt.log.error(err.message);
done(false);
}
})();
return;
}
grunt.log.writeln(`Building React components in ${isDev ? 'DEVELOPMENT' : 'PRODUCTION'} mode...`);
(async() => {
try {
const {generateAliases} = await import('../../.esbuild/generate-aliases.mjs');
const {buildPluginComponents} = await import('../../.esbuild/plugin/plugincomponents.mjs');
generateAliases();
await buildPluginComponents(isDev);
done();
} catch (err) {
grunt.log.error(err.message);
done(false);
}
})();
});
};
-43
View File
@@ -1,43 +0,0 @@
// 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/>.
/* jshint node: true, browser: false */
/* eslint-env node */
/**
* @copyright 2021 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
module.exports = grunt => {
grunt.loadNpmTasks('grunt-sass');
grunt.config.merge({
sass: {
dist: {
files: {
"public/theme/boost/style/moodle.css": "public/theme/boost/scss/preset/default.scss",
"public/theme/classic/style/moodle.css": "public/theme/classic/scss/classicgrunt.scss"
}
},
options: {
implementation: require('sass'),
includePaths: [
"public/theme/boost/scss/",
"public/theme/classic/scss/",
]
}
},
});
};
-155
View File
@@ -1,155 +0,0 @@
// 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/>.
/* jshint node: true, browser: false */
/* eslint-env node */
/**
* @copyright 2021 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/* eslint-env node */
module.exports = grunt => {
/**
* Shifter task. Is configured with a path to a specific file or a directory,
* in the case of a specific file it will work out the right module to be built.
*
* Note that this task runs the invidiaul shifter jobs async (becase it spawns
* so be careful to to call done().
*/
const handler = function() {
const done = this.async();
const options = grunt.config('shifter.options');
const async = require('async');
const path = require('path');
// Run the shifter processes one at a time to avoid confusing output.
async.eachSeries(options.paths, function(src, filedone) {
var args = [];
args.push(path.normalize(process.cwd() + '/node_modules/shifter/bin/shifter'));
// Always ignore the node_modules directory.
args.push('--excludes', 'node_modules');
// Determine the most appropriate options to run with based upon the current location.
if (grunt.file.isMatch('**/yui/**/*.js', src)) {
// When passed a JS file, build our containing module (this happen with
// watch).
grunt.log.debug('Shifter passed a specific JS file');
src = path.dirname(path.dirname(src));
options.recursive = false;
} else if (grunt.file.isMatch('**/yui/src', src)) {
// When in a src directory --walk all modules.
grunt.log.debug('In a src directory');
args.push('--walk');
options.recursive = false;
} else if (grunt.file.isMatch('**/yui/src/*', src)) {
// When in module, only build our module.
grunt.log.debug('In a module directory');
options.recursive = false;
} else if (grunt.file.isMatch('**/yui/src/*/js', src)) {
// When in module src, only build our module.
grunt.log.debug('In a source directory');
src = path.dirname(src);
options.recursive = false;
}
if (grunt.option('watch')) {
grunt.fail.fatal('The --watch option has been removed, please use `grunt watch` instead');
}
// Add the stderr option if appropriate
if (grunt.option('verbose')) {
args.push('--lint-stderr');
}
if (grunt.option('no-color')) {
args.push('--color=false');
}
var execShifter = function() {
grunt.log.ok("Running shifter on " + src);
grunt.util.spawn({
cmd: "node",
args: args,
opts: {cwd: src, stdio: 'inherit', env: process.env}
}, function(error, result, code) {
if (code) {
grunt.fail.fatal('Shifter failed with code: ' + code);
} else {
grunt.log.ok('Shifter build complete.');
filedone();
}
});
};
// Actually run shifter.
if (!options.recursive) {
execShifter();
} else {
// Check that there are yui modules otherwise shifter ends with exit code 1.
if (grunt.file.expand({cwd: src}, '**/yui/src/**/*.js').length > 0) {
args.push('--recursive');
execShifter();
} else {
grunt.log.ok('No YUI modules to build.');
filedone();
}
}
}, done);
};
// Register the shifter task.
grunt.registerTask('shifter', 'Run Shifter against the current directory', handler);
// Configure it.
grunt.config.set('shifter', {
options: {
recursive: true,
// Shifter takes a relative path.
paths: grunt.moodleEnv.files ? grunt.moodleEnv.files : [grunt.moodleEnv.runDir]
}
});
grunt.config.merge({
watch: {
yui: {
files: grunt.moodleEnv.inComponent
? ['yui/src/*.json', 'yui/src/**/*.js']
: ['**/yui/src/**/*.js'],
tasks: ['yui']
},
},
});
// On watch, we dynamically modify config to build only affected files. This
// method is slightly complicated to deal with multiple changed files at once (copied
// from the grunt-contrib-watch readme).
let changedFiles = Object.create(null);
const onChange = grunt.util._.debounce(function() {
const files = Object.keys(changedFiles);
grunt.config('shifter.options.paths', files);
changedFiles = Object.create(null);
}, 200);
grunt.event.on('watch', (action, filepath) => {
changedFiles[filepath] = action;
onChange();
});
return handler;
};
-49
View File
@@ -1,49 +0,0 @@
// 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/>.
/* jshint node: true, browser: false */
/* eslint-env node */
/**
* @copyright 2021 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
module.exports = grunt => {
/**
* Generate ignore files (utilising thirdpartylibs.xml data)
*/
const handler = function() {
const path = require('path');
// Are we in a YUI directory?
if (path.basename(path.resolve(grunt.moodleEnv.cwd, '../../')) == 'yui') {
grunt.task.run('yui');
// Are we in an AMD directory?
} else if (grunt.moodleEnv.inAMD) {
grunt.task.run('amd');
// Are we in an ESM source directory?
} else if (grunt.moodleEnv.inEsm) {
grunt.task.run('react');
} else {
// Run all of the requested startup tasks.
grunt.moodleEnv.startupTasks.forEach(taskName => grunt.task.run(taskName));
}
};
// Register the startup task.
grunt.registerTask('startup', 'Run the correct tasks for the current directory', handler);
return handler;
};
-35
View File
@@ -1,35 +0,0 @@
// 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/>.
/* jshint node: true, browser: false */
/* eslint-env node */
/**
* @copyright 2021 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
module.exports = grunt => {
// Load the ignorefiles tasks.
require('./ignorefiles')(grunt);
// Load the Style Lint tasks.
require('./stylelint')(grunt);
// Load the SASS tasks.
require('./sass')(grunt);
// Add the 'css' task as a startup task.
grunt.moodleEnv.startupTasks.push('css');
};
-187
View File
@@ -1,187 +0,0 @@
// 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/>.
/* jshint node: true, browser: false */
/* eslint-env node */
/**
* @copyright 2021 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
module.exports = grunt => {
const getCssConfigForFiles = files => {
return {
stylelint: {
css: {
// Use a fully-qualified path.
src: files,
options: {
quietDeprecationWarnings: true,
configOverrides: {
rules: {
// These rules have to be disabled in .stylelintrc for scss compat.
"at-rule-no-unknown": true,
}
}
}
},
},
};
};
const getScssConfigForFiles = files => {
return {
stylelint: {
scss: {
options: {
quietDeprecationWarnings: true,
customSyntax: 'postcss-scss',
},
src: files,
},
},
};
};
/**
* Register any stylelint tasks.
*
* @param {Object} grunt
* @param {Array} files
* @param {String} fullRunDir
*/
const registerStyleLintTasks = () => {
const glob = require('glob');
// The stylelinters do not handle the case where a configuration was provided but no files were included.
// Keep track of whether any files were found.
let hasCss = false;
let hasScss = false;
// The stylelint processors do not take a path argument. They always check all provided values.
// As a result we must check through each glob and determine if any files match the current directory.
const scssFiles = [];
const cssFiles = [];
const requestedFiles = grunt.moodleEnv.files;
if (requestedFiles) {
// Grunt was called with a files argument.
// Check whether each of the requested files matches either the CSS or SCSS source file list.
requestedFiles.forEach(changedFilePath => {
let matchesGlob;
// Check whether this watched path matches any watched SCSS file.
matchesGlob = grunt.moodleEnv.scssSrc.some(watchedPathGlob => {
return glob.sync(watchedPathGlob).indexOf(changedFilePath) !== -1;
});
if (matchesGlob) {
scssFiles.push(changedFilePath);
hasScss = true;
}
// Check whether this watched path matches any watched CSS file.
matchesGlob = grunt.moodleEnv.cssSrc.some(watchedPathGlob => {
return glob.sync(watchedPathGlob).indexOf(changedFilePath) !== -1;
});
if (matchesGlob) {
cssFiles.push(changedFilePath);
hasCss = true;
}
});
} else {
// Grunt was called without a list of files.
// The start directory (runDir) may be a child dir of the project.
// Check each scssSrc file to see if it's in the start directory.
// This means that we can lint just mod/*/styles.css if started in the mod directory.
grunt.moodleEnv.scssSrc.forEach(path => {
if (path.startsWith(grunt.moodleEnv.runDir)) {
scssFiles.push(path);
hasScss = true;
}
});
grunt.moodleEnv.cssSrc.forEach(path => {
if (path.startsWith(grunt.moodleEnv.runDir)) {
cssFiles.push(path);
hasCss = true;
}
});
}
// Register the tasks.
const scssTasks = ['sass'];
if (hasScss) {
grunt.config.merge(getScssConfigForFiles(scssFiles));
scssTasks.unshift('stylelint:scss');
}
scssTasks.unshift('ignorefiles');
const cssTasks = ['ignorefiles'];
if (hasCss) {
grunt.config.merge(getCssConfigForFiles(cssFiles));
cssTasks.push('stylelint:css');
}
// The tasks must be registered, even if empty to ensure a consistent command list.
// They jsut won't run anything.
grunt.registerTask('scss', scssTasks);
grunt.registerTask('rawcss', cssTasks);
};
// Register CSS tasks.
grunt.loadNpmTasks('grunt-stylelint');
// Register the style lint tasks.
registerStyleLintTasks();
grunt.registerTask('css', ['scss', 'rawcss']);
const getCoreThemeMatches = () => {
const scssMatch = 'scss/**/*.scss';
if (grunt.moodleEnv.inTheme) {
return [scssMatch];
}
if (grunt.moodleEnv.runDir.startsWith('theme')) {
return [`*/${scssMatch}`];
}
return [`theme/*/${scssMatch}`];
};
// Add the watch configuration for rawcss, and scss.
grunt.config.merge({
watch: {
rawcss: {
files: [
'**/*.css',
],
excludes: [
'**/moodle.css',
'**/editor.css',
'jsdoc/styles/*.css',
],
tasks: ['rawcss']
},
scss: {
files: getCoreThemeMatches(),
tasks: ['scss']
},
},
});
};
-42
View File
@@ -1,42 +0,0 @@
// 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/>.
/* jshint node: true, browser: false */
/* eslint-env node */
/**
* @copyright 2023 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
module.exports = grunt => {
/**
* Generate upgradable third-party libraries (utilising thirdpartylibs.xml data)
*/
grunt.registerTask('upgradablelibs', 'Generate upgradable third-party libraries', async function() {
const done = this.async();
const path = require('path');
const ComponentList = require(path.join(process.cwd(), '.grunt', 'components.js'));
// An array of third party libraries that have a newer version in their repositories.
const thirdPartyLibs = await ComponentList.getThirdPartyLibsUpgradable({progress: true});
for (let library of thirdPartyLibs) {
grunt.log.ok(JSON.stringify(Object.assign({}, library), null, 4));
}
done();
});
};
-276
View File
@@ -1,276 +0,0 @@
/**
* This is a wrapper task to handle the grunt watch command. It attempts to use
* Watchman to monitor for file changes, if it's installed, because it's much faster.
*
* If Watchman isn't installed then it falls back to the grunt-contrib-watch file
* watcher for backwards compatibility.
*/
/* eslint-env node */
module.exports = grunt => {
/**
* This is a wrapper task to handle the grunt watch command. It attempts to use
* Watchman to monitor for file changes, if it's installed, because it's much faster.
*
* If Watchman isn't installed then it falls back to the grunt-contrib-watch file
* watcher for backwards compatibility.
*/
const watchHandler = function() {
const async = require('async');
const watchTaskDone = this.async();
let watchInitialised = false;
let watchTaskQueue = {};
let processingQueue = false;
const watchman = require('fb-watchman');
const watchmanClient = new watchman.Client();
// Grab the tasks and files that have been queued up and execute them.
var processWatchTaskQueue = function() {
if (!Object.keys(watchTaskQueue).length || processingQueue) {
// If there is nothing in the queue or we're already processing then wait.
return;
}
processingQueue = true;
// Grab all tasks currently in the queue.
var queueToProcess = watchTaskQueue;
// Reset the queue.
watchTaskQueue = {};
async.forEachSeries(
Object.keys(queueToProcess),
function(task, next) {
var files = queueToProcess[task];
var filesOption = '--files=' + files.join(',');
grunt.log.ok('Running task ' + task + ' for files ' + filesOption);
// Spawn the task in a child process so that it doesn't kill this one
// if it failed.
var args = [task, filesOption];
if (grunt.option('force')) {
args.push('--force');
}
grunt.util.spawn(
{
// Spawn with the grunt bin.
grunt: true,
// Run from current working dir and inherit stdio from process.
opts: {
cwd: grunt.moodleEnv.fullRunDir,
stdio: 'inherit'
},
args: args
},
function(err, res, code) {
if (code !== 0) {
// The grunt task failed.
grunt.log.error(err);
}
// Move on to the next task.
next();
}
);
},
function() {
// No longer processing.
processingQueue = false;
// Once all of the tasks are done then recurse just in case more tasks
// were queued while we were processing.
processWatchTaskQueue();
}
);
};
const originalWatchConfig = grunt.config.get(['watch']);
const watchConfig = Object.keys(originalWatchConfig).reduce(function(carry, key) {
if (key == 'options') {
return carry;
}
const value = originalWatchConfig[key];
const taskNames = value.tasks;
const files = value.files;
let excludes = [];
if (value.excludes) {
excludes = value.excludes;
}
taskNames.forEach(function(taskName) {
carry[taskName] = {
files,
excludes,
};
});
return carry;
}, {});
watchmanClient.on('error', function(error) {
// We have to add an error handler here and parse the error string because the
// example way from the docs to check if Watchman is installed doesn't actually work!!
// See: https://github.com/facebook/watchman/issues/509
if (error.message.match('Watchman was not found')) {
// If watchman isn't installed then we should fallback to the other watch task.
grunt.log.ok('It is recommended that you install Watchman for better performance using the "watch" command.');
// Fallback to the old grunt-contrib-watch task.
grunt.renameTask('watch-grunt', 'watch');
grunt.task.run(['watch']);
// This task is finished.
watchTaskDone(0);
} else {
grunt.log.error(error);
// Fatal error.
watchTaskDone(1);
}
});
watchmanClient.on('subscription', function(resp) {
if (resp.subscription !== 'grunt-watch') {
return;
}
resp.files.forEach(function(file) {
grunt.log.ok('File changed: ' + file.name);
var fullPath = grunt.moodleEnv.fullRunDir + '/' + file.name;
Object.keys(watchConfig).forEach(function(task) {
const fileGlobs = watchConfig[task].files;
var match = fileGlobs.some(function(fileGlob) {
return grunt.file.isMatch(`**/${fileGlob}`, fullPath);
});
if (match) {
// If we are watching a subdirectory then the file.name will be relative
// to that directory. However the grunt tasks expect the file paths to be
// relative to the Gruntfile.js location so let's normalise them before
// adding them to the queue.
var relativePath = fullPath.replace(grunt.moodleEnv.gruntFilePath + '/', '');
if (task in watchTaskQueue) {
if (!watchTaskQueue[task].includes(relativePath)) {
watchTaskQueue[task] = watchTaskQueue[task].concat(relativePath);
}
} else {
watchTaskQueue[task] = [relativePath];
}
}
});
});
processWatchTaskQueue();
});
process.on('SIGINT', function() {
// Let the user know that they may need to manually stop the Watchman daemon if they
// no longer want it running.
if (watchInitialised) {
grunt.log.ok('The Watchman daemon may still be running and may need to be stopped manually.');
}
process.exit();
});
// Initiate the watch on the current directory.
watchmanClient.command(['watch-project', grunt.moodleEnv.fullRunDir], function(watchError, watchResponse) {
if (watchError) {
grunt.log.error('Error initiating watch:', watchError);
watchTaskDone(1);
return;
}
if ('warning' in watchResponse) {
grunt.log.error('warning: ', watchResponse.warning);
}
var watch = watchResponse.watch;
var relativePath = watchResponse.relative_path;
watchInitialised = true;
watchmanClient.command(['clock', watch], function(clockError, clockResponse) {
if (clockError) {
grunt.log.error('Failed to query clock:', clockError);
watchTaskDone(1);
return;
}
// Generate the expression query used by watchman.
// Documentation is limited, but see https://facebook.github.io/watchman/docs/expr/allof.html for examples.
// We generate an expression to match any value in the files list of all of our tasks, but excluding
// all value in the excludes list of that task.
//
// [anyof, [
// [allof, [
// [anyof, [
// ['match', validPath, 'wholename'],
// ['match', validPath, 'wholename'],
// ],
// [not,
// [anyof, [
// ['match', invalidPath, 'wholename'],
// ['match', invalidPath, 'wholename'],
// ],
// ],
// ],
var matchWholeName = fileGlob => ['match', fileGlob, 'wholename'];
var matches = Object.keys(watchConfig).map(function(task) {
const matchAll = [];
matchAll.push(['anyof'].concat(watchConfig[task].files.map(matchWholeName)));
if (watchConfig[task].excludes.length) {
matchAll.push(['not', ['anyof'].concat(watchConfig[task].excludes.map(matchWholeName))]);
}
return ['allof'].concat(matchAll);
});
matches = ['anyof'].concat(matches);
var sub = {
expression: matches,
// Which fields we're interested in.
fields: ["name", "size", "type"],
// Add our time constraint.
since: clockResponse.clock
};
if (relativePath) {
/* eslint-disable camelcase */
sub.relative_root = relativePath;
}
watchmanClient.command(['subscribe', watch, 'grunt-watch', sub], function(subscribeError) {
if (subscribeError) {
// Probably an error in the subscription criteria.
grunt.log.error('failed to subscribe: ', subscribeError);
watchTaskDone(1);
return;
}
grunt.log.ok('Listening for changes to files in ' + grunt.moodleEnv.fullRunDir);
});
});
});
};
// Rename the grunt-contrib-watch "watch" task because we're going to wrap it.
grunt.renameTask('watch', 'watch-grunt');
// Register the new watch handler.
grunt.registerTask('watch', 'Run tasks on file changes', watchHandler);
grunt.config.merge({
watch: {
options: {
nospawn: true // We need not to spawn so config can be changed dynamically.
},
},
});
return watchHandler;
};
-111
View File
@@ -1,111 +0,0 @@
#!/usr/bin/env node
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
import { Argument, Option, program } from 'commander';
import chalk from 'chalk';
import { getNoteNames } from './notes/src/noteTypes.mjs';
import createAction from './notes/src/create.mjs';
import generateAction from './notes/src/generate.mjs';
import generateReleaseNotesLinks from './notes/src/releasenotes.mjs';
import logger from './notes/src/logger.mjs';
console.log(`
${chalk.bold(chalk.underline(chalk.green('Moodle Upgrade Notes Generator')))}
This tool is used to generate the upgrade notes for changes you make in Moodle.
Please remember that the intended audience of these changes is
${chalk.italic('plugin developers')} who need to know how to update their plugins
for a new Moodle version.
Upgrade notes should not be used to document changes for site administrators, or
for internal API changes which are not expected to be used outside of the
relevant component.
`)
program.configureHelp({
helpWidth: 100,
});
program.on('option:verbose', () => {
logger.level = 'verbose';
});
program.addOption(
new Option(
'-v, --verbose',
'Output more information during the generation process',
)
.default(false)
);
// Define the command line options.
program
.command('create')
.summary('Generate a new upgrade note')
.addOption(
new Option('-t, --type <type>', `The type of change to document. Valid types are: ${getNoteNames().join(', ')}`)
)
.addOption(new Option('-i, --issue <issue>', 'The tracker issue number'))
.addOption(new Option('-c, --component <component>', 'The component to write a note for'))
.addOption(new Option(
'-m, --message <message>',
'The message to use for the upgrade note',
))
.action((options) => createAction(options));
program
.command('summary')
.summary('Generate a local copy of the upgrade notes summary')
.addArgument(
new Argument('[version]', 'The Moodle version to create the summary notes for')
)
.action((version) => generateAction(version));
program
.command('releasenotes')
.summary('Generate a list of upgrade notes for individual components in markdown format for the release notes')
.addArgument(
new Argument('[tag]', 'The Moodle tag')
)
.action((tag) => generateReleaseNotesLinks(tag));
program
.command('release')
.summary('Generate the markdown copies of the upgrade notes for a Moodle release')
.addArgument(
new Argument('[version]', 'The Moodle version to create the release notes for')
)
.addOption(
new Option(
'--generate-upgrade-notes',
'Generate the UPGRADING.md notes for the release. ' +
'Note: This option is intended for use by the release manager when generating the upgrade notes.',
)
.default(true)
)
.addOption(
new Option(
'-d, --delete-notes',
'Delete the notes after generating the UPGRADING.md notes for the release. ' +
'Note: This option is intended for use by the release manager when generating the upgrade notes.' +
'This option has no effect unless --generate-upgrade-notes is also set.'
)
.default(false)
)
.action((version, options) => generateAction(version, options));
program.parse();
-2
View File
@@ -1,2 +0,0 @@
**/amd/**
/*.js
-58
View File
@@ -1,58 +0,0 @@
// NOTE: We use eslint now. This file is used only by shifter. We keep the configuration
// here because shifter uses jshint after modules have been concating. Eslint can't
// currently do this.
{
"asi": false,
"bitwise": true,
"boss": false,
"browser": true,
"curly": true,
"debug": false,
"eqeqeq": false,
"eqnull": false,
"es5": false,
"esnext": false,
"evil": false,
"expr": false,
"forin": false,
"funcscope": false,
"globalstrict": false,
"immed": true,
"indent": 4,
"iterator": false,
"lastsemic": false,
"latedef": true,
"laxbreak": true,
"laxcomma": false,
"loopfunc": false,
"maxerr": 500,
"maxlen": 132,
"multistr": false,
"newcap": true,
"noarg": true,
"noempty": true,
"nomen": false,
"onecase": false,
"onevar": false,
"passfail": false,
"plusplus": false,
"predef": [
"M",
"define",
"require"
],
"proto": false,
"regexdash": false,
"regexp": false,
"scripturl": false,
"shadow": false,
"smarttabs": false,
"strict": false,
"sub": false,
"supernew": false,
"trailing": true,
"undef": true,
"unused": true,
"white": false,
"yui": true
}
-1
View File
@@ -1 +0,0 @@
lts/jod
-40
View File
@@ -1,40 +0,0 @@
<?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/>.
/**
* Helper file for PhpStorm, and other IDEs, to provide better code completion.
*
* @package core
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace PHPSTORM_META;
// Assume that anything returned by \core\di::get('...') is an instance of the first argument.
override(\core\di::get(0), map([
'' => '@',
]));
// Assume that anything returned by \Psr\Container\ContainerInterface::get('...') is an instance of the first argument.
override(\Psr\Container\ContainerInterface::get(0), map([
'' => '@',
]));
// Assume that anything returned by \DI\Container::get('...') is an instance of the first argument.
override(\DI\Container::get(0), map([
'' => '@',
]));
-5
View File
@@ -1,5 +0,0 @@
{
"coverage": false,
"lint": "config",
"clean": true
}
-107
View File
@@ -1,107 +0,0 @@
{
"customSyntax": "postcss-scss",
"plugins": [
"stylelint-csstree-validator"
],
"rules": {
"csstree/validator": {
"syntaxExtensions": [
"sass"
]
},
"at-rule-empty-line-before": [ "always",
{"except": [ "blockless-after-blockless"], ignore: ["after-comment", "inside-block"]}
],
"at-rule-name-case": "lower",
"at-rule-name-space-after": "always-single-line",
"at-rule-no-unknown": null, # Enabled for non-scss in grunt.
"at-rule-semicolon-newline-after": "always",
"at-rule-semicolon-space-before": "never",
"block-closing-brace-newline-after": "always",
"block-closing-brace-newline-before": "always",
"block-closing-brace-space-before": "always-single-line",
"block-no-empty": true,
"block-opening-brace-newline-after": "always",
"block-opening-brace-space-after": "always-single-line",
"block-opening-brace-space-before": "always",
"color-hex-case": ["lower", { "severity": "warning" }],
"color-hex-length": ["short", { "severity": "warning" }],
"color-no-invalid-hex": true,
"declaration-bang-space-after": "never",
"declaration-bang-space-before": "always",
"declaration-block-no-duplicate-properties": true,
"declaration-block-no-shorthand-property-overrides": true,
"declaration-block-semicolon-newline-after": "always-multi-line",
"declaration-block-semicolon-space-after": "always-single-line",
"declaration-block-semicolon-space-before": "never",
"declaration-block-single-line-max-declarations": 1,
"declaration-block-trailing-semicolon": "always",
"declaration-colon-newline-after": "always-multi-line",
"declaration-colon-space-after": "always-single-line",
"declaration-colon-space-before": "never",
"declaration-no-important": true,
"font-family-no-duplicate-names": true,
"function-calc-no-unspaced-operator": true,
"function-comma-newline-after": "always-multi-line",
"function-comma-space-after": "always-single-line",
"function-comma-space-before": "never",
"function-linear-gradient-no-nonstandard-direction": true,
"function-max-empty-lines": 0,
"function-name-case": "lower",
"function-parentheses-newline-inside": "always-multi-line",
"function-parentheses-space-inside": "never-single-line",
"function-url-scheme-disallowed-list": ["data"],
"function-whitespace-after": "always",
"indentation": 4,
"keyframe-declaration-no-important": true,
"length-zero-no-unit": [true, { "severity": "warning" }],
"max-empty-lines": 2,
"max-line-length": [132, { "severity": "warning" }],
"media-feature-colon-space-after": "always",
"media-feature-colon-space-before": "never",
"media-feature-parentheses-space-inside": "never",
"media-feature-range-operator-space-after": "always",
"media-feature-range-operator-space-before": "always",
"media-query-list-comma-newline-after": "always-multi-line",
"media-query-list-comma-space-after": "always-single-line",
"media-query-list-comma-space-before": "never",
"no-empty-source": true,
"no-eol-whitespace": true,
"no-extra-semicolons": [true, { "severity": "warning" }],
"no-invalid-double-slash-comments": true,
"no-unknown-animations": true,
"property-case": "lower",
"property-no-unknown": true,
"selector-attribute-brackets-space-inside": "never",
"selector-attribute-operator-space-after": "never",
"selector-attribute-operator-space-before": "never",
"selector-combinator-space-after": "always",
"selector-combinator-space-before": "always",
"selector-list-comma-newline-after": "always",
"selector-list-comma-space-before": "never",
"selector-max-empty-lines": 0,
"selector-pseudo-class-case": "lower",
"selector-pseudo-class-no-unknown": true,
"selector-pseudo-class-parentheses-space-inside": "never",
"selector-pseudo-element-case": "lower",
"selector-pseudo-element-no-unknown": true,
"selector-type-case": "lower",
"selector-type-no-unknown": true,
"string-no-newline": true,
"time-min-milliseconds": 100,
"unit-disallowed-list": ["pt"],
"unit-case": "lower",
"unit-no-unknown": true,
"value-keyword-case": ["lower", {"ignoreKeywords": ["/(@|$)/"]}],
"value-list-comma-newline-after": "always-multi-line",
"value-list-comma-space-after": "always-single-line",
"value-list-comma-space-before": "never",
},
"overrides": [
{
"files": ["**/yui/**/*.css"],
"rules": {
}
}
]
}
@@ -1,7 +0,0 @@
issueNumber: MDL-51152
notes:
enrol_manual:
- message: >-
The unused parameter 'roleid' has been removed from the external
function `unenrol_users()`
type: removed
@@ -1,22 +0,0 @@
issueNumber: MDL-71368
notes:
core:
- message: >
The `arg_separator.output` has been changed from a default of `amp;` to
a default of `&` in line with PHP defaults.
If you were previously relying on the legacy default when using
`http_build_query()` then you should _actively_ specify the value as the
third
parameter, for example:
```
http_build_query($formdata, '', '&amp;');
```
type: changed
@@ -1,8 +0,0 @@
issueNumber: MDL-73602
notes:
core:
- message: >-
Removed `qtype_random` from core.
`core\component::is_valid_plugin_name` has an additional check to ensure no-one can create a new plugin called qtype_random,
as this would conflict with the support for restoring old backups.
type: removed
@@ -1,8 +0,0 @@
issueNumber: MDL-75067
notes:
tool_behat:
- message: >-
The `behat_session_trait::ensure_element_[does_not_]exists(...)` methods
now accept optional `$container` parameter to define the parent node to
look within
type: improved
@@ -1,5 +0,0 @@
issueNumber: MDL-77894
notes:
core:
- message: Appending an exclamation mark to template names ignores theme overrides
type: improved
@@ -1,10 +0,0 @@
issueNumber: MDL-78325
notes:
core_grades:
- message: >-
In Moodle 4.2, the legacy Gradebook base widget from 4.1 has been removed and
replaced with a simpler class-based system due to a breaking change and
excessive complexity in the old pattern. The files `core/grades/basewidget.js` and
templates in `grade/templates/searchwidget/` have been deleted,
with minimal expected third-party impact.
type: removed
@@ -1,13 +0,0 @@
issueNumber: MDL-79182
notes:
core:
- message: >
The following AMD modules have been removed following the final
deprecation process. It is no longer possible or necessary to manually
register modal types. See MDL-78324 for further details.
- `core/modal_registry`
- `core/modal_factory`
type: removed
@@ -1,7 +0,0 @@
issueNumber: MDL-79574
notes:
core:
- message: >-
The `\core\persistent::get_records(...)` class method now returns
instances keyed by record ID
type: changed
@@ -1,13 +0,0 @@
issueNumber: MDL-81514
notes:
core_group:
- message: >
`groups_print_activity_menu()` and `groups_get_activity_group()` now
include an additional `$participationonly` parameter, which is true by
default. This can be set false when we want the user to be able to
select a non-participation group within an activity, for example if a
teacher wants to filter assignment submissions by non-participation
groups. It should never be used when the menu is displayed to students,
as this may allow them to participate using non-participation groups.
Non-participation groups are labeled as such.
type: improved
@@ -1,8 +0,0 @@
issueNumber: MDL-81608
notes:
mod_quiz:
- message: >-
The language strings `addpagebreak` and `removepagebreak` have been
deprecated and should no longer be used. These have been replaced by
the `addpagebreakafter` and `removepagebreakafter` language strings.
type: deprecated
@@ -1,12 +0,0 @@
issueNumber: MDL-81961
notes:
core_question:
- message: |
core_question:
- message: |
Final deprecation of `moodle-question-chooser` YUI module.
type: removed
- message: |
Final deprecation of `core_question/qbank_chooser` template.
type: removed
type: removed
-13
View File
@@ -1,13 +0,0 @@
issueNumber: MDL-81962
notes:
core:
- message: The `moodle-core-notification-confirm` YUI module has been removed after an extensive deprecation period. Please use the AMD `core/modal` module as a replacement.
type: removed
- message: |
The following feaetures of the `moodle-core-notification` YUI module have been removed after an extensive deprecation process.:
- method: `keyDelegation`
- property: `lightbox`
There is no direct replacement for these features, but they were not widely used and their removal should not cause significant issues. If you were using these features, you will need to implement your own solutions for key delegation and lightbox functionality.
type: removed
@@ -1,7 +0,0 @@
issueNumber: MDL-82284
notes:
core:
- message: >-
Legacy constructors have been removed. These relate to PHP 4 and
earlier.
type: removed
@@ -1,15 +0,0 @@
issueNumber: MDL-83902
notes:
core_badges:
- message: >-
The following classes and functions are now also available in the following new
locations. They will continue to work in their old locations:
| Old classname/function | New classname/function |
| --- | --- |
| `\badge_award_selector_base` | `\core_badges\award_selector_base` |
| `\badge_potential_users_selector` | `\core_badges\potential_award_selector` |
| `\badge_existing_users_selector` | `\core_badges\existing_award_selector` |
| `process_manual_award()` | `\core_badges\award_manager::process_manual_award()` |
| `process_manual_revoke()` | `\core_badges\award_manager::process_manual_revoke()` |
type: improved
@@ -1,7 +0,0 @@
issueNumber: MDL-84082
notes:
core_reportbuilder:
- message: >-
The text filter "Contains" and "Not contains" operators now support `*`
and `?` wildcard characters for better text content filtering
type: improved
@@ -1,30 +0,0 @@
issueNumber: MDL-84960
notes:
core:
- message: >-
Several core web services now include a new initials field in user data
structures. This change is backward-compatible and only adds an optional
field; no existing fields or field semantics have been changed.
Client applications should ensure they can handle the additional initials
field in web service responses, but clients that ignore unknown fields can
continue working without changes. The affected web services are:
- core_enrol_get_enrolled_users
- core_enrol_get_enrolled_users_with_capability
- core_enrol_get_potential_users
- core_enrol_search_users
- core_user_get_users_by_field
- core_user_get_users
- core_user_get_course_user_profiles
- core_grades_get_enrolled_users_for_selector
- core_grades_get_gradable_users
- gradereport_grader_get_users_in_report
- core_message_get_contact_requests
- core_message_get_conversation_members
- core_message_message_search_users
- core_message_get_user_contacts
- core_message_get_member_info
- core_message_get_conversation_messages
- mod_assign_list_participants
- mod_assign_get_participant
- mod_forum_get_forum_discussions
type: improved
@@ -1,9 +0,0 @@
issueNumber: MDL-85233
notes:
core_filters:
- message: >-
MimeTeX support has been removed from both `filter_tex` and
`filter_algebra`. These filters now depend on LaTeX tools (`latex`,
`dvips`, and `convert`/`dvisvgm`), and as a result, the `pathmimetex`
setting has been removed.
type: removed
@@ -1,8 +0,0 @@
issueNumber: MDL-85322
notes:
block_html:
- message: >-
Treat Dashboard (pagetype 'my-index') as trusted in web services so
get_content_for_external preserves embedded HTML (e.g. iframes) on user
Dashboard.
type: changed
@@ -1,8 +0,0 @@
issueNumber: MDL-85336
notes:
core:
- message: >-
Redis connection timeout settings for cachestores and sessions have been
split into connection timeout and read timeout to allow for finer
control. These settings now also accept floats.
type: improved
@@ -1,9 +0,0 @@
issueNumber: MDL-85405
notes:
core_course:
- message: >-
The external function `core_course_get_course_contents` now includes the
`candisplay` property for each returned module. If this is false, the
module should not be displayed on the course page (for example, for
question banks).
type: improved
@@ -1,5 +0,0 @@
issueNumber: MDL-85621
notes:
core_badges:
- message: The create_issued_badge generator now returns the issued badge object.
type: changed
@@ -1,11 +0,0 @@
issueNumber: MDL-85621
notes:
core_badges:
- message: >-
The class core_badges_assertion has been deprecated and replaced by
\core_badges\achievement_credential.
The method badges_get_default_issuer() has also been deprecated because
it is no longer needed.
The file badges/endorsement.php has been removed because it stopped being used
when MDL-84323 was integrated.
type: deprecated
@@ -1,17 +0,0 @@
issueNumber: MDL-85975
notes:
core:
- message: >
`restore_qtype_plugin::unset_excluded_fields` now returns the modified
questiondata structure,
in order to support structures that contain arrays.
If your qtype plugin overrides
`restore_qtype_plugin::remove_excluded_question_data` without
calling the parent method, you may need to modify your overridden method
to use the returned
value.
type: fixed
@@ -1,8 +0,0 @@
issueNumber: MDL-86332
notes:
core_completion:
- message: >-
The `completion_info::clear_criteria` method takes an optional
`$removetypecriteria` to determine whether to remove course type
criteria from other courses that refer to the current course
type: changed
@@ -1,26 +0,0 @@
issueNumber: MDL-86493
notes:
mod_quiz:
- message: >-
The quiz overrides cache implementation has been replaced with a faster alternative with a different API.
This should be a transparent change but any direct references will still need to be updated.
type: deprecated
- message: >
`mod_quiz_cm_info_dynamic()` now uses the new `quiz_overrides` cache via `override_manager`, performing a single cache fetch per quiz/user.
This significantly reduces cache calls on course pages with many quizzes and groups.
The new `mod_quiz:quiz_overrides` cache is keyed by `quizid_userid` using datasource `\mod_quiz\cache\quiz_overrides_cache`.
This cache returns all applicable overrides for a user in a quiz (the user override, if any, plus all group overrides for groups they belong to in the quiz's course).
New class `\mod_quiz\local\quiz_overrides_cache_manager` to interact with the cache:
- `get_overrides(int $quizid, int $userid): array`
- `purge_for_user(int $quizid, int $userid): void`
- `purge_for_users(int $quizid, array $userids): void`
- `purge_for_group(int $quizid, int $groupid): void`
- `purge_for_group_members(int $groupid, array $userids): void`
Hook callbacks in `db/hooks.php` to keep the cache in sync with group
membership changes:
- `\core_group\hook\after_group_membership_added`
- `\core_group\hook\after_group_membership_removed`
type: improved
@@ -1,12 +0,0 @@
issueNumber: MDL-86524
notes:
core_question:
- message: >-
During restore of a question_set_reference, mapping of IDs in the
filtercondition is now delegated to qbank plugins.
If your qbank plugin defines a filter condition that uses database
IDs, add an override of `restore_filtercondition()` to the `condition`
class, which checks the condition's data and replaces the IDs with
mapped values if required. See
`qbank_managecategories\category_condition` for an example.
type: improved
@@ -1,7 +0,0 @@
issueNumber: MDL-86607
notes:
mod_feedback:
- message: >-
The method `feedback_init_feedback_session()` has been deprecated, along
with all other direct access to `$SESSION` from the module
type: deprecated
@@ -1,7 +0,0 @@
issueNumber: MDL-86660
notes:
core_courseformat:
- message: >-
The `$cm` attribute in `activityoverviewbase` has been updated to public
visibility, allowing direct access to the course module instance
type: changed
@@ -1,8 +0,0 @@
issueNumber: MDL-86660
notes:
core_courseformat:
- message: >-
A new `available` attribute has been added to `activityname_exporter`
class. It allows the external API to return the activity's availability
status relative to the current user.
type: changed
@@ -1,9 +0,0 @@
issueNumber: MDL-86660
notes:
core_courseformat:
- message: >-
Two new public static methods have been added to the `overviewtable`
class:
- `is_cm_displayable`: Determines if a course module should be listed in the overview table.
- `is_cm_available`: Checks if a course module is accessible to the user (and should therefore be rendered as a link).
type: changed
@@ -1,19 +0,0 @@
issueNumber: MDL-86678
notes:
core_reportbuilder:
- message: >-
The base entity class now implements a default `initialise` method, that
will automatically call each of the following methods to load entity
report data:
* `get_available_columns()`
* `get_available_filters()`
* `get_available_conditions()`
This change allows for a lot of boilerplate to be removed from report
entity classes
type: improved
@@ -1,12 +0,0 @@
issueNumber: MDL-86699
notes:
core_reportbuilder:
- message: >-
There are two new entities intended for reports specific to course
module data, in order to provide a baseline in terms of module reporting
and API usage:
*
`core_course\reportbuilder\local\entities\{course_module,course_module_base}`
type: improved
@@ -1,7 +0,0 @@
issueNumber: MDL-86699
notes:
mod_forum:
- message: >-
The forum report entity `->get_context_joins()` method is deprecated,
replaced with `->get_course_modules_joins(...)`
type: deprecated
@@ -1,17 +0,0 @@
issueNumber: MDL-86798
notes:
core_question:
- message: >
In order to prevent re-use of question version numbers after a version
is deleted, the `nextversion` column was added to
`question_bank_entries`. This serves as a counter incremented each time
a version is created.
Do not query this field directly. Instead use
`core_question\versions::get_next_version()` to read the value, which
will initialise it based on the existing versions if it is not set yet.
By default, it will increment the version number automatically, unless
you pass `increment: false`. Because of this, it is advisable to call
it inside a transaction, that is only committed after the version number
is used in a `question_versions` record.
type: fixed
@@ -1,7 +0,0 @@
issueNumber: MDL-86798
notes:
core_question:
- message: >-
`get_next_version()` from questionlib.php is now deprecated. Use
`\core_question\versions::get_next_version()` instead.
type: deprecated
@@ -1,5 +0,0 @@
issueNumber: MDL-86832
notes:
core_block:
- message: Removed block_activity_modules from Moodle 5.2.
type: removed
@@ -1,10 +0,0 @@
issueNumber: MDL-86839
notes:
core:
- message: >
"grunt watch" now accepts a force flag. Run "grunt watch -f" or "grunt
watch --force" to prevent grunt from cancelling builds when errors
occur. This is especially useful during development, because js build
files will be built even if, for example, jslint errors are still
present in the files.
type: improved
@@ -1,8 +0,0 @@
issueNumber: MDL-86846
notes:
core:
- message: >-
The `core/drag_handle` template has been modified to use a native HTML
button for a more accessible experience and a consistent look with other
buttons on the page.
type: changed
@@ -1,7 +0,0 @@
issueNumber: MDL-86854
notes:
core_courseformat:
- message: >-
Add core_courseformat\cmactions::move_before that will allow to move a coursemodule to a position before another coursemodule.
Add core_courseformat\cmactions::move_end_section that will allow to move a coursemodule the end of a section.
type: improved
@@ -1,8 +0,0 @@
issueNumber: MDL-86854
notes:
core_course:
- message: >-
Deprecates moveto_module (core_course) in favor of
cmactions::move_before or cmactions::move_end_section
(core_courseformat\local\cmactions).
type: deprecated
@@ -1,11 +0,0 @@
issueNumber: MDL-86855
notes:
qbank_columnsortorder:
- message: >-
The Behat selector `column move handle` for the `qbank_columnsortorder` plugin has been removed.
When interacting with the column's move handle, please use the move handle's accessible name and type.
For example:
- `And I drag "Move Created by" "button" and I drop it in "Move T" "button"`
type: removed
@@ -1,9 +0,0 @@
issueNumber: MDL-86856
notes:
core_course:
- message: >-
The following methods have been deprecated and should no longer be used:
- `course_delete_module`
- `course_module_flag_for_async_deletion`
Please consider using the equivalent methods, delete and delete_async, in `core_courseformat\local\cmactions` instead.
type: deprecated
@@ -1,5 +0,0 @@
issueNumber: MDL-86856
notes:
core_courseformat:
- message: Add delete method to the core_courseformat\cmactions
type: improved
@@ -1,7 +0,0 @@
issueNumber: MDL-86857
notes:
core_courseformat:
- message: >-
Add set_groupmode method to the core_courseformat\cmactions (course
format actions)
type: improved
@@ -1,7 +0,0 @@
issueNumber: MDL-86857
notes:
core_course:
- message: >-
Deprecates set_coursemodule_groupmode in favor of
core_courseformat\cmactions::set_groupmode
type: deprecated
@@ -1,7 +0,0 @@
issueNumber: MDL-86858
notes:
core_courseformat:
- message: >-
Create a new core_courseformat\cmactions::duplicate aiming to replace
the course/lib.php version
type: improved

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