Compare commits

...

260 Commits

Author SHA1 Message Date
David Monllao ccf7578373 Moodle release 3.4rc2 2017-11-08 09:47:39 +01:00
David Monllao 68ba214c65 Merge branch 'install_master' of https://git.in.moodle.com/amosbot/moodle-install 2017-11-08 09:47:38 +01:00
Damyon Wiese b6319f70bd Merge branch 'MDL-60590-master' of git://github.com/andrewnicols/moodle 2017-11-08 15:46:44 +08:00
Andrew Nicols e759c9ed83 MDL-60590 enrol_manual: Fix for race condition in button calculation 2017-11-08 15:31:10 +08:00
Jun Pataleta c9f995872f Merge branch 'MDL-60700-master-fix' of git://github.com/andrewnicols/moodle 2017-11-08 15:17:25 +13:00
Andrew Nicols 2e36c42625 MDL-60700 calendar: Update behat test or new normal 2017-11-08 10:13:20 +08:00
Jun Pataleta 7a2300fd82 Merge branch 'MDL-60717-master-enfix' of git://github.com/mudrd8mz/moodle 2017-11-08 14:43:15 +13:00
AMOS bot f3342e1010 Automatically generated installer lang files 2017-11-08 00:05:03 +00:00
David Mudrák ea90412944 MDL-60717 lang: Reflect the string change in behat scenario file 2017-11-07 21:49:18 +01:00
Helen Foster 8d64443452 MDL-60717 lang: Merge English strings from the en_fix language pack
Significant string changes:

- needbcmath,auth_ldap - 'grace logins' changed to 'expired password
  checking'
2017-11-07 21:30:14 +01:00
David Monllao d6d2cec4a0 Merge branch 'MDL-59758_notes-fix' of git://github.com/dmonllao/moodle 2017-11-07 14:01:35 +01:00
David Monllao 57c9b7fc37 MDL-59758 user: Notes publish state adapted to ws 2017-11-07 13:53:21 +01:00
David Monllao cae37299f3 Merge branch 'MDL-59758-master' of git://github.com/damyon/moodle 2017-11-07 12:37:30 +01:00
Jake Dallimore 5243e93661 Merge branch 'MDL-59256_master' of git://github.com/dmonllao/moodle 2017-11-07 16:03:43 +08:00
Jake Dallimore b135c42a17 Merge branch 'MDL-60700-int-fix-1' of github.com:ryanwyllie/moodle 2017-11-07 15:44:12 +08:00
Ryan Wyllie ec8f488f30 MDL-60700 clean: add course link to activity event summary modal 2017-11-07 07:36:49 +00:00
David Monllao aa30dc8860 MDL-59256 analytics: Respect activities specific access conditions 2017-11-07 08:14:53 +01:00
David Monllao 76f98c3bd5 MDL-59256 analytics: Move get_activities method to CoI scope
CoI = community of inquiry model
2017-11-07 08:14:49 +01:00
Jake Dallimore 7783fd0ec1 Merge branch 'MDL-57772-master' of git://github.com/damyon/moodle 2017-11-07 15:01:31 +08:00
Damyon Wiese a6943ebb59 MDL-59758 core_user: Pluralise bulk actions
Don't say 1 people.
2017-11-07 14:39:04 +08:00
Jake Dallimore eaa43a132c Merge branch 'MDL-60700-master' of git://github.com/ryanwyllie/moodle 2017-11-07 14:23:50 +08:00
Damyon Wiese 81aa5e196f MDL-59758 core_user: Clean up user renderer
These functions are not used anywhere by anything.
2017-11-07 14:18:54 +08:00
Damyon Wiese b5b81de3d6 MDL-59758 core_user: Replace old bulk actions
The participants page has some clunky multi page forms for bulk actions. Replaces it with an ajax alternative.
2017-11-07 14:18:49 +08:00
Ryan Wyllie f0fc8a194a MDL-60700 calendar: remove bottom margin for description in summary 2017-11-07 06:03:01 +00:00
Andrew Nicols 03561ea899 Merge branch 'MDL-60062-master-2' of git://github.com/ryanwyllie/moodle 2017-11-07 13:49:47 +08:00
Ryan Wyllie 6688ae2ba2 MDL-60062 calendar: prevent drag and drop of override events 2017-11-07 05:47:18 +00:00
Jake Dallimore b44bc9bf6c Merge branch 'MDL-60686-master' of git://github.com/damyon/moodle 2017-11-07 13:39:33 +08:00
Damyon Wiese 1ccc9f1953 Merge branch 'MDL-60590_master-fix' of git://github.com/markn86/moodle 2017-11-07 11:59:49 +08:00
Damyon Wiese efe2681894 MDL-57772 chat: BEEP is back baby! 2017-11-07 11:25:02 +08:00
Mark Nelson e3a6d44845 MDL-60590 core_enrol: fixed failing Behat scenarios 2017-11-07 11:24:34 +08:00
Damyon Wiese 8aa01fcf61 Merge branch 'MDL-60671-master' of https://github.com/sammarshallou/moodle 2017-11-07 11:05:43 +08:00
Jake Dallimore 8072eb1384 Merge branch 'MDL-60705-master' of https://github.com/sammarshallou/moodle 2017-11-07 10:50:53 +08:00
Damyon Wiese b9ae53f86b MDL-59758 js: auto_rows module easier to use
Allow passing the direct text area, or a container.
Trigger change events on the text area, not the container.
Properly handle starting with rows > 1
2017-11-07 10:36:13 +08:00
Damyon Wiese 5616a31987 MDL-60686 core_users: Reorder filters by utility
Order the list of filters on the participant page from most useful to least useful.
2017-11-07 10:12:45 +08:00
Ryan Wyllie 8926030555 MDL-60700 calendar: normalise event type in summmary modal render 2017-11-07 02:08:00 +00:00
Jun Pataleta 13b19e40cb Merge branch 'wip-MDL-60391-master' of git://github.com/marinaglancy/moodle 2017-11-07 14:30:58 +13:00
Ryan Wyllie 478b1d194f MDL-60062 calendar: remove validate_event_timestart callback 2017-11-07 01:25:33 +00:00
Ryan Wyllie 9aac9f7074 MDL-60062 quiz: add support for drag drop of calendar events 2017-11-07 01:25:32 +00:00
Jun Pataleta 3a0951f027 Merge branch 'MDL-60361-master' of git://github.com/andrewnicols/moodle 2017-11-07 13:37:52 +13:00
Andrew Nicols 7d416f9c2c MDL-60361 calendar: Update behat test lang 2017-11-07 08:36:29 +08:00
Jun Pataleta 589b602be3 Merge branch 'MDL-60611-master-fix' of git://github.com/junpataleta/moodle 2017-11-07 11:31:45 +13:00
Jun Pataleta 8001199766 MDL-60611 calendar: Remove usage of deprecated setExpectedException() 2017-11-07 11:21:29 +13:00
Jun Pataleta a56833d49d Merge branch 'MDL-60611' of git://github.com/stronk7/moodle 2017-11-07 10:14:24 +13:00
David Monllao be88f0993d Merge branch 'MDL-60638-master' of git://github.com/lameze/moodle 2017-11-06 21:45:14 +01:00
sam marshall 11e60032be MDL-60671 core_search: Cron output should use mtrace
The mtrace function is preferable for plain text logging/progress
output because it can be redirected if necessary. By convention it
is normally used in cron and can be used in CLI tasks if required.

This change makes the text_progress_trace class use mtrace instead
of echo and then flush. (Default behaviour of mtrace is to do
exactly that, but it can be redirected too.)

Before this change, if anybody is relying on mtrace behaviour to
redirect logs of cron output, this will fail for some parts of the
search cron output (and some other places like auth plugins).
2017-11-06 11:55:29 +00:00
sam marshall 401710883b MDL-60705 core_search: Unit tests get time wrong by factor 1 million 2017-11-06 11:50:43 +00:00
Damyon Wiese 40f05d2312 Merge branch 'MDL-60590_master' of git://github.com/markn86/moodle 2017-11-06 14:18:40 +08:00
Andrew Nicols 3602df58b2 Merge branch 'MDL-60667_master' of git://github.com/dmonllao/moodle 2017-11-06 13:27:11 +08:00
Andrew Nicols f3743b3041 Merge branch 'MDL-60626-master' of git://github.com/damyon/moodle 2017-11-06 12:43:24 +08:00
Mark Nelson e2d635bbaf MDL-60590 core_user: added Behat test for cohort list presence 2017-11-06 12:39:31 +08:00
Mark Nelson a764855641 MDL-60590 core_cohort: removed unused variables from search_cohorts() 2017-11-06 12:39:31 +08:00
Mark Nelson 9b0f29239b MDL-60590 enrol_manual: button title depends on cohort list existence
Also added a SELECTORS object to tidy the code by keeping the
selectors in a central location.
2017-11-06 12:35:05 +08:00
Mark Nelson 8ce86ebba3 MDL-60590 enrol_manual: check cohorts exist before displaying select 2017-11-06 12:35:05 +08:00
Damyon Wiese 30890c2971 MDL-60626 calendar: Fix for overrides
Override events do not have the courseid so we need to get it from the module instance.
2017-11-06 12:21:27 +08:00
Mark Nelson 1bd70073c5 MDL-60590 enrol_manual: only show cohorts with necessary capabilities 2017-11-06 11:22:45 +08:00
Damyon Wiese 9e0884d410 MDL-60626 calendar: Unit tests for capabilities
Check various calendar external functions for capabilities.
2017-11-06 11:11:28 +08:00
Damyon Wiese af25b8fc7f MDL-60626 calendar: Check capabilities for api
All external functions must check all capabilities before returning data.
The calendar API itself does not check capabilities (I wish it did), so we must be
careful exposing these functions to webservices.
2017-11-06 11:11:28 +08:00
Damyon Wiese ef29782f31 Merge branch 'MDL-59858-master' of git://github.com/junpataleta/moodle 2017-11-06 10:45:34 +08:00
Damyon Wiese 1ada616c56 Merge branch 'MDL-60596_master' of git://github.com/dmonllao/moodle 2017-11-06 10:19:10 +08:00
Damyon Wiese 8443b88995 Merge branch 'MDL-60437-multilang-html-block-titles' of https://github.com/lucaboesch/moodle 2017-11-06 10:09:08 +08:00
Jake Dallimore eb354bdefa Merge branch 'MDL-60361-master' of git://github.com/andrewnicols/moodle 2017-11-06 09:29:46 +08:00
Jun Pataleta 4cd3927c4f MDL-59858 amd: Focus on the element that actually triggers the modal 2017-11-06 13:18:03 +13:00
Jun Pataleta 6a973d261a Merge branch 'MDL-60598_master' of git://github.com/dmonllao/moodle 2017-11-06 10:18:46 +13:00
Eloy Lafuente (stronk7) 796e48a58b MDL-60611 phpunit: remove deprecated stuff and other minor fixes
Thanks @scara for MDL-60611 phpunit: improved redis skipping reasons
2017-11-05 01:45:04 +01:00
AMOS bot 496b39974f Automatically generated installer lang files 2017-11-05 00:05:09 +00:00
Luca Bösch 6586118e0f MDL-60437 blocks: HTML block title multilang 2017-11-03 16:35:59 +01:00
David Monllao 4a145a8ae5 MDL-60598 analytics: Info message for predictions based on data 2017-11-03 12:45:40 +01:00
David Monllao da8a0c97f0 MDL-60596 analytics: Check cm completion if available
Previous code was restricted to course modules origin.
2017-11-03 12:44:21 +01:00
David Monllao 20b2565c38 MDL-60667 mlbackend_python: Bump to non-linear neural net version 2017-11-03 12:43:52 +01:00
David Monllao 36e84e96fc MDL-59256 analytics: Move get_activities test scope 2017-11-03 12:42:20 +01:00
David Monllao 159b4e5d8c Moodle release 3.4rc1 2017-11-03 10:26:18 +01:00
Jake Dallimore aa95085969 Merge branch 'MDL-58047-master-fix1' of http://github.com/damyon/moodle 2017-11-03 16:09:29 +08:00
Damyon Wiese 79ea89fea2 MDL-58047 assign: submission sorting
Only exclude new submission records from the sorting, draft and reopened submission are valid.
2017-11-03 15:55:10 +08:00
Jun Pataleta 928bcb02d6 Merge branch 'MDL-60657-int-fix-1' of github.com:ryanwyllie/moodle 2017-11-03 16:51:24 +13:00
Andrew Nicols b9fd516407 MDL-60361 calendar: Support all event types for calendar subs 2017-11-03 11:27:40 +08:00
Ryan Wyllie c2502375ff MDL-60657 boost: fix styles when extending modal components
I've wrapped the custom modal styling in the .modal class. This prevents
those custom styles from being pulled in when the SCSS @extend
functionality is used to extend the modal components.
2017-11-03 03:18:02 +00:00
Simey Lameze 20a5056df6 MDL-60638 calendar: fix event subscription variable names 2017-11-03 10:03:17 +08:00
Jake Dallimore 4071a2b6fe Merge branch 'MDL-60587-master-4' of git://github.com/andrewnicols/moodle 2017-11-03 09:14:52 +08:00
Andrew Nicols 6630b38c95 MDL-60587 calendar: Adjust use of time() in unit tests 2017-11-03 09:04:10 +08:00
Jake Dallimore b974b5d05d Merge branch 'MDL-60587-master-4' of git://github.com/andrewnicols/moodle 2017-11-03 08:37:31 +08:00
Andrew Nicols 1ab858314a MDL-60587 block: Correct courseid/categoryid check
pagelib uses magic getters for both the course, and category.
It does not define the magic __isset function though.
2017-11-03 08:14:56 +08:00
Eloy Lafuente (stronk7) 6be1923d81 MDL-60611 phpunit: ignore risky (empty) unit tests
At some point we should review them, removing or moving stuff.
2017-11-02 23:45:35 +01:00
Eloy Lafuente (stronk7) 801a372dad MDL-60611 phpunit: switch to namespaced phunit classes 2017-11-02 23:45:34 +01:00
Eloy Lafuente (stronk7) 1322351b92 MDL-60611 phpunit: Bump phpunit to 6.4 and travis to use PHP72 2017-11-02 23:45:21 +01:00
David Monllao bb2071ca17 Merge branch 'MDL-58047-34-sortbysubmission' of https://github.com/roperto/moodle 2017-11-02 16:57:03 +01:00
David Monllao be713b7144 Merge branch 'MDL-51945-master' of git://github.com/jleyva/moodle 2017-11-02 16:54:34 +01:00
Andrew Nicols e20752c4f5 MDL-60587 calendar: Fix un-validated params 2017-11-02 21:49:28 +08:00
Andrew Nicols 30ff29dc9f MDL-60587 calendar: Improve link params for calendar buttons 2017-11-02 21:49:28 +08:00
Andrew Nicols 6607c5ffb6 MDL-60587 calendar: Switch WS to use new helper 2017-11-02 21:49:28 +08:00
Andrew Nicols 7f14def230 MDL-60587 calendar: Add missing categoryid in upcoming WS 2017-11-02 21:49:28 +08:00
Andrew Nicols 39e364515f MDL-60587 calendar: Switch to new helper in main view 2017-11-02 21:49:28 +08:00
Andrew Nicols 557d8528eb MDL-60587 block_calendar_upcoming: Setup calendar using new helper 2017-11-02 21:49:28 +08:00
Andrew Nicols 0bd6039f40 MDL-60587 block_calendar_month: Setup calendar using new helper 2017-11-02 21:49:28 +08:00
Andrew Nicols 57e8c9f748 MDL-60587 calendar: Correct category subscription in information 2017-11-02 21:49:28 +08:00
Andrew Nicols 7b7bf31d4c MDL-60587 calendar: Add helper to setup the calendar_information 2017-11-02 21:49:28 +08:00
David Monllao 59aff5f949 Merge branch 'MDL-60634-master' of git://github.com/jleyva/moodle 2017-11-02 13:24:58 +01:00
Eloy Lafuente (stronk7) f816ea9a67 Merge branch 'MDL-60659-master' of git://github.com/junpataleta/moodle 2017-11-02 12:45:22 +01:00
David Monllao 6e3314f044 Merge branch 'MDL-60633-master' of git://github.com/andrewnicols/moodle 2017-11-02 12:04:07 +01:00
David Monllao 2a542d0265 Merge branch 'MDL-60657-master' of git://github.com/ryanwyllie/moodle 2017-11-02 11:59:30 +01:00
Damyon Wiese f18fb611c9 Merge branch 'MDL-60558-master' of git://github.com/andrewnicols/moodle 2017-11-02 15:46:42 +08:00
Andrew Nicols 43caf0217f MDL-60558 block_calendar_upcoming: Render the template when no events 2017-11-02 15:43:17 +08:00
David Monllao 7b03092121 Merge branch 'MDL-53501-master' of git://github.com/jleyva/moodle 2017-11-02 08:30:24 +01:00
Damyon Wiese 5dbd051502 Merge branch 'MDL-59950_master' of git://github.com/dmonllao/moodle 2017-11-02 14:39:32 +08:00
Jun Pataleta c39a22f600 MDL-60659 mod_forum: Use PHP EOL for line breaks in expected values 2017-11-02 19:11:00 +13:00
Ryan Wyllie f0f38bf1d3 MDL-60657 core: fix upgrade notice modal header padding 2017-11-02 06:07:00 +00:00
Damyon Wiese 41080050b0 Merge branch 'MDL-60558-master' of git://github.com/andrewnicols/moodle 2017-11-02 13:03:35 +08:00
Damyon Wiese ae348e78a0 Merge branch 'MDL-59982_master' of git://github.com/markn86/moodle 2017-11-02 12:21:39 +08:00
Damyon Wiese 71fc06aae5 Merge branch 'MDL-60645-master' of git://github.com/andrewnicols/moodle 2017-11-02 12:10:04 +08:00
Andrew Nicols e3491a093a MDL-60558 calendar: Upcoming block should use the right template 2017-11-02 11:55:49 +08:00
Andrew Nicols a0a78199b8 MDL-60558 calendar: Add event metadata to upcoming template 2017-11-02 11:55:49 +08:00
Andrew Nicols d523cbcb26 MDL-60558 calendar: Upcoming should support categories 2017-11-02 11:54:43 +08:00
Andrew Nicols 5c9acb8164 MDL-60558 calendar: Adjust URL change
The URL change belongs in the calendar_view, not the view_manager.
The view_manager is responsibe for updating the view, not for the
resultant actions from it.

This also changes to use pushState so as not to overwrite the current
location in history.
2017-11-02 11:54:13 +08:00
Andrew Nicols 8242027da3 MDL-60558 calendar: Move edit event to CRUD 2017-11-02 11:53:52 +08:00
Andrew Nicols fd2f1ae146 MDL-60558 calendar: Remove old SELECTORS 2017-11-02 11:53:49 +08:00
Andrew Nicols c8419f87c0 MDL-60558 calendar: Harden event link detection 2017-11-02 11:53:26 +08:00
Damyon Wiese 4837fea0aa Merge branch 'MDL-60619-master' of https://github.com/snake/moodle 2017-11-02 11:51:35 +08:00
Damyon Wiese 671440d708 Merge branch 'MDL-60576-master' of git://github.com/andrewnicols/moodle 2017-11-02 11:38:57 +08:00
Andrew Nicols 6c3f463dbd MDL-60558 calendar: Move the summary modal to view_manager 2017-11-02 10:56:00 +08:00
Damyon Wiese fca7e60c81 Merge branch 'MDL-60655-master' of git://github.com/junpataleta/moodle 2017-11-02 10:45:07 +08:00
Jake Dallimore 1e83eec993 Merge branch 'MDL-60555-master' of https://github.com/sammarshallou/moodle 2017-11-02 10:40:43 +08:00
Jake Dallimore 216bb33940 Merge branch 'master_MDL-60007' of https://github.com/CengageEng/moodle 2017-11-02 08:50:42 +08:00
Andrew Nicols 58280ae0b8 Merge branch 'wip-MDL-60515-master' of git://github.com/abgreeve/moodle 2017-11-02 08:47:17 +08:00
Andrew Nicols 0df5c743ad MDL-60361 calendar: Make eventtype functionality a trait 2017-11-02 07:57:44 +08:00
Jun Pataleta e8da33d86e MDL-60655 tool_httpsreplace: Use \core_text string functions 2017-11-02 10:42:33 +13:00
Jun Pataleta 02331dcd48 MDL-60655 tool_httpsreplace: Use appropriate classes for PHPDocs params 2017-11-02 10:42:33 +13:00
Eloy Lafuente (stronk7) c8e7777766 on-demand release 3.4beta+ 2017-11-01 13:40:07 +01:00
Eloy Lafuente (stronk7) d25f1c3c3c Merge branch 'MDL-60503-master' of git://github.com/damyon/moodle 2017-11-01 10:41:48 +01:00
Adrian Greeve ab2095fb0f MDL-60515 groups: Behat test for viewing the group description. 2017-11-01 15:24:38 +08:00
Adrian Greeve 6b839f3222 MDL-60515 groups: Group details template change.
The group details template has been changed to be
simpler and is styled a bit nicer.
2017-11-01 15:24:38 +08:00
Damyon Wiese 66242b95e0 MDL-60503 npm: Lock required node 8.9.x (LTS) 2017-11-01 14:52:16 +08:00
Mark Nelson 9a428ae012 MDL-59982 core_user: allow user to use filters from same category 2017-11-01 12:55:17 +08:00
Daniel Thee Roperto b8243e4164 MDL-58047 gradebook: fix sort by submission order. 2017-11-01 15:12:48 +11:00
Damyon Wiese 14a1672cff Merge branch 'MDL-52037-master-fix' of https://github.com/andrewnicols/moodle 2017-11-01 11:14:50 +08:00
Andrew Nicols 89cf38c6d9 MDL-52037 theme: Correct question feedback width
This relates to the cloze question answer when hovered.
2017-11-01 11:12:16 +08:00
Andrew Nicols 43a1e9a2a5 MDL-60645 calendar: Add missing clean classes 2017-11-01 09:36:15 +08:00
sam marshall 82735dec4d MDL-60555 core_search: Change unit tests to use fake time
Using real time caused rare failures. I've changed it to use fake
measurement for the indexing timeouts, which were unit-tested.
2017-10-31 14:48:09 +00:00
Vervoort, Claude 76f11421a4 MDL-60007 mod lti: allow delete without content-type 2017-10-31 09:57:38 -04:00
Juan Leyva 1b0978abf3 MDL-60433 mod_data: Fix strict check for groupid 2017-10-31 13:24:07 +01:00
Juan Leyva 098b16fb13 MDL-60634 tool_mobile: Change page used for https test
We need to do the HTTPS check using a page that:
- Does not redirect (login/index.php may redirect because of auth
plugins)
- Does not return moodle exceptions (some custom exceptions handlers
may launch custom HTTP errors)
2017-10-31 11:57:25 +01:00
David Monllao 4916c9bb66 MDL-52037 theme: Update compiled css 2017-10-31 11:00:59 +01:00
Luca Bösch 8fcb479246 MDL-52037 questions: „Whether correct“-feedback marks adaptive width 2017-10-31 10:42:49 +01:00
Jun Pataleta 73ec9b4778 Merge branch 'MDL-60635-master' of git://github.com/andrewnicols/moodle 2017-10-31 21:30:01 +13:00
Jake Dallimore 9cca2db570 Merge branch 'MDL-60472_master' of git://github.com/markn86/moodle 2017-10-31 15:18:37 +08:00
Jake Dallimore 9daa5874a0 Merge branch 'MDL-60607' of git://github.com/timhunt/moodle 2017-10-31 12:57:36 +08:00
Andrew Nicols 60829fbe6d Merge branch 'MDL-60631_master' of git://github.com/dmonllao/moodle 2017-10-31 12:08:01 +08:00
Mark Nelson 9f33d5ac6e MDL-60472 core_user: filter by group id when specified in URL 2017-10-31 12:05:13 +08:00
Jun Pataleta 1cc7a14ce4 Merge branch 'MDL-59936-int-fix-1' of github.com:ryanwyllie/moodle 2017-10-31 16:02:50 +13:00
Jun Pataleta 958c9c17ca Merge branch 'MDL-57560-master' of git://github.com/damyon/moodle 2017-10-31 15:40:19 +13:00
Ryan Wyllie 4397da0c4d MDL-59936 boost: improve focus outline on modal header 2017-10-31 02:38:29 +00:00
Andrew Nicols d8966238dd Merge branch 'wip-MDL-60569-master' of git://github.com/marinaglancy/moodle 2017-10-31 10:15:56 +08:00
Andrew Nicols 065a754111 Merge branch 'MDL-60575-master' of https://github.com/snake/moodle 2017-10-31 10:05:38 +08:00
Andrew Nicols 3e4de62607 Merge branch 'MDL-60139' of git://github.com/timhunt/moodle 2017-10-31 09:14:32 +08:00
Andrew Nicols a37faf9c70 MDL-60635 calendar: Fix end of month issue with previous/next month
This reverts to the previous way in which this was calculated.
2017-10-31 08:51:50 +08:00
Andrew Nicols aba60237cd Merge branch 'MDL-60522-master' of https://github.com/Kathrin84/moodle 2017-10-31 08:12:31 +08:00
Jake Dallimore 5836baf593 MDL-60619 core: fix autocomplete suggestions css for RTL 2017-10-31 07:44:05 +08:00
Jake Dallimore ed44df22a8 MDL-60575 lang: Change registration string to html from markdown 2017-10-31 07:30:33 +08:00
Andrew Nicols 314b8093e7 MDL-60633 core: Add pendingjs checks for inplace editable 2017-10-31 07:03:13 +08:00
Tim Hunt ade8dd5d08 MDL-60607 quiz reports: catch separate groups & user has no group case 2017-10-30 17:57:30 +00:00
David Monllao 28196427a2 Merge branch 'MDL-60528-m' of https://github.com/micaherne/moodle 2017-10-30 17:58:39 +01:00
Juan Leyva 8c4d7cee79 MDL-51945 core_user: Prevent duplicated emails in update_users WS 2017-10-30 17:42:03 +01:00
David Monllao 64476c520e Merge branch 'MDL-52037-cloze-whether-correct-feedback-width' of https://github.com/lucaboesch/moodle 2017-10-30 15:58:39 +01:00
Juan Leyva c851ee5ddb MDL-53501 webservice: Avoid values higher than PHP_INT_MAX
Integers coming from site settings needs casting to int to avoid
returning values higher than PHP_INT_MAX.
2017-10-30 15:55:49 +01:00
David Monllao 860d33c90a Merge branch 'MDL-60348-master' of git://github.com/jleyva/moodle 2017-10-30 15:53:53 +01:00
Tim Hunt eefa2c88e0 MDL-60607 quiz reports: move duplicate code to superclass method 2017-10-30 13:43:36 +00:00
Tim Hunt 284c795d23 MDL-60139 question manual grading: move code to reduce confusion
Move the variable definition closer to where it is used.
2017-10-30 12:55:11 +00:00
Tim Hunt 808b68835b MDL-60139 question manual grading: redisplay mark as typed if editing
When the teacher is upgrading a previously entered grade, we re-display
exactly what they typed before if possible, rather than displaying with
a set number of decimal places.
2017-10-30 12:52:02 +00:00
Tim Hunt 46a3753424 MDL-60139 phpunit: Make assertTag verify attribute values with ===
I had a test that cared about the difference between value="5.0" and value="5".
2017-10-30 12:52:00 +00:00
Juan Leyva 7336f854e6 MDL-60348 mod_workshop: Add parameters checks in evaluate_assessment WS 2017-10-30 13:50:57 +01:00
David Monllao 6065875eac Merge branch 'MDL-60433-master' of git://github.com/jleyva/moodle 2017-10-30 13:23:35 +01:00
David Monllao 3fdecc2841 Merge branch 'MDL-60377-master' of git://github.com/jleyva/moodle 2017-10-30 11:42:11 +01:00
Luca Bösch 8c00d189f4 MDL-52037 questions: „Whether correct“-feedback marks adaptive width 2017-10-30 10:34:09 +00:00
David Monllao 8c7db03a56 MDL-60631 analytics: Pass evaluation analysables filters as objects 2017-10-30 10:54:07 +01:00
Adrian Greeve c550fd0e11 MDL-60515 groups: Update to weblib group function.
Added a new function to get the url for the group picture.
2017-10-30 16:43:19 +08:00
Adrian Greeve 349b705d31 MDL-60515 groups: Display group information on the participants page. 2017-10-30 16:43:18 +08:00
Kathrin Osswald 9cc55074d5 MDL-60522 message: Removed additional tooltips
I removed the additional tooltips because I think that they are just used
in the notifications and messages context and duplicate the all over used
title tag that gives the same information when hovering. So the whole system
stays more consistently.
2017-10-30 08:01:10 +01:00
Damyon Wiese 4fab795ac9 MDL-57560 boost: File upload progress
Copy / adapt some styles from the bootstrap progress widget to match the DOM used for file upload progress.

Bootstrap changed this alot in the current beta so I opted to do a minimal change (affect the scss for boost only), rather
than adapt the HTML to something that will change again anyway when we upgrade to the release version of bootstrap 4.
2017-10-30 13:12:05 +08:00
Jake Dallimore 51af04ab2f Merge branch 'MDL-59606' of git://github.com/timhunt/moodle 2017-10-30 11:23:18 +08:00
Andrew Nicols 9e6d1a3884 Merge branch 'MDL-60605_filetree-spacing' of https://github.com/dvdcastro/moodle 2017-10-30 10:50:26 +08:00
Jun Pataleta 391ea5b7de Merge branch 'MDL-59936-master' of git://github.com/andrewnicols/moodle 2017-10-30 14:34:58 +13:00
Andrew Nicols 9d0e1fca5a Merge branch 'MDL-60360_enable_file_indexing_retroactively' of https://github.com/mattporritt/moodle 2017-10-30 09:17:13 +08:00
Ryan Wyllie 8f730872a7 MDL-59936 calendar: use event colour in summary modal header 2017-10-30 08:34:26 +08:00
Ryan Wyllie 4febb36efe MDL-59936 calendar: use icons in event summary modal 2017-10-30 08:33:54 +08:00
Ryan Wyllie 368832d51c MDL-59936 javascript: hide modal footer without content 2017-10-30 08:33:54 +08:00
Jun Pataleta cc80554892 Merge branch 'MDL-60608-master' of git://github.com/lameze/moodle 2017-10-30 11:47:54 +13:00
Jun Pataleta e905b656f5 Merge branch 'MDL-60570-master' of git://github.com/andrewnicols/moodle 2017-10-30 11:00:44 +13:00
Jun Pataleta 526d354991 Merge branch 'MDL-60572-master' of git://github.com/jleyva/moodle 2017-10-30 10:10:34 +13:00
Andrew Nicols eb3b38f04c MDL-60570 calendar: Correct course filter label in day view 2017-10-29 21:05:55 +08:00
Andrew Nicols a1b696f943 MDL-60576 calendar: Remove underline between icons and text 2017-10-29 21:01:40 +08:00
David Castro 4e52eb8eb0 MDL-60605 assignment: Added some styles to fix spacing in YUI file tree 2017-10-27 11:06:25 -05:00
Tim Hunt dae0ccfc45 MDL-59606 quiz responses: fix edge cases in the report
This patch conbines valuable contributions from Kashmira Nagwekar and
Luca Bösch. Many thanks to them. However, the final form of the fix,
and hence the blame, falls to me -- Tim.

There were several issues here:

* The load_questions_usages_by_activity method in
  question/engine/datalib.php was incorrectly treating the case
  when no data was returned. (Looks like a historic copy-pase from
  other methods that fetch one item by unique id, which therefore
  must exist.)

* The report was not correctly handling the display when the 'Which
  tries' was set to 'with, and without, attempts'.

* It was possible to select the 'All tries' option when also saying
  'Users without attempts'. This combination makes not sense, so
  a disabledIf rule was added to the form.
2017-10-27 16:57:01 +01:00
David Monllao 40f1801c4b on-demand release 3.4beta+ 2017-10-27 12:49:10 +02:00
Juan Leyva 6078d420bf MDL-60572 admin: Enforce URLs in forgottenpasswordurl setting
Also display warnings for admins.
2017-10-27 11:15:01 +02:00
Juan Leyva 2479a7c446 MDL-60572 admin: Fix forgottenpasswordurl for WS
We should expect URLs in that field.
The tool_mobile change is to not break the WS response if
forgottenpasswordurl does not contain a URL.
2017-10-27 11:12:32 +02:00
David Monllao ef9f60a635 MDL-59950 analytics: Use cognitive depth and social breadth constants 2017-10-27 10:48:42 +02:00
David Monllao db9d745c7b MDL-59950 analytics: Add constants for cognitive and social levels 2017-10-27 10:48:38 +02:00
Simey Lameze 0d049757c0 MDL-60608 calendar: change eventdefaultcontext default to 0 2017-10-27 14:19:16 +08:00
Matt Porritt 0f56f6e6da MDL-60360 Global Search: Enable file indexing message update.
If you have Global Search set up on a site without file
indexing enabled, then at a later date you enable file indexing
files associated with existing objects are not added.

The only way to have files for existing objects indexed is to
run a reindex of all content.

This patches updates the file indexing language string in the
solr search settings of Global Search to make this clear
to the site administrator.
2017-10-27 11:11:12 +11:00
David Monllao ff340a0afa Merge branch 'wip-MDL-57055-master' of git://github.com/abgreeve/moodle 2017-10-26 19:17:28 +02:00
David Monllao 62a38a4ece Merge branch 'MDL-60559-master' of git://github.com/andrewnicols/moodle 2017-10-26 19:15:44 +02:00
David Monllao a272c25649 Merge branch 'MDL-60600_master' of https://github.com/marxjohnson/moodle 2017-10-26 18:33:49 +02:00
Mark Johnson a9ca3fa167 MDL-60600 core_output: Clear static icon system cache between unit tests 2017-10-26 14:59:35 +01:00
David Monllao 7ec04e0e09 Merge branch 'MDL-60430-master' of https://github.com/mihailges/moodle 2017-10-26 10:27:34 +02:00
Jun Pataleta 1fc642324a Merge branch 'MDL-59909-master' of git://github.com/damyon/moodle 2017-10-26 21:09:12 +13:00
Damyon Wiese 7f91fb036b Merge branch 'MDL-60366_feedback_upgrade' of git://github.com/davosmith/moodle 2017-10-26 16:02:13 +08:00
David Monllao fd39db29e2 Merge branch 'MDL-59973-master' of git://github.com/damyon/moodle 2017-10-26 09:47:22 +02:00
David Monllao 341505e5de Merge branch 'MDL-60458-master' of git://github.com/damyon/moodle 2017-10-26 09:41:09 +02:00
Damyon Wiese ef7df2ae0a Merge branch 'MDL-60564_master' of git://github.com/markn86/moodle 2017-10-26 14:43:39 +08:00
Damyon Wiese ed6d81c9f7 MDL-59973 calendar: Remove duplication
The function calendar_get_default_courses is responsible for loading all variants of calendar
course lists.

1 the list of courses that the current user will see events for in the site calendar.
2 the list of courses the current user will see when they are looking at a specific course calendar.
3 the list of course calendars the current user can create / edit events in.
2017-10-26 14:39:35 +08:00
Damyon Wiese 01ed53df0f MDL-59973 calendar: Only load required fields
When loading courses for the course filter on the calendar, we only need the id and shortname.
2017-10-26 14:21:29 +08:00
Andrew Nicols 4d03dbf13d Merge branch 'wip-MDL-60543-master' of git://github.com/marinaglancy/moodle 2017-10-26 14:19:43 +08:00
Andrew Nicols 4cc469b2cc Merge branch 'MDL-60162' of git://github.com/timhunt/moodle 2017-10-26 14:04:45 +08:00
Damyon Wiese 1b7d75f19f MDL-59973 calendar: Improve help for admin setting 2017-10-26 13:28:01 +08:00
Damyon Wiese 4dc645d64d MDL-59973 calendar: Unittest calendar_get_default_courses 2017-10-26 13:28:01 +08:00
Damyon Wiese a5327e449d MDL-59973 calendar: Admin calendar course lists
Consolidate the logic for the list of courses an admin will see in the calendar.

They will see all courses if calendar_admincanseeall is enabled.

They will see only courses they are enrolled in of calendar_admincanseeall is not enabled.

They will also see the current course, if they are admin and it is not already in the list.

This applies to the course selector and the create and update event forms.

The calendar link in the navigation block has also been changed to link to the current course calendar instead of the global
one.
2017-10-26 13:28:00 +08:00
Andrew Nicols 4d4b8ae6bf Merge branch 'MDL-60535-questionbank-button-styled' of https://github.com/lucaboesch/moodle 2017-10-26 13:17:56 +08:00
Jake Dallimore 1e67827131 Merge branch 'MDL-60434_master' of git://github.com/dmonllao/moodle 2017-10-26 13:11:46 +08:00
Damyon Wiese 7dfc9ea6b3 Merge branch 'wip-MDL-60390-fix' of https://github.com/marinaglancy/moodle 2017-10-26 12:52:11 +08:00
Marina Glancy 69e89f4a82 MDL-60390 tool_messageinboud: not strict index comparision
This is something that changed in the upgraded Horde library. structure->findBody() returns string but structure->contentTypeMap is indexed with ints
2017-10-26 12:44:51 +08:00
Andrew Nicols b799cde890 Merge branch 'wip-MDL-60104-m34' of git://github.com/amygroshek/moodle 2017-10-26 11:32:54 +08:00
Andrew Nicols 9fd91f8eec Merge branch 'master-MDL-51892' of https://github.com/fwsl/moodle 2017-10-26 11:08:50 +08:00
Jake Dallimore 6e186498fd Merge branch 'MDL-59686-master' of git://github.com/lameze/moodle 2017-10-26 10:16:21 +08:00
Simey Lameze 3c2b703e00 MDL-59686 form: fix autosave support for editor field 2017-10-26 09:03:31 +08:00
Adrian Greeve e6ccfad242 MDL-57055 mod_label: Add label administration for boost.
This patch adds the label adminstration settings to the
label edit page.
2017-10-26 08:41:33 +08:00
Jun Pataleta 75909e3c92 Merge branch 'MDL-59938-master' of git://github.com/andrewnicols/moodle 2017-10-26 11:22:30 +13:00
Jun Pataleta 2a1f7a4442 Merge branch 'MDL-60571-manualgrading-savegotonextpage-styled' of https://github.com/lucaboesch/moodle 2017-10-26 10:02:27 +13:00
David Monllao 7851132177 Merge branch 'MDL-60516-master' of git://github.com/andrewnicols/moodle 2017-10-25 15:16:21 +02:00
David Monllao 4f7a80341e Merge branch 'MDL-59942-master' of git://github.com/andrewnicols/moodle 2017-10-25 15:14:54 +02:00
David Monllao 3b4e9a9fc8 Merge branch 'wip_MDL-60250_master' of git://github.com/gjb2048/moodle 2017-10-25 15:12:23 +02:00
David Monllao 507335e2de Merge branch 'MDL-60538-master' of https://github.com/mihailges/moodle 2017-10-25 15:08:49 +02:00
David Monllao 5b93dc8539 Merge branch 'MDL-60560-master' of git://github.com/andrewnicols/moodle 2017-10-25 15:06:49 +02:00
David Monllao 3a89d0b541 MDL-60434 analytics: Analysables lazy loading 2017-10-25 14:16:35 +02:00
David Monllao cc4d7ce61b MDL-60434 analytics: Missing dependency
Sneaking it in...
2017-10-25 14:13:53 +02:00
David Monllao b8bb8fe8de MDL-60434 analytics: Remove legacy is_valid method
This method was used when the API was tied to students at risk model,
this method does not make sense any more as it is up to each target
to define what is a valid course.
2017-10-25 12:47:12 +02:00
Damyon Wiese a66de3ca70 MDL-59909 calendar: Try hard not to fail the adhoc task.
Triggering a fatal error in an adhoc task is bad. It will be retried indefinitely.
Even though we are not sure how to get a module instance without a course module record,
it is possible and should not kill the Moodle site.
2017-10-25 15:42:14 +08:00
Marina Glancy 13e7626834 MDL-60569 registration: fix bug with showing email in the form 2017-10-25 15:12:43 +08:00
Mihail Geshoski 6a633d36c2 MDL-60430 assignment: Assignment save and show next for 1 user 2017-10-25 14:18:16 +08:00
Damyon Wiese 7b18365be7 MDL-60458 javascript: MathJax Workaround
Mathjax currently has a bug in an upstream library that is incorrectly
loading 'process' because it things it's running under Node.js.

We don't load modules by shortname - so it should be fine to map process to
something else.

Because of the syntax they used we need to map it to something that is already loaded
and evaluates to null (Their code will then assume the module could not be loaded and continue).

We have a handy module that is guaranteed to be loaded first and evaluates to null -> core/first.

We should remove this workaround when MathJax releases a fixed version on their CDN and update the docs.
2017-10-25 12:30:21 +08:00
Damyon Wiese 89ceede3bf MDL-60458 Mathjax: Restore the accessible config
Undo the changes to the accessible config that were introduced by the previous library upgrade.
2017-10-25 12:30:14 +08:00
Mark Nelson 8e85371800 MDL-60564 calendar: removed duplication of exporter properties 2017-10-25 12:06:26 +08:00
Marina Glancy 786d9cd3a1 MDL-60543 env: Moodle 3.2 and 3.3 do no support PHP7.2 2017-10-25 09:02:22 +08:00
Mihail Geshoski 994ccb7845 MDL-60538 lesson: check if max attempts exceeded, use new lang string 2017-10-25 09:00:20 +08:00
Tim Hunt c282157fc3 MDL-60162 quiz reports: refactor duplicated code into the base class
This is needed to fix the unit test in the last commit.
2017-10-24 18:06:46 +01:00
Tim Hunt ff456b59ea MDL-60162 quiz reports: too many rows fetched on download
This happened if one user had multiple enrolments in a course, and was
quite inefficient.
2017-10-24 18:06:44 +01:00
Amy Groshek 88924ff772 MDL-60104 mod_scorm: Fix scorm description display overflow 2017-10-24 10:42:23 -05:00
Luca Bösch 57f4e9608d MDL-60535 quiz: question bank button styled 2017-10-24 16:08:17 +01:00
Luca Bösch 1003e7e57e MDL-60571 quiz: Manual grading Save and go to next page button styled 2017-10-24 12:11:51 +01:00
Mark Nelson 429f78412d MDL-60564 calendar: removed unnecessary get_new_event_button() 2017-10-24 17:49:28 +08:00
Mark Nelson d2065abd0b MDL-60564 calendar: removed unnecessary get_course_filter_selector() 2017-10-24 17:49:26 +08:00
Grzegorz Ziółek 5fd7c11d17 MDL-51892 logging: Proper description of login errors. 2017-10-24 11:00:05 +02:00
Andrew Nicols acd146fe7c MDL-59938 calendar: Only show repeat information if repeating event 2017-10-24 16:00:36 +08:00
Andrew Nicols 79dff18711 MDL-59942 calendar: Add missing attribute to narrow day link 2017-10-24 15:35:31 +08:00
Andrew Nicols 9da0be8c46 MDL-60559 calendar: Upcoming block should respect type filter 2017-10-24 15:25:34 +08:00
Andrew Nicols 80e4426aeb MDL-60516 calendar: Update mini calendar on course change 2017-10-24 10:43:01 +08:00
Andrew Nicols 9eced5050b MDL-60560 calendar: Correct type of JS event on update 2017-10-24 09:45:27 +08:00
Gareth J Barnard 6d5f9d1218 MDL-60250 theme_bootstrapbase: Calendar popover misplaced in RTL langs. 2017-10-23 11:15:25 +01:00
Michael Aherne 71826ac268 MDL-60528 grading: Accurate class names in PHPDoc 2017-10-20 14:13:19 +01:00
Juan Leyva 21824a9325 MDL-60433 mod_data: Fix groups in Web Services 2017-10-13 12:42:10 +02:00
Marina Glancy 9016f772ce MDL-60391 libraries: patch bennu for php 7.2 2017-10-13 16:42:34 +08:00
Juan Leyva 84346754f6 MDL-60377 webservice: Format correctly text returned by WS 2017-10-10 15:06:23 +02:00
Davo Smith 5ef66eaf7e MDL-60366 mod_feedback: fix deletion of valid anonymous duplicates 2017-10-06 13:38:08 +01:00
320 changed files with 7868 additions and 3383 deletions
+5 -5
View File
@@ -13,7 +13,7 @@ language: php
php:
# We only run the highest and lowest supported versions to reduce the load on travis-ci.org.
- 7.1
- 7.2
- 7.0
addons:
@@ -30,7 +30,7 @@ env:
# Although we want to run these jobs and see failures as quickly as possible, we also want to get the slowest job to
# start first so that the total run time is not too high.
#
# We only run MySQL on PHP 7.1, so run that first.
# We only run MySQL on PHP 7.2, so run that first.
# CI Tests should be second-highest in priority as these only take <= 60 seconds to run under normal circumstances.
# Postgres is significantly is pretty reasonable in its run-time.
@@ -54,12 +54,12 @@ matrix:
include:
# Run grunt/npm install on highest version ('node' is an alias for the latest node.js version.)
- php: 7.1
env: DB=none TASK=GRUNT NVM_VERSION='node'
- php: 7.2
env: DB=none TASK=GRUNT NVM_VERSION='8.9'
exclude:
# MySQL - it's just too slow.
# Exclude it on all versions except for 7.1
# Exclude it on all versions except for 7.2
- env: DB=mysqli TASK=PHPUNIT
php: 7.0
+2
View File
@@ -1754,6 +1754,7 @@
<VENDOR name="oracle" version="10.2" />
</DATABASE>
<PHP version="5.6.5" level="required">
<RESTRICT function="restrict_php_version_72" message="unsupportedphpversion72" />
</PHP>
<PCREUNICODE level="optional">
<FEEDBACK>
@@ -1919,6 +1920,7 @@
<VENDOR name="oracle" version="10.2" />
</DATABASE>
<PHP version="5.6.5" level="required">
<RESTRICT function="restrict_php_version_72" message="unsupportedphpversion72" />
</PHP>
<PCREUNICODE level="optional">
<FEEDBACK>
+2 -1
View File
@@ -868,6 +868,7 @@ $cachewarnings = cache_helper::warnings();
$eventshandlers = $DB->get_records_sql('SELECT DISTINCT component FROM {events_handlers}');
$themedesignermode = !empty($CFG->themedesignermode);
$mobileconfigured = !empty($CFG->enablemobilewebservice);
$invalidforgottenpasswordurl = !empty($CFG->forgottenpasswordurl) && empty(clean_param($CFG->forgottenpasswordurl, PARAM_URL));
// Check if a directory with development libraries exists.
if (empty($CFG->disabledevlibdirscheck) && (is_dir($CFG->dirroot.'/vendor') || is_dir($CFG->dirroot.'/node_modules'))) {
@@ -885,4 +886,4 @@ $output = $PAGE->get_renderer('core', 'admin');
echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, $cronoverdue, $dbproblems,
$maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
$registered, $cachewarnings, $eventshandlers, $themedesignermode, $devlibdir,
$mobileconfigured, $overridetossl);
$mobileconfigured, $overridetossl, $invalidforgottenpasswordurl);
+1 -1
View File
@@ -36,6 +36,6 @@ class core_register_renderer extends plugin_renderer_base {
* @return string
*/
public function moodleorg_registration_message() {
return format_text(get_string('registermoodlenet', 'admin'), FORMAT_MARKDOWN, ['noclean' => true]);
return format_text(get_string('registermoodlenet', 'admin'), FORMAT_HTML, ['noclean' => true]);
}
}
+21 -1
View File
@@ -280,6 +280,7 @@ class core_admin_renderer extends plugin_renderer_base {
* @param bool $devlibdir Warn about development libs directory presence.
* @param bool $mobileconfigured Whether the mobile web services have been enabled
* @param bool $overridetossl Whether or not ssl is being forced.
* @param bool $invalidforgottenpasswordurl Whether the forgotten password URL does not link to a valid URL.
*
* @return string HTML to output.
*/
@@ -287,7 +288,7 @@ class core_admin_renderer extends plugin_renderer_base {
$cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch,
$buggyiconvnomb, $registered, array $cachewarnings = array(), $eventshandlers = 0,
$themedesignermode = false, $devlibdir = false, $mobileconfigured = false,
$overridetossl = false) {
$overridetossl = false, $invalidforgottenpasswordurl = false) {
global $CFG;
$output = '';
@@ -308,6 +309,7 @@ class core_admin_renderer extends plugin_renderer_base {
$output .= $this->events_handlers($eventshandlers);
$output .= $this->registration_warning($registered);
$output .= $this->mobile_configuration_warning($mobileconfigured);
$output .= $this->forgotten_password_url_warning($invalidforgottenpasswordurl);
//////////////////////////////////////////////////////////////////////////////////////////////////
//// IT IS ILLEGAL AND A VIOLATION OF THE GPL TO HIDE, REMOVE OR MODIFY THIS COPYRIGHT NOTICE ///
@@ -866,6 +868,24 @@ class core_admin_renderer extends plugin_renderer_base {
return $output;
}
/**
* Display a warning about the forgotten password URL not linking to a valid URL.
*
* @param boolean $invalidforgottenpasswordurl true if the forgotten password URL is not valid
* @return string HTML to output.
*/
protected function forgotten_password_url_warning($invalidforgottenpasswordurl) {
$output = '';
if ($invalidforgottenpasswordurl) {
$settingslink = new moodle_url('/admin/settings.php', ['section' => 'manageauths']);
$configurebutton = $this->single_button($settingslink, get_string('check', 'moodle'));
$output .= $this->warning(get_string('invalidforgottenpasswordurl', 'admin') . '&nbsp;' . $configurebutton,
'error alert alert-danger');
}
return $output;
}
/**
* Helper method to render the information about the available Moodle update
*
+1 -1
View File
@@ -102,7 +102,7 @@ if ($hassiteconfig) {
$temp->add(new admin_setting_configtext('alternateloginurl', new lang_string('alternateloginurl', 'auth'),
new lang_string('alternatelogin', 'auth', htmlspecialchars(get_login_url())), ''));
$temp->add(new admin_setting_configtext('forgottenpasswordurl', new lang_string('forgottenpasswordurl', 'auth'),
new lang_string('forgottenpassword', 'auth'), ''));
new lang_string('forgottenpassword', 'auth'), '', PARAM_URL));
$temp->add(new admin_setting_confightmleditor('auth_instructions', new lang_string('instructions', 'auth'),
new lang_string('authinstructions', 'auth'), ''));
$setting = new admin_setting_configtext('allowemailaddresses', new lang_string('allowemailaddresses', 'admin'),
@@ -156,11 +156,12 @@ function tool_analytics_calculate_course_dates($course, $options) {
$format = course_get_format($course);
$formatoptions = $format->get_format_options();
if ($course->format === 'weeks' && $formatoptions['automaticenddate']) {
// Special treatment for weeks with automatic end date.
// Change this for a course formats API level call in MDL-60702.
if (method_exists($format, 'update_end_date') && $formatoptions['automaticenddate']) {
// Special treatment for weeks-based formats with automatic end date.
if ($options['update']) {
format_weeks::update_end_date($course->id);
$format::update_end_date($course->id);
$course->enddate = $DB->get_field('course', 'enddate', array('id' => $course->id));
$notification .= PHP_EOL . ' ' . get_string('weeksenddateautomaticallyset', 'tool_analytics') . ': ' .
userdate($course->enddate);
@@ -24,6 +24,9 @@
namespace tool_httpsreplace;
use database_column_info;
use progress_bar;
defined('MOODLE_INTERNAL') || die();
/**
@@ -60,7 +63,7 @@ class url_finder {
* for less straightforward swaps.
*
* @param string $table
* @param string $column
* @param database_column_info $column
* @param string $domain
* @param string $search search string that has prefix, protocol, domain name and one extra character,
* example1: src="http://host.com/
@@ -174,7 +177,7 @@ class url_finder {
$regex = '#((src|data)\ *=\ *[\'\"])(http://)([^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))[\'\"]#i';
preg_match_all($regex, $record->$columnname, $match);
foreach ($match[0] as $i => $fullmatch) {
if (strpos($fullmatch, $CFG->wwwroot) !== false) {
if (\core_text::strpos($fullmatch, $CFG->wwwroot) !== false) {
continue;
}
$prefix = $match[1][$i];
@@ -186,7 +189,7 @@ class url_finder {
}
if ($replacing) {
// For replace string use: prefix, protocol, host and one extra character.
$found[$prefix . substr($url, 0, strlen($host) + 8)] = $host;
$found[$prefix . \core_text::substr($url, 0, \core_text::strlen($host) + 8)] = $host;
} else {
$entry["table"] = $table;
$entry["columnname"] = $columnname;
@@ -584,10 +584,10 @@ class manager {
'usestream' => true,
));
if ($part === $plainpartid) {
if ($part == $plainpartid) {
$contentplain = $this->process_message_part_body($messagedata, $partdata, $part);
} else if ($part === $htmlpartid) {
} else if ($part == $htmlpartid) {
$contenthtml = $this->process_message_part_body($messagedata, $partdata, $part);
} else if ($filename = $partdata->getName($part)) {
+3 -2
View File
@@ -140,7 +140,7 @@ class api {
'rememberusername' => $CFG->rememberusername,
'authloginviaemail' => $CFG->authloginviaemail,
'registerauth' => $CFG->registerauth,
'forgottenpasswordurl' => $CFG->forgottenpasswordurl,
'forgottenpasswordurl' => clean_param($CFG->forgottenpasswordurl, PARAM_URL), // We may expect a mailto: here.
'authinstructions' => $authinstructions,
'authnoneenabled' => (int) is_enabled_auth('none'),
'enablewebservices' => $CFG->enablewebservices,
@@ -386,7 +386,8 @@ class api {
// Return certificate information and verify the certificate.
$curl->setopt(array('CURLOPT_CERTINFO' => 1, 'CURLOPT_SSL_VERIFYPEER' => true));
$httpswwwroot = str_replace('http:', 'https:', $CFG->wwwroot); // Force https url.
$curl->head($httpswwwroot . "/login/index.php");
// Check https using a page not redirecting or returning exceptions.
$curl->head($httpswwwroot . "/$CFG->admin/tool/mobile/mobile.webmanifest.php");
$info = $curl->get_info();
// First of all, check the server certificate (if any).
@@ -97,11 +97,13 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
set_config('typeoflogin', api::LOGIN_VIA_BROWSER, 'tool_mobile');
set_config('logo', 'mock.png', 'core_admin');
set_config('logocompact', 'mock.png', 'core_admin');
set_config('forgottenpasswordurl', 'mailto:fake@email.zy'); // Test old hack.
list($authinstructions, $notusedformat) = external_format_text($authinstructions, FORMAT_MOODLE, $context->id);
$expected['registerauth'] = 'email';
$expected['authinstructions'] = $authinstructions;
$expected['typeoflogin'] = api::LOGIN_VIA_BROWSER;
$expected['forgottenpasswordurl'] = ''; // Expect empty when it's not an URL.
if ($logourl = $OUTPUT->get_logo_url()) {
$expected['logourl'] = $logourl->out(false);
+1 -1
View File
@@ -74,7 +74,7 @@ if ($options['run']) {
}
}
$_SERVER['argv'] = array_values($_SERVER['argv']);
PHPUnit_TextUI_Command::main();
PHPUnit\TextUI\Command::main();
exit(0);
}
+4
View File
@@ -29,6 +29,10 @@ defined('MOODLE_INTERNAL') || die();
/**
* Any element analysers can analyse.
*
* Analysers get_analysers method return all analysable elements in the site;
* it is important that analysable elements implement lazy loading to avoid
* big memory footprints. See \core_analytics\course example.
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+85 -233
View File
@@ -40,9 +40,19 @@ require_once($CFG->dirroot . '/lib/enrollib.php');
class course implements \core_analytics\analysable {
/**
* @var \core_analytics\course[] $instances
* @var bool Has this course data been already loaded.
*/
protected static $instances = array();
protected $loaded = false;
/**
* @var int $cachedid self::$cachedinstance analysable id.
*/
protected static $cachedid = 0;
/**
* @var \core_analytics\course $cachedinstance
*/
protected static $cachedinstance = null;
/**
* Course object
@@ -122,7 +132,7 @@ class course implements \core_analytics\analysable {
* Use self::instance() instead to get cached copies of the course. Instances obtained
* through this constructor will not be cached.
*
* Loads course students and teachers.
* Lazy load of course data, students and teachers.
*
* @param int|stdClass $course Course id
* @return void
@@ -130,18 +140,70 @@ class course implements \core_analytics\analysable {
public function __construct($course) {
if (is_scalar($course)) {
$this->course = get_course($course);
$this->course = new \stdClass();
$this->course->id = $course;
} else {
$this->course = $course;
}
}
$this->coursecontext = \context_course::instance($this->course->id);
/**
* Returns an analytics course instance.
*
* Lazy load of course data, students and teachers.
*
* @param int|stdClass $course Course object or course id
* @return \core_analytics\course
*/
public static function instance($course) {
$courseid = $course;
if (!is_scalar($courseid)) {
$courseid = $course->id;
}
if (self::$cachedid === $courseid) {
return self::$cachedinstance;
}
$cachedinstance = new \core_analytics\course($course);
self::$cachedinstance = $cachedinstance;
self::$cachedid = (int)$courseid;
return self::$cachedinstance;
}
/**
* get_id
*
* @return int
*/
public function get_id() {
return $this->course->id;
}
/**
* Loads the analytics course object.
*
* @return null
*/
protected function load() {
// The instance constructor could be already loaded with the full course object. Using shortname
// because it is a required course field.
if (empty($this->course->shortname)) {
$this->course = get_course($this->course->id);
}
$this->coursecontext = $this->get_context();
$this->now = time();
// Get the course users, including users assigned to student and teacher roles at an higher context.
$cache = \cache::make_from_params(\cache_store::MODE_REQUEST, 'core_analytics', 'rolearchetypes');
// Flag the instance as loaded.
$this->loaded = true;
if (!$studentroles = $cache->get('student')) {
$studentroles = array_keys(get_archetype_roles('student'));
$cache->set('student', $studentroles);
@@ -155,53 +217,13 @@ class course implements \core_analytics\analysable {
$this->teacherids = $this->get_user_ids($teacherroles);
}
/**
* Returns an analytics course instance.
*
* @param int|stdClass $course Course id
* @return \core_analytics\course
*/
public static function instance($course) {
$courseid = $course;
if (!is_scalar($courseid)) {
$courseid = $course->id;
}
if (!empty(self::$instances[$courseid])) {
return self::$instances[$courseid];
}
$instance = new \core_analytics\course($course);
self::$instances[$courseid] = $instance;
return self::$instances[$courseid];
}
/**
* Clears all statically cached instances.
*
* @return void
*/
public static function reset_caches() {
self::$instances = array();
}
/**
* get_id
*
* @return int
*/
public function get_id() {
return $this->course->id;
}
/**
* The course short name
*
* @return string
*/
public function get_name() {
return format_string($this->course->shortname, true, array('context' => $this->get_context()));
return format_string($this->get_course_data()->shortname, true, array('context' => $this->get_context()));
}
/**
@@ -228,8 +250,8 @@ class course implements \core_analytics\analysable {
}
// The field always exist but may have no valid if the course is created through a sync process.
if (!empty($this->course->startdate)) {
$this->starttime = (int)$this->course->startdate;
if (!empty($this->get_course_data()->startdate)) {
$this->starttime = (int)$this->get_course_data()->startdate;
} else {
$this->starttime = 0;
}
@@ -256,7 +278,7 @@ class course implements \core_analytics\analysable {
// We first try to find current course student logs.
$firstlogs = array();
foreach ($this->studentids as $studentid) {
foreach ($this->get_students() as $studentid) {
// Grrr, we are limited by logging API, we could do this easily with a
// select min(timecreated) from xx where courseid = yy group by userid.
@@ -278,7 +300,7 @@ class course implements \core_analytics\analysable {
sort($firstlogs);
$firstlogsmedian = $this->median($firstlogs);
$studentenrolments = enrol_get_course_users($this->get_id(), $this->studentids);
$studentenrolments = enrol_get_course_users($this->get_id(), $this->get_students());
if (empty($studentenrolments)) {
return 0;
}
@@ -306,8 +328,8 @@ class course implements \core_analytics\analysable {
}
// The enddate field is only available from Moodle 3.2 (MDL-22078).
if (!empty($this->course->enddate)) {
$this->endtime = (int)$this->course->enddate;
if (!empty($this->get_course_data()->enddate)) {
$this->endtime = (int)$this->get_course_data()->enddate;
return $this->endtime;
}
@@ -362,21 +384,12 @@ class course implements \core_analytics\analysable {
* @return \stdClass
*/
public function get_course_data() {
return $this->course;
}
/**
* Is the course valid to extract indicators from it?
*
* @return bool
*/
public function is_valid() {
if (!$this->was_started() || !$this->is_finished()) {
return false;
if (!$this->loaded) {
$this->load();
}
return true;
return $this->course;
}
/**
@@ -427,7 +440,7 @@ class course implements \core_analytics\analysable {
public function get_user_ids($roleids) {
// We need to index by ra.id as a user may have more than 1 $roles role.
$records = get_role_users($roleids, $this->coursecontext, true, 'ra.id, u.id AS userid, r.id AS roleid', 'ra.id ASC');
$records = get_role_users($roleids, $this->get_context(), true, 'ra.id, u.id AS userid, r.id AS roleid', 'ra.id ASC');
// If a user have more than 1 $roles role array_combine will discard the duplicate.
$callable = array($this, 'filter_user_id');
@@ -441,6 +454,11 @@ class course implements \core_analytics\analysable {
* @return stdClass[]
*/
public function get_students() {
if (!$this->loaded) {
$this->load();
}
return $this->studentids;
}
@@ -453,7 +471,7 @@ class course implements \core_analytics\analysable {
global $DB;
// No logs if no students.
if (empty($this->studentids)) {
if (empty($this->get_students())) {
return 0;
}
@@ -529,172 +547,6 @@ class course implements \core_analytics\analysable {
return $grades;
}
/**
* Guesses all activities that were available during a period of time.
*
* @param string $activitytype
* @param int $starttime
* @param int $endtime
* @param \stdClass $student
* @return array
*/
public function get_activities($activitytype, $starttime, $endtime, $student = false) {
// Var $student may not be available, default to not calculating dynamic data.
$studentid = -1;
if ($student) {
$studentid = $student->id;
}
$modinfo = get_fast_modinfo($this->get_course_data(), $studentid);
$activities = $modinfo->get_instances_of($activitytype);
$timerangeactivities = array();
foreach ($activities as $activity) {
if (!$this->completed_by($activity, $starttime, $endtime)) {
continue;
}
$timerangeactivities[$activity->context->id] = $activity;
}
return $timerangeactivities;
}
/**
* Was the activity supposed to be completed during the provided time range?.
*
* @param \cm_info $activity
* @param int $starttime
* @param int $endtime
* @return bool
*/
protected function completed_by(\cm_info $activity, $starttime, $endtime) {
// We can't check uservisible because:
// - Any activity with available until would not be counted.
// - Sites may block student's course view capabilities once the course is closed.
// Students can not view hidden activities by default, this is not reliable 100% but accurate in most of the cases.
if ($activity->visible === false) {
return false;
}
// We skip activities that were not yet visible or their 'until' was not in this $starttime - $endtime range.
if ($activity->availability) {
$info = new \core_availability\info_module($activity);
$activityavailability = $this->availability_completed_by($info, $starttime, $endtime);
if ($activityavailability === false) {
return false;
} else if ($activityavailability === true) {
// This activity belongs to this time range.
return true;
}
}
// We skip activities in sections that were not yet visible or their 'until' was not in this $starttime - $endtime range.
$section = $activity->get_modinfo()->get_section_info($activity->sectionnum);
if ($section->availability) {
$info = new \core_availability\info_section($section);
$sectionavailability = $this->availability_completed_by($info, $starttime, $endtime);
if ($sectionavailability === false) {
return false;
} else if ($sectionavailability === true) {
// This activity belongs to this section time range.
return true;
}
}
// When the course is using format weeks we use the week's end date.
$format = course_get_format($activity->get_modinfo()->get_course());
if ($this->course->format === 'weeks') {
$dates = $format->get_section_dates($section);
// We need to consider the +2 hours added by get_section_dates.
// Avoid $starttime <= $dates->end because $starttime may be the start of the next week.
if ($starttime < ($dates->end - 7200) && $endtime >= ($dates->end - 7200)) {
return true;
} else {
return false;
}
}
if ($activity->sectionnum == 0) {
return false;
}
if (!$this->get_end() || !$this->get_start()) {
debugging('Activities which due date is in a time range can not be calculated ' .
'if the course doesn\'t have start and end date', DEBUG_DEVELOPER);
return false;
}
if (!course_format_uses_sections($this->course->format)) {
// If it does not use sections and there are no availability conditions to access it it is available
// and we can not magically classify it into any other time range than this one.
return true;
}
// Split the course duration in the number of sections and consider the end of each section the due
// date of all activities contained in that section.
$formatoptions = $format->get_format_options();
if (!empty($formatoptions['numsections'])) {
$nsections = $formatoptions['numsections'];
} else {
// There are course format that use sections but without numsections, we fallback to the number
// of cached sections in get_section_info_all, not that accurate though.
$coursesections = $activity->get_modinfo()->get_section_info_all();
$nsections = count($coursesections);
if (isset($coursesections[0])) {
// We don't count section 0 if it exists.
$nsections--;
}
}
$courseduration = $this->get_end() - $this->get_start();
$sectionduration = round($courseduration / $nsections);
$activitysectionenddate = $this->get_start() + ($sectionduration * $activity->sectionnum);
if ($activitysectionenddate > $starttime && $activitysectionenddate <= $endtime) {
return true;
}
return false;
}
/**
* Check if the activity/section should have been completed during the provided period according to its availability rules.
*
* @param \core_availability\info $info
* @param int $starttime
* @param int $endtime
* @return bool|null
*/
protected function availability_completed_by(\core_availability\info $info, $starttime, $endtime) {
$dateconditions = $info->get_availability_tree()->get_all_children('\availability_date\condition');
foreach ($dateconditions as $condition) {
// Availability API does not allow us to check from / to dates nicely, we need to be naughty.
$conditiondata = $condition->save();
if ($conditiondata->d === \availability_date\condition::DIRECTION_FROM &&
$conditiondata->t > $endtime) {
// Skip this activity if any 'from' date is later than the end time.
return false;
} else if ($conditiondata->d === \availability_date\condition::DIRECTION_UNTIL &&
($conditiondata->t < $starttime || $conditiondata->t > $endtime)) {
// Skip activity if any 'until' date is not in $starttime - $endtime range.
return false;
} else if ($conditiondata->d === \availability_date\condition::DIRECTION_UNTIL &&
$conditiondata->t < $endtime && $conditiondata->t > $starttime) {
return true;
}
}
// This can be interpreted as 'the activity was available but we don't know if its expected completion date
// was during this period.
return null;
}
/**
* Used by get_user_ids to extract the user id.
*
@@ -731,7 +583,7 @@ class course implements \core_analytics\analysable {
}
// Check the amount of student logs in the 4 previous weeks.
list($studentssql, $studentsparams) = $DB->get_in_or_equal($this->studentids, SQL_PARAMS_NAMED);
list($studentssql, $studentsparams) = $DB->get_in_or_equal($this->get_students(), SQL_PARAMS_NAMED);
$filterselect = $prefix . 'courseid = :courseid AND ' . $prefix . 'userid ' . $studentssql;
$filterparams = array('courseid' => $this->course->id) + $studentsparams;
@@ -44,17 +44,21 @@ abstract class by_course extends base {
// Default to all system courses.
if (!empty($this->options['filter'])) {
$courses = $this->options['filter'];
$courses = array();
foreach ($this->options['filter'] as $courseid) {
$courses[$courseid] = new \stdClass();
$courses[$courseid]->id = $courseid;
}
} else {
// Iterate through all potentially valid courses.
$courses = get_courses('all', 'c.sortorder ASC');
$courses = get_courses('all', 'c.sortorder ASC', 'c.id');
}
unset($courses[SITEID]);
$analysables = array();
foreach ($courses as $course) {
// Skip the frontpage course.
$analysable = \core_analytics\course::instance($course);
$analysable = \core_analytics\course::instance($course->id);
$analysables[$analysable->get_id()] = $analysable;
}
@@ -35,6 +35,13 @@ defined('MOODLE_INTERNAL') || die();
*/
abstract class community_of_inquiry_activity extends linear {
/**
* instancedata
*
* @var array
*/
protected $instancedata = array();
/**
* @var \core_analytics\course
*/
@@ -60,6 +67,56 @@ abstract class community_of_inquiry_activity extends linear {
*/
const INDICATOR_SOCIAL = "social";
/**
* Constant for this cognitive level.
*/
const COGNITIVE_LEVEL_1 = 1;
/**
* Constant for this cognitive level.
*/
const COGNITIVE_LEVEL_2 = 2;
/**
* Constant for this cognitive level.
*/
const COGNITIVE_LEVEL_3 = 3;
/**
* Constant for this cognitive level.
*/
const COGNITIVE_LEVEL_4 = 4;
/**
* Constant for this cognitive level.
*/
const COGNITIVE_LEVEL_5 = 5;
/**
* Constant for this social level.
*/
const SOCIAL_LEVEL_1 = 1;
/**
* Constant for this social level.
*/
const SOCIAL_LEVEL_2 = 2;
/**
* Constant for this social level.
*/
const SOCIAL_LEVEL_3 = 3;
/**
* Constant for this social level.
*/
const SOCIAL_LEVEL_4 = 4;
/**
* Constant for this social level.
*/
const SOCIAL_LEVEL_5 = 5;
/**
* Max cognitive depth level accepted.
*/
@@ -428,8 +485,8 @@ abstract class community_of_inquiry_activity extends linear {
// Samples are at cm level or below.
$useractivities = array(\context_module::instance($cm->id)->id => $cm);
} else {
// All course activities.
$useractivities = $this->course->get_activities($this->get_activity_type(), $starttime, $endtime, $user);
// Activities that should be completed during this time period.
$useractivities = $this->get_activities($starttime, $endtime, $user);
}
return $useractivities;
@@ -530,13 +587,15 @@ abstract class community_of_inquiry_activity extends linear {
foreach ($useractivities as $contextid => $cm) {
$potentiallevel = $this->get_cognitive_depth_level($cm);
if (!is_int($potentiallevel) || $potentiallevel > self::MAX_COGNITIVE_LEVEL || $potentiallevel < 1) {
if (!is_int($potentiallevel)
|| $potentiallevel > self::MAX_COGNITIVE_LEVEL
|| $potentiallevel < self::COGNITIVE_LEVEL_1) {
throw new \coding_exception('Activities\' potential cognitive depth go from 1 to 5.');
}
$scoreperlevel = $scoreperactivity / $potentiallevel;
switch ($potentiallevel) {
case 5:
case self::COGNITIVE_LEVEL_5:
// Cognitive level 5 is to submit after feedback.
if ($this->any_feedback('submitted', $cm, $contextid, $user)) {
$score += $scoreperlevel * 5;
@@ -544,7 +603,7 @@ abstract class community_of_inquiry_activity extends linear {
}
// The user didn't reach the activity max cognitive depth, continue with level 2.
case 4:
case self::COGNITIVE_LEVEL_4:
// Cognitive level 4 is to comment on feedback.
if ($this->any_feedback('replied', $cm, $contextid, $user)) {
$score += $scoreperlevel * 4;
@@ -552,7 +611,7 @@ abstract class community_of_inquiry_activity extends linear {
}
// The user didn't reach the activity max cognitive depth, continue with level 2.
case 3:
case self::COGNITIVE_LEVEL_3:
// Cognitive level 3 is to view feedback.
if ($this->any_feedback('viewed', $cm, $contextid, $user)) {
@@ -562,7 +621,7 @@ abstract class community_of_inquiry_activity extends linear {
}
// The user didn't reach the activity max cognitive depth, continue with level 2.
case 2:
case self::COGNITIVE_LEVEL_2:
// Cognitive depth level 2 is to submit content.
if ($this->any_write_log($contextid, $user)) {
@@ -571,7 +630,7 @@ abstract class community_of_inquiry_activity extends linear {
}
// The user didn't reach the activity max cognitive depth, continue with level 1.
case 1:
case self::COGNITIVE_LEVEL_1:
// Cognitive depth level 1 is just accessing the activity.
if ($this->any_log($contextid, $user)) {
@@ -617,16 +676,18 @@ abstract class community_of_inquiry_activity extends linear {
foreach ($useractivities as $contextid => $cm) {
$potentiallevel = $this->get_social_breadth_level($cm);
if (!is_int($potentiallevel) || $potentiallevel > self::MAX_SOCIAL_LEVEL || $potentiallevel < 1) {
if (!is_int($potentiallevel)
|| $potentiallevel > self::MAX_SOCIAL_LEVEL
|| $potentiallevel < self::SOCIAL_LEVEL_1) {
throw new \coding_exception('Activities\' potential social breadth go from 1 to ' .
community_of_inquiry_activity::MAX_SOCIAL_LEVEL . '.');
}
$scoreperlevel = $scoreperactivity / $potentiallevel;
switch ($potentiallevel) {
case 2:
case 3:
case 4:
case 5:
case self::SOCIAL_LEVEL_2:
case self::SOCIAL_LEVEL_3:
case self::SOCIAL_LEVEL_4:
case self::SOCIAL_LEVEL_5:
// Core activities social breadth only reaches level 2, until core activities social
// breadth do not reach level 5 we limit it to what we currently support, which is level 2.
@@ -639,7 +700,7 @@ abstract class community_of_inquiry_activity extends linear {
}
// The user didn't reach the activity max social breadth, continue with level 1.
case 1:
case self::SOCIAL_LEVEL_1:
// Social breadth level 1 is just accessing the activity.
if ($this->any_log($contextid, $user)) {
$score += $scoreperlevel;
@@ -687,6 +748,250 @@ abstract class community_of_inquiry_activity extends linear {
$this->grades = $course->get_student_grades($courseactivities);
}
/**
* Guesses all activities that were available during a period of time.
*
* @param int $starttime
* @param int $endtime
* @param \stdClass|false $student
* @return array
*/
protected function get_activities($starttime, $endtime, $student = false) {
$activitytype = $this->get_activity_type();
// Var $student may not be available, default to not calculating dynamic data.
$studentid = -1;
if ($student) {
$studentid = $student->id;
}
$modinfo = get_fast_modinfo($this->course->get_course_data(), $studentid);
$activities = $modinfo->get_instances_of($activitytype);
$timerangeactivities = array();
foreach ($activities as $activity) {
if (!$this->activity_completed_by($activity, $starttime, $endtime, $student)) {
continue;
}
$timerangeactivities[$activity->context->id] = $activity;
}
return $timerangeactivities;
}
/**
* Was the activity supposed to be completed during the provided time range?.
*
* @param \cm_info $activity
* @param int $starttime
* @param int $endtime
* @param \stdClass|false $student
* @return bool
*/
protected function activity_completed_by(\cm_info $activity, $starttime, $endtime, $student = false) {
// We can't check uservisible because:
// - Any activity with available until would not be counted.
// - Sites may block student's course view capabilities once the course is closed.
// Students can not view hidden activities by default, this is not reliable 100% but accurate in most of the cases.
if ($activity->visible === false) {
return false;
}
// Give priority to the different methods activities have to set a "due" date.
$return = $this->activity_type_completed_by($activity, $starttime, $endtime, $student);
if (!is_null($return)) {
// Method activity_type_completed_by returns null if there is no due date method or there is but it is not set.
return $return;
}
// We skip activities that were not yet visible or their 'until' was not in this $starttime - $endtime range.
if ($activity->availability) {
$info = new \core_availability\info_module($activity);
$activityavailability = $this->availability_completed_by($info, $starttime, $endtime);
if ($activityavailability === false) {
return false;
} else if ($activityavailability === true) {
// This activity belongs to this time range.
return true;
}
}
// We skip activities in sections that were not yet visible or their 'until' was not in this $starttime - $endtime range.
$section = $activity->get_modinfo()->get_section_info($activity->sectionnum);
if ($section->availability) {
$info = new \core_availability\info_section($section);
$sectionavailability = $this->availability_completed_by($info, $starttime, $endtime);
if ($sectionavailability === false) {
return false;
} else if ($sectionavailability === true) {
// This activity belongs to this section time range.
return true;
}
}
// When the course is using format weeks we use the week's end date.
$format = course_get_format($activity->get_modinfo()->get_course());
// We should change this in MDL-60702.
if (method_exists($format, 'get_section_dates')) {
$dates = $format->get_section_dates($section);
// We need to consider the +2 hours added by get_section_dates.
// Avoid $starttime <= $dates->end because $starttime may be the start of the next week.
if ($starttime < ($dates->end - 7200) && $endtime >= ($dates->end - 7200)) {
return true;
} else {
return false;
}
}
if ($activity->sectionnum == 0) {
return false;
}
if (!$this->course->get_end() || !$this->course->get_start()) {
debugging('Activities which due date is in a time range can not be calculated ' .
'if the course doesn\'t have start and end date', DEBUG_DEVELOPER);
return false;
}
if (!course_format_uses_sections($this->course->get_course_data()->format)) {
// If it does not use sections and there are no availability conditions to access it it is available
// and we can not magically classify it into any other time range than this one.
return true;
}
// Split the course duration in the number of sections and consider the end of each section the due
// date of all activities contained in that section.
$formatoptions = $format->get_format_options();
if (!empty($formatoptions['numsections'])) {
$nsections = $formatoptions['numsections'];
} else {
// There are course format that use sections but without numsections, we fallback to the number
// of cached sections in get_section_info_all, not that accurate though.
$coursesections = $activity->get_modinfo()->get_section_info_all();
$nsections = count($coursesections);
if (isset($coursesections[0])) {
// We don't count section 0 if it exists.
$nsections--;
}
}
$courseduration = $this->course->get_end() - $this->course->get_start();
$sectionduration = round($courseduration / $nsections);
$activitysectionenddate = $this->course->get_start() + ($sectionduration * $activity->sectionnum);
if ($activitysectionenddate > $starttime && $activitysectionenddate <= $endtime) {
return true;
}
return false;
}
/**
* True if the activity is due or it has been closed during this period, false if during another period, null if no due time.
*
* It can be overwritten by activities that allow teachers to set a due date or a time close separately
* from Moodle availability system. Note that in most of the cases overwriting get_timeclose_field should
* be enough.
*
* Returns true or false if the time close date falls into the provided time range. Null otherwise.
*
* @param \cm_info $activity
* @param int $starttime
* @param int $endtime
* @param \stdClass|false $student
* @return null
*/
protected function activity_type_completed_by(\cm_info $activity, $starttime, $endtime, $student = false) {
$fieldname = $this->get_timeclose_field();
if (!$fieldname) {
// This activity type do not have its own availability control.
return null;
}
$this->fill_instance_data($activity);
$instance = $this->instancedata[$activity->instance];
if (!$instance->{$fieldname}) {
return null;
}
if ($starttime < $instance->{$fieldname} && $endtime >= $instance->{$fieldname}) {
return true;
}
return false;
}
/**
* Returns the name of the field that controls activity availability.
*
* Should be overwritten by activities that allow teachers to set a due date or a time close separately
* from Moodle availability system.
*
* Just 1 field will not be enough for all cases, but for the most simple ones without
* overrides and stuff like that.
*
* @return null|string
*/
protected function get_timeclose_field() {
return null;
}
/**
* Check if the activity/section should have been completed during the provided period according to its availability rules.
*
* @param \core_availability\info $info
* @param int $starttime
* @param int $endtime
* @return bool|null
*/
protected function availability_completed_by(\core_availability\info $info, $starttime, $endtime) {
$dateconditions = $info->get_availability_tree()->get_all_children('\availability_date\condition');
foreach ($dateconditions as $condition) {
// Availability API does not allow us to check from / to dates nicely, we need to be naughty.
$conditiondata = $condition->save();
if ($conditiondata->d === \availability_date\condition::DIRECTION_FROM &&
$conditiondata->t > $endtime) {
// Skip this activity if any 'from' date is later than the end time.
return false;
} else if ($conditiondata->d === \availability_date\condition::DIRECTION_UNTIL &&
($conditiondata->t < $starttime || $conditiondata->t > $endtime)) {
// Skip activity if any 'until' date is not in $starttime - $endtime range.
return false;
} else if ($conditiondata->d === \availability_date\condition::DIRECTION_UNTIL &&
$conditiondata->t < $endtime && $conditiondata->t > $starttime) {
return true;
}
}
// This can be interpreted as 'the activity was available but we don't know if its expected completion date
// was during this period.
return null;
}
/**
* Fills in activity instance data.
*
* @param \cm_info $cm
* @return void
*/
protected function fill_instance_data(\cm_info $cm) {
global $DB;
if (!isset($this->instancedata[$cm->instance])) {
$this->instancedata[$cm->instance] = $DB->get_record($this->get_activity_type(), array('id' => $cm->instance),
'*', MUST_EXIST);
}
}
/**
* Defines indicator type.
*
@@ -197,6 +197,10 @@ abstract class base {
$dataset = $this->calculate_indicators($sampleids, $samplesorigin, $indicators, $ranges);
if (empty($dataset)) {
return false;
}
// Now that we have the indicators in place we can add the time range indicators (and target if provided) to each of them.
$this->fill_dataset($dataset, $calculatedtarget);
@@ -15,7 +15,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Unit tests for course activities.
* Unit tests for activities completed by classification.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
@@ -25,13 +25,13 @@
defined('MOODLE_INTERNAL') || die();
/**
* Unit tests for course activities
* Unit tests for activities completed by classification.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_analytics_course_activities_testcase extends advanced_testcase {
class community_of_inquiry_activities_completed_by_testcase extends advanced_testcase {
/**
* availability_levels
@@ -59,8 +59,6 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
// Forum1 is ignored as section 0 does not count.
$forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
$courseman = new \core_analytics\course($course);
$modinfo = get_fast_modinfo($course, $stu1->id);
$cm = $modinfo->get_cm($forum->cmid);
@@ -93,20 +91,23 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
$cm = $modinfo->get_cm($forum->cmid);
$course = new \core_analytics\course($course);
list($indicator, $method) = $this->instantiate_indicator('mod_forum', $course);
// Condition from after provided end time.
$this->assertCount(0, $courseman->get_activities('forum', strtotime('2015-10-20 00:00:00 GMT'),
$this->assertCount(0, $method->invoke($indicator, strtotime('2015-10-20 00:00:00 GMT'),
strtotime('2015-10-21 00:00:00 GMT'), $stu1));
// Condition until before provided start time
$this->assertCount(0, $courseman->get_activities('forum', strtotime('2015-10-25 00:00:00 GMT'),
$this->assertCount(0, $method->invoke($indicator, strtotime('2015-10-25 00:00:00 GMT'),
strtotime('2015-10-26 00:00:00 GMT'), $stu1));
// Condition until after provided end time.
$this->assertCount(0, $courseman->get_activities('forum', strtotime('2015-10-22 00:00:00 GMT'),
$this->assertCount(0, $method->invoke($indicator, strtotime('2015-10-22 00:00:00 GMT'),
strtotime('2015-10-23 00:00:00 GMT'), $stu1));
// Condition until after provided start time and before provided end time.
$this->assertCount(1, $courseman->get_activities('forum', strtotime('2015-10-22 00:00:00 GMT'),
$this->assertCount(1, $method->invoke($indicator, strtotime('2015-10-22 00:00:00 GMT'),
strtotime('2015-10-25 00:00:00 GMT'), $stu1));
}
@@ -138,7 +139,8 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
$forum5 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
array('section' => 4));
$courseman = new \core_analytics\course($course);
$course = new \core_analytics\course($course);
list($indicator, $method) = $this->instantiate_indicator('mod_forum', $course);
$this->setUser($stu1);
@@ -146,10 +148,10 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
$second = $startdate + WEEKSECS;
$third = $startdate + (WEEKSECS * 2);
$forth = $startdate + (WEEKSECS * 3);
$this->assertCount(1, $courseman->get_activities('forum', $first, $first + WEEKSECS, $stu1));
$this->assertCount(1, $courseman->get_activities('forum', $second, $second + WEEKSECS, $stu1));
$this->assertCount(0, $courseman->get_activities('forum', $third, $third + WEEKSECS, $stu1));
$this->assertCount(2, $courseman->get_activities('forum', $forth, $forth + WEEKSECS, $stu1));
$this->assertCount(1, $method->invoke($indicator, $first, $first + WEEKSECS, $stu1));
$this->assertCount(1, $method->invoke($indicator, $second, $second + WEEKSECS, $stu1));
$this->assertCount(0, $method->invoke($indicator, $third, $third + WEEKSECS, $stu1));
$this->assertCount(2, $method->invoke($indicator, $forth, $forth + WEEKSECS, $stu1));
}
/**
@@ -189,7 +191,8 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
$forum6 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
array('section' => 12));
$courseman = new \core_analytics\course($course);
$course = new \core_analytics\course($course);
list($indicator, $method) = $this->instantiate_indicator('mod_forum', $course);
$this->setUser($stu1);
@@ -199,10 +202,10 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
$second = $startdate + $duration;
$third = $startdate + ($duration * 2);
$forth = $startdate + ($duration * 3);
$this->assertCount(1, $courseman->get_activities('forum', $first, $first + $duration, $stu1));
$this->assertCount(1, $courseman->get_activities('forum', $second, $second + $duration, $stu1));
$this->assertCount(1, $courseman->get_activities('forum', $third, $third + $duration, $stu1));
$this->assertCount(2, $courseman->get_activities('forum', $forth, $forth + $duration, $stu1));
$this->assertCount(1, $method->invoke($indicator, $first, $first + $duration, $stu1));
$this->assertCount(1, $method->invoke($indicator, $second, $second + $duration, $stu1));
$this->assertCount(1, $method->invoke($indicator, $third, $third + $duration, $stu1));
$this->assertCount(2, $method->invoke($indicator, $forth, $forth + $duration, $stu1));
// Split the course in as many parts as sections.
$duration = ($enddate - $startdate) / $numsections;
@@ -210,15 +213,94 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
// The -1 because section 1 start represents the course start.
$timeranges[$i] = $startdate + ($duration * ($i - 1));
}
$this->assertCount(1, $courseman->get_activities('forum', $timeranges[1], $timeranges[1] + $duration, $stu1));
$this->assertCount(1, $courseman->get_activities('forum', $timeranges[4], $timeranges[4] + $duration, $stu1));
$this->assertCount(1, $courseman->get_activities('forum', $timeranges[8], $timeranges[8] + $duration, $stu1));
$this->assertCount(1, $courseman->get_activities('forum', $timeranges[10], $timeranges[10] + $duration, $stu1));
$this->assertCount(1, $courseman->get_activities('forum', $timeranges[12], $timeranges[12] + $duration, $stu1));
$this->assertCount(1, $method->invoke($indicator, $timeranges[1], $timeranges[1] + $duration, $stu1));
$this->assertCount(1, $method->invoke($indicator, $timeranges[4], $timeranges[4] + $duration, $stu1));
$this->assertCount(1, $method->invoke($indicator, $timeranges[8], $timeranges[8] + $duration, $stu1));
$this->assertCount(1, $method->invoke($indicator, $timeranges[10], $timeranges[10] + $duration, $stu1));
$this->assertCount(1, $method->invoke($indicator, $timeranges[12], $timeranges[12] + $duration, $stu1));
// Nothing here.
$this->assertCount(0, $courseman->get_activities('forum', $timeranges[2], $timeranges[2] + $duration, $stu1));
$this->assertCount(0, $courseman->get_activities('forum', $timeranges[3], $timeranges[3] + $duration, $stu1));
$this->assertCount(0, $method->invoke($indicator, $timeranges[2], $timeranges[2] + $duration, $stu1));
$this->assertCount(0, $method->invoke($indicator, $timeranges[3], $timeranges[3] + $duration, $stu1));
}
/**
* test_get_activities_with_specific_restrictions
*
* @return void
*/
public function test_get_activities_with_specific_restrictions() {
list($course, $stu1) = $this->setup_course();
$end = strtotime('2015-10-24 00:00:00 GMT');
// 1 with time close, one without.
$params = array('course' => $course->id);
$assign1 = $this->getDataGenerator()->create_module('assign', $params);
$params['duedate'] = $end;
$assign2 = $this->getDataGenerator()->create_module('assign', $params);
// 1 with time close, one without.
$params = array('course' => $course->id);
$choice1 = $this->getDataGenerator()->create_module('choice', $params);
$params['timeclose'] = $end;
$choice1 = $this->getDataGenerator()->create_module('choice', $params);
// 1 with time close, one without.
$params = array('course' => $course->id);
$data1 = $this->getDataGenerator()->create_module('data', $params);
$params['timeavailableto'] = $end;
$data1 = $this->getDataGenerator()->create_module('data', $params);
// 1 with time close, one without.
$params = array('course' => $course->id);
$feedback1 = $this->getDataGenerator()->create_module('feedback', $params);
$params['timeclose'] = $end;
$feedback1 = $this->getDataGenerator()->create_module('feedback', $params);
// 1 with time close, one without.
$params = array('course' => $course->id);
$lesson1 = $this->getDataGenerator()->create_module('lesson', $params);
$params['deadline'] = $end;
$lesson1 = $this->getDataGenerator()->create_module('lesson', $params);
// 1 with time close, one without.
$params = array('course' => $course->id);
$quiz1 = $this->getDataGenerator()->create_module('quiz', $params);
$params['timeclose'] = $end;
$quiz1 = $this->getDataGenerator()->create_module('quiz', $params);
// 1 with time close, one without.
$params = array('course' => $course->id);
$scorm1 = $this->getDataGenerator()->create_module('scorm', $params);
$params['timeclose'] = $end;
$scorm1 = $this->getDataGenerator()->create_module('scorm', $params);
// 1 with time close, one without.
$params = array('course' => $course->id);
$workshop1 = $this->getDataGenerator()->create_module('workshop', $params);
$params['submissionend'] = $end;
$workshop1 = $this->getDataGenerator()->create_module('workshop', $params);
$course = new \core_analytics\course($course);
$activitytypes = array('mod_assign', 'mod_choice', 'mod_data', 'mod_feedback', 'mod_lesson',
'mod_quiz', 'mod_scorm', 'mod_workshop');
foreach ($activitytypes as $activitytype) {
list($indicator, $method) = $this->instantiate_indicator($activitytype, $course);
$message = $activitytype . ' activity type returned activities do not match expected size';
$this->assertCount(0, $method->invoke($indicator, strtotime('2015-10-20 00:00:00 GMT'),
strtotime('2015-10-21 00:00:00 GMT'), $stu1), $message);
$this->assertCount(0, $method->invoke($indicator, strtotime('2015-10-25 00:00:00 GMT'),
strtotime('2015-10-26 00:00:00 GMT'), $stu1), $message);
$this->assertCount(1, $method->invoke($indicator, strtotime('2015-10-22 00:00:00 GMT'),
strtotime('2015-10-25 00:00:00 GMT'), $stu1), $message);
}
}
/**
@@ -243,4 +325,27 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
return array($course, $stu1);
}
/**
* Returns the module cognitive depth indicator and the reflection method.
*
* @param string $modulename
* @param \core_analytics\course $course
* @return array
*/
private function instantiate_indicator($modulename, \core_analytics\course $course) {
$classname = '\\' . $modulename . '\analytics\indicator\cognitive_depth';
$indicator = new $classname();
$class = new ReflectionClass($indicator);
$property = $class->getProperty('course');
$property->setAccessible(true);
$property->setValue($indicator, $course);
$method = new ReflectionMethod($indicator, 'get_activities');
$method->setAccessible(true);
return array($indicator, $method);
}
}
+4 -6
View File
@@ -86,7 +86,6 @@ class core_analytics_course_testcase extends advanced_testcase {
$courseman = new \core_analytics\course($this->course->id);
$this->assertFalse($courseman->was_started());
$this->assertFalse($courseman->is_finished());
$this->assertFalse($courseman->is_valid());
// Nothing should change when assigning as teacher.
for ($i = 0; $i < 10; $i++) {
@@ -94,7 +93,8 @@ class core_analytics_course_testcase extends advanced_testcase {
$this->getDataGenerator()->enrol_user($user->id, $this->course->id, $this->teacherroleid);
}
$courseman = new \core_analytics\course($this->course->id);
$this->assertFalse($courseman->is_valid());
$this->assertFalse($courseman->was_started());
$this->assertFalse($courseman->is_finished());
// More students now.
for ($i = 0; $i < 10; $i++) {
@@ -102,7 +102,8 @@ class core_analytics_course_testcase extends advanced_testcase {
$this->getDataGenerator()->enrol_user($user->id, $this->course->id, $this->studentroleid);
}
$courseman = new \core_analytics\course($this->course->id);
$this->assertFalse($courseman->is_valid());
$this->assertFalse($courseman->was_started());
$this->assertFalse($courseman->is_finished());
// Valid start date unknown end date.
$this->course->startdate = gmmktime('0', '0', '0', 10, 24, 2015);
@@ -110,7 +111,6 @@ class core_analytics_course_testcase extends advanced_testcase {
$courseman = new \core_analytics\course($this->course->id);
$this->assertTrue($courseman->was_started());
$this->assertFalse($courseman->is_finished());
$this->assertFalse($courseman->is_valid());
// Valid start and end date.
$this->course->enddate = gmmktime('0', '0', '0', 8, 27, 2016);
@@ -118,7 +118,6 @@ class core_analytics_course_testcase extends advanced_testcase {
$courseman = new \core_analytics\course($this->course->id);
$this->assertTrue($courseman->was_started());
$this->assertTrue($courseman->is_finished());
$this->assertTrue($courseman->is_valid());
// Valid start and ongoing course.
$this->course->enddate = gmmktime('0', '0', '0', 8, 27, 2286);
@@ -126,7 +125,6 @@ class core_analytics_course_testcase extends advanced_testcase {
$courseman = new \core_analytics\course($this->course->id);
$this->assertTrue($courseman->was_started());
$this->assertFalse($courseman->is_finished());
$this->assertFalse($courseman->is_valid());
}
/**
+1
View File
@@ -24,6 +24,7 @@
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/fixtures/test_indicator_max.php');
require_once(__DIR__ . '/fixtures/test_indicator_min.php');
require_once(__DIR__ . '/fixtures/test_indicator_fullname.php');
+1 -1
View File
@@ -121,7 +121,7 @@ $string['didntgetusersfromldap'] = "Did not get any users from LDAP -- error? --
$string['gotcountrecordsfromldap'] = "Got {\$a} records from LDAP\n";
$string['ldapnotconfigured'] = 'The LDAP host url is currently not configured';
$string['morethanoneuser'] = 'Strange! More than one user record found in ldap. Only using the first one.';
$string['needbcmath'] = 'You need the BCMath extension to use grace logins with Active Directory';
$string['needbcmath'] = 'You need the BCMath extension to use expired password checking with Active Directory.';
$string['needmbstring'] = 'You need the mbstring extension to change passwords in Active Directory';
$string['nodnforusername'] = 'Error in user_update_password(). No DN for: {$a->username}';
$string['noemail'] = 'Tried to send you an email but failed!';
+4 -31
View File
@@ -49,41 +49,14 @@ class block_calendar_month extends block_base {
$this->content->footer = '';
$courseid = $this->page->course->id;
$issite = ($courseid == SITEID);
$course = null;
$courses = null;
$categories = null;
if ($issite) {
// Being displayed at site level. This will cause the filter to fall back to auto-detecting
// the list of courses it will be grabbing events from.
$course = get_site();
$courses = calendar_get_default_courses();
if ($this->page->context->contextlevel === CONTEXT_COURSECAT) {
// Restrict to categories, and their parents, and the courses that the user is enrolled in within those
// categories.
$categories = array_keys($this->page->categories);
$courses = array_filter($courses, function($course) use ($categories) {
return array_search($course->category, $categories) !== false;
});
}
} else {
// Forcibly filter events to include only those from the particular course we are in.
$course = $this->page->course;
$courses = [$course->id => $course];
}
$categoryid = ($this->page->context->contextlevel === CONTEXT_COURSECAT) ? $this->page->category->id : null;
$calendar = \calendar_information::create(time(), $courseid, $categoryid);
list($data, $template) = calendar_get_view($calendar, 'mini');
$renderer = $this->page->get_renderer('core_calendar');
$calendar = new calendar_information();
$calendar->set_sources($course, $courses, $this->page->category);
list($data, $template) = calendar_get_view($calendar, 'mini');
$this->content->text .= $renderer->render_from_template($template, $data);
if (!$issite) {
if ($this->page->course->id != SITEID) {
$this->content->text .= $renderer->event_filter();
}
@@ -45,38 +45,28 @@ class block_calendar_upcoming extends block_base {
}
$this->content = new stdClass;
$this->content->text = '';
$this->content->footer = '';
$renderer = $this->page->get_renderer('core_calendar');
$courseid = $this->page->course->id;
$issite = ($courseid == SITEID);
if ($issite) {
// Being displayed at site level. This will cause the filter to fall back to auto-detecting
// the list of courses it will be grabbing events from.
$course = get_site();
$courses = calendar_get_default_courses();
} else {
// Forcibly filter events to include only those from the particular course we are in.
$course = $this->page->course;
$courses = [$course->id => $course];
}
$calendar = new calendar_information(0, 0, 0, time());
$calendar->set_sources($course, $courses);
$categoryid = ($this->page->context->contextlevel === CONTEXT_COURSECAT) ? $this->page->category->id : null;
$calendar = \calendar_information::create(time(), $courseid, $categoryid);
list($data, $template) = calendar_get_view($calendar, 'upcoming_mini');
if (empty($data->events)) {
$this->content->text = '<div class="post">'. get_string('noupcomingevents', 'calendar').'</div>';
} else {
$this->content->text .= $renderer->render_from_template($template, $data);
$renderer = $this->page->get_renderer('core_calendar');
$this->content->text .= $renderer->render_from_template($template, $data);
$url = new \moodle_url('/calendar/view.php', ['view' => 'upcoming']);
if ($courseid != SITEID) {
$url->param('course', $this->page->course->id);
} else if (!empty($categoryid)) {
$url->param('category', $this->page->category->id);
}
$this->content->footer = '<div class="gotocal">
<a href="'.$CFG->wwwroot.'/calendar/view.php?view=upcoming&amp;course='.$courseid.'">'.
get_string('gotocalendar', 'calendar').'</a>...</div>';
$this->content->footer = html_writer::div(
html_writer::link($url, get_string('gotocalendar', 'block_calendar_upcoming')),
'gotocal'
);
return $this->content;
}
}
@@ -24,4 +24,5 @@
$string['calendar_upcoming:addinstance'] = 'Add a new upcoming events block';
$string['calendar_upcoming:myaddinstance'] = 'Add a new upcoming events block to Dashboard';
$string['gotocalendar'] = 'Go to calendar...';
$string['pluginname'] = 'Upcoming events';
+5 -1
View File
@@ -37,7 +37,11 @@ class block_html extends block_base {
}
function specialization() {
$this->title = isset($this->config->title) ? format_string($this->config->title) : format_string(get_string('newhtmlblock', 'block_html'));
if (isset($this->config->title)) {
$this->title = $this->title = format_string($this->config->title, true, ['context' => $this->context]);
} else {
$this->title = get_string('newhtmlblock', 'block_html');
}
}
function instance_allow_multiple() {
+1 -1
View File
@@ -1 +1 @@
define(["jquery","core/ajax","core/str","core/templates","core/notification","core/custom_interaction_events","core/modal_events","core/modal_factory","core_calendar/modal_event_form","core_calendar/summary_modal","core_calendar/repository","core_calendar/events","core_calendar/view_manager","core_calendar/crud","core_calendar/selectors"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o){var p={ROOT:"[data-region='calendar']",DAY:"[data-region='day']",EVENT_ITEM:"[data-region='event-item']",EVENT_LINK:"[data-action='view-event']",NEW_EVENT_BUTTON:"[data-action='new-event-button']",DAY_CONTENT:"[data-region='day-content']",LOADING_ICON:".loading-icon",VIEW_DAY_LINK:"[data-action='view-day-link']",CALENDAR_MONTH_WRAPPER:".calendarwrapper",COURSE_SELECTOR:'select[name="course"]',TODAY:".today"},q=function(a){var b="type"+a;return c.get_string(b,"core_calendar").then(function(a){return a})},r=function(a){k.getEventById(a).then(function(b){if(!b.event)throw new Error("Error encountered while trying to fetch calendar event with ID: "+a);var c=b.event;return q(c.eventtype).then(function(a){return c.eventtype=a,c})}).then(function(a){var b={title:a.name,type:j.TYPE,body:d.render("core_calendar/event_summary_body",a),templateContext:{canedit:a.canedit,candelete:a.candelete,isactionevent:a.isactionevent,url:a.url}};return h.create(b)}).done(function(a){a.getRoot().on(g.hidden,function(){a.destroy()}),a.show()}).fail(e.exception)},s=function(b,c,f,g){var h=null,i=g.attr("data-day-timestamp");f&&(h=f.attr("data-day-timestamp")),f&&h==i||d.render("core/loading",{}).then(function(a,b){g.find(p.DAY_CONTENT).addClass("hidden"),d.appendNodeContents(g,a,b),f&&(f.find(p.DAY_CONTENT).addClass("hidden"),d.appendNodeContents(f,a,b))}).then(function(){return k.updateEventStartDay(c,i)}).then(function(){a("body").trigger(l.eventMoved,[c,f,g])}).always(function(){var a=g.find(p.LOADING_ICON);if(g.find(p.DAY_CONTENT).removeClass("hidden"),d.replaceNode(a,"",""),f){var b=f.find(p.LOADING_ICON);f.find(p.DAY_CONTENT).removeClass("hidden"),d.replaceNode(b,"","")}}).fail(e.exception)},t=function(b,c){var d=a("body");d.on(l.created,function(){m.reloadCurrentMonth(b)}),d.on(l.deleted,function(){m.reloadCurrentMonth(b)}),d.on(l.updated,function(){m.reloadCurrentMonth(b)}),d.on(l.editActionEvent,function(a,b){window.location.assign(b)}),d.on(l.moveEvent,s),d.on(l.eventMoved,function(){m.reloadCurrentMonth(b)}),c.then(function(a){d.on(l.editEvent,function(c,d){var e=b.find(o.wrapper);a.setEventId(d),a.setContextId(e.data("contextId")),a.show()})}).fail(e.exception)},u=function(b){b.on("click",p.EVENT_ITEM,function(b){b.preventDefault(),b.stopPropagation();var c=a(b.target),d=null,e=c.closest(p.EVENT_LINK);d=e.length?e.data("eventId"):c.find(p.EVENT_LINK).data("eventId"),r(d)}),b.on("change",p.COURSE_SELECTOR,function(){var c=a(this),d=c.val();m.reloadCurrentMonth(b,d,null).then(function(){return b.find(p.COURSE_SELECTOR).val(d)}).fail(e.exception)});var c=n.registerEventFormModal(b);t(b,c),b.on("click",p.DAY,function(b){var d=a(b.target);if(!d.is(p.VIEW_DAY_LINK)){var f=a(this).attr("data-new-event-timestamp");c.then(function(a){var b=d.closest(o.wrapper);a.setCourseId(b.data("courseid"));var c=b.data("categoryid");"undefined"!=typeof c&&a.setCategoryId(c),a.setContextId(b.data("contextId")),a.setStartTime(f),a.show()}).fail(e.exception),b.preventDefault()}})};return{init:function(b){b=a(b),m.init(b),u(b)}}});
define(["jquery","core/ajax","core/str","core/templates","core/notification","core/custom_interaction_events","core/modal_events","core/modal_factory","core_calendar/modal_event_form","core_calendar/summary_modal","core_calendar/repository","core_calendar/events","core_calendar/view_manager","core_calendar/crud","core_calendar/selectors"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o){var p={ROOT:"[data-region='calendar']",DAY:"[data-region='day']",NEW_EVENT_BUTTON:"[data-action='new-event-button']",DAY_CONTENT:"[data-region='day-content']",LOADING_ICON:".loading-icon",VIEW_DAY_LINK:"[data-action='view-day-link']",CALENDAR_MONTH_WRAPPER:".calendarwrapper",TODAY:".today"},q=function(b,c,f,g){var h=null,i=g.attr("data-day-timestamp");f&&(h=f.attr("data-day-timestamp")),f&&h==i||d.render("core/loading",{}).then(function(a,b){g.find(p.DAY_CONTENT).addClass("hidden"),d.appendNodeContents(g,a,b),f&&(f.find(p.DAY_CONTENT).addClass("hidden"),d.appendNodeContents(f,a,b))}).then(function(){return k.updateEventStartDay(c,i)}).then(function(){a("body").trigger(l.eventMoved,[c,f,g])}).always(function(){var a=g.find(p.LOADING_ICON);if(g.find(p.DAY_CONTENT).removeClass("hidden"),d.replaceNode(a,"",""),f){var b=f.find(p.LOADING_ICON);f.find(p.DAY_CONTENT).removeClass("hidden"),d.replaceNode(b,"","")}}).fail(e.exception)},r=function(b,c){var d=a("body");d.on(l.created,function(){m.reloadCurrentMonth(b)}),d.on(l.deleted,function(){m.reloadCurrentMonth(b)}),d.on(l.updated,function(){m.reloadCurrentMonth(b)}),d.on(l.editActionEvent,function(a,b){window.location.assign(b)}),d.on(l.moveEvent,q),d.on(l.eventMoved,function(){m.reloadCurrentMonth(b)}),n.registerEditListeners(b,c)},s=function(b){b.on("change",o.elements.courseSelector,function(){var c=a(this),d=c.val();m.reloadCurrentMonth(b,d,null).then(function(){return b.find(o.elements.courseSelector).val(d)}).fail(e.exception)});var c=n.registerEventFormModal(b);r(b,c),b.on("click",p.DAY,function(b){var d=a(b.target);if(!d.is(p.VIEW_DAY_LINK)){var f=a(this).attr("data-new-event-timestamp");c.then(function(a){var b=d.closest(o.wrapper);a.setCourseId(b.data("courseid"));var c=b.data("categoryid");"undefined"!=typeof c&&a.setCategoryId(c),a.setContextId(b.data("contextId")),a.setStartTime(f),a.show()}).fail(e.exception),b.preventDefault()}})};return{init:function(b){b=a(b),m.init(b),s(b)}}});
+1 -1
View File
@@ -1 +1 @@
define(["jquery","core_calendar/selectors","core_calendar/events","core_calendar/view_manager"],function(a,b,c,d){var e=function(b){var d=a("body"),e="."+b.attr("id");d.on(c.created+e,b,f),d.on(c.deleted+e,b,f),d.on(c.updated+e,b,f),d.on(c.eventMoved+e,b,f)},f=function(b){var e=b.data,f=a("body"),g="."+e.attr("id");e.is(":visible")?d.reloadCurrentMonth(e):(f.on(c.created+g),f.on(c.deleted+g),f.on(c.updated+g),f.on(c.eventMoved+g))},g=function(d){a("body").on(c.filterChanged,function(a,c){var e=d.find(b.eventType[c.type]);e.toggleClass("calendar_event_"+c.type,!c.hidden)})};return{init:function(b){b=a(b),d.init(b),g(b),e(b)}}});
define(["jquery","core_calendar/selectors","core_calendar/events","core_calendar/view_manager"],function(a,b,c,d){var e=function(b){var d=a("body"),e="."+b.attr("id");d.on(c.created+e,b,f),d.on(c.deleted+e,b,f),d.on(c.updated+e,b,f),d.on(c.eventMoved+e,b,f)},f=function(b){var e=b.data,f=a("body"),g="."+e.attr("id");e.is(":visible")?d.reloadCurrentMonth(e):(f.off(c.created+g),f.off(c.deleted+g),f.off(c.updated+g),f.off(c.eventMoved+g))},g=function(e){a("body").on(c.filterChanged,function(a,c){var d=e.find(b.eventType[c.type]);d.toggleClass("calendar_event_"+c.type,!c.hidden)});var f="."+e.attr("id");a("body").on("change"+f,b.elements.courseSelector,function(){if(e.is(":visible")){var b=a(this),c=b.val(),g=null;d.reloadCurrentMonth(e,c,g)}else a("body").off("change"+f)})};return{init:function(b){b=a(b),d.init(b),g(b),e(b)}}});
+1 -1
View File
@@ -1 +1 @@
define(["jquery","core/str","core/notification","core_calendar/selectors","core_calendar/events","core_calendar/view_manager","core_calendar/repository","core/modal_factory","core_calendar/modal_event_form","core/modal_events","core_calendar/crud"],function(a,b,c,d,e,f,g,h,i,j,k){var l=function(b,g){var h=a("body");k.registerEventFormModal(b),k.registerRemove(b);var i="reloadCurrent"+g.charAt(0).toUpperCase()+g.slice(1);h.on(e.created,function(){f[i](b)}),h.on(e.deleted,function(){f[i](b)}),h.on(e.updated,function(){f[i](b)}),b.on("change",d.courseSelector,function(){var e=a(this),g=e.val();f[i](b,g,null).then(function(){return b.find(d.courseSelector).val(g)}).fail(c.exception)}),h.on(e.filterChanged,function(a,c){var e=b.find(d.eventType[c.type]);1==c.hidden?e.addClass("hidden"):e.removeClass("hidden")})};return{init:function(b,c){b=a(b),f.init(b,c),l(b,c)}}});
define(["jquery","core/str","core/notification","core_calendar/selectors","core_calendar/events","core_calendar/view_manager","core_calendar/repository","core/modal_factory","core_calendar/modal_event_form","core/modal_events","core_calendar/crud"],function(a,b,c,d,e,f,g,h,i,j,k){var l=function(b,g){var h=a("body");k.registerRemove(b);var i="reloadCurrent"+g.charAt(0).toUpperCase()+g.slice(1);h.on(e.created,function(){f[i](b)}),h.on(e.deleted,function(){f[i](b)}),h.on(e.updated,function(){f[i](b)}),b.on("change",d.courseSelector,function(){var e=a(this),g=e.val();f[i](b,g,null).then(function(){return b.find(d.courseSelector).val(g)}).then(function(){window.history.pushState({},"","?view=upcoming&course="+g)}).fail(c.exception)}),h.on(e.filterChanged,function(a,c){var e=b.find(d.eventType[c.type]);1==c.hidden?e.addClass("hidden"):e.removeClass("hidden")});var j=k.registerEventFormModal(b);k.registerEditListeners(b,j)};return{init:function(b,c){b=a(b),f.init(b,c),l(b,c)}}});
+1 -1
View File
@@ -1 +1 @@
define(["jquery","core/str","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","core/modal_factory","core/modal_events","core_calendar/modal_event_form","core_calendar/repository","core_calendar/events","core_calendar/modal_delete","core_calendar/selectors"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){function n(d,e,f){var i=[{key:"deleteevent",component:"calendar"}];f=parseInt(f,10);var m,n=f>1;n?(i.push({key:"confirmeventseriesdelete",component:"calendar",param:{name:e,count:f}}),m=g.create({type:l.TYPE})):(i.push({key:"confirmeventdelete",component:"calendar",param:e}),m=g.create({type:g.types.SAVE_CANCEL})),m.then(function(a){a.show()}).fail(c.exception);var o=b.get_strings(i),p=a.when(o,m).then(function(b,e){return e.setTitle(b[0]),e.setBody(b[1]),n||e.setSaveButtonText(b[0]),e.getRoot().on(h.save,function(){j.deleteEvent(d,!1).then(function(){a("body").trigger(k.deleted,[d,!1])})["catch"](c.exception)}),e.getRoot().on(k.deleteAll,function(){j.deleteEvent(d,!0).then(function(){a("body").trigger(k.deleted,[d,!0])})["catch"](c.exception)}),e}).fail(c.exception);return p}function o(b){b.on("click",m.actions.remove,function(b){var c=a(this).closest(m.eventItem),d=c.data("eventId"),e=c.data("eventTitle"),f=c.data("eventCount");n(d,e,f),b.preventDefault()})}var p=function(b){var d=g.create({type:i.TYPE,large:!0});return b.on("click",m.actions.create,function(a){d.then(function(a){var c=b.find(m.wrapper),d=c.data("categoryid");"undefined"!=typeof d&&a.setCategoryId(d);var e=b.find(m.today),f=b.find(m.day);!e.length&&f.length&&a.setStartTime(f.data("newEventTimestamp")),a.setContextId(c.data("contextId")),a.setCourseId(c.data("courseid")),a.show()}).fail(c.exception),a.preventDefault()}),b.on("click",m.actions.edit,function(b){b.preventDefault();var e=a(b.currentTarget),f=e.closest(m.wrapper),g=e.closest(m.eventItem);d.then(function(a){a.setEventId(g.data("eventId")),a.setContextId(f.data("contextId")),a.show()}).fail(c.exception)}),d};return{registerRemove:o,registerEventFormModal:p}});
define(["jquery","core/str","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","core/modal_factory","core/modal_events","core_calendar/modal_event_form","core_calendar/repository","core_calendar/events","core_calendar/modal_delete","core_calendar/selectors"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){function n(d,e,f){var i=[{key:"deleteevent",component:"calendar"}];f=parseInt(f,10);var m,n=f>1;n?(i.push({key:"confirmeventseriesdelete",component:"calendar",param:{name:e,count:f}}),m=g.create({type:l.TYPE})):(i.push({key:"confirmeventdelete",component:"calendar",param:e}),m=g.create({type:g.types.SAVE_CANCEL})),m.then(function(a){a.show()}).fail(c.exception);var o=b.get_strings(i),p=a.when(o,m).then(function(b,e){return e.setTitle(b[0]),e.setBody(b[1]),n||e.setSaveButtonText(b[0]),e.getRoot().on(h.save,function(){j.deleteEvent(d,!1).then(function(){a("body").trigger(k.deleted,[d,!1])})["catch"](c.exception)}),e.getRoot().on(k.deleteAll,function(){j.deleteEvent(d,!0).then(function(){a("body").trigger(k.deleted,[d,!0])})["catch"](c.exception)}),e}).fail(c.exception);return p}function o(b){b.on("click",m.actions.remove,function(b){var c=a(this).closest(m.eventItem),d=c.data("eventId"),e=c.data("eventTitle"),f=c.data("eventCount");n(d,e,f),b.preventDefault()})}function p(b,d){return d.then(function(c){a("body").on(k.editEvent,function(a,d){var e=b.find(m.wrapper);c.setEventId(d),c.setContextId(e.data("contextId")),c.show(),a.stopImmediatePropagation()})}).fail(c.exception),d}var q=function(b){var d=g.create({type:i.TYPE,large:!0});return b.on("click",m.actions.create,function(a){d.then(function(a){var c=b.find(m.wrapper),d=c.data("categoryid");"undefined"!=typeof d&&a.setCategoryId(d);var e=b.find(m.today),f=b.find(m.day);!e.length&&f.length&&a.setStartTime(f.data("newEventTimestamp")),a.setContextId(c.data("contextId")),a.setCourseId(c.data("courseid")),a.show()}).fail(c.exception),a.preventDefault()}),b.on("click",m.actions.edit,function(b){b.preventDefault();var e=a(b.currentTarget),f=e.closest(m.wrapper),g=e.closest(m.eventItem);d.then(function(a){a.setEventId(g.data("eventId")),a.setContextId(f.data("contextId")),a.show(),b.stopImmediatePropagation()}).fail(c.exception)}),d};return{registerRemove:o,registerEditListeners:p,registerEventFormModal:q}});
+1 -1
View File
@@ -1 +1 @@
define(["jquery","core/event","core/str","core/notification","core/templates","core/custom_interaction_events","core/modal","core/modal_registry","core/fragment","core_calendar/events","core_calendar/repository"],function(a,b,c,d,e,f,g,h,i,j,k){var l=!1,m={SAVE_BUTTON:'[data-action="save"]',LOADING_ICON_CONTAINER:'[data-region="loading-icon-container"]'},n=function(a){g.call(this,a),this.eventId=null,this.startTime=null,this.courseId=null,this.categoryId=null,this.contextId=null,this.reloadingBody=!1,this.reloadingTitle=!1,this.saveButton=this.getFooter().find(m.SAVE_BUTTON)};return n.TYPE="core_calendar-modal_event_form",n.prototype=Object.create(g.prototype),n.prototype.constructor=n,n.prototype.setContextId=function(a){this.contextId=a},n.prototype.getContextId=function(){return this.contextId},n.prototype.setCourseId=function(a){this.courseId=a},n.prototype.getCourseId=function(){return this.courseId},n.prototype.setCategoryId=function(a){this.categoryId=a},n.prototype.getCategoryId=function(){return this.categoryId},n.prototype.hasCourseId=function(){return null!==this.courseId},n.prototype.hasCategoryId=function(){return null!==this.categoryId},n.prototype.setEventId=function(a){this.eventId=a},n.prototype.getEventId=function(){return this.eventId},n.prototype.hasEventId=function(){return null!==this.eventId},n.prototype.setStartTime=function(a){this.startTime=a},n.prototype.getStartTime=function(){return this.startTime},n.prototype.hasStartTime=function(){return null!==this.startTime},n.prototype.getForm=function(){return this.getBody().find("form")},n.prototype.disableButtons=function(){this.saveButton.prop("disabled",!0)},n.prototype.enableButtons=function(){this.saveButton.prop("disabled",!1)},n.prototype.reloadTitleContent=function(){return this.reloadingTitle?this.titlePromise:(this.reloadingTitle=!0,this.hasEventId()?this.titlePromise=c.get_string("editevent","calendar"):this.titlePromise=c.get_string("newevent","calendar"),this.titlePromise.then(function(a){return this.setTitle(a),a}.bind(this)).always(function(){this.reloadingTitle=!1}.bind(this)).fail(d.exception),this.titlePromise)},n.prototype.reloadBodyContent=function(a){if(this.reloadingBody)return this.bodyPromise;this.reloadingBody=!0,this.disableButtons();var b={};return this.hasEventId()&&(b.eventid=this.getEventId()),this.hasStartTime()&&(b.starttime=this.getStartTime()),this.hasCourseId()&&(b.courseid=this.getCourseId()),this.hasCategoryId()&&(b.categoryid=this.getCategoryId()),"undefined"!=typeof a&&(b.formdata=a),this.bodyPromise=i.loadFragment("calendar","event_form",this.getContextId(),b),this.setBody(this.bodyPromise),this.bodyPromise.then(function(){this.enableButtons()}.bind(this)).fail(d.exception).always(function(){this.reloadingBody=!1}.bind(this)).fail(d.exception),this.bodyPromise},n.prototype.reloadAllContent=function(){return a.when(this.reloadTitleContent(),this.reloadBodyContent())},n.prototype.show=function(){this.reloadAllContent(),g.prototype.show.call(this)},n.prototype.hide=function(){g.prototype.hide.call(this),this.setEventId(null),this.setStartTime(null),this.setCourseId(null),this.setCategoryId(null)},n.prototype.getFormData=function(){return this.getForm().serialize()},n.prototype.save=function(){var b=this.saveButton.find(m.LOADING_ICON_CONTAINER);b.removeClass("hidden"),this.disableButtons();var c=this.getFormData();return k.submitCreateUpdateForm(c).then(function(b){return b.validationerror?void this.reloadBodyContent(c):(this.hide(),void(this.hasEventId()?a("body").trigger(j.updated,[b.event]):a("body").trigger(j.created,[b.event])))}.bind(this)).always(function(){b.addClass("hidden"),this.enableButtons()}.bind(this)).fail(d.exception)},n.prototype.registerEventListeners=function(){g.prototype.registerEventListeners.call(this),this.getModal().on(f.events.activate,m.SAVE_BUTTON,function(a,b){this.getForm().submit(),b.originalEvent.preventDefault(),a.stopPropagation()}.bind(this)),this.getModal().on("submit",function(a){this.save(),a.preventDefault(),a.stopPropagation()}.bind(this))},l||(h.register(n.TYPE,n,"calendar/modal_event_form"),l=!0),n});
define(["jquery","core/event","core/str","core/notification","core/templates","core/custom_interaction_events","core/modal","core/modal_registry","core/fragment","core_calendar/events","core_calendar/repository"],function(a,b,c,d,e,f,g,h,i,j,k){var l=!1,m={SAVE_BUTTON:'[data-action="save"]',LOADING_ICON_CONTAINER:'[data-region="loading-icon-container"]'},n=function(a){g.call(this,a),this.eventId=null,this.startTime=null,this.courseId=null,this.categoryId=null,this.contextId=null,this.reloadingBody=!1,this.reloadingTitle=!1,this.saveButton=this.getFooter().find(m.SAVE_BUTTON)};return n.TYPE="core_calendar-modal_event_form",n.prototype=Object.create(g.prototype),n.prototype.constructor=n,n.prototype.setContextId=function(a){this.contextId=a},n.prototype.getContextId=function(){return this.contextId},n.prototype.setCourseId=function(a){this.courseId=a},n.prototype.getCourseId=function(){return this.courseId},n.prototype.setCategoryId=function(a){this.categoryId=a},n.prototype.getCategoryId=function(){return this.categoryId},n.prototype.hasCourseId=function(){return null!==this.courseId},n.prototype.hasCategoryId=function(){return null!==this.categoryId},n.prototype.setEventId=function(a){this.eventId=a},n.prototype.getEventId=function(){return this.eventId},n.prototype.hasEventId=function(){return null!==this.eventId},n.prototype.setStartTime=function(a){this.startTime=a},n.prototype.getStartTime=function(){return this.startTime},n.prototype.hasStartTime=function(){return null!==this.startTime},n.prototype.getForm=function(){return this.getBody().find("form")},n.prototype.disableButtons=function(){this.saveButton.prop("disabled",!0)},n.prototype.enableButtons=function(){this.saveButton.prop("disabled",!1)},n.prototype.reloadTitleContent=function(){return this.reloadingTitle?this.titlePromise:(this.reloadingTitle=!0,this.hasEventId()?this.titlePromise=c.get_string("editevent","calendar"):this.titlePromise=c.get_string("newevent","calendar"),this.titlePromise.then(function(a){return this.setTitle(a),a}.bind(this)).always(function(){this.reloadingTitle=!1}.bind(this)).fail(d.exception),this.titlePromise)},n.prototype.reloadBodyContent=function(a){if(this.reloadingBody)return this.bodyPromise;this.reloadingBody=!0,this.disableButtons();var b={};return this.hasEventId()&&(b.eventid=this.getEventId()),this.hasStartTime()&&(b.starttime=this.getStartTime()),this.hasCourseId()&&(b.courseid=this.getCourseId()),this.hasCategoryId()&&(b.categoryid=this.getCategoryId()),"undefined"!=typeof a&&(b.formdata=a),this.bodyPromise=i.loadFragment("calendar","event_form",this.getContextId(),b),this.setBody(this.bodyPromise),this.bodyPromise.then(function(){this.enableButtons()}.bind(this)).fail(d.exception).always(function(){this.reloadingBody=!1}.bind(this)).fail(d.exception),this.bodyPromise},n.prototype.reloadAllContent=function(){return a.when(this.reloadTitleContent(),this.reloadBodyContent())},n.prototype.show=function(){this.reloadAllContent(),g.prototype.show.call(this)},n.prototype.hide=function(){g.prototype.hide.call(this),this.setEventId(null),this.setStartTime(null),this.setCourseId(null),this.setCategoryId(null)},n.prototype.getFormData=function(){return this.getForm().serialize()},n.prototype.save=function(){var b=this.saveButton.find(m.LOADING_ICON_CONTAINER);b.removeClass("hidden"),this.disableButtons();var c=this.getFormData();return k.submitCreateUpdateForm(c).then(function(b){if(b.validationerror)return void this.reloadBodyContent(c);var d=this.hasEventId();this.hide(),d?a("body").trigger(j.updated,[b.event]):a("body").trigger(j.created,[b.event])}.bind(this)).always(function(){b.addClass("hidden"),this.enableButtons()}.bind(this)).fail(d.exception)},n.prototype.registerEventListeners=function(){g.prototype.registerEventListeners.call(this),this.getModal().on(f.events.activate,m.SAVE_BUTTON,function(a,b){this.getForm().submit(),b.originalEvent.preventDefault(),a.stopPropagation()}.bind(this)),this.getModal().on("submit",function(a){this.save(),a.preventDefault(),a.stopPropagation()}.bind(this))},l||(h.register(n.TYPE,n,"calendar/modal_event_form"),l=!0),n});
+1 -1
View File
@@ -1 +1 @@
define(["jquery","core/ajax"],function(a,b){var c=function(a,c){"undefined"==typeof c&&(c=!1);var d={methodname:"core_calendar_delete_calendar_events",args:{events:[{eventid:a,repeat:c}]}};return b.call([d])[0]},d=function(a){var c={methodname:"core_calendar_get_calendar_event_by_id",args:{eventid:a}};return b.call([c])[0]},e=function(a){var c={methodname:"core_calendar_submit_create_update_form",args:{formdata:a}};return b.call([c])[0]},f=function(a,c,d,e,f){var g={methodname:"core_calendar_get_calendar_monthly_view",args:{year:a,month:c,courseid:d,categoryid:e,includenavigation:f}};return b.call([g])[0]},g=function(a,c,d,e,f){var g={methodname:"core_calendar_get_calendar_day_view",args:{year:a,month:c,day:d,courseid:e,categoryid:f}};return b.call([g])[0]},h=function(a,c){var d={methodname:"core_calendar_update_event_start_day",args:{eventid:a,daytimestamp:c}};return b.call([d])[0]},i=function(a){var c={methodname:"core_calendar_get_calendar_upcoming_view",args:{courseid:a}};return b.call([c])[0]};return{getEventById:d,deleteEvent:c,updateEventStartDay:h,submitCreateUpdateForm:e,getCalendarMonthData:f,getCalendarDayData:g,getCalendarUpcomingData:i}});
define(["jquery","core/ajax"],function(a,b){var c=function(a,c){"undefined"==typeof c&&(c=!1);var d={methodname:"core_calendar_delete_calendar_events",args:{events:[{eventid:a,repeat:c}]}};return b.call([d])[0]},d=function(a){var c={methodname:"core_calendar_get_calendar_event_by_id",args:{eventid:a}};return b.call([c])[0]},e=function(a){var c={methodname:"core_calendar_submit_create_update_form",args:{formdata:a}};return b.call([c])[0]},f=function(a,c,d,e,f){var g={methodname:"core_calendar_get_calendar_monthly_view",args:{year:a,month:c,courseid:d,categoryid:e,includenavigation:f}};return b.call([g])[0]},g=function(a,c,d,e,f){var g={methodname:"core_calendar_get_calendar_day_view",args:{year:a,month:c,day:d,courseid:e,categoryid:f}};return b.call([g])[0]},h=function(a,c){var d={methodname:"core_calendar_update_event_start_day",args:{eventid:a,daytimestamp:c}};return b.call([d])[0]},i=function(a,c){var d={methodname:"core_calendar_get_calendar_upcoming_view",args:{courseid:a,categoryid:c}};return b.call([d])[0]};return{getEventById:d,deleteEvent:c,updateEventStartDay:h,submitCreateUpdateForm:e,getCalendarMonthData:f,getCalendarDayData:g,getCalendarUpcomingData:i}});
+1 -1
View File
@@ -1 +1 @@
define([],function(){return{eventFilterItem:"[data-action='filter-event-type']",eventType:{site:"[data-eventtype-site]",category:"[data-eventtype-category]",course:"[data-eventtype-course]",group:"[data-eventtype-group]",user:"[data-eventtype-user]"},popoverType:{site:"[data-popover-eventtype-site]",category:"[data-popover-eventtype-category]",course:"[data-popover-eventtype-course]",group:"[data-popover-eventtype-group]",user:"[data-popover-eventtype-user]"},calendarPeriods:{month:"[data-period='month']"},courseSelector:'select[name="course"]',actions:{create:'[data-action="new-event-button"]',edit:'[data-action="edit"]',remove:'[data-action="delete"]'},today:".today",day:'[data-region="day"]',wrapper:".calendarwrapper",eventItem:'[data-type="event"]'}});
define([],function(){return{eventFilterItem:"[data-action='filter-event-type']",eventType:{site:"[data-eventtype-site]",category:"[data-eventtype-category]",course:"[data-eventtype-course]",group:"[data-eventtype-group]",user:"[data-eventtype-user]"},popoverType:{site:"[data-popover-eventtype-site]",category:"[data-popover-eventtype-category]",course:"[data-popover-eventtype-course]",group:"[data-popover-eventtype-group]",user:"[data-popover-eventtype-user]"},calendarPeriods:{month:"[data-period='month']"},courseSelector:'select[name="course"]',actions:{create:'[data-action="new-event-button"]',edit:'[data-action="edit"]',remove:'[data-action="delete"]',viewEvent:'[data-action="view-event"]'},elements:{courseSelector:'select[name="course"]'},today:".today",day:'[data-region="day"]',wrapper:".calendarwrapper",eventItem:'[data-type="event"]',links:{navLink:".calendarwrapper .arrow_link",eventLink:"[data-region='event-item']"},containers:{loadingIcon:'[data-region="overlay-icon-container"]'}}});
+1 -1
View File
@@ -1 +1 @@
define(["jquery","core/templates","core/notification","core_calendar/repository","core_calendar/events","core_calendar/selectors"],function(a,b,c,d,e,f){var g={CALENDAR_NAV_LINK:".calendarwrapper .arrow_link",LOADING_ICON_CONTAINER:'[data-region="overlay-icon-container"]'},h=function(b){b=a(b),b.on("click",g.CALENDAR_NAV_LINK,function(c){var d=b.find(f.wrapper),e=d.data("view"),g=d.data("courseid"),h=d.data("categoryid"),i=a(c.currentTarget);"month"===e?(j(b,i.attr("href"),i.data("year"),i.data("month"),g,h),c.preventDefault()):"day"===e&&(n(b,i.attr("href"),i.data("year"),i.data("month"),i.data("day"),g,h),c.preventDefault())})},i=function(g,h,i,j,k,l){o(g),l=l||g.find(f.wrapper),M.util.js_pending([g.get("id"),h,i,j].join("-"));var m=g.data("includenavigation");return d.getCalendarMonthData(h,i,j,k,m).then(function(a){return b.render(g.attr("data-template"),a)}).then(function(a,c){return b.replaceNode(l,a,c)}).then(function(){a("body").trigger(e.viewUpdated)}).always(function(){return M.util.js_complete([g.get("id"),h,i,j].join("-")),p(g)}).fail(c.exception)},j=function(b,c,d,f,g,h){return i(b,d,f,g,h).then(function(){return c.length&&"#"!==c&&window.history.pushState({},"",c),arguments}).then(function(){return a("body").trigger(e.monthChanged,[d,f,g,h]),arguments})},k=function(a,b,c){var d=a.find(f.wrapper).data("year"),e=a.find(f.wrapper).data("month");return"undefined"==typeof b&&(b=a.find(f.wrapper).data("courseid")),"undefined"==typeof c&&(c=a.find(f.wrapper).data("categoryid")),i(a,d,e,b,c)},l=function(g,h,i,j,k,l,m){o(g),m=m||g.find(f.wrapper),M.util.js_pending([g.get("id"),h,i,j,k,l].join("-"));var n=g.data("includenavigation");return d.getCalendarDayData(h,i,j,k,l,n).then(function(a){return b.render(g.attr("data-template"),a)}).then(function(a,c){return b.replaceNode(m,a,c)}).then(function(){a("body").trigger(e.viewUpdated)}).always(function(){return M.util.js_complete([g.get("id"),h,i,j,k,l].join("-")),p(g)}).fail(c.exception)},m=function(a,b,c){var d=a.find(f.wrapper),e=d.data("year"),g=d.data("month"),h=d.data("day");return b||(b=a.find(f.wrapper).data("courseid")),"undefined"==typeof c&&(c=a.find(f.wrapper).data("categoryid")),l(a,e,g,h,b,c)},n=function(b,c,d,f,g,h,i){return l(b,d,f,g,h,i).then(function(){return c.length&&"#"!==c&&window.history.pushState({},"",c),arguments}).then(function(){return a("body").trigger(e.dayChanged,[d,f,g,h,i]),arguments})},o=function(a){var b=a.find(g.LOADING_ICON_CONTAINER);b.removeClass("hidden")},p=function(a){var b=a.find(g.LOADING_ICON_CONTAINER);b.addClass("hidden")},q=function(g,h){o(g);var i=g.find(f.wrapper);return h||(h=g.find(f.wrapper).data("courseid")),d.getCalendarUpcomingData(h).then(function(a){return b.render(g.attr("data-template"),a)}).then(function(a,c){return window.history.replaceState(null,null,"?view=upcoming&course="+h),b.replaceNode(i,a,c)}).then(function(){a("body").trigger(e.viewUpdated)}).always(function(){return p(g)}).fail(c.exception)};return{init:function(a){h(a)},reloadCurrentMonth:k,changeMonth:j,refreshMonthContent:i,reloadCurrentDay:m,changeDay:n,refreshDayContent:l,reloadCurrentUpcoming:q}});
define(["jquery","core/templates","core/str","core/notification","core_calendar/repository","core_calendar/events","core_calendar/selectors","core/modal_factory","core/modal_events","core_calendar/summary_modal"],function(a,b,c,d,e,f,g,h,i,j){var k=function(b){b=a(b),b.on("click",g.links.eventLink,function(b){var c,d=a(b.target),e=null;c=d.is(g.actions.viewEvent)?d:d.closest(g.actions.viewEvent),e=c.length?c.data("eventId"):d.find(g.actions.viewEvent).data("eventId"),e&&(b.preventDefault(),b.stopPropagation(),w(e))}),b.on("click",g.links.navLink,function(c){var d=b.find(g.wrapper),e=d.data("view"),f=d.data("courseid"),h=d.data("categoryid"),i=a(c.currentTarget);"month"===e?(m(b,i.attr("href"),i.data("year"),i.data("month"),f,h),c.preventDefault()):"day"===e&&(q(b,i.attr("href"),i.data("year"),i.data("month"),i.data("day"),f,h),c.preventDefault())})},l=function(c,h,i,j,k,l){r(c),l=l||c.find(g.wrapper),M.util.js_pending([c.get("id"),h,i,j].join("-"));var m=c.data("includenavigation");return e.getCalendarMonthData(h,i,j,k,m).then(function(a){return b.render(c.attr("data-template"),a)}).then(function(a,c){return b.replaceNode(l,a,c)}).then(function(){a("body").trigger(f.viewUpdated)}).always(function(){return M.util.js_complete([c.get("id"),h,i,j].join("-")),s(c)}).fail(d.exception)},m=function(b,c,d,e,g,h){return l(b,d,e,g,h).then(function(){return c.length&&"#"!==c&&window.history.pushState({},"",c),arguments}).then(function(){return a("body").trigger(f.monthChanged,[d,e,g,h]),arguments})},n=function(a,b,c){var d=a.find(g.wrapper).data("year"),e=a.find(g.wrapper).data("month");return"undefined"==typeof b&&(b=a.find(g.wrapper).data("courseid")),"undefined"==typeof c&&(c=a.find(g.wrapper).data("categoryid")),l(a,d,e,b,c)},o=function(c,h,i,j,k,l,m){r(c),m=m||c.find(g.wrapper),M.util.js_pending([c.get("id"),h,i,j,k,l].join("-"));var n=c.data("includenavigation");return e.getCalendarDayData(h,i,j,k,l,n).then(function(a){return b.render(c.attr("data-template"),a)}).then(function(a,c){return b.replaceNode(m,a,c)}).then(function(){a("body").trigger(f.viewUpdated)}).always(function(){return M.util.js_complete([c.get("id"),h,i,j,k,l].join("-")),s(c)}).fail(d.exception)},p=function(a,b,c){var d=a.find(g.wrapper),e=d.data("year"),f=d.data("month"),h=d.data("day");return b||(b=a.find(g.wrapper).data("courseid")),"undefined"==typeof c&&(c=a.find(g.wrapper).data("categoryid")),o(a,e,f,h,b,c)},q=function(b,c,d,e,g,h,i){return o(b,d,e,g,h,i).then(function(){return c.length&&"#"!==c&&window.history.pushState({},"",c),arguments}).then(function(){return a("body").trigger(f.dayChanged,[d,e,g,h,i]),arguments})},r=function(a){var b=a.find(g.containers.loadingIcon);b.removeClass("hidden")},s=function(a){var b=a.find(g.containers.loadingIcon);b.addClass("hidden")},t=function(c,h,i){r(c);var j=c.find(g.wrapper);return"undefined"==typeof h&&(h=c.find(g.wrapper).data("courseid")),"undefined"==typeof i&&(i=c.find(g.wrapper).data("categoryid")),e.getCalendarUpcomingData(h,i).then(function(a){return b.render(c.attr("data-template"),a)}).then(function(a,c){return b.replaceNode(j,a,c)}).then(function(){a("body").trigger(f.viewUpdated)}).always(function(){return s(c)}).fail(d.exception)},u=function(a){switch(a){case"user":return"user";case"site":return"site";case"group":return"group";case"category":return"category";default:return"course"}},v=function(a){return"calendar_event_"+u(a)},w=function(a){var c="";e.getEventById(a).then(function(b){if(!b.event)throw new Error("Error encountered while trying to fetch calendar event with ID: "+a);var d=b.event;return c=v(d.eventtype),x(d.eventtype).then(function(a){return d.eventtype=a,d})}).then(function(a){var d={title:a.name,type:j.TYPE,body:b.render("core_calendar/event_summary_body",a),templateContext:{canedit:a.canedit,candelete:a.candelete,headerclasses:c,isactionevent:a.isactionevent,url:a.url}};return h.create(d)}).done(function(a){a.getRoot().on(i.hidden,function(){a.destroy()}),a.show()}).fail(d.exception)},x=function(a){var b="type"+u(a);return c.get_string(b,"core_calendar").then(function(a){return a})};return{init:function(a){k(a)},reloadCurrentMonth:n,changeMonth:m,refreshMonthContent:l,reloadCurrentDay:p,changeDay:q,refreshDayContent:o,reloadCurrentUpcoming:t}});
+3 -99
View File
@@ -63,77 +63,14 @@ define([
var SELECTORS = {
ROOT: "[data-region='calendar']",
DAY: "[data-region='day']",
EVENT_ITEM: "[data-region='event-item']",
EVENT_LINK: "[data-action='view-event']",
NEW_EVENT_BUTTON: "[data-action='new-event-button']",
DAY_CONTENT: "[data-region='day-content']",
LOADING_ICON: '.loading-icon',
VIEW_DAY_LINK: "[data-action='view-day-link']",
CALENDAR_MONTH_WRAPPER: ".calendarwrapper",
COURSE_SELECTOR: 'select[name="course"]',
TODAY: '.today',
};
/**
* Get the event type lang string.
*
* @param {String} eventType The event type.
* @return {promise} The lang string promise.
*/
var getEventType = function(eventType) {
var lang = 'type' + eventType;
return Str.get_string(lang, 'core_calendar').then(function(langStr) {
return langStr;
});
};
/**
* Render the event summary modal.
*
* @param {Number} eventId The calendar event id.
*/
var renderEventSummaryModal = function(eventId) {
// Calendar repository promise.
CalendarRepository.getEventById(eventId).then(function(getEventResponse) {
if (!getEventResponse.event) {
throw new Error('Error encountered while trying to fetch calendar event with ID: ' + eventId);
}
var eventData = getEventResponse.event;
return getEventType(eventData.eventtype).then(function(eventType) {
eventData.eventtype = eventType;
return eventData;
});
}).then(function(eventData) {
// Build the modal parameters from the event data.
var modalParams = {
title: eventData.name,
type: SummaryModal.TYPE,
body: Templates.render('core_calendar/event_summary_body', eventData),
templateContext: {
canedit: eventData.canedit,
candelete: eventData.candelete,
isactionevent: eventData.isactionevent,
url: eventData.url
}
};
// Create the modal.
return ModalFactory.create(modalParams);
}).done(function(modal) {
// Handle hidden event.
modal.getRoot().on(ModalEvents.hidden, function() {
// Destroy when hidden.
modal.destroy();
});
// Finally, render the modal!
modal.show();
}).fail(Notification.exception);
};
/**
* Handler for the drag and drop move event. Provides a loading indicator
* while the request is sent to the server to update the event start date.
@@ -226,19 +163,7 @@ define([
CalendarViewManager.reloadCurrentMonth(root);
});
eventFormModalPromise
.then(function(modal) {
// When something within the calendar tells us the user wants
// to edit an event then show the event form modal.
body.on(CalendarEvents.editEvent, function(e, eventId) {
var calendarWrapper = root.find(CalendarSelectors.wrapper);
modal.setEventId(eventId);
modal.setContextId(calendarWrapper.data('contextId'));
modal.show();
});
return;
})
.fail(Notification.exception);
CalendarCrud.registerEditListeners(root, eventFormModalPromise);
};
/**
@@ -247,34 +172,13 @@ define([
* @param {object} root The calendar root element
*/
var registerEventListeners = function(root) {
// Bind click events to event links.
root.on('click', SELECTORS.EVENT_ITEM, function(e) {
e.preventDefault();
// We've handled the event so stop it from bubbling
// and causing the day click handler to fire.
e.stopPropagation();
var target = $(e.target);
var eventId = null;
var eventLink = target.closest(SELECTORS.EVENT_LINK);
if (eventLink.length) {
eventId = eventLink.data('eventId');
} else {
eventId = target.find(SELECTORS.EVENT_LINK).data('eventId');
}
renderEventSummaryModal(eventId);
});
root.on('change', SELECTORS.COURSE_SELECTOR, function() {
root.on('change', CalendarSelectors.elements.courseSelector, function() {
var selectElement = $(this);
var courseId = selectElement.val();
CalendarViewManager.reloadCurrentMonth(root, courseId, null)
.then(function() {
// We need to get the selector again because the content has changed.
return root.find(SELECTORS.COURSE_SELECTOR).val(courseId);
return root.find(CalendarSelectors.elements.courseSelector).val(courseId);
})
.fail(Notification.exception);
});
+18 -4
View File
@@ -69,10 +69,10 @@ function(
} else {
// The root has been removed.
// Remove all events in the namespace.
body.on(CalendarEvents.created + namespace);
body.on(CalendarEvents.deleted + namespace);
body.on(CalendarEvents.updated + namespace);
body.on(CalendarEvents.eventMoved + namespace);
body.off(CalendarEvents.created + namespace);
body.off(CalendarEvents.deleted + namespace);
body.off(CalendarEvents.updated + namespace);
body.off(CalendarEvents.eventMoved + namespace);
}
};
@@ -82,6 +82,20 @@ function(
daysWithEvent.toggleClass('calendar_event_' + data.type, !data.hidden);
});
var namespace = '.' + root.attr('id');
$('body').on('change' + namespace, CalendarSelectors.elements.courseSelector, function() {
if (root.is(':visible')) {
var selectElement = $(this);
var courseId = selectElement.val();
var categoryId = null;
CalendarViewManager.reloadCurrentMonth(root, courseId, categoryId);
} else {
$('body').off('change' + namespace);
}
});
};
return {
+8 -1
View File
@@ -51,7 +51,6 @@ define([
var registerEventListeners = function(root, type) {
var body = $('body');
CalendarCrud.registerEventFormModal(root);
CalendarCrud.registerRemove(root);
var reloadFunction = 'reloadCurrent' + type.charAt(0).toUpperCase() + type.slice(1);
@@ -74,6 +73,11 @@ define([
// We need to get the selector again because the content has changed.
return root.find(CalendarSelectors.courseSelector).val(courseId);
})
.then(function() {
window.history.pushState({}, '', '?view=upcoming&course=' + courseId);
return;
})
.fail(Notification.exception);
});
@@ -85,6 +89,9 @@ define([
daysWithEvent.removeClass('hidden');
}
});
var eventFormPromise = CalendarCrud.registerEventFormModal(root);
CalendarCrud.registerEditListeners(root, eventFormPromise);
};
return {
+30
View File
@@ -198,6 +198,8 @@ function(
modal.setContextId(calendarWrapper.data('contextId'));
modal.show();
e.stopImmediatePropagation();
return;
}).fail(Notification.exception);
});
@@ -223,8 +225,36 @@ function(
});
}
/**
* Register the listeners required to edit the event.
*
* @param {jQuery} root
* @param {Promise} eventFormModalPromise
* @returns {Promise}
*/
function registerEditListeners(root, eventFormModalPromise) {
eventFormModalPromise
.then(function(modal) {
// When something within the calendar tells us the user wants
// to edit an event then show the event form modal.
$('body').on(CalendarEvents.editEvent, function(e, eventId) {
var calendarWrapper = root.find(CalendarSelectors.wrapper);
modal.setEventId(eventId);
modal.setContextId(calendarWrapper.data('contextId'));
modal.show();
e.stopImmediatePropagation();
});
return;
})
.fail(Notification.exception);
return eventFormModalPromise;
}
return {
registerRemove: registerRemove,
registerEditListeners: registerEditListeners,
registerEventFormModal: registerEventFormModal
};
});
+6 -3
View File
@@ -423,12 +423,15 @@ define([
this.reloadBodyContent(formData);
return;
} else {
// Check whether this was a new event or not.
// The hide function unsets the form data so grab this before the hide.
var isExisting = this.hasEventId();
// No problemo! Our work here is done.
this.hide();
// Trigger the appropriate calendar event so that the view can
// be updated.
if (this.hasEventId()) {
// Trigger the appropriate calendar event so that the view can be updated.
if (isExisting) {
$('body').trigger(CalendarEvents.updated, [response.event]);
} else {
$('body').trigger(CalendarEvents.created, [response.event]);
+3 -1
View File
@@ -164,13 +164,15 @@ define(['jquery', 'core/ajax'], function($, Ajax) {
*
* @method getCalendarUpcomingData
* @param {Number} courseid The course id.
* @param {Number} categoryid The category id.
* @return {promise} Resolved with the month view data.
*/
var getCalendarUpcomingData = function(courseid) {
var getCalendarUpcomingData = function(courseid, categoryid) {
var request = {
methodname: 'core_calendar_get_calendar_upcoming_view',
args: {
courseid: courseid,
categoryid: categoryid,
}
};
+11
View File
@@ -46,10 +46,21 @@ define([], function() {
create: '[data-action="new-event-button"]',
edit: '[data-action="edit"]',
remove: '[data-action="delete"]',
viewEvent: '[data-action="view-event"]',
},
elements: {
courseSelector: 'select[name="course"]',
},
today: '.today',
day: '[data-region="day"]',
wrapper: '.calendarwrapper',
eventItem: '[data-type="event"]',
links: {
navLink: '.calendarwrapper .arrow_link',
eventLink: "[data-region='event-item']",
},
containers: {
loadingIcon: '[data-region="overlay-icon-container"]',
},
};
});
+147 -13
View File
@@ -24,24 +24,27 @@
define([
'jquery',
'core/templates',
'core/str',
'core/notification',
'core_calendar/repository',
'core_calendar/events',
'core_calendar/selectors',
'core/modal_factory',
'core/modal_events',
'core_calendar/summary_modal',
], function(
$,
Templates,
Str,
Notification,
CalendarRepository,
CalendarEvents,
CalendarSelectors
CalendarSelectors,
ModalFactory,
ModalEvents,
SummaryModal
) {
var SELECTORS = {
CALENDAR_NAV_LINK: ".calendarwrapper .arrow_link",
LOADING_ICON_CONTAINER: '[data-region="overlay-icon-container"]'
};
/**
* Register event listeners for the module.
*
@@ -50,7 +53,38 @@ define([
var registerEventListeners = function(root) {
root = $(root);
root.on('click', SELECTORS.CALENDAR_NAV_LINK, function(e) {
// Bind click events to event links.
root.on('click', CalendarSelectors.links.eventLink, function(e) {
var target = $(e.target);
var eventId = null;
var eventLink;
if (target.is(CalendarSelectors.actions.viewEvent)) {
eventLink = target;
} else {
eventLink = target.closest(CalendarSelectors.actions.viewEvent);
}
if (eventLink.length) {
eventId = eventLink.data('eventId');
} else {
eventId = target.find(CalendarSelectors.actions.viewEvent).data('eventId');
}
if (eventId) {
// A link was found. Show the modal.
e.preventDefault();
// We've handled the event so stop it from bubbling
// and causing the day click handler to fire.
e.stopPropagation();
renderEventSummaryModal(eventId);
}
});
root.on('click', CalendarSelectors.links.navLink, function(e) {
var wrapper = root.find(CalendarSelectors.wrapper);
var view = wrapper.data('view');
var courseId = wrapper.data('courseid');
@@ -249,7 +283,7 @@ define([
* @method startLoading
*/
var startLoading = function(root) {
var loadingIconContainer = root.find(SELECTORS.LOADING_ICON_CONTAINER);
var loadingIconContainer = root.find(CalendarSelectors.containers.loadingIcon);
loadingIconContainer.removeClass('hidden');
};
@@ -261,7 +295,7 @@ define([
* @method stopLoading
*/
var stopLoading = function(root) {
var loadingIconContainer = root.find(SELECTORS.LOADING_ICON_CONTAINER);
var loadingIconContainer = root.find(CalendarSelectors.containers.loadingIcon);
loadingIconContainer.addClass('hidden');
};
@@ -271,23 +305,27 @@ define([
*
* @param {object} root The container element.
* @param {Number} courseId The course id.
* @param {Number} categoryId The id of the category whose events are shown
* @return {promise}
*/
var reloadCurrentUpcoming = function(root, courseId) {
var reloadCurrentUpcoming = function(root, courseId, categoryId) {
startLoading(root);
var target = root.find(CalendarSelectors.wrapper);
if (!courseId) {
if (typeof courseId === 'undefined') {
courseId = root.find(CalendarSelectors.wrapper).data('courseid');
}
return CalendarRepository.getCalendarUpcomingData(courseId)
if (typeof categoryId === 'undefined') {
categoryId = root.find(CalendarSelectors.wrapper).data('categoryid');
}
return CalendarRepository.getCalendarUpcomingData(courseId, categoryId)
.then(function(context) {
return Templates.render(root.attr('data-template'), context);
})
.then(function(html, js) {
window.history.replaceState(null, null, '?view=upcoming&course=' + courseId);
return Templates.replaceNode(target, html, js);
})
.then(function() {
@@ -300,6 +338,102 @@ define([
.fail(Notification.exception);
};
/**
* Convert the given event type into one of either user, site,
* group, category, or course.
*
* @param {String} eventType The calendar event type
* @return {String}
*/
var normaliseEventType = function(eventType) {
switch (eventType) {
case 'user':
return 'user';
case 'site':
return 'site';
case 'group':
return 'group';
case 'category':
return 'category';
default:
return 'course';
}
};
/**
* Get the CSS class to apply for the given event type.
*
* @param {String} eventType The calendar event type
* @return {String}
*/
var getEventTypeClassFromType = function(eventType) {
return 'calendar_event_' + normaliseEventType(eventType);
};
/**
* Render the event summary modal.
*
* @param {Number} eventId The calendar event id.
*/
var renderEventSummaryModal = function(eventId) {
var typeClass = '';
// Calendar repository promise.
CalendarRepository.getEventById(eventId).then(function(getEventResponse) {
if (!getEventResponse.event) {
throw new Error('Error encountered while trying to fetch calendar event with ID: ' + eventId);
}
var eventData = getEventResponse.event;
typeClass = getEventTypeClassFromType(eventData.eventtype);
return getEventType(eventData.eventtype).then(function(eventType) {
eventData.eventtype = eventType;
return eventData;
});
}).then(function(eventData) {
// Build the modal parameters from the event data.
var modalParams = {
title: eventData.name,
type: SummaryModal.TYPE,
body: Templates.render('core_calendar/event_summary_body', eventData),
templateContext: {
canedit: eventData.canedit,
candelete: eventData.candelete,
headerclasses: typeClass,
isactionevent: eventData.isactionevent,
url: eventData.url
}
};
// Create the modal.
return ModalFactory.create(modalParams);
}).done(function(modal) {
// Handle hidden event.
modal.getRoot().on(ModalEvents.hidden, function() {
// Destroy when hidden.
modal.destroy();
});
// Finally, render the modal!
modal.show();
}).fail(Notification.exception);
};
/**
* Get the event type lang string.
*
* @param {String} eventType The event type.
* @return {promise} The lang string promise.
*/
var getEventType = function(eventType) {
var lang = 'type' + normaliseEventType(eventType);
return Str.get_string(lang, 'core_calendar').then(function(langStr) {
return langStr;
});
};
return {
init: function(root) {
registerEventListeners(root);
+2 -2
View File
@@ -74,7 +74,7 @@ class calendar_day_exporter extends exporter {
],
'defaulteventcontext' => [
'type' => PARAM_INT,
'default' => null,
'default' => 0,
],
'filter_selector' => [
'type' => PARAM_RAW,
@@ -238,7 +238,7 @@ class calendar_day_exporter extends exporter {
* @return string The html code for the course filter selector.
*/
protected function get_course_filter_selector(renderer_base $output) {
$langstr = get_string('upcomingeventsfor', 'calendar');
$langstr = get_string('dayviewfor', 'calendar');
return $output->course_filter_selector($this->url, $langstr, $this->calendar->course->id);
}
+24 -3
View File
@@ -74,6 +74,10 @@ class calendar_event_exporter extends event_exporter_base {
'type' => PARAM_TEXT,
'optional' => true
];
$values['draggable'] = [
'type' => PARAM_BOOL,
'default' => false
];
return $values;
}
@@ -90,6 +94,10 @@ class calendar_event_exporter extends event_exporter_base {
$values = parent::get_other_values($output);
$event = $this->event;
// By default all events that can be edited are
// draggable.
$values['draggable'] = $values['canedit'];
if ($moduleproxy = $event->get_course_module()) {
$modulename = $moduleproxy->get('modname');
$moduleid = $moduleproxy->get('id');
@@ -213,17 +221,30 @@ class calendar_event_exporter extends event_exporter_base {
* @return array
*/
protected function get_module_timestamp_limits($event) {
global $DB;
$values = [];
$mapper = container::get_event_mapper();
$starttime = $event->get_times()->get_start_time();
$modname = $event->get_course_module()->get('modname');
$modid = $event->get_course_module()->get('instance');
$moduleinstance = $DB->get_record($modname, ['id' => $modid]);
list($min, $max) = component_callback(
'mod_' . $event->get_course_module()->get('modname'),
'mod_' . $modname,
'core_calendar_get_valid_event_timestart_range',
[$mapper->from_event_to_legacy_event($event)],
[null, null]
[$mapper->from_event_to_legacy_event($event), $moduleinstance],
[false, false]
);
// The callback will return false for either of the
// min or max cutoffs to indicate that there are no
// valid timestart values. In which case the event is
// not draggable.
if ($min === false || $max === false) {
return ['draggable' => false];
}
if ($min) {
$values = array_merge($values, $this->get_module_timestamp_min_limit($starttime, $min));
}
+11 -1
View File
@@ -74,7 +74,7 @@ class calendar_upcoming_exporter extends exporter {
],
'defaulteventcontext' => [
'type' => PARAM_INT,
'default' => null,
'default' => 0,
],
'filter_selector' => [
'type' => PARAM_RAW,
@@ -82,6 +82,11 @@ class calendar_upcoming_exporter extends exporter {
'courseid' => [
'type' => PARAM_INT,
],
'categoryid' => [
'type' => PARAM_INT,
'optional' => true,
'default' => 0,
],
];
}
@@ -127,6 +132,11 @@ class calendar_upcoming_exporter extends exporter {
}
$return['filter_selector'] = $this->get_course_filter_selector($output);
$return['courseid'] = $this->calendar->courseid;
if ($this->calendar->categoryid) {
$return['categoryid'] = $this->calendar->categoryid;
}
return $return;
}
-90
View File
@@ -147,20 +147,10 @@ class day_exporter extends exporter {
'navigation' => [
'type' => PARAM_RAW,
],
'popovertitle' => [
'type' => PARAM_RAW,
'default' => '',
],
'haslastdayofevent' => [
'type' => PARAM_BOOL,
'default' => false,
],
'filter_selector' => [
'type' => PARAM_RAW,
],
'new_event_button' => [
'type' => PARAM_RAW,
],
];
}
@@ -190,8 +180,6 @@ class day_exporter extends exporter {
'previousperiod' => $this->get_previous_day_timestamp($daytimestamp),
'nextperiod' => $this->get_next_day_timestamp($daytimestamp),
'navigation' => $this->get_navigation(),
'filter_selector' => $this->get_course_filter_selector($output),
'new_event_button' => $this->get_new_event_button(),
'viewdaylink' => $this->url->out(false),
];
@@ -277,82 +265,4 @@ class day_exporter extends exporter {
'time' => $this->calendar->time,
]);
}
/**
* Get the course filter selector.
*
* This is a temporary solution, this code will be removed by MDL-60096.
*
* @param renderer_base $output
* @return string The html code for the course filter selector.
*/
protected function get_course_filter_selector(renderer_base $output) {
global $CFG;
// TODO remove this code on MDL-60096.
if (!isloggedin() or isguestuser()) {
return '';
}
if (has_capability('moodle/calendar:manageentries', \context_system::instance()) && !empty($CFG->calendar_adminseesall)) {
$courses = get_courses('all', 'c.shortname', 'c.id, c.shortname');
} else {
$courses = enrol_get_my_courses();
}
unset($courses[SITEID]);
$courseoptions = array();
$courseoptions[SITEID] = get_string('fulllistofcourses');
foreach ($courses as $course) {
$coursecontext = \context_course::instance($course->id);
$courseoptions[$course->id] = format_string($course->shortname, true, array('context' => $coursecontext));
}
if ($this->calendar->courseid !== SITEID) {
$selected = $this->calendar->courseid;
} else {
$selected = '';
}
$courseurl = new moodle_url($this->url);
$courseurl->remove_params('course');
$select = new \single_select($courseurl, 'courseselect', $courseoptions, $selected, null);
$select->class = 'm-r-1';
$label = get_string('dayviewfor', 'calendar');
if ($label !== null) {
$select->set_label($label);
} else {
$select->set_label(get_string('listofcourses'), array('class' => 'accesshide'));
}
return $output->render($select);
}
/**
* Get the course filter selector.
*
* This is a temporary solution, this code will be removed by MDL-60096.
*
* @return string The html code for the course filter selector.
*/
protected function get_new_event_button() {
// TODO remove this code on MDL-60096.
$output = \html_writer::start_tag('div', array('class' => 'buttons'));
$output .= \html_writer::start_tag('form',
array('action' => CALENDAR_URL . 'event.php', 'method' => 'get'));
$output .= \html_writer::start_tag('div');
$output .= \html_writer::empty_tag('input',
array('type' => 'hidden', 'name' => 'action', 'value' => 'new'));
$output .= \html_writer::empty_tag('input',
array('type' => 'hidden', 'name' => 'course', 'value' => $this->calendar->courseid));
$output .= \html_writer::empty_tag('input',
array('type' => 'hidden', 'name' => 'time', 'value' => $this->calendar->time));
$attributes = array('type' => 'submit', 'value' => get_string('newevent', 'calendar'),
'class' => 'btn btn-secondary');
$output .= \html_writer::empty_tag('input', $attributes);
$output .= \html_writer::end_tag('div');
$output .= \html_writer::end_tag('form');
$output .= \html_writer::end_tag('div');
return $output;
}
}
+22 -5
View File
@@ -72,8 +72,8 @@ class footer_options_exporter extends exporter {
* @return \single_button The export calendar button html.
*/
protected function get_export_calendar_button() {
$exportcalendarurl = new moodle_url('/calendar/export.php', ['course' => $this->calendar->course->id]);
return new \single_button($exportcalendarurl, get_string('exportcalendar', 'calendar'));
$exportcalendarurl = new moodle_url('/calendar/export.php', $this->get_link_params());
return new \single_button($exportcalendarurl, get_string('exportcalendar', 'calendar'), 'get');
}
/**
@@ -83,13 +83,30 @@ class footer_options_exporter extends exporter {
*/
protected function get_manage_subscriptions_button() {
if (calendar_user_can_add_event($this->calendar->course)) {
$managesubscriptionurl = new moodle_url('/calendar/managesubscriptions.php',
['course' => $this->calendar->course->id]);
$managesubscriptionurl = new moodle_url('/calendar/managesubscriptions.php', $this->get_link_params());
return new \single_button($managesubscriptionurl,
get_string('managesubscriptions', 'calendar'));
get_string('managesubscriptions', 'calendar'), 'get');
}
}
/**
* Get the list of URL parameters for calendar links.
*
* @return array
*/
protected function get_link_params() {
$params = [];
if (SITEID !== $this->calendar->course->id) {
$params['course'] = $this->calendar->course->id;
} else if (null !== $this->calendar->categoryid && $this->calendar->categoryid > 0) {
$params['category'] = $this->calendar->categoryid;
} else {
$params['course'] = SITEID;
}
return $params;
}
/**
* Get the additional values to inject while exporting.
*
+1 -1
View File
@@ -171,7 +171,7 @@ class month_exporter extends exporter {
],
'defaulteventcontext' => [
'type' => PARAM_INT,
'default' => null,
'default' => 0,
],
];
}
-18
View File
@@ -67,28 +67,10 @@ class week_day_exporter extends day_exporter {
protected static function define_other_properties() {
$return = parent::define_other_properties();
$return = array_merge($return, [
'timestamp' => [
'type' => PARAM_INT,
],
'neweventtimestamp' => [
'type' => PARAM_INT,
],
'viewdaylink' => [
'type' => PARAM_URL,
'optional' => true,
],
'calendareventtypes' => [
'type' => PARAM_RAW,
'multiple' => true,
],
'popovertitle' => [
'type' => PARAM_RAW,
'default' => '',
],
'haslastdayofevent' => [
'type' => PARAM_BOOL,
'default' => false,
],
]);
return $return;
+36 -10
View File
@@ -231,9 +231,12 @@ class api {
event_interface $event,
\DateTimeInterface $startdate
) {
global $DB;
$mapper = container::get_event_mapper();
$legacyevent = $mapper->from_event_to_legacy_event($event);
$hascoursemodule = !empty($event->get_course_module());
$moduleinstance = null;
$starttime = $event->get_times()->get_start_time()->setDate(
$startdate->format('Y'),
$startdate->format('n'),
@@ -241,15 +244,38 @@ class api {
);
if ($hascoursemodule) {
$legacyevent->timestart = $starttime->getTimestamp();
// If this event is from an activity then we need to call
// the activity callback to let it validate that the changes
// to the event are correct.
component_callback(
'mod_' . $event->get_course_module()->get('modname'),
'core_calendar_validate_event_timestart',
[$legacyevent]
$moduleinstance = $DB->get_record(
$event->get_course_module()->get('modname'),
['id' => $event->get_course_module()->get('instance')],
'*',
MUST_EXIST
);
$legacyevent->timestart = $starttime->getTimestamp();
// If there is a timestart range callback implemented then we can
// use the values returned from the valid timestart range to apply
// some default validation on the event's timestart value to ensure
// that it falls within the specified range.
list($min, $max) = component_callback(
'mod_' . $event->get_course_module()->get('modname'),
'core_calendar_get_valid_event_timestart_range',
[$legacyevent, $moduleinstance],
[false, false]
);
// If the callback returns false for either value it means that
// there is no valid time start range.
if ($min === false || $max === false) {
throw new \moodle_exception('The start day of this event can not be modified');
}
if ($min && $legacyevent->timestart < $min[0]) {
throw new \moodle_exception($min[1]);
}
if ($max && $legacyevent->timestart > $max[0]) {
throw new \moodle_exception($max[1]);
}
}
// This function does our capability checks.
@@ -262,7 +288,7 @@ class api {
// We don't want to call the event update callback if the user isn't allowed
// to modify course modules because depending on the callback it can make
// some changes that would be considered security issues, such as updating the
// due date for and assignment.
// due date for an assignment.
if ($hascoursemodule && calendar_edit_event_allowed($legacyevent, true)) {
// If this event is from an activity then we need to call
// the activity callback to let it know that the event it
@@ -270,7 +296,7 @@ class api {
component_callback(
'mod_' . $event->get_course_module()->get('modname'),
'core_calendar_event_timestart_updated',
[$legacyevent]
[$legacyevent, $moduleinstance]
);
}
+6 -96
View File
@@ -23,6 +23,8 @@
*/
namespace core_calendar\local\event\forms;
use context_system;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/lib/formslib.php');
@@ -35,6 +37,8 @@ require_once($CFG->dirroot.'/lib/formslib.php');
*/
class create extends \moodleform {
use eventtype;
/**
* Build the editor options using the given context.
*
@@ -48,7 +52,8 @@ class create extends \moodleform {
'context' => $context,
'maxfiles' => EDITOR_UNLIMITED_FILES,
'maxbytes' => $CFG->maxbytes,
'noclean' => true
'noclean' => true,
'autosave' => false
];
}
@@ -180,101 +185,6 @@ class create extends \moodleform {
$mform->setDefault('visible', 1);
}
/**
* Add the appropriate elements for the available event types.
*
* If the only event type available is 'user' then we add a hidden
* element because there is nothing for the user to choose.
*
* If more than one type is available then we add the elements as
* follows:
* - Always add the event type selector
* - Elements per type:
* - course: add an additional select element with each
* course as an option.
* - group: add a select element for the course (different
* from the above course select) and a select
* element for the group.
*
* @param MoodleQuickForm $mform
* @param array $eventtypes The available event types for the user
*/
protected function add_event_type_elements($mform, $eventtypes) {
$options = [];
if (isset($eventtypes['user'])) {
$options['user'] = get_string('user');
}
if (isset($eventtypes['group'])) {
$options['group'] = get_string('group');
}
if (isset($eventtypes['course'])) {
$options['course'] = get_string('course');
}
if (isset($eventtypes['category'])) {
$options['category'] = get_string('category');
}
if (isset($eventtypes['site'])) {
$options['site'] = get_string('site');
}
// If we only have one event type and it's 'user' event then don't bother
// rendering the select boxes because there is no choice for the user to
// make.
if (count(array_keys($eventtypes)) == 1 && isset($eventtypes['user'])) {
$mform->addElement('hidden', 'eventtype');
$mform->setType('eventtype', PARAM_TEXT);
$mform->setDefault('eventtype', 'user');
// Render a static element to tell the user what type of event will
// be created.
$mform->addElement('static', 'staticeventtype', get_string('eventkind', 'calendar'), $options['user']);
return;
} else {
$mform->addElement('select', 'eventtype', get_string('eventkind', 'calendar'), $options);
}
if (isset($eventtypes['category'])) {
$categoryoptions = [];
foreach ($eventtypes['category'] as $id => $category) {
$categoryoptions[$id] = $category;
}
$mform->addElement('select', 'categoryid', get_string('category'), $categoryoptions);
$mform->hideIf('categoryid', 'eventtype', 'noteq', 'category');
}
if (isset($eventtypes['course'])) {
$mform->addElement('course', 'courseid', get_string('course'), ['limittoenrolled' => true]);
$mform->hideIf('courseid', 'eventtype', 'noteq', 'course');
}
if (isset($eventtypes['group'])) {
$options = ['limittoenrolled' => true];
// Exclude courses without group.
if (isset($eventtypes['course']) && isset($eventtypes['groupcourses'])) {
$options['exclude'] = array_diff(array_keys($eventtypes['course']),
array_keys($eventtypes['groupcourses']));
}
$mform->addElement('course', 'groupcourseid', get_string('course'), $options);
$mform->hideIf('groupcourseid', 'eventtype', 'noteq', 'group');
$groupoptions = [];
foreach ($eventtypes['group'] as $group) {
// We are formatting it this way in order to provide the javascript both
// the course and group ids so that it can enhance the form for the user.
$index = "{$group->courseid}-{$group->id}";
$groupoptions[$index] = format_string($group->name, true,
['context' => \context_course::instance($group->courseid)]);
}
$mform->addElement('select', 'groupid', get_string('group'), $groupoptions);
$mform->hideIf('groupid', 'eventtype', 'noteq', 'group');
// We handle the group select hide/show actions on the event_form module.
}
}
/**
* Add the various elements to express the duration options available
* for an event.
@@ -0,0 +1,131 @@
<?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/>.
/**
* The trait for adding eventtype fields to a form.
*
* @package core_calendar
* @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_calendar\local\event\forms;
defined('MOODLE_INTERNAL') || die();
/**
* The trait for adding eventtype fields to a form.
*
* @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait eventtype {
/**
* Add the appropriate elements for the available event types.
*
* If the only event type available is 'user' then we add a hidden
* element because there is nothing for the user to choose.
*
* If more than one type is available then we add the elements as
* follows:
* - Always add the event type selector
* - Elements per type:
* - course: add an additional select element with each
* course as an option.
* - group: add a select element for the course (different
* from the above course select) and a select
* element for the group.
*
* @param MoodleQuickForm $mform
* @param array $eventtypes The available event types for the user
*/
protected function add_event_type_elements($mform, $eventtypes) {
$options = [];
if (isset($eventtypes['user'])) {
$options['user'] = get_string('user');
}
if (isset($eventtypes['group'])) {
$options['group'] = get_string('group');
}
if (isset($eventtypes['course'])) {
$options['course'] = get_string('course');
}
if (isset($eventtypes['category'])) {
$options['category'] = get_string('category');
}
if (isset($eventtypes['site'])) {
$options['site'] = get_string('site');
}
// If we only have one event type and it's 'user' event then don't bother
// rendering the select boxes because there is no choice for the user to
// make.
if (count(array_keys($eventtypes)) == 1 && isset($eventtypes['user'])) {
$mform->addElement('hidden', 'eventtype');
$mform->setType('eventtype', PARAM_TEXT);
$mform->setDefault('eventtype', 'user');
// Render a static element to tell the user what type of event will
// be created.
$mform->addElement('static', 'staticeventtype', get_string('eventkind', 'calendar'), $options['user']);
return;
} else {
$mform->addElement('select', 'eventtype', get_string('eventkind', 'calendar'), $options);
}
if (isset($eventtypes['category'])) {
$categoryoptions = [];
foreach ($eventtypes['category'] as $id => $category) {
$categoryoptions[$id] = $category;
}
$mform->addElement('select', 'categoryid', get_string('category'), $categoryoptions);
$mform->hideIf('categoryid', 'eventtype', 'noteq', 'category');
}
if (isset($eventtypes['course'])) {
$limit = !has_capability('moodle/calendar:manageentries', \context_system::instance());
$mform->addElement('course', 'courseid', get_string('course'), ['limittoenrolled' => $limit]);
$mform->hideIf('courseid', 'eventtype', 'noteq', 'course');
}
if (isset($eventtypes['group'])) {
$options = ['limittoenrolled' => true];
// Exclude courses without group.
if (isset($eventtypes['course']) && isset($eventtypes['groupcourses'])) {
$options['exclude'] = array_diff(array_keys($eventtypes['course']),
array_keys($eventtypes['groupcourses']));
}
$mform->addElement('course', 'groupcourseid', get_string('course'), $options);
$mform->hideIf('groupcourseid', 'eventtype', 'noteq', 'group');
$groupoptions = [];
foreach ($eventtypes['group'] as $group) {
// We are formatting it this way in order to provide the javascript both
// the course and group ids so that it can enhance the form for the user.
$index = "{$group->courseid}-{$group->id}";
$groupoptions[$index] = format_string($group->name, true,
['context' => \context_course::instance($group->courseid)]);
}
$mform->addElement('select', 'groupid', get_string('group'), $groupoptions);
$mform->hideIf('groupid', 'eventtype', 'noteq', 'group');
// We handle the group select hide/show actions on the event_form module.
}
}
}
@@ -21,26 +21,30 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @package calendar
*/
namespace core_calendar\local\event\forms;
if (!defined('MOODLE_INTERNAL')) {
die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page
}
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/formslib.php');
require_once($CFG->libdir . '/formslib.php');
/**
* Form for adding a subscription to a Moodle course calendar.
* @copyright 2012 Jonathan Harker
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class calendar_addsubscription_form extends moodleform {
class managesubscriptions extends \moodleform {
use eventtype;
/**
* Defines the form used to add calendar subscriptions.
*/
public function definition() {
$mform = $this->_form;
$courseid = optional_param('course', 0, PARAM_INT);
$eventtypes = calendar_get_all_allowed_types();
if (empty($eventtypes)) {
print_error('nopermissiontoupdatecalendar');
}
$mform->addElement('header', 'addsubscriptionform', get_string('importcalendarheading', 'calendar'));
@@ -77,24 +81,10 @@ class calendar_addsubscription_form extends moodleform {
$mform->disabledIf('url', 'importfrom', 'eq', CALENDAR_IMPORT_FROM_FILE);
$mform->disabledIf('importfile', 'importfrom', 'eq', CALENDAR_IMPORT_FROM_URL);
// Add the select elements for the available event types.
$this->add_event_type_elements($mform, $eventtypes);
// Eventtype: 0 = user, 1 = global, anything else = course ID.
list($choices, $groups) = calendar_get_eventtype_choices($courseid);
$mform->addElement('select', 'eventtype', get_string('eventkind', 'calendar'), $choices);
$mform->addRule('eventtype', get_string('required'), 'required');
$mform->setType('eventtype', PARAM_ALPHA);
if (!empty($groups) and is_array($groups)) {
$groupoptions = array();
foreach ($groups as $group) {
$groupoptions[$group->id] = $group->name;
}
$mform->addElement('select', 'groupid', get_string('typegroup', 'calendar'), $groupoptions);
$mform->setType('groupid', PARAM_INT);
$mform->disabledIf('groupid', 'eventtype', 'noteq', 'group');
}
$mform->addElement('hidden', 'course');
$mform->setType('course', PARAM_INT);
$mform->addElement('submit', 'add', get_string('add'));
}
@@ -110,6 +100,14 @@ class calendar_addsubscription_form extends moodleform {
$errors = parent::validation($data, $files);
$coursekey = isset($data['groupcourseid']) ? 'groupcourseid' : 'courseid';
$eventtypes = calendar_get_all_allowed_types();
$eventtype = isset($data['eventtype']) ? $data['eventtype'] : null;
if (empty($eventtype) || !isset($eventtypes[$eventtype])) {
$errors['eventtype'] = get_string('invalideventtype', 'calendar');
}
if ($data['importfrom'] == CALENDAR_IMPORT_FROM_FILE) {
if (empty($data['importfile'])) {
$errors['importfile'] = get_string('errorrequiredurlorfile', 'calendar');
@@ -117,7 +115,7 @@ class calendar_addsubscription_form extends moodleform {
// Make sure the file area is not empty and contains only one file.
$draftitemid = $data['importfile'];
$fs = get_file_storage();
$usercontext = context_user::instance($USER->id);
$usercontext = \context_user::instance($USER->id);
$files = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id DESC', false);
if (count($files) !== 1) {
$errors['importfile'] = get_string('errorrequiredurlorfile', 'calendar');
@@ -140,7 +138,7 @@ class calendar_addsubscription_form extends moodleform {
public function definition_after_data() {
$mform =& $this->_form;
$mform->applyFilter('url', 'calendar_addsubscription_form::strip_webcal');
$mform->applyFilter('url', static::class . '::strip_webcal');
$mform->applyFilter('url', 'trim');
}
@@ -45,13 +45,15 @@ class update extends create {
$mform->addElement('hidden', 'repeatid');
$mform->setType('repeatid', PARAM_INT);
$group = [];
$group[] = $mform->createElement('radio', 'repeateditall', null, get_string('repeateditall', 'calendar',
$event->eventrepeats), 1);
$group[] = $mform->createElement('radio', 'repeateditall', null, get_string('repeateditthis', 'calendar'), 0);
$mform->addGroup($group, 'repeatgroup', get_string('repeatedevents', 'calendar'), '<br />', false);
if (!empty($event->repeatid)) {
$group = [];
$group[] = $mform->createElement('radio', 'repeateditall', null, get_string('repeateditall', 'calendar',
$event->eventrepeats), 1);
$group[] = $mform->createElement('radio', 'repeateditall', null, get_string('repeateditthis', 'calendar'), 0);
$mform->addGroup($group, 'repeatgroup', get_string('repeatedevents', 'calendar'), '<br />', false);
$mform->setDefault('repeateditall', 1);
$mform->setAdvanced('repeatgroup');
$mform->setDefault('repeateditall', 1);
$mform->setAdvanced('repeatgroup');
}
}
}
+28 -75
View File
@@ -203,6 +203,8 @@ class core_calendar_external extends external_api {
$courses = $params['events']['courseids'];
$funcparam['courses'] = $courses;
}
// Now get categories we can get events from.
$categories = \coursecat::get_all();
// Let us findout groups that we can return events from.
if (!$hassystemcap) {
@@ -273,7 +275,8 @@ class core_calendar_external extends external_api {
} else {
// Can the user actually see this event?
$eventobj = calendar_event::load($eventobj);
if (($eventobj->courseid == $SITE->id) ||
if ((($eventobj->courseid == $SITE->id) && (empty($eventobj->categoryid))) ||
(!empty($eventobj->categoryid) && in_array($eventobj->categoryid, $categories)) ||
(!empty($eventobj->groupid) && in_array($eventobj->groupid, $groups)) ||
(!empty($eventobj->courseid) && in_array($eventobj->courseid, $courses)) ||
($USER->id == $eventobj->userid) ||
@@ -716,14 +719,21 @@ class core_calendar_external extends external_api {
global $CFG, $PAGE, $USER;
require_once($CFG->dirroot."/calendar/lib.php");
$params = ['eventid' => $eventid];
self::validate_parameters(self::get_calendar_event_by_id_parameters(), $params);
$params = self::validate_parameters(self::get_calendar_event_by_id_parameters(), ['eventid' => $eventid]);
$context = \context_user::instance($USER->id);
self::validate_context($context);
$warnings = array();
$legacyevent = calendar_event::load($eventid);
// Must check we can see this event.
if (!calendar_view_event_allowed($legacyevent)) {
// We can't return a warning in this case because the event is not optional.
// We don't know the context for the event and it's not worth loading it.
$syscontext = context_system::instance();
throw new \required_capability_exception($syscontext, 'moodle/course:view', 'nopermission', '');
}
$legacyevent->count_repeats();
$eventmapper = event_container::get_event_mapper();
@@ -899,41 +909,15 @@ class core_calendar_external extends external_api {
'includenavigation' => $includenavigation,
]);
// TODO: Copy what we do in calendar/view.php.
$context = \context_user::instance($USER->id);
self::validate_context($context);
$PAGE->set_url('/calendar/');
if ($courseid != SITEID && !empty($courseid)) {
// Course ID must be valid and existing.
$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
$courses = [$course->id => $course];
$coursecat = \coursecat::get($course->category);
$category = $coursecat->get_db_record();
} else {
$course = get_site();
$courses = calendar_get_default_courses();
$category = null;
if ($categoryid) {
self::validate_context(context_coursecat::instance($categoryid));
$ids = [$categoryid];
$category = \coursecat::get($categoryid);
$ids += $category->get_parents();
$categories = \coursecat::get_many($ids);
$courses = array_filter($courses, function($course) use ($categories) {
return array_search($course->category, $categories) !== false;
});
$category = $category->get_db_record();
}
}
$type = \core_calendar\type_factory::get_calendar_instance();
$time = $type->convert_to_timestamp($year, $month, 1);
$calendar = new calendar_information(0, 0, 0, $time);
$calendar->set_sources($course, $courses, $category);
$time = $type->convert_to_timestamp($params['year'], $params['month'], 1);
$calendar = \calendar_information::create($time, $params['courseid'], $params['categoryid']);
self::validate_context($calendar->context);
list($data, $template) = calendar_get_view($calendar, 'month', $params['includenavigation']);
@@ -994,42 +978,16 @@ class core_calendar_external extends external_api {
'categoryid' => $categoryid,
]);
if ($courseid != SITEID && !empty($courseid)) {
// Course ID must be valid and existing.
$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
$courses = [$course->id => $course];
$coursecat = \coursecat::get($course->category);
$category = $coursecat->get_db_record();
} else {
$course = get_site();
$courses = calendar_get_default_courses();
$category = null;
if ($categoryid) {
self::validate_context(context_coursecat::instance($categoryid));
$ids = [$categoryid];
$category = \coursecat::get($categoryid);
$ids += $category->get_parents();
$categories = \coursecat::get_many($ids);
$courses = array_filter($courses, function($course) use ($categories) {
return array_search($course->category, $categories) !== false;
});
$category = $category->get_db_record();
}
}
// TODO: Copy what we do in calendar/view.php.
$context = \context_user::instance($USER->id);
self::validate_context($context);
$type = \core_calendar\type_factory::get_calendar_instance();
$time = $type->convert_to_timestamp($year, $month, $day);
$calendar = new calendar_information(0, 0, 0, $time);
$calendar->set_sources($course, $courses, $category);
$time = $type->convert_to_timestamp($params['year'], $params['month'], $params['day']);
$calendar = \calendar_information::create($time, $params['courseid'], $params['categoryid']);
self::validate_context($calendar->context);
list($data, $template) = calendar_get_view($calendar, 'day', $params['includenavigation']);
list($data, $template) = calendar_get_view($calendar, 'day');
return $data;
}
@@ -1145,31 +1103,25 @@ class core_calendar_external extends external_api {
* Get data for the monthly calendar view.
*
* @param int $courseid The course to be included
* @param int $categoryid The category to be included
* @return array
*/
public static function get_calendar_upcoming_view($courseid) {
public static function get_calendar_upcoming_view($courseid, $categoryid) {
global $CFG, $DB, $USER, $PAGE;
require_once($CFG->dirroot."/calendar/lib.php");
// Parameter validation.
self::validate_parameters(self::get_calendar_upcoming_view_parameters(), [
$params = self::validate_parameters(self::get_calendar_upcoming_view_parameters(), [
'courseid' => $courseid,
'categoryid' => $categoryid,
]);
if ($courseid != SITEID && !empty($courseid)) {
// Course ID must be valid and existing.
$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
$courses = [$course->id => $course];
} else {
$course = get_site();
$courses = calendar_get_default_courses();
}
$context = \context_user::instance($USER->id);
self::validate_context($context);
$PAGE->set_url('/calendar/');
$calendar = new calendar_information(0, 0, 0, time());
$calendar->set_sources($course, $courses);
$calendar = \calendar_information::create(time(), $params['courseid'], $params['categoryid']);
self::validate_context($calendar->context);
list($data, $template) = calendar_get_view($calendar, 'upcoming');
@@ -1185,6 +1137,7 @@ class core_calendar_external extends external_api {
return new external_function_parameters(
[
'courseid' => new external_value(PARAM_INT, 'Course being viewed', VALUE_DEFAULT, SITEID, NULL_ALLOWED),
'categoryid' => new external_value(PARAM_INT, 'Category being viewed', VALUE_DEFAULT, null, NULL_ALLOWED),
]
);
}
+314 -47
View File
@@ -998,6 +998,9 @@ class calendar_information {
/** @var array An array of users */
public $users = array();
/** @var context The anticipated context that the calendar is viewed in */
public $context = null;
/**
* Creates a new instance
*
@@ -1032,6 +1035,55 @@ class calendar_information {
$this->set_time($time);
}
/**
* Creates and set up a instance.
*
* @param int $time the unixtimestamp representing the date we want to view.
* @param int $courseid The ID of the course the user wishes to view.
* @param int $categoryid The ID of the category the user wishes to view
* If a courseid is specified, this value is ignored.
* @return calendar_information
*/
public static function create($time, int $courseid, int $categoryid = null) : calendar_information {
$calendar = new static(0, 0, 0, $time);
if ($courseid != SITEID && !empty($courseid)) {
// Course ID must be valid and existing.
$course = get_course($courseid);
$calendar->context = context_course::instance($course->id);
if (!$course->visible) {
require_capability('moodle/course:viewhiddencourses', $calendar->context);
}
$courses = [$course->id => $course];
$category = (\coursecat::get($course->category))->get_db_record();
} else if (!empty($categoryid)) {
$course = get_site();
$courses = calendar_get_default_courses();
// Filter available courses to those within this category or it's children.
$ids = [$categoryid];
$category = \coursecat::get($categoryid);
$ids = array_merge($ids, array_keys($category->get_children()));
$courses = array_filter($courses, function($course) use ($ids) {
return array_search($course->category, $ids) !== false;
});
$category = $category->get_db_record();
$calendar->context = context_coursecat::instance($categoryid);
} else {
$course = get_site();
$courses = calendar_get_default_courses();
$category = null;
$calendar->context = context_system::instance();
}
$calendar->set_sources($course, $courses, $category);
return $calendar;
}
/**
* Set the time period of this instance.
*
@@ -1073,6 +1125,8 @@ class calendar_information {
* @param stdClass $category The current category to show.
*/
public function set_sources(stdClass $course, array $courses, stdClass $category = null) {
global $USER;
// A cousre must always be specified.
$this->course = $course;
$this->courseid = $course->id;
@@ -1086,33 +1140,63 @@ class calendar_information {
$this->categoryid = null;
$this->categories = null;
if (null !== $category && $category->id > 0) {
// A specific category was requested - set the current category, and include all parents of that category.
$category = \coursecat::get($category->id);
// Determine the correct category information to show.
// When called with a course, the category of that course is usually included too.
// When a category was specifically requested, it should be requested with the site id.
if (SITEID !== $this->courseid) {
// A specific course was requested.
// Fetch the category that this course is in, along with all parents.
// Do not include child categories of this category, as the user many not have enrolments in those siblings or children.
$category = \coursecat::get($course->category);
$this->categoryid = $category->id;
$this->categories = $category->get_parents();
$this->categories[] = $category->id;
} else if (SITEID === $this->courseid) {
// This is the site.
// Show categories for all courses the user has access to.
$this->categories = true;
$categories = [];
foreach ($courses as $course) {
if ($category = \coursecat::get($course->category, IGNORE_MISSING)) {
$categories = array_merge($categories, $category->get_parents());
$categories[] = $category->id;
} else if (null !== $category && $category->id > 0) {
// A specific category was requested.
// Fetch all parents of this category, along with all children too.
$category = \coursecat::get($category->id);
$this->categoryid = $category->id;
// Build the category list.
// This includes the current category.
$this->categories = [$category->id];
// All of its descendants.
foreach (\coursecat::get_all() as $cat) {
if (array_search($category->id, $cat->get_parents()) !== false) {
$this->categories[] = $cat->id;
}
}
// And all categories that the user can manage.
foreach (\coursecat::get_all() as $category) {
if (!$category->has_manage_capability()) {
continue;
}
// And all of its parents.
$this->categories = array_merge($this->categories, $category->get_parents());
} else if (SITEID === $this->courseid) {
// The site was requested.
// Fetch all categories where this user has any enrolment, and all categories that this user can manage.
$categories = array_merge($categories, $category->get_parents());
$categories[] = $category->id;
// Grab the list of categories that this user has courses in.
$coursecategories = array_flip(array_map(function($course) {
return $course->category;
}, $courses));
$categories = [];
foreach (\coursecat::get_all() as $category) {
if (has_capability('moodle/category:manage', $category->get_context(), $USER, false)) {
// If a user can manage a category, then they can see all child categories. as well as all parent categories.
$categories[] = $category->id;
foreach (\coursecat::get_all() as $cat) {
if (array_search($category->id, $cat->get_parents()) !== false) {
$categories[] = $cat->id;
}
}
$categories = array_merge($categories, $category->get_parents());
} else if (isset($coursecategories[$category->id])) {
// The user has access to a course in this category.
// Fetch all of the parents too.
$categories = array_merge($categories, [$category->id], $category->get_parents());
$categories[] = $category->id;
}
}
$this->categories = array_unique($categories);
@@ -2029,6 +2113,91 @@ function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false) {
return array($courses, $group, $user);
}
/**
* Return the capability for viewing a calendar event.
*
* @param calendar_event $event event object
* @return boolean
*/
function calendar_view_event_allowed(calendar_event $event) {
global $USER;
// Anyone can see site events.
if ($event->courseid && $event->courseid == SITEID) {
return true;
}
// If a user can manage events at the site level they can see any event.
$sitecontext = \context_system::instance();
// If user has manageentries at site level, return true.
if (has_capability('moodle/calendar:manageentries', $sitecontext)) {
return true;
}
if (!empty($event->groupid)) {
// If it is a group event we need to be able to manage events in the course, or be in the group.
if (has_capability('moodle/calendar:manageentries', $event->context) ||
has_capability('moodle/calendar:managegroupentries', $event->context)) {
return true;
}
$mycourses = enrol_get_my_courses('id');
return isset($mycourses[$event->courseid]) && groups_is_member($event->groupid);
} else if ($event->modulename) {
// If this is a module event we need to be able to see the module.
$coursemodules = [];
$courseid = 0;
// Override events do not have the courseid set.
if ($event->courseid) {
$courseid = $event->courseid;
$coursemodules = get_fast_modinfo($event->courseid)->instances;
} else {
$cmraw = get_coursemodule_from_instance($event->modulename, $event->instance, 0, false, MUST_EXIST);
$courseid = $cmraw->course;
$coursemodules = get_fast_modinfo($cmraw->course)->instances;
}
$hasmodule = isset($coursemodules[$event->modulename]);
$hasinstance = isset($coursemodules[$event->modulename][$event->instance]);
// If modinfo doesn't know about the module, return false to be safe.
if (!$hasmodule || !$hasinstance) {
return false;
}
// Must be able to see the course and the module - MDL-59304.
$cm = $coursemodules[$event->modulename][$event->instance];
if (!$cm->uservisible) {
return false;
}
$mycourses = enrol_get_my_courses('id');
return isset($mycourses[$courseid]);
} else if ($event->categoryid) {
// If this is a category we need to be able to see the category.
$cat = \coursecat::get($event->categoryid, IGNORE_MISSING);
if (!$cat) {
return false;
}
return true;
} else if (!empty($event->courseid)) {
// If it is a course event we need to be able to manage events in the course, or be in the course.
if (has_capability('moodle/calendar:manageentries', $event->context)) {
return true;
}
$mycourses = enrol_get_my_courses('id');
return isset($mycourses[$event->courseid]);
} else if ($event->userid) {
if ($event->userid != $USER->id) {
// No-one can ever see another users events.
return false;
}
return true;
} else {
throw new moodle_exception('unknown event type');
}
return false;
}
/**
* Return the capability for editing calendar event.
*
@@ -2137,31 +2306,41 @@ function calendar_delete_event_allowed($event) {
* Returns the default courses to display on the calendar when there isn't a specific
* course to display.
*
* @param int $courseid (optional) If passed, an additional course can be returned for admins (the current course).
* @param string $fields Comma separated list of course fields to return.
* @param bool $canmanage If true, this will return the list of courses the current user can create events in, rather
* than the list of courses they see events from (an admin can always add events in a course
* calendar, even if they are not enrolled in the course).
* @return array $courses Array of courses to display
*/
function calendar_get_default_courses() {
function calendar_get_default_courses($courseid = null, $fields = '*', $canmanage=false) {
global $CFG, $DB;
if (!isloggedin()) {
return array();
}
if (!empty($CFG->calendar_adminseesall) && has_capability('moodle/calendar:manageentries', \context_system::instance())) {
$select = ', ' . \context_helper::get_preload_record_columns_sql('ctx');
$join = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
$sql = "SELECT c.* $select
FROM {course} c
$join
WHERE EXISTS (SELECT 1 FROM {event} e WHERE e.courseid = c.id)
";
$courses = $DB->get_records_sql($sql, array('contextlevel' => CONTEXT_COURSE), 0, 20);
foreach ($courses as $course) {
\context_helper::preload_from_record($course);
}
return $courses;
if (has_capability('moodle/calendar:manageentries', context_system::instance()) &&
(!empty($CFG->calendar_adminseesall) || $canmanage)) {
// Add a c. prefix to every field as expected by get_courses function.
$fieldlist = explode(',', $fields);
$prefixedfields = array_map(function($value) {
return 'c.' . trim($value);
}, $fieldlist);
$courses = get_courses('all', 'c.shortname', implode(',', $prefixedfields));
} else {
$courses = enrol_get_my_courses($fields);
}
$courses = enrol_get_my_courses();
if ($courseid && $courseid != SITEID) {
if (empty($courses[$courseid]) && has_capability('moodle/calendar:manageentries', context_system::instance())) {
// Allow a site admin to see calendars from courses he is not enrolled in.
// This will come from $COURSE.
$courses[$courseid] = get_course($courseid);
}
}
return $courses;
}
@@ -2404,6 +2583,8 @@ function calendar_get_all_allowed_types() {
$types = [];
$allowed = new stdClass();
calendar_get_allowed_types($allowed);
if ($allowed->user) {
@@ -2421,7 +2602,8 @@ function calendar_get_all_allowed_types() {
// This function warms the context cache for the course so the calls
// to load the course context in calendar_get_allowed_types don't result
// in additional DB queries.
$courses = enrol_get_users_courses($USER->id, true);
$courses = calendar_get_default_courses(null, '*', true);
// We want to pre-fetch all of the groups for each course in a single
// query to avoid calendar_get_allowed_types from hitting the DB for
// each separate course.
@@ -2567,12 +2749,36 @@ function calendar_get_eventtype_choices($courseid) {
function calendar_add_subscription($sub) {
global $DB, $USER, $SITE;
// Undo the form definition work around to allow us to have two different
// course selectors present depending on which event type the user selects.
if (!empty($sub->groupcourseid)) {
$sub->courseid = $sub->groupcourseid;
unset($sub->groupcourseid);
}
// Pull the group id back out of the value. The form saves the value
// as "<courseid>-<groupid>" to allow the javascript to work correctly.
if (!empty($sub->groupid)) {
list($courseid, $groupid) = explode('-', $sub->groupid);
$sub->courseid = $courseid;
$sub->groupid = $groupid;
}
// Default course id if none is set.
if (empty($sub->courseid)) {
if ($sub->eventtype === 'site') {
$sub->courseid = SITEID;
} else {
$sub->courseid = 0;
}
}
if ($sub->eventtype === 'site') {
$sub->courseid = $SITE->id;
} else if ($sub->eventtype === 'group' || $sub->eventtype === 'course') {
$sub->courseid = $sub->course;
$sub->courseid = $sub->courseid;
} else if ($sub->eventtype === 'category') {
$sub->categoryid = $sub->category;
$sub->categoryid = $sub->categoryid;
} else {
// User events.
$sub->courseid = 0;
@@ -2592,8 +2798,25 @@ function calendar_add_subscription($sub) {
// Trigger event, calendar subscription added.
$eventparams = array('objectid' => $sub->id,
'context' => calendar_get_calendar_context($sub),
'other' => array('eventtype' => $sub->eventtype, 'courseid' => $sub->courseid)
'other' => array(
'eventtype' => $sub->eventtype,
)
);
switch ($sub->eventtype) {
case 'category':
$eventparams['other']['categoryid'] = $sub->categoryid;
break;
case 'course':
$eventparams['other']['courseid'] = $sub->courseid;
break;
case 'group':
$eventparams['other']['courseid'] = $sub->courseid;
$eventparams['other']['groupid'] = $sub->groupid;
break;
default:
$eventparams['other']['courseid'] = $sub->courseid;
}
$event = \core\event\calendar_subscription_created::create($eventparams);
$event->trigger();
return $id;
@@ -2611,13 +2834,13 @@ function calendar_add_subscription($sub) {
* Add an iCalendar event to the Moodle calendar.
*
* @param stdClass $event The RFC-2445 iCalendar event
* @param int $courseid The course ID
* @param int $unused Deprecated
* @param int $subscriptionid The iCalendar subscription ID
* @param string $timezone The X-WR-TIMEZONE iCalendar property if provided
* @throws dml_exception A DML specific exception is thrown for invalid subscriptionids.
* @return int Code: CALENDAR_IMPORT_EVENT_UPDATED = updated, CALENDAR_IMPORT_EVENT_INSERTED = inserted, 0 = error
*/
function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timezone='UTC') {
function calendar_add_icalendar_event($event, $unused = null, $subscriptionid, $timezone='UTC') {
global $DB;
// Probably an unsupported X-MICROSOFT-CDO-BUSYSTATUS event.
@@ -2690,6 +2913,7 @@ function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timez
$eventrecord->userid = $sub->userid;
$eventrecord->groupid = $sub->groupid;
$eventrecord->courseid = $sub->courseid;
$eventrecord->categoryid = $sub->categoryid;
$eventrecord->eventtype = $sub->eventtype;
if ($updaterecord = $DB->get_record('event', array('uuid' => $eventrecord->uuid,
@@ -2770,8 +2994,24 @@ function calendar_delete_subscription($subscription) {
// Trigger event, calendar subscription deleted.
$eventparams = array('objectid' => $subscription->id,
'context' => calendar_get_calendar_context($subscription),
'other' => array('courseid' => $subscription->courseid)
'other' => array(
'eventtype' => $subscription->eventtype,
)
);
switch ($subscription->eventtype) {
case 'category':
$eventparams['other']['categoryid'] = $subscription->categoryid;
break;
case 'course':
$eventparams['other']['courseid'] = $subscription->courseid;
break;
case 'group':
$eventparams['other']['courseid'] = $subscription->courseid;
$eventparams['other']['groupid'] = $subscription->groupid;
break;
default:
$eventparams['other']['courseid'] = $subscription->courseid;
}
$event = \core\event\calendar_subscription_deleted::create($eventparams);
$event->trigger();
}
@@ -2810,7 +3050,7 @@ function calendar_get_icalendar($url) {
* @param int $subscriptionid The subscription ID.
* @return string A log of the import progress, including errors.
*/
function calendar_import_icalendar_events($ical, $courseid, $subscriptionid = null) {
function calendar_import_icalendar_events($ical, $unused = null, $subscriptionid = null) {
global $DB;
$return = '';
@@ -2837,7 +3077,7 @@ function calendar_import_icalendar_events($ical, $courseid, $subscriptionid = nu
$return = '';
foreach ($ical->components['VEVENT'] as $event) {
$res = calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timezone);
$res = calendar_add_icalendar_event($event, null, $subscriptionid, $timezone);
switch ($res) {
case CALENDAR_IMPORT_EVENT_UPDATED:
$updatecount++;
@@ -2887,7 +3127,7 @@ function calendar_update_subscription_events($subscriptionid) {
}
$ical = calendar_get_icalendar($sub->url);
$return = calendar_import_icalendar_events($ical, $sub->courseid, $subscriptionid);
$return = calendar_import_icalendar_events($ical, null, $subscriptionid);
$sub->lastupdated = time();
calendar_update_subscription($sub);
@@ -2922,8 +3162,24 @@ function calendar_update_subscription($subscription) {
$eventparams = array('userid' => $subscription->userid,
'objectid' => $subscription->id,
'context' => calendar_get_calendar_context($subscription),
'other' => array('eventtype' => $subscription->eventtype, 'courseid' => $subscription->courseid)
'other' => array(
'eventtype' => $subscription->eventtype,
)
);
switch ($subscription->eventtype) {
case 'category':
$eventparams['other']['categoryid'] = $subscription->categoryid;
break;
case 'course':
$eventparams['other']['courseid'] = $subscription->courseid;
break;
case 'group':
$eventparams['other']['courseid'] = $subscription->courseid;
$eventparams['other']['groupid'] = $subscription->groupid;
break;
default:
$eventparams['other']['courseid'] = $subscription->courseid;
}
$event = \core\event\calendar_subscription_updated::create($eventparams);
$event->trigger();
}
@@ -2945,9 +3201,14 @@ function calendar_can_edit_subscription($subscriptionorid) {
$allowed = new \stdClass;
$courseid = $subscription->courseid;
$categoryid = $subscription->categoryid;
$groupid = $subscription->groupid;
$category = null;
calendar_get_allowed_types($allowed, $courseid);
if (!empty($categoryid)) {
$category = \coursecat::get($categoryid);
}
calendar_get_allowed_types($allowed, $courseid, null, $category);
switch ($subscription->eventtype) {
case 'user':
return $allowed->user;
@@ -2957,6 +3218,12 @@ function calendar_can_edit_subscription($subscriptionorid) {
} else {
return false;
}
case 'category':
if (isset($allowed->categories[$categoryid])) {
return $allowed->categories[$categoryid];
} else {
return false;
}
case 'site':
return $allowed->site;
case 'group':
@@ -3207,7 +3474,7 @@ function calendar_get_view(\calendar_information $calendar, $view, $includenavig
if ($view == "upcoming") {
$template = 'core_calendar/calendar_upcoming';
} else if ($view == "upcoming_mini") {
$template = 'core_calendar/upcoming_mini';
$template = 'core_calendar/calendar_upcoming_mini';
}
}
+55 -9
View File
@@ -26,10 +26,10 @@ require_once('../config.php');
require_once($CFG->libdir.'/bennu/bennu.inc.php');
require_once($CFG->dirroot.'/course/lib.php');
require_once($CFG->dirroot.'/calendar/lib.php');
require_once($CFG->dirroot.'/calendar/managesubscriptions_form.php');
// Required use.
$courseid = optional_param('course', SITEID, PARAM_INT);
$courseid = optional_param('course', null, PARAM_INT);
$categoryid = optional_param('category', null, PARAM_INT);
// Used for processing subscription actions.
$subscriptionid = optional_param('id', 0, PARAM_INT);
$pollinterval = optional_param('pollinterval', 0, PARAM_INT);
@@ -39,6 +39,9 @@ $url = new moodle_url('/calendar/managesubscriptions.php');
if ($courseid != SITEID) {
$url->param('course', $courseid);
}
if ($categoryid) {
$url->param('categoryid', $categoryid);
}
navigation_node::override_active_url(new moodle_url('/calendar/view.php', array('view' => 'month')));
$PAGE->set_url($url);
$PAGE->set_pagelayout('admin');
@@ -58,7 +61,7 @@ if (!calendar_user_can_add_event($course)) {
print_error('errorcannotimport', 'calendar');
}
$form = new calendar_addsubscription_form(null);
$form = new \core_calendar\local\event\forms\managesubscriptions();
$form->set_data(array(
'course' => $course->id
));
@@ -75,7 +78,7 @@ if (!empty($formdata)) {
$calendar = $form->get_file_content('importfile');
$ical = new iCalendar();
$ical->unserialize($calendar);
$importresults = calendar_import_icalendar_events($ical, $courseid, $subscriptionid);
$importresults = calendar_import_icalendar_events($ical, null, $subscriptionid);
} else {
try {
$importresults = calendar_update_subscription_events($subscriptionid);
@@ -102,11 +105,54 @@ if (!empty($formdata)) {
}
}
$sql = 'SELECT *
FROM {event_subscriptions}
WHERE courseid = :courseid
OR (courseid = 0 AND userid = :userid)';
$params = array('courseid' => $courseid, 'userid' => $USER->id);
$types = calendar_get_all_allowed_types();
$searches = [];
$params = [];
$usedefaultfilters = true;
if (!empty($courseid) && $courseid == SITEID && isset($types['site'])) {
$searches[] = "(eventtype = 'site')";
$searches[] = "(eventtype = 'user' AND userid = :userid)";
$params['userid'] = $USER->id;
$usedefaultfilters = false;
}
if (!empty($courseid) && isset($types['course']) && array_key_exists($courseid, $types['course'])) {
$searches[] = "((eventtype = 'course' OR eventtype = 'group') AND courseid = :courseid)";
$params += ['courseid' => $courseid];
$usedefaultfilters = false;
}
if (!empty($categoryid) && isset($types['category']) && array_key_exists($categoryid, $types['category'])) {
$searches[] = "(eventtype = 'category' AND categoryid = :categoryid)";
$params += ['categoryid' => $categoryid];
$usedefaultfilters = false;
}
if ($usedefaultfilters) {
$searches[] = "(eventtype = 'user' AND userid = :userid)";
$params['userid'] = $USER->id;
if (isset($types['site'])) {
$searches[] = "(eventtype = 'site' AND courseid = :siteid)";
$params += ['siteid' => SITEID];
}
if (isset($types['course'])) {
list($courseinsql, $courseparams) = $DB->get_in_or_equal(array_keys($types['course']), SQL_PARAMS_NAMED, 'course');
$searches[] = "((eventtype = 'course' OR eventtype = 'group') AND courseid {$courseinsql})";
$params += $courseparams;
}
if (isset($types['category'])) {
list($categoryinsql, $categoryparams) = $DB->get_in_or_equal(array_keys($types['category']), SQL_PARAMS_NAMED, 'category');
$searches[] = "(eventtype = 'category' AND categoryid {$categoryinsql})";
$params += $categoryparams;
}
}
$sql = "SELECT * FROM {event_subscriptions} WHERE " . implode(' OR ', $searches);;
$subscriptions = $DB->get_records_sql($sql, $params);
// Print title and header.
+13 -17
View File
@@ -64,15 +64,17 @@ class core_calendar_renderer extends plugin_renderer_base {
$time = $calendartype->timestamp_to_date_array($calendar->time);
$current = $calendar->time;
$prevmonthyear = $calendartype->get_prev_month($time['year'], $time['mon']);
$prev = $calendartype->convert_to_timestamp(
$time['year'],
$time['mon'] - 1,
$time['mday']
$prevmonthyear[1],
$prevmonthyear[0],
1
);
$nextmonthyear = $calendartype->get_next_month($time['year'], $time['mon']);
$next = $calendartype->convert_to_timestamp(
$time['year'],
$time['mon'] + 1,
$time['mday']
$nextmonthyear[1],
$nextmonthyear[0],
1
);
$content = '';
@@ -247,11 +249,7 @@ class core_calendar_renderer extends plugin_renderer_base {
return '';
}
if (has_capability('moodle/calendar:manageentries', context_system::instance()) && !empty($CFG->calendar_adminseesall)) {
$courses = get_courses('all', 'c.shortname','c.id,c.shortname');
} else {
$courses = enrol_get_my_courses();
}
$courses = calendar_get_default_courses($courseid, 'id, shortname');
unset($courses[SITEID]);
@@ -285,12 +283,12 @@ class core_calendar_renderer extends plugin_renderer_base {
/**
* Renders a table containing information about calendar subscriptions.
*
* @param int $courseid
* @param int $unused
* @param array $subscriptions
* @param string $importresults
* @return string
*/
public function subscription_details($courseid, $subscriptions, $importresults = '') {
public function subscription_details($unused = null, $subscriptions, $importresults = '') {
$table = new html_table();
$table->head = array(
get_string('colcalendar', 'calendar'),
@@ -320,7 +318,7 @@ class core_calendar_renderer extends plugin_renderer_base {
$lastupdated = userdate($sub->lastupdated, get_string('strftimedatetimeshort', 'langconfig'));
}
$cell = new html_table_cell($this->subscription_action_form($sub, $courseid));
$cell = new html_table_cell($this->subscription_action_form($sub));
$cell->colspan = 2;
$type = $sub->eventtype . 'events';
@@ -344,10 +342,9 @@ class core_calendar_renderer extends plugin_renderer_base {
* Creates a form to perform actions on a given subscription.
*
* @param stdClass $subscription
* @param int $courseid
* @return string
*/
protected function subscription_action_form($subscription, $courseid) {
protected function subscription_action_form($subscription) {
// Assemble form for the subscription row.
$html = html_writer::start_tag('form', array('action' => new moodle_url('/calendar/managesubscriptions.php'), 'method' => 'post'));
if (empty($subscription->url)) {
@@ -369,7 +366,6 @@ class core_calendar_renderer extends plugin_renderer_base {
$html .= html_writer::end_tag('div');
}
$html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
$html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'course', 'value' => $courseid));
$html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'id', 'value' => $subscription->id));
$html .= html_writer::start_tag('div', array('class' => 'btn-group pull-right'));
if (!empty($subscription->url)) {
@@ -0,0 +1,41 @@
{{!
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 core_calendar/calendar_upcoming_block
Calendar upcoming view.
The purpose of this template is to render the calendar upcoming view.
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
}
}}
<div id="calendar-upcoming-block-{{uniqid}}" data-template="core_calendar/upcoming_mini">
{{> core_calendar/upcoming_mini}}
</div>
{{#js}}
require(['jquery', 'core_calendar/calendar_view'], function($, CalendarView) {
CalendarView.init($("#calendar-upcoming-block-{{uniqid}}"), 'upcoming');
});
{{/js}}
+60 -20
View File
@@ -29,6 +29,7 @@
}
}}
<div{{!
}} class="summary-modal-container"{{!
}} data-region="summary-modal-container"{{!
}} data-event-id="{{id}}"{{!
}} data-event-title="{{name}}"{{!
@@ -37,24 +38,63 @@
}} data-action-event="{{isactionevent}}"{{!
}} data-edit-url="{{editurl}}"{{!
}}>
<h4>{{#str}} when, core_calendar {{/str}}</h4>
{{#userdate}} {{timestart}}, {{#str}} strftimerecentfull {{/str}} {{/userdate}}
<br>
{{#description}}
<h4>{{#str}} description {{/str}}</h4>
{{{description}}}
{{/description}}
<h4>{{#str}} eventtype, core_calendar {{/str}}</h4>
{{eventtype}}
{{#iscategoryevent}}
<div>{{{category.nestedname}}}</div>
{{/iscategoryevent}}
{{#iscourseevent}}
<div><a href="{{url}}">{{course.fullname}}</a></div>
{{/iscourseevent}}
{{> core_calendar/event_subscription}}
{{#groupname}}
<div><a href="{{url}}">{{{course.fullname}}}</a></div>
<div>{{{groupname}}}</div>
{{/groupname}}
<div class="container-fluid">
<div class="row">
<div class="col-xs-1">{{#pix}} i/calendareventtime, core, {{#str}} when, core_calendar {{/str}} {{/pix}}</div>
<div class="col-xs-11">{{#userdate}} {{timestart}}, {{#str}} strftimerecentfull {{/str}} {{/userdate}}</div>
</div>
<div class="row m-t-1">
<div class="col-xs-1">{{#pix}} i/calendar, core, {{#str}} eventtype, core_calendar {{/str}} {{/pix}}</div>
<div class="col-xs-11">{{eventtype}}</div>
</div>
{{#description}}
<div class="row m-t-1">
<div class="col-xs-1">{{#pix}} i/calendareventdescription, core, {{#str}} description {{/str}} {{/pix}}</div>
<div class="description-content col-xs-11">{{{.}}}</div>
</div>
{{/description}}
{{#isactionevent}}
<div class="row m-t-1">
<div class="col-xs-1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
<div class="col-xs-11"><a href="{{course.viewurl}}">{{{course.fullname}}}</a></div>
</div>
{{/isactionevent}}
{{#iscategoryevent}}
<div class="row m-t-1">
<div class="col-xs-1">{{#pix}} i/categoryevent, core, {{#str}} category {{/str}} {{/pix}}</div>
<div class="col-xs-11">{{{category.nestedname}}}</div>
</div>
{{/iscategoryevent}}
{{#iscourseevent}}
<div class="row m-t-1">
<div class="col-xs-1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
<div class="col-xs-11"><a href="{{url}}">{{{course.fullname}}}</a></div>
</div>
{{/iscourseevent}}
{{#groupname}}
<div class="row m-t-1">
<div class="col-xs-1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
<div class="col-xs-11"><a href="{{url}}">{{{course.fullname}}}</a></div>
</div>
<div class="row m-t-1">
<div class="col-xs-1">{{#pix}} i/groupevent, core, {{#str}} group {{/str}} {{/pix}}</div>
<div class="col-xs-11">{{{groupname}}}</div>
</div>
{{/groupname}}
{{#subscription}}
{{#displayeventsource}}
<div class="row m-t-1">
<div class="col-xs-1">{{#pix}} i/rss, core, {{#str}} eventsource, core_calendar {{/str}} {{/pix}}</div>
<div class="col-xs-11">
{{#subscriptionurl}}
<p><a href="{{subscriptionurl}}">{{#str}}subscriptionsource, core_calendar, {{{subscriptionname}}}{{/str}}</a></p>
{{/subscriptionurl}}
{{^subscriptionurl}}
<p>{{#str}}subscriptionsource, core_calendar, {{{subscriptionname}}}{{/str}}</p>
{{/subscriptionurl}}
</div>
</div>
{{/displayeventsource}}
{{/subscription}}
</div>
</div>
+6 -6
View File
@@ -70,7 +70,7 @@
data-drop-zone="month-view-day"
data-region="day"
data-new-event-timestamp="{{neweventtimestamp}}">
<div class="hidden-sm-down text-xs-center">
<div class="hidden-sm-down hidden-phone text-xs-center">
{{#hasevents}}
<a data-action="view-day-link" href="{{viewdaylink}}" class="day" title="{{viewdaylinktitle}}">{{mday}}</a>
{{/hasevents}}
@@ -87,7 +87,7 @@
{{^underway}}
<li data-region="event-item"
data-eventtype-{{calendareventtype}}="1"
{{#canedit}}
{{#draggable}}
draggable="true"
data-drag-type="move"
{{#mindaytimestamp}}
@@ -102,14 +102,14 @@
{{#maxdayerror}}
data-max-day-error="{{.}}"
{{/maxdayerror}}
{{/canedit}}>
{{/draggable}}>
<a data-action="view-event" data-event-id="{{id}}" href="{{url}}" title="{{name}}">
<span class="badge badge-circle calendar_event_{{calendareventtype}}">
&nbsp;
</span>
{{> core_calendar/event_icon}}
{{name}}
<span class="eventname">{{name}}</span>
</a>
</li>
{{/underway}}
@@ -118,9 +118,9 @@
</div>
{{/hasevents}}
</div>
<div class="hidden-md-up hidden-desktop">
<div class="hidden-md-up hidden-desktop hidden-tablet">
{{#hasevents}}
<a href="{{viewdaylink}}" class="day" title="{{viewdaylinktitle}}">{{mday}}</a>
<a data-action="view-day-link" href="{{viewdaylink}}" class="day" title="{{viewdaylinktitle}}">{{mday}}</a>
{{/hasevents}}
{{^hasevents}}
<div data-region="day-content">
+62 -4
View File
@@ -31,13 +31,71 @@
{
}
}}
<div class="card-text content">
<div class="card-text content calendarwrapper"{{!
}} id="month-upcoming-mini-{{uniqid}}"{{!
}} data-context-id="{{defaulteventcontext}}"{{!
}} data-courseid="{{courseid}}"{{!
}} data-categoryid="{{categoryid}}"{{!
}}>
{{> core/overlay_loading}}
{{#events}}
<div class="event">
<div{{!
}} class="event"{{!
}} data-eventtype-{{calendareventtype}}="1"{{!
}} data-region="event-item"{{!
}}>
<span>{{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}}</span>
<a href="{{viewurl}}">{{{name}}}</a>
<a{{!
}} data-type="event"{{!
}} data-action="view-event"{{!
}} data-event-id="{{id}}"{{!
}} href="{{viewurl}}"{{!
}}>{{{name}}}</a>
<div class="date">{{{formattedtime}}}</div>
<hr>
</div>
<hr>
{{/events}}
{{^events}}
{{#str}}noupcomingevents, calendar{{/str}}
{{/events}}
</div>
{{#js}}
require([
'jquery',
'core_calendar/selectors',
'core_calendar/events',
], function(
$,
CalendarSelectors,
CalendarEvents
) {
var root = $('#month-upcoming-mini-{{uniqid}}');
$('body').on(CalendarEvents.filterChanged, function(e, data) {
M.util.js_pending("month-upcoming-mini-{{uniqid}}-filterChanged");
// A filter value has been changed.
// Find all matching cells in the popover data, and hide them.
var target = $("#month-upcoming-mini-{{uniqid}}").find(CalendarSelectors.eventType[data.type]);
var transitionPromise = $.Deferred();
if (data.hidden) {
transitionPromise.then(function() {
return target.slideUp('fast').promise();
});
} else {
transitionPromise.then(function() {
return target.slideDown('fast').promise();
});
}
transitionPromise.then(function() {
M.util.js_complete("month-upcoming-mini-{{uniqid}}-filterChanged");
return;
});
transitionPromise.resolve();
});
});
{{/js}}
+2 -2
View File
@@ -141,7 +141,7 @@ Feature: Perform basic calendar functionality
When I click on "Test choice opens" "link"
Then "Delete" "button" should not exist
And "Edit" "button" should not exist
And I should see "Open event"
And I should see "Course event"
When I click on "Go to activity" "link"
And I wait to be redirected
Then I should see "Test choice"
@@ -150,7 +150,7 @@ Feature: Perform basic calendar functionality
When I click on "Test choice closes" "link"
Then "Delete" "button" should not exist
And "Edit" "button" should not exist
And I should see "Close event"
And I should see "Course event"
When I click on "Go to activity" "link"
And I wait to be redirected
Then I should see "Test choice"
+1 -1
View File
@@ -22,7 +22,7 @@ Feature: Import and edit calendar events
And I set the following fields to these values:
| Calendar name | Test Import |
| Import from | Calendar file (.ics) |
| Type of event | User events |
| Type of event | User |
And I upload "calendar/tests/fixtures/import.ics" file to "Calendar file (.ics)" filemanager
And I press "Add"
And I should see "Events imported: 2"
File diff suppressed because it is too large Load Diff
+310 -7
View File
@@ -423,9 +423,9 @@ class core_calendar_events_testcase extends advanced_testcase {
}
/**
* Tests for calendar_subscription_added event.
* Tests for calendar_subscription_added event for a site subscription.
*/
public function test_calendar_subscription_created() {
public function test_calendar_subscription_created_site() {
global $CFG;
require_once($CFG->dirroot . '/calendar/lib.php');
$this->resetAfterTest(true);
@@ -447,15 +447,115 @@ class core_calendar_events_testcase extends advanced_testcase {
$this->assertEquals($id, $event->objectid);
$this->assertEquals($subscription->courseid, $event->other['courseid']);
$this->assertEquals($subscription->eventtype, $event->other['eventtype']);
$this->assertArrayNotHasKey('categoryid', $event->other);
$this->assertArrayNotHasKey('groupid', $event->other);
$this->assertDebuggingNotCalled();
$sink->close();
}
/**
* Tests for calendar_subscription_added event for a category subscription.
*/
public function test_calendar_subscription_created_category() {
global $CFG;
require_once($CFG->dirroot . '/calendar/lib.php');
$this->resetAfterTest(true);
$categoryid = $this->course->category;
// Create a mock subscription.
$subscription = new stdClass();
$subscription->eventtype = 'category';
$subscription->name = 'test';
$subscription->categoryid = $categoryid;
// Trigger and capture the event.
$sink = $this->redirectEvents();
$id = calendar_add_subscription($subscription);
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\calendar_subscription_created', $event);
$this->assertEquals($id, $event->objectid);
$this->assertEquals($categoryid, $event->other['categoryid']);
$this->assertEquals($subscription->eventtype, $event->other['eventtype']);
$this->assertArrayNotHasKey('courseid', $event->other);
$this->assertArrayNotHasKey('groupid', $event->other);
$this->assertDebuggingNotCalled();
$sink->close();
}
/**
* Tests for calendar_subscription_added event for a course subscription.
*/
public function test_calendar_subscription_created_course() {
global $CFG;
require_once($CFG->dirroot . '/calendar/lib.php');
$this->resetAfterTest(true);
// Create a mock subscription.
$subscription = new stdClass();
$subscription->eventtype = 'course';
$subscription->name = 'test';
$subscription->courseid = $this->course->id;
// Trigger and capture the event.
$sink = $this->redirectEvents();
$id = calendar_add_subscription($subscription);
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\calendar_subscription_created', $event);
$this->assertEquals($id, $event->objectid);
$this->assertEquals($subscription->courseid, $event->other['courseid']);
$this->assertEquals($subscription->eventtype, $event->other['eventtype']);
$this->assertArrayNotHasKey('categoryid', $event->other);
$this->assertArrayNotHasKey('groupid', $event->other);
$this->assertDebuggingNotCalled();
$sink->close();
}
/**
* Tests for calendar_subscription_updated event.
* Tests for calendar_subscription_added event for a group subscription.
*/
public function test_calendar_subscription_updated() {
public function test_calendar_subscription_created_group() {
global $CFG;
require_once($CFG->dirroot . '/calendar/lib.php');
$this->resetAfterTest(true);
$courseid = $this->course->id;
$groupid = 42;
// Create a mock subscription.
$subscription = new stdClass();
$subscription->eventtype = 'group';
$subscription->name = 'test';
$subscription->groupid = "{$courseid}-{$groupid}";
// Trigger and capture the event.
$sink = $this->redirectEvents();
$id = calendar_add_subscription($subscription);
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\calendar_subscription_created', $event);
$this->assertEquals($id, $event->objectid);
$this->assertEquals($courseid, $event->other['courseid']);
$this->assertEquals($groupid, $event->other['groupid']);
$this->assertEquals($subscription->eventtype, $event->other['eventtype']);
$this->assertArrayNotHasKey('categoryid', $event->other);
$this->assertDebuggingNotCalled();
$sink->close();
}
/**
* Tests for calendar_subscription_updated event for a site subscription.
*/
public function test_calendar_subscription_updated_site() {
global $CFG;
require_once($CFG->dirroot . '/calendar/lib.php');
$this->resetAfterTest(true);
@@ -479,15 +579,120 @@ class core_calendar_events_testcase extends advanced_testcase {
$this->assertEquals($subscription->id, $event->objectid);
$this->assertEquals($subscription->courseid, $event->other['courseid']);
$this->assertEquals($subscription->eventtype, $event->other['eventtype']);
$this->assertArrayNotHasKey('categoryid', $event->other);
$this->assertArrayNotHasKey('groupid', $event->other);
$this->assertDebuggingNotCalled();
$sink->close();
}
/**
* Tests for calendar_subscription_deleted event.
* Tests for calendar_subscription_updated event for a category subscription.
*/
public function test_calendar_subscription_deleted() {
public function test_calendar_subscription_updated_category() {
global $CFG;
require_once($CFG->dirroot . '/calendar/lib.php');
$this->resetAfterTest(true);
$categoryid = $this->course->category;
// Create a mock subscription.
$subscription = new stdClass();
$subscription->eventtype = 'category';
$subscription->name = 'test';
$subscription->categoryid = $categoryid;
$subscription->id = calendar_add_subscription($subscription);
// Now edit it.
$subscription->name = 'awesome';
// Trigger and capture the event.
$sink = $this->redirectEvents();
calendar_update_subscription($subscription);
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\calendar_subscription_updated', $event);
$this->assertEquals($subscription->id, $event->objectid);
$this->assertEquals($categoryid, $event->other['categoryid']);
$this->assertEquals($subscription->eventtype, $event->other['eventtype']);
$this->assertArrayNotHasKey('courseid', $event->other);
$this->assertArrayNotHasKey('groupid', $event->other);
$this->assertDebuggingNotCalled();
$sink->close();
}
/**
* Tests for calendar_subscription_updated event for a group subscription.
*/
public function test_calendar_subscription_updated_course() {
global $CFG;
require_once($CFG->dirroot . '/calendar/lib.php');
$this->resetAfterTest(true);
// Create a mock subscription.
$subscription = new stdClass();
$subscription->eventtype = 'course';
$subscription->name = 'test';
$subscription->courseid = $this->course->id;
$subscription->id = calendar_add_subscription($subscription);
// Now edit it.
$subscription->name = 'awesome';
// Trigger and capture the event.
$sink = $this->redirectEvents();
calendar_update_subscription($subscription);
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\calendar_subscription_updated', $event);
$this->assertEquals($subscription->id, $event->objectid);
$this->assertEquals($this->course->id, $event->other['courseid']);
$this->assertEquals($subscription->eventtype, $event->other['eventtype']);
$this->assertArrayNotHasKey('categoryid', $event->other);
$this->assertArrayNotHasKey('groupid', $event->other);
$this->assertDebuggingNotCalled();
$sink->close();
}
/**
* Tests for calendar_subscription_updated event for a course subscription.
*/
public function test_calendar_subscription_updated_group() {
global $CFG;
require_once($CFG->dirroot . '/calendar/lib.php');
$this->resetAfterTest(true);
$courseid = $this->course->id;
$groupid = 42;
// Create a mock subscription.
$subscription = new stdClass();
$subscription->eventtype = 'group';
$subscription->name = 'test';
$subscription->groupid = "{$courseid}-{$groupid}";
$subscription->id = calendar_add_subscription($subscription);
// Now edit it.
$subscription->name = 'awesome';
// Trigger and capture the event.
$sink = $this->redirectEvents();
calendar_update_subscription($subscription);
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\calendar_subscription_updated', $event);
$this->assertEquals($subscription->id, $event->objectid);
$this->assertEquals($this->course->id, $event->other['courseid']);
$this->assertEquals($groupid, $event->other['groupid']);
$this->assertEquals($subscription->eventtype, $event->other['eventtype']);
$this->assertArrayNotHasKey('categoryid', $event->other);
$this->assertDebuggingNotCalled();
$sink->close();
}
/**
* Tests for calendar_subscription_deleted event for a site subscription.
*/
public function test_calendar_subscription_deleted_site() {
global $CFG;
require_once($CFG->dirroot . '/calendar/lib.php');
$this->resetAfterTest(true);
@@ -512,4 +717,102 @@ class core_calendar_events_testcase extends advanced_testcase {
$sink->close();
}
/**
* Tests for calendar_subscription_deleted event for a category subscription.
*/
public function test_calendar_subscription_deleted_category() {
global $CFG;
require_once($CFG->dirroot . '/calendar/lib.php');
$this->resetAfterTest(true);
$categoryid = $this->course->category;
// Create a mock subscription.
$subscription = new stdClass();
$subscription->eventtype = 'category';
$subscription->name = 'test';
$subscription->categoryid = $categoryid;
$subscription->id = calendar_add_subscription($subscription);
// Trigger and capture the event.
$sink = $this->redirectEvents();
calendar_delete_subscription($subscription);
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\calendar_subscription_deleted', $event);
$this->assertEquals($subscription->id, $event->objectid);
$this->assertEquals($categoryid, $event->other['categoryid']);
$this->assertEquals($subscription->eventtype, $event->other['eventtype']);
$this->assertArrayNotHasKey('courseid', $event->other);
$this->assertArrayNotHasKey('groupid', $event->other);
$this->assertDebuggingNotCalled();
$sink->close();
}
/**
* Tests for calendar_subscription_deleted event for a course.
*/
public function test_calendar_subscription_deleted_course() {
global $CFG;
require_once($CFG->dirroot . '/calendar/lib.php');
$this->resetAfterTest(true);
// Create a mock subscription.
$subscription = new stdClass();
$subscription->eventtype = 'course';
$subscription->name = 'test';
$subscription->courseid = $this->course->id;
$subscription->id = calendar_add_subscription($subscription);
// Trigger and capture the event.
$sink = $this->redirectEvents();
calendar_delete_subscription($subscription);
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\calendar_subscription_deleted', $event);
$this->assertEquals($subscription->id, $event->objectid);
$this->assertEquals($this->course->id, $event->other['courseid']);
$this->assertEquals($subscription->eventtype, $event->other['eventtype']);
$this->assertArrayNotHasKey('categoryid', $event->other);
$this->assertArrayNotHasKey('groupid', $event->other);
$this->assertDebuggingNotCalled();
$sink->close();
}
/**
* Tests for calendar_subscription_deleted event for a group.
*/
public function test_calendar_subscription_deleted_group() {
global $CFG;
require_once($CFG->dirroot . '/calendar/lib.php');
$this->resetAfterTest(true);
$courseid = $this->course->id;
$groupid = 42;
// Create a mock subscription.
$subscription = new stdClass();
$subscription->eventtype = 'group';
$subscription->name = 'test';
$subscription->groupid = "{$courseid}-{$groupid}";
$subscription->id = calendar_add_subscription($subscription);
// Trigger and capture the event.
$sink = $this->redirectEvents();
calendar_delete_subscription($subscription);
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\calendar_subscription_deleted', $event);
$this->assertEquals($subscription->id, $event->objectid);
$this->assertEquals($this->course->id, $event->other['courseid']);
$this->assertEquals($groupid, $event->other['groupid']);
$this->assertEquals($subscription->eventtype, $event->other['eventtype']);
$this->assertArrayNotHasKey('categoryid', $event->other);
$this->assertDebuggingNotCalled();
$sink->close();
}
}
+204
View File
@@ -2201,4 +2201,208 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
$this->assertTrue($result['validationerror']);
}
/**
* A user should not be able load the calendar monthly view for a course they cannot access.
*/
public function test_get_calendar_monthly_view_no_course_permission() {
global $USER;
$this->resetAfterTest(true);
$this->setAdminUser();
$generator = $this->getDataGenerator();
$user1 = $generator->create_user();
$user2 = $generator->create_user();
$course = $generator->create_course();
$generator->enrol_user($user1->id, $course->id, 'student');
$name = 'Course Event (course' . $course->id . ')';
$record = new stdClass();
$record->courseid = $course->id;
$courseevent = $this->create_calendar_event($name, $USER->id, 'course', 0, time(), $record);
$timestart = new DateTime();
// Admin can load the course.
$data = external_api::clean_returnvalue(
core_calendar_external::get_calendar_monthly_view_returns(),
core_calendar_external::get_calendar_monthly_view($timestart->format('n'), $timestart->format('Y'),
$course->id, null, false)
);
$this->assertEquals($data['courseid'], $course->id);
// User enrolled in the course can load the course calendar.
$this->setUser($user1);
$data = external_api::clean_returnvalue(
core_calendar_external::get_calendar_monthly_view_returns(),
core_calendar_external::get_calendar_monthly_view($timestart->format('n'), $timestart->format('Y'),
$course->id, null, false)
);
$this->assertEquals($data['courseid'], $course->id);
// User not enrolled in the course cannot load the course calendar.
$this->setUser($user2);
$this->expectException('require_login_exception');
$data = external_api::clean_returnvalue(
core_calendar_external::get_calendar_monthly_view_returns(),
core_calendar_external::get_calendar_monthly_view($timestart->format('n'), $timestart->format('Y'),
$course->id, null, false)
);
}
/**
* A user should not be able load the calendar day view for a course they cannot access.
*/
public function test_get_calendar_day_view_no_course_permission() {
global $USER;
$this->resetAfterTest(true);
$this->setAdminUser();
$generator = $this->getDataGenerator();
$user1 = $generator->create_user();
$user2 = $generator->create_user();
$course = $generator->create_course();
$generator->enrol_user($user1->id, $course->id, 'student');
$name = 'Course Event (course' . $course->id . ')';
$record = new stdClass();
$record->courseid = $course->id;
$courseevent = $this->create_calendar_event($name, $USER->id, 'course', 0, time(), $record);
$timestart = new DateTime();
// Admin can load the course.
$data = external_api::clean_returnvalue(
core_calendar_external::get_calendar_day_view_returns(),
core_calendar_external::get_calendar_day_view($timestart->format('n'), $timestart->format('Y'),
$timestart->format('j'), $course->id, null)
);
$this->assertEquals($data['courseid'], $course->id);
// User enrolled in the course can load the course calendar.
$this->setUser($user1);
$data = external_api::clean_returnvalue(
core_calendar_external::get_calendar_day_view_returns(),
core_calendar_external::get_calendar_day_view($timestart->format('n'), $timestart->format('Y'),
$timestart->format('j'), $course->id, null)
);
$this->assertEquals($data['courseid'], $course->id);
// User not enrolled in the course cannot load the course calendar.
$this->setUser($user2);
$this->expectException('require_login_exception');
$data = external_api::clean_returnvalue(
core_calendar_external::get_calendar_day_view_returns(),
core_calendar_external::get_calendar_day_view($timestart->format('n'), $timestart->format('Y'),
$timestart->format('j'), $course->id, null)
);
}
/**
* A user should not be able load the calendar upcoming view for a course they cannot access.
*/
public function test_get_calendar_upcoming_view_no_course_permission() {
global $USER;
$this->resetAfterTest(true);
$this->setAdminUser();
$generator = $this->getDataGenerator();
$user1 = $generator->create_user();
$user2 = $generator->create_user();
$course = $generator->create_course();
$generator->enrol_user($user1->id, $course->id, 'student');
$name = 'Course Event (course' . $course->id . ')';
$record = new stdClass();
$record->courseid = $course->id;
$courseevent = $this->create_calendar_event($name, $USER->id, 'course', 0, time(), $record);
// Admin can load the course.
$data = external_api::clean_returnvalue(
core_calendar_external::get_calendar_upcoming_view_returns(),
core_calendar_external::get_calendar_upcoming_view($course->id, null)
);
$this->assertEquals($data['courseid'], $course->id);
// User enrolled in the course can load the course calendar.
$this->setUser($user1);
$data = external_api::clean_returnvalue(
core_calendar_external::get_calendar_upcoming_view_returns(),
core_calendar_external::get_calendar_upcoming_view($course->id, null)
);
$this->assertEquals($data['courseid'], $course->id);
// User not enrolled in the course cannot load the course calendar.
$this->setUser($user2);
$this->expectException('require_login_exception');
$data = external_api::clean_returnvalue(
core_calendar_external::get_calendar_upcoming_view_returns(),
core_calendar_external::get_calendar_upcoming_view($course->id, null)
);
}
/**
* A user should not be able load the calendar event for a course they cannot access.
*/
public function test_get_calendar_event_by_id_no_course_permission() {
global $USER;
$this->resetAfterTest(true);
$this->setAdminUser();
$generator = $this->getDataGenerator();
$user1 = $generator->create_user();
$user2 = $generator->create_user();
$course = $generator->create_course();
$generator->enrol_user($user1->id, $course->id, 'student');
$name = 'Course Event (course' . $course->id . ')';
$record = new stdClass();
$record->courseid = $course->id;
$courseevent = $this->create_calendar_event($name, $USER->id, 'course', 0, time(), $record);
// Admin can load the course event.
$data = external_api::clean_returnvalue(
core_calendar_external::get_calendar_event_by_id_returns(),
core_calendar_external::get_calendar_event_by_id($courseevent->id)
);
$this->assertEquals($data['event']['id'], $courseevent->id);
// User enrolled in the course can load the course event.
$this->setUser($user1);
$data = external_api::clean_returnvalue(
core_calendar_external::get_calendar_event_by_id_returns(),
core_calendar_external::get_calendar_event_by_id($courseevent->id)
);
$this->assertEquals($data['event']['id'], $courseevent->id);
// User not enrolled in the course cannot load the course event.
$this->setUser($user2);
$this->expectException('required_capability_exception');
$data = external_api::clean_returnvalue(
core_calendar_external::get_calendar_event_by_id_returns(),
core_calendar_external::get_calendar_event_by_id($courseevent->id)
);
}
/**
* A user should not be able load the calendar events for a category they cannot see.
*/
public function test_get_calendar_events_hidden_category() {
global $USER;
$this->resetAfterTest(true);
$this->setAdminUser();
$generator = $this->getDataGenerator();
$user1 = $generator->create_user();
$category = $generator->create_category(['visible' => 0]);
$name = 'Category Event (category: ' . $category->id . ')';
$record = new stdClass();
$record->categoryid = $category->id;
$categoryevent = $this->create_calendar_event($name, $USER->id, 'category', 0, time(), $record);
$events = [
'eventids' => [$categoryevent->id]
];
$options = [];
// Admin can load the category event.
$data = external_api::clean_returnvalue(
core_calendar_external::get_calendar_events_returns(),
core_calendar_external::get_calendar_events($events, $options)
);
$this->assertEquals($data['events'][0]['id'], $categoryevent->id);
// User with no special permission to see hidden categories will not see the event.
$this->setUser($user1);
$data = external_api::clean_returnvalue(
core_calendar_external::get_calendar_events_returns(),
core_calendar_external::get_calendar_events($events, $options)
);
$this->assertCount(0, $data['events']);
$this->assertEquals('nopermissions', $data['warnings'][0]['warningcode']);
}
}
+59 -3
View File
@@ -188,7 +188,7 @@ class core_calendar_lib_testcase extends advanced_testcase {
$this->assertEquals($ical->parser_errors, array());
$sub = calendar_get_subscription($id);
calendar_import_icalendar_events($ical, $sub->courseid, $sub->id);
calendar_import_icalendar_events($ical, null, $sub->id);
$count = $DB->count_records('event', array('subscriptionid' => $sub->id));
$this->assertEquals($count, 1);
@@ -205,7 +205,7 @@ class core_calendar_lib_testcase extends advanced_testcase {
$this->assertEquals($ical->parser_errors, array());
$sub = calendar_get_subscription($id);
calendar_import_icalendar_events($ical, $sub->courseid, $sub->id);
calendar_import_icalendar_events($ical, null, $sub->id);
$count = $DB->count_records('event', array('subscriptionid' => $sub->id));
$this->assertEquals($count, 1);
@@ -222,7 +222,7 @@ class core_calendar_lib_testcase extends advanced_testcase {
$this->assertEquals($ical->parser_errors, array());
$sub = calendar_get_subscription($id);
calendar_import_icalendar_events($ical, $sub->courseid, $sub->id);
calendar_import_icalendar_events($ical, null, $sub->id);
$count = $DB->count_records('event', array('subscriptionid' => $sub->id));
$this->assertEquals($count, 1);
}
@@ -672,4 +672,60 @@ class core_calendar_lib_testcase extends advanced_testcase {
$this->assertEquals($group1->id, $typegroups[0]->id);
$this->assertEquals($group2->id, $typegroups[1]->id);
}
public function test_calendar_get_default_courses() {
global $USER, $CFG;
$this->resetAfterTest(true);
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$course1 = $generator->create_course();
$course2 = $generator->create_course();
$course3 = $generator->create_course();
$context = context_course::instance($course1->id);
$this->setAdminUser();
$admin = clone $USER;
$teacher = $generator->create_user();
$generator->enrol_user($teacher->id, $course1->id, 'teacher');
$generator->enrol_user($admin->id, $course1->id, 'teacher');
$CFG->calendar_adminseesall = false;
$courses = calendar_get_default_courses();
// Only enrolled in one course.
$this->assertCount(1, $courses);
$courses = calendar_get_default_courses($course2->id);
// Enrolled course + current course.
$this->assertCount(2, $courses);
$CFG->calendar_adminseesall = true;
$courses = calendar_get_default_courses();
// All courses + SITE.
$this->assertCount(4, $courses);
$courses = calendar_get_default_courses($course2->id);
// All courses + SITE.
$this->assertCount(4, $courses);
$this->setUser($teacher);
$CFG->calendar_adminseesall = false;
$courses = calendar_get_default_courses();
// Only enrolled in one course.
$this->assertCount(1, $courses);
$courses = calendar_get_default_courses($course2->id);
// Enrolled course only (ignore current).
$this->assertCount(1, $courses);
// This setting should not affect teachers.
$CFG->calendar_adminseesall = true;
$courses = calendar_get_default_courses();
// Only enrolled in one course.
$this->assertCount(1, $courses);
$courses = calendar_get_default_courses($course2->id);
// Enrolled course only (ignore current).
$this->assertCount(1, $courses);
}
}
+357
View File
@@ -44,6 +44,37 @@ class core_calendar_local_api_testcase extends advanced_testcase {
$this->resetAfterTest();
}
/**
* Create a feedback activity instance and a calendar event for
* that instance.
*
* @param array $feedbackproperties Properties to set on the feedback activity
* @param array $eventproperties Properties to set on the calendar event
* @return array The feedback activity and the calendar event
*/
protected function create_feedback_activity_and_event(array $feedbackproperties = [], array $eventproperties = []) {
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$mapper = container::get_event_mapper();
$feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
$feedback = $feedbackgenerator->create_instance(array_merge(
['course' => $course->id],
$feedbackproperties
));
$event = create_event(array_merge(
[
'courseid' => $course->id,
'modulename' => 'feedback',
'instance' => $feedback->id
],
$eventproperties
));
$event = $mapper->from_legacy_event_to_event($event);
return [$feedback, $event];
}
/**
* Requesting calendar events from a given time should return all events with a sort
* time at or after the requested time. All events prior to that time should not
@@ -928,4 +959,330 @@ class core_calendar_local_api_testcase extends advanced_testcase {
$this->expectException('moodle_exception');
$newEvent = \core_calendar\local\api::update_event_start_day($event, $newstartdate);
}
/**
* Updating the start day of an event with no maximum cutoff should
* update the corresponding activity property.
*
* Note: This test uses the feedback activity because it requires
* module callbacks to be in place to test.
*/
public function test_update_event_start_day_activity_event_no_max() {
global $CFG, $DB;
require_once($CFG->dirroot . '/mod/feedback/lib.php');
$this->resetAfterTest(true);
$this->setAdminUser();
$timeopen = new DateTimeImmutable('2017-01-1T15:00:00+08:00');
$newstartdate = new DateTimeImmutable('2018-02-2T10:00:00+08:00');
$expected = new DateTimeImmutable('2018-02-2T15:00:00+08:00');
list($feedback, $event) = $this->create_feedback_activity_and_event(
[
'timeopen' => $timeopen->getTimestamp(),
'timeclose' => 0
],
[
'eventtype' => FEEDBACK_EVENT_TYPE_OPEN,
'timestart' => $timeopen->getTimestamp()
]
);
$newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate);
$actual = $newevent->get_times()->get_start_time();
$feedback = $DB->get_record('feedback', ['id' => $feedback->id]);
$this->assertEquals($expected->getTimestamp(), $actual->getTimestamp());
$this->assertEquals($expected->getTimestamp(), $feedback->timeopen);
}
/**
* Updating the start day of an event belonging to an activity to a value
* less than the maximum cutoff should update the corresponding activity
* property.
*
* Note: This test uses the feedback activity because it requires
* module callbacks to be in place to test.
*/
public function test_update_event_start_day_activity_event_less_than_max() {
global $CFG, $DB;
require_once($CFG->dirroot . '/mod/feedback/lib.php');
$this->resetAfterTest(true);
$this->setAdminUser();
$timeopen = new DateTimeImmutable('2017-01-1T15:00:00+08:00');
$timeclose = new DateTimeImmutable('2019-01-1T15:00:00+08:00');
$newstartdate = new DateTimeImmutable('2018-02-2T10:00:00+08:00');
$expected = new DateTimeImmutable('2018-02-2T15:00:00+08:00');
list($feedback, $event) = $this->create_feedback_activity_and_event(
[
'timeopen' => $timeopen->getTimestamp(),
'timeclose' => $timeclose->getTimestamp()
],
[
'eventtype' => FEEDBACK_EVENT_TYPE_OPEN,
'timestart' => $timeopen->getTimestamp()
]
);
$newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate);
$actual = $newevent->get_times()->get_start_time();
$feedback = $DB->get_record('feedback', ['id' => $feedback->id]);
$this->assertEquals($expected->getTimestamp(), $actual->getTimestamp());
$this->assertEquals($expected->getTimestamp(), $feedback->timeopen);
}
/**
* Updating the start day of an event belonging to an activity to a value
* equal to the maximum cutoff should update the corresponding activity
* property.
*
* Note: This test uses the feedback activity because it requires
* module callbacks to be in place to test.
*/
public function test_update_event_start_day_activity_event_equal_to_max() {
global $CFG, $DB;
require_once($CFG->dirroot . '/mod/feedback/lib.php');
$this->resetAfterTest(true);
$this->setAdminUser();
$timeopen = new DateTimeImmutable('2017-01-1T15:00:00+08:00');
$timeclose = new DateTimeImmutable('2018-02-2T15:00:00+08:00');
$newstartdate = new DateTimeImmutable('2018-02-2T10:00:00+08:00');
list($feedback, $event) = $this->create_feedback_activity_and_event(
[
'timeopen' => $timeopen->getTimestamp(),
'timeclose' => $timeclose->getTimestamp(),
],
[
'eventtype' => FEEDBACK_EVENT_TYPE_OPEN,
'timestart' => $timeopen->getTimestamp()
]
);
$newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate);
$actual = $newevent->get_times()->get_start_time();
$feedback = $DB->get_record('feedback', ['id' => $feedback->id]);
$this->assertEquals($timeclose->getTimestamp(), $actual->getTimestamp());
$this->assertEquals($timeclose->getTimestamp(), $feedback->timeopen);
}
/**
* Updating the start day of an event belonging to an activity to a value
* after the maximum cutoff should not update the corresponding activity
* property. Instead it should throw an exception.
*
* Note: This test uses the feedback activity because it requires
* module callbacks to be in place to test.
*/
public function test_update_event_start_day_activity_event_after_max() {
global $CFG, $DB;
require_once($CFG->dirroot . '/mod/feedback/lib.php');
$this->resetAfterTest(true);
$this->setAdminUser();
$timeopen = new DateTimeImmutable('2017-01-1T15:00:00+08:00');
$timeclose = new DateTimeImmutable('2017-02-2T15:00:00+08:00');
$newstartdate = new DateTimeImmutable('2018-02-2T10:00:00+08:00');
list($feedback, $event) = $this->create_feedback_activity_and_event(
[
'timeopen' => $timeopen->getTimestamp(),
'timeclose' => $timeclose->getTimestamp(),
],
[
'eventtype' => FEEDBACK_EVENT_TYPE_OPEN,
'timestart' => $timeopen->getTimestamp()
]
);
$this->expectException('moodle_exception');
$newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate);
}
/**
* Updating the start day of an event with no minimum cutoff should
* update the corresponding activity property.
*
* Note: This test uses the feedback activity because it requires
* module callbacks to be in place to test.
*/
public function test_update_event_start_day_activity_event_no_min() {
global $CFG, $DB;
require_once($CFG->dirroot . '/mod/feedback/lib.php');
$this->resetAfterTest(true);
$this->setAdminUser();
$timeclose = new DateTimeImmutable('2017-01-1T15:00:00+08:00');
$newstartdate = new DateTimeImmutable('2016-02-2T10:00:00+08:00');
$expected = new DateTimeImmutable('2016-02-2T15:00:00+08:00');
list($feedback, $event) = $this->create_feedback_activity_and_event(
[
'timeopen' => 0,
'timeclose' => $timeclose->getTimestamp()
],
[
'eventtype' => FEEDBACK_EVENT_TYPE_OPEN,
'timestart' => $timeclose->getTimestamp()
]
);
$newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate);
$actual = $newevent->get_times()->get_start_time();
$feedback = $DB->get_record('feedback', ['id' => $feedback->id]);
$this->assertEquals($expected->getTimestamp(), $actual->getTimestamp());
$this->assertEquals($expected->getTimestamp(), $feedback->timeopen);
}
/**
* Updating the start day of an event belonging to an activity to a value
* greater than the minimum cutoff should update the corresponding activity
* property.
*
* Note: This test uses the feedback activity because it requires
* module callbacks to be in place to test.
*/
public function test_update_event_start_day_activity_event_greater_than_min() {
global $CFG, $DB;
require_once($CFG->dirroot . '/mod/feedback/lib.php');
$this->resetAfterTest(true);
$this->setAdminUser();
$timeopen = new DateTimeImmutable('2016-01-1T15:00:00+08:00');
$timeclose = new DateTimeImmutable('2019-01-1T15:00:00+08:00');
$newstartdate = new DateTimeImmutable('2018-02-2T10:00:00+08:00');
$expected = new DateTimeImmutable('2018-02-2T15:00:00+08:00');
list($feedback, $event) = $this->create_feedback_activity_and_event(
[
'timeopen' => $timeopen->getTimestamp(),
'timeclose' => $timeclose->getTimestamp()
],
[
'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE,
'timestart' => $timeclose->getTimestamp()
]
);
$newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate);
$actual = $newevent->get_times()->get_start_time();
$feedback = $DB->get_record('feedback', ['id' => $feedback->id]);
$this->assertEquals($expected->getTimestamp(), $actual->getTimestamp());
$this->assertEquals($expected->getTimestamp(), $feedback->timeclose);
}
/**
* Updating the start day of an event belonging to an activity to a value
* equal to the minimum cutoff should update the corresponding activity
* property.
*
* Note: This test uses the feedback activity because it requires
* module callbacks to be in place to test.
*/
public function test_update_event_start_day_activity_event_equal_to_min() {
global $CFG, $DB;
require_once($CFG->dirroot . '/mod/feedback/lib.php');
$this->resetAfterTest(true);
$this->setAdminUser();
$timeopen = new DateTimeImmutable('2017-01-1T15:00:00+08:00');
$timeclose = new DateTimeImmutable('2018-02-2T15:00:00+08:00');
$newstartdate = new DateTimeImmutable('2017-01-1T10:00:00+08:00');
$expected = new DateTimeImmutable('2017-01-1T15:00:00+08:00');
list($feedback, $event) = $this->create_feedback_activity_and_event(
[
'timeopen' => $timeopen->getTimestamp(),
'timeclose' => $timeclose->getTimestamp(),
],
[
'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE,
'timestart' => $timeclose->getTimestamp()
]
);
$newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate);
$actual = $newevent->get_times()->get_start_time();
$feedback = $DB->get_record('feedback', ['id' => $feedback->id]);
$this->assertEquals($expected->getTimestamp(), $actual->getTimestamp());
$this->assertEquals($expected->getTimestamp(), $feedback->timeclose);
}
/**
* Updating the start day of an event belonging to an activity to a value
* before the minimum cutoff should not update the corresponding activity
* property. Instead it should throw an exception.
*
* Note: This test uses the feedback activity because it requires
* module callbacks to be in place to test.
*/
public function test_update_event_start_day_activity_event_before_min() {
global $CFG, $DB;
require_once($CFG->dirroot . '/mod/feedback/lib.php');
$this->resetAfterTest(true);
$this->setAdminUser();
$timeopen = new DateTimeImmutable('2017-01-1T15:00:00+08:00');
$timeclose = new DateTimeImmutable('2017-02-2T15:00:00+08:00');
$newstartdate = new DateTimeImmutable('2016-02-2T10:00:00+08:00');
list($feedback, $event) = $this->create_feedback_activity_and_event(
[
'timeopen' => $timeopen->getTimestamp(),
'timeclose' => $timeclose->getTimestamp(),
],
[
'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE,
'timestart' => $timeclose->getTimestamp()
]
);
$this->expectException('moodle_exception');
$newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate);
}
/**
* Updating the start day of an overridden event belonging to an activity
* should result in an exception. This is to prevent the drag and drop
* of override events.
*
* Note: This test uses the quiz activity because it requires
* module callbacks to be in place and override event support to test.
*/
public function test_update_event_start_day_activity_event_override() {
global $CFG, $DB;
require_once($CFG->dirroot . '/calendar/lib.php');
require_once($CFG->dirroot . '/mod/quiz/lib.php');
$this->resetAfterTest(true);
$this->setAdminUser();
$mapper = container::get_event_mapper();
$timeopen = new DateTimeImmutable('2017-01-1T15:00:00+08:00');
$newstartdate = new DateTimeImmutable('2016-02-2T10:00:00+08:00');
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$course = $generator->create_course();
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance([
'course' => $course->id,
'timeopen' => $timeopen->getTimestamp(),
]);
$event = create_event([
'courseid' => $course->id,
'userid' => $user->id,
'modulename' => 'quiz',
'instance' => $quiz->id,
'eventtype' => QUIZ_EVENT_TYPE_OPEN,
'timestart' => $timeopen->getTimestamp()
]);
$event = $mapper->from_legacy_event_to_event($event);
$record = (object) [
'quiz' => $quiz->id,
'userid' => $user->id
];
$DB->insert_record('quiz_overrides', $record);
$this->expectException('moodle_exception');
$newevent = \core_calendar\local\api::update_event_start_day($event, $newstartdate);
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ information provided here is intended especially for developers.
=== 3.4 ===
* calendar_get_mini has been deprecated. Please update to use the new
exporters and renderers.
* added core_calendar_validate_event_timestart and core_calendar_event_timestart_updated callbacks for module events
* added core_calendar_get_valid_event_timestart_range and core_calendar_event_timestart_updated callbacks for module events
when the update_event_start_day function is used in the local api.
=== 3.3 ===
+7 -21
View File
@@ -77,34 +77,20 @@ $url->param('time', $time);
$PAGE->set_url($url);
$course = get_course($courseid);
if ($courseid != SITEID && !empty($courseid)) {
// Course ID must be valid and existing.
$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
$courses = array($course->id => $course);
navigation_node::override_active_url(new moodle_url('/course/view.php', array('id' => $course->id)));
} else if (!empty($categoryid)) {
$PAGE->set_category_by_id($categoryid);
navigation_node::override_active_url(new moodle_url('/course/index.php', array('categoryid' => $categoryid)));
} else {
$course = get_site();
$courses = calendar_get_default_courses();
if ($categoryid) {
$PAGE->set_category_by_id($categoryid);
} else {
$PAGE->set_context(context_system::instance());
}
if ($PAGE->context->contextlevel === CONTEXT_COURSECAT) {
// Restrict to categories, and their parents, and the courses that the user is enrolled in within those
// categories.
$categories = array_keys($PAGE->categories);
$courses = array_filter($courses, function($course) use ($categories) {
return array_search($course->category, $categories) !== false;
});
navigation_node::override_active_url(new moodle_url('/course/index.php', array('categoryid' => $categoryid)));
}
$PAGE->set_context(context_system::instance());
}
require_login($course, false);
$calendar = new calendar_information(0, 0, 0, $time);
$calendar->set_sources($course, $courses, $PAGE->category);
$calendar = calendar_information::create($time, $courseid, $categoryid);
$pagetitle = '';
+1 -2
View File
@@ -324,7 +324,7 @@ class core_cohort_external extends external_api {
* @return array
*/
public static function search_cohorts($query, $context, $includes = 'parents', $limitfrom = 0, $limitnum = 25) {
global $DB, $CFG, $PAGE;
global $CFG;
require_once($CFG->dirroot . '/cohort/lib.php');
$params = self::validate_parameters(self::search_cohorts_parameters(), array(
@@ -341,7 +341,6 @@ class core_cohort_external extends external_api {
$limitnum = $params['limitnum'];
self::validate_context($context);
$output = $PAGE->get_renderer('tool_lp');
$manager = has_capability('moodle/cohort:manage', $context);
if (!$manager) {
+4 -1
View File
@@ -59,7 +59,10 @@ class api {
if (is_object($instanceorid)) {
$instance = $instanceorid;
} else {
$instance = $DB->get_record($modulename, array('id' => $instanceorid), '*', MUST_EXIST);
$instance = $DB->get_record($modulename, array('id' => $instanceorid), '*', IGNORE_MISSING);
}
if (!$instance) {
return false;
}
$course = get_course($instance->course);
+7
View File
@@ -72,6 +72,13 @@ class core_completion_api_testcase extends advanced_testcase {
$this->assertEquals(\core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED, $event->eventtype);
$this->assertEquals($time, $event->timestart);
$this->assertEquals($time, $event->timesort);
require_once($CFG->dirroot . '/course/lib.php');
// Delete the module.
course_delete_module($assign->cmid);
// Check we don't get a failure when called on a deleted module.
\core_completion\api::update_completion_date_event($assign->cmid, 'assign', null, $time);
}
public function test_update_completion_date_event_update() {
+2 -2
View File
@@ -5,8 +5,8 @@
"type": "project",
"homepage": "https://moodle.org",
"require-dev": {
"phpunit/phpunit": "5.5.*",
"phpunit/dbUnit": "1.4.*",
"phpunit/phpunit": "6.4.*",
"phpunit/dbUnit": "3.0.*",
"moodlehq/behat-extension": "3.34.1",
"mikey179/vfsStream": "^1.6"
}
Generated
+403 -217
View File
File diff suppressed because it is too large Load Diff
@@ -69,10 +69,8 @@ class completion_enabled extends \core_analytics\local\indicator\binary {
$course = $this->retrieve('course', $sampleid);
$cm = false;
if ($sampleorigin === 'course_modules') {
$cm = $this->retrieve('course_modules', $sampleid);
}
// It may not be available, but if it is the indicator checks if completion is enabled for the cm.
$cm = $this->retrieve('course_modules', $sampleid);
$completion = new \completion_info($course);
+2 -1
View File
@@ -268,8 +268,9 @@ class core_course_external extends external_api {
if (!empty($cm->showdescription) or $cm->modname == 'label') {
// We want to use the external format. However from reading get_formatted_content(), $cm->content format is always FORMAT_HTML.
$options = array('noclean' => true);
list($module['description'], $descriptionformat) = external_format_text($cm->content,
FORMAT_HTML, $modcontext->id, $cm->modname, 'intro', $cm->id);
FORMAT_HTML, $modcontext->id, $cm->modname, 'intro', $cm->id, $options);
}
//url of the module
+6 -3
View File
@@ -1404,7 +1404,9 @@ function course_module_update_calendar_events($modulename, $instance = null, $cm
if (!isset($cm)) {
$cm = get_coursemodule_from_instance($modulename, $instance->id, $instance->course);
}
course_module_calendar_event_update_process($instance, $cm);
if (!empty($cm)) {
course_module_calendar_event_update_process($instance, $cm);
}
return true;
}
return false;
@@ -1433,8 +1435,9 @@ function course_module_bulk_update_calendar_events($modulename, $courseid = 0) {
}
foreach ($instances as $instance) {
$cm = get_coursemodule_from_instance($modulename, $instance->id, $instance->course);
course_module_calendar_event_update_process($instance, $cm);
if ($cm = get_coursemodule_from_instance($modulename, $instance->id, $instance->course)) {
course_module_calendar_event_update_process($instance, $cm);
}
}
return true;
}
+2 -1
View File
@@ -25,9 +25,10 @@
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../../lib/completionlib.php');
require_once(__DIR__ . '/../../completion/criteria/completion_criteria_self.php');
/**
* Unit tests for core_course indicators.
*
+1 -1
View File
@@ -1 +1 @@
define(["core/templates","jquery","core/str","core/config","core/notification","core/modal_factory","core/modal_events","core/fragment"],function(a,b,c,d,e,f,g,h){var i=function(a){this.contextid=a.contextid,this.initModal()};return i.prototype.courseid=0,i.prototype.modal=null,i.prototype.initModal=function(){var a=b('.enrolusersbutton.enrol_manual_plugin [type="submit"]'),d=c.get_strings([{key:"enroluserscohorts",component:"enrol_manual"},{key:"enrolusers",component:"enrol_manual"}]),h=d.then(function(a){return a[1]}),i=d.then(function(a){return a[0]});return f.create({type:f.types.SAVE_CANCEL,large:!0,title:h,body:this.getBody()},a).then(function(a){return this.modal=a,this.modal.setSaveButtonText(i),this.modal.getRoot().on(g.hidden,function(){this.modal.setBody(this.getBody())}.bind(this)),this.modal.getRoot().on(g.save,this.submitForm.bind(this)),this.modal.getRoot().on("submit","form",this.submitFormAjax.bind(this)),a}.bind(this)).fail(e.exception)},i.prototype.submitForm=function(a){a.preventDefault(),this.modal.getRoot().find("form").submit()},i.prototype.submitFormAjax=function(a){a.preventDefault();var c=this.modal.getRoot().find("form"),f=c.find(':input[value="_qf__force_multiselect_submission"]');f.each(function(){this.remove()});var g=c.serialize();this.modal.hide();var h={type:"GET",processData:!1,contentType:"application/json"},i=d.wwwroot+"/enrol/manual/ajax.php?"+g;b.ajax(i,h).then(function(a){a.error?e.addNotification({message:a.error,type:"error"}):("undefined"!=typeof window.M.core_formchangechecker&&window.M.core_formchangechecker.reset_form_dirty_state(),window.location.reload())}).fail(e.exception)},i.prototype.getBody=function(){return h.loadFragment("enrol_manual","enrol_users_form",this.contextid,{}).fail(e.exception)},i.prototype.getFooter=function(){return a.render("enrol_manual/enrol_modal_footer",{})},{init:function(a){new i(a)}}});
define(["core/templates","jquery","core/str","core/config","core/notification","core/modal_factory","core/modal_events","core/fragment"],function(a,b,c,d,e,f,g,h){var i={COHORTSELECT:"#id_cohortlist",TRIGGERBUTTONS:".enrolusersbutton.enrol_manual_plugin [type='submit']",UNWANTEDHIDDENFIELDS:":input[value='_qf__force_multiselect_submission']"},j=function(a){this.contextid=a.contextid,this.initModal()};return j.prototype.courseid=0,j.prototype.modal=null,j.prototype.initModal=function(){var a=b(i.TRIGGERBUTTONS);b.when(c.get_strings([{key:"enroluserscohorts",component:"enrol_manual"},{key:"enrolusers",component:"enrol_manual"}]),f.create({type:f.types.SAVE_CANCEL,large:!0},a)).then(function(a,c){this.modal=c,c.setTitle(a[1]),c.setSaveButtonText(a[1]),c.getRoot().on(g.save,this.submitForm.bind(this)),c.getRoot().on("submit","form",this.submitFormAjax.bind(this)),c.getRoot().on(g.hidden,function(){c.setBody("")}),c.getRoot().on(g.shown,function(){var d=this.getBody();d.then(function(d){var e=b(d).find(i.COHORTSELECT).length?0:1;c.setSaveButtonText(a[e])}).fail(e.exception),c.setBody(d)}.bind(this))}.bind(this)).fail(e.exception)},j.prototype.submitForm=function(a){a.preventDefault(),this.modal.getRoot().find("form").submit()},j.prototype.submitFormAjax=function(a){a.preventDefault();var c=this.modal.getRoot().find("form"),f=c.find(i.UNWANTEDHIDDENFIELDS);f.each(function(){this.remove()});var g=c.serialize();this.modal.hide();var h={type:"GET",processData:!1,contentType:"application/json"},j=d.wwwroot+"/enrol/manual/ajax.php?"+g;b.ajax(j,h).then(function(a){a.error?e.addNotification({message:a.error,type:"error"}):("undefined"!=typeof window.M.core_formchangechecker&&window.M.core_formchangechecker.reset_form_dirty_state(),window.location.reload())}).fail(e.exception)},j.prototype.getBody=function(){return h.loadFragment("enrol_manual","enrol_users_form",this.contextid,{}).fail(e.exception)},j.prototype.getFooter=function(){return a.render("enrol_manual/enrol_modal_footer",{})},{init:function(a){new j(a)}}});
+42 -30
View File
@@ -31,6 +31,13 @@ define(['core/templates',
],
function(Template, $, Str, Config, Notification, ModalFactory, ModalEvents, Fragment) {
/** @type {Object} The list of selectors for the quick enrolment modal. */
var SELECTORS = {
COHORTSELECT: "#id_cohortlist",
TRIGGERBUTTONS: ".enrolusersbutton.enrol_manual_plugin [type='submit']",
UNWANTEDHIDDENFIELDS: ":input[value='_qf__force_multiselect_submission']"
};
/**
* Constructor
*
@@ -57,41 +64,46 @@ define(['core/templates',
* @private
*/
QuickEnrolment.prototype.initModal = function() {
var triggerButtons = $('.enrolusersbutton.enrol_manual_plugin [type="submit"]');
var triggerButtons = $(SELECTORS.TRIGGERBUTTONS);
var stringsPromise = Str.get_strings([
{key: 'enroluserscohorts', component: 'enrol_manual'},
{key: 'enrolusers', component: 'enrol_manual'},
]);
var titlePromise = stringsPromise.then(function(strings) {
return strings[1];
});
var buttonPromise = stringsPromise.then(function(strings) {
return strings[0];
});
return ModalFactory.create({
type: ModalFactory.types.SAVE_CANCEL,
large: true,
title: titlePromise,
body: this.getBody()
}, triggerButtons)
.then(function(modal) {
$.when(
Str.get_strings([
{key: 'enroluserscohorts', component: 'enrol_manual'},
{key: 'enrolusers', component: 'enrol_manual'},
]),
ModalFactory.create({
type: ModalFactory.types.SAVE_CANCEL,
large: true,
}, triggerButtons)
)
.then(function(strings, modal) {
this.modal = modal;
this.modal.setSaveButtonText(buttonPromise);
modal.setTitle(strings[1]);
modal.setSaveButtonText(strings[1]);
modal.getRoot().on(ModalEvents.save, this.submitForm.bind(this));
modal.getRoot().on('submit', 'form', this.submitFormAjax.bind(this));
// We want the reset the form every time it is opened.
this.modal.getRoot().on(ModalEvents.hidden, function() {
this.modal.setBody(this.getBody());
modal.getRoot().on(ModalEvents.hidden, function() {
modal.setBody('');
});
modal.getRoot().on(ModalEvents.shown, function() {
var bodyPromise = this.getBody();
bodyPromise.then(function(html) {
var stringIndex = $(html).find(SELECTORS.COHORTSELECT).length ? 0 : 1;
modal.setSaveButtonText(strings[stringIndex]);
return;
})
.fail(Notification.exception);
modal.setBody(bodyPromise);
}.bind(this));
this.modal.getRoot().on(ModalEvents.save, this.submitForm.bind(this));
this.modal.getRoot().on('submit', 'form', this.submitFormAjax.bind(this));
return modal;
return;
}.bind(this))
.fail(Notification.exception);
};
@@ -123,8 +135,8 @@ define(['core/templates',
// Before send the data through AJAX, we need to parse and remove some unwanted hidden fields.
// This hidden fields are added automatically by mforms and when it reaches the AJAX we get an error.
var hidden = form.find(':input[value="_qf__force_multiselect_submission"]');
hidden.each(function () {
var hidden = form.find(SELECTORS.UNWANTEDHIDDENFIELDS);
hidden.each(function() {
this.remove();
});
+18 -2
View File
@@ -102,8 +102,24 @@ class enrol_manual_enrol_users_form extends moodleform {
);
$mform->addElement('autocomplete', 'userlist', get_string('selectusers', 'enrol_manual'), array(), $options);
$options = ['contextid' => $context->id, 'multiple' => true];
$mform->addElement('cohort', 'cohortlist', get_string('selectcohorts', 'enrol_manual'), $options);
// Confirm the user can search for cohorts before displaying select.
if (has_capability('moodle/cohort:manage', $context) || has_capability('moodle/cohort:view', $context)) {
// Check to ensure there is at least one visible cohort before displaying the select box.
// Ideally it would be better to call external_api::call_external_function('core_cohort_search_cohorts')
// (which is used to populate the select box) instead of duplicating logic but there is an issue with globals
// being borked (in this case $PAGE) when combining the usage of fragments and call_external_function().
require_once($CFG->dirroot . '/cohort/lib.php');
$availablecohorts = cohort_get_cohorts($context->id, 0, 1, '');
$availablecohorts = $availablecohorts['cohorts'];
if (!($context instanceof context_system)) {
$availablecohorts = array_merge($availablecohorts,
cohort_get_available_cohorts($context, COHORT_ALL, 0, 1, ''));
}
if (!empty($availablecohorts)) {
$options = ['contextid' => $context->id, 'multiple' => true];
$mform->addElement('cohort', 'cohortlist', get_string('selectcohorts', 'enrol_manual'), $options);
}
}
$roles = get_assignable_roles($context);
$mform->addElement('select', 'roletoassign', get_string('assignrole', 'enrol_manual'), $roles);
+2 -1
View File
@@ -100,7 +100,8 @@ class behat_enrol extends behat_base {
// We have a div here, not a tr.
$this->execute('behat_forms::i_set_the_field_to', array(get_string('selectusers', 'enrol_manual'), $userfullname));
$this->execute("behat_forms::press_button", get_string('enroluserscohorts', 'enrol_manual'));
$enrolusers = get_string('enrolusers', 'enrol_manual');
$this->execute('behat_general::i_click_on_in_the', [$enrolusers, 'button', $enrolusers, 'dialogue']);
} else {
$this->execute('behat_forms::i_set_the_field_to', array(get_string('assignrole', 'role'), $rolename));
+28
View File
@@ -169,5 +169,33 @@ MathJax.Hub.Config({
upgrade_plugin_savepoint(true, 2017101200, 'filter', 'mathjaxloader');
}
if ($oldversion < 2017102000) {
// Re-add Accessible.js (we should not have removed it).
$previousdefault = '
MathJax.Hub.Config({
config: ["default.js", "MMLorHTML.js", "Safe.js"],
errorSettings: { message: ["!"] },
skipStartupTypeset: true,
messageStyle: "none"
});
';
$newdefault = '
MathJax.Hub.Config({
config: ["Accessible.js", "Safe.js"],
errorSettings: { message: ["!"] },
skipStartupTypeset: true,
messageStyle: "none"
});
';
$mathjaxconfig = get_config('filter_mathjaxloader', 'mathjaxconfig');
if (empty($mathjaxconfig) || filter_mathjaxloader_upgrade_mathjaxconfig_equal($mathjaxconfig, $previousdefault)) {
set_config('mathjaxconfig', $newdefault, 'filter_mathjaxloader');
}
upgrade_plugin_savepoint(true, 2017102000, 'filter', 'mathjaxloader');
}
return true;
}
-6
View File
@@ -18,9 +18,3 @@ Upgrading the default MathJax version
3. Check and eventually update the list of language mappings in filter.php.
Also see the unit test for the language mappings.
Changes
-------
* The MathJax 2.7.2 seems to have a bug causing the accessibility extensions
fail in web apps using RequireJS (such as Moodle). We had to stop using the
Accessible.js config for that reason. See MDL-60209 for details.
+1 -1
View File
@@ -45,7 +45,7 @@ if ($ADMIN->fulltree) {
$default = '
MathJax.Hub.Config({
config: ["default.js", "MMLorHTML.js", "Safe.js"],
config: ["Accessible.js", "Safe.js"],
errorSettings: { message: ["!"] },
skipStartupTypeset: true,
messageStyle: "none"
+1 -1
View File
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2017101200;
$plugin->version = 2017102000;
$plugin->requires = 2017050500; // Requires this Moodle version.
$plugin->component= 'filter_mathjaxloader';
+2 -2
View File
@@ -489,7 +489,7 @@ class grading_manager {
* Returns the given method's controller in the gradable area
*
* @param string $method the method name, eg 'rubric' (must be available)
* @return grading_controller
* @return gradingform_controller
*/
public function get_controller($method) {
global $CFG, $DB;
@@ -534,7 +534,7 @@ class grading_manager {
/**
* Returns the controller for the active method if it is available
*
* @return null|grading_controller
* @return null|gradingform_controller
*/
public function get_active_controller() {
if ($gradingmethod = $this->get_active_method()) {
+93
View File
@@ -0,0 +1,93 @@
<?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/>.
/**
* Group details page.
*
* @package core_group
* @copyright 2017 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_group\output;
defined('MOODLE_INTERNAL') || die();
use renderable;
use renderer_base;
use stdClass;
use templatable;
use context_course;
use moodle_url;
/**
* Group details page class.
*
* @package core_group
* @copyright 2017 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class group_details implements renderable, templatable {
/** @var stdClass $group An object with the group information. */
protected $group;
/**
* group_details constructor.
*
* @param int $groupid Group ID to show details of.
*/
public function __construct($groupid) {
$this->group = groups_get_group($groupid, '*', MUST_EXIST);
}
/**
* Export the data.
*
* @param renderer_base $output
* @return stdClass
*/
public function export_for_template(renderer_base $output) {
if (!empty($this->group->description) || (!empty($this->group->picture) && empty($this->group->hidepicture))) {
$context = context_course::instance($this->group->courseid);
$description = file_rewrite_pluginfile_urls($this->group->description,
'pluginfile.php',
$context->id,
'group',
'description',
$this->group->id);
$descriptionformat = $this->group->descriptionformat ?? FORMAT_MOODLE;
$options = [
'overflowdiv' => true,
'context' => $context
];
$data = new stdClass();
$data->name = format_string($this->group->name, true, ['context' => $context]);
$data->pictureurl = get_group_picture_url($this->group, $this->group->courseid, true);
$data->description = format_text($description, $descriptionformat, $options);
if (has_capability('moodle/course:managegroups', $context)) {
$url = new moodle_url('/group/group.php', ['id' => $this->group->id, 'courseid' => $this->group->courseid]);
$data->editurl = $url->out(false);
}
return $data;
} else {
return;
}
}
}
+11
View File
@@ -47,4 +47,15 @@ class renderer extends plugin_renderer_base {
$data = $page->export_for_template($this);
return parent::render_from_template('core_group/index', $data);
}
/**
* Defer to template.
*
* @param group_details $page Group details page object.
* @return string HTML to render the group details.
*/
public function group_details(group_details $page) {
$data = $page->export_for_template($this);
return parent::render_from_template('core_group/group_details', $data);
}
}
+3 -28
View File
@@ -102,36 +102,11 @@ echo $OUTPUT->heading(get_string('adduserstogroup', 'group').": $groupname", 3);
// Store the rows we want to display in the group info.
$groupinforow = array();
// Check if there is a picture to display.
if (!empty($group->picture)) {
$picturecell = new html_table_cell();
$picturecell->attributes['class'] = 'left side picture';
$picturecell->text = print_group_picture($group, $course->id, true, true, false);
$groupinforow[] = $picturecell;
}
// Check if there is a description to display.
$group->description = file_rewrite_pluginfile_urls($group->description, 'pluginfile.php', $context->id, 'group', 'description', $group->id);
if (!empty($group->description)) {
if (!isset($group->descriptionformat)) {
$group->descriptionformat = FORMAT_MOODLE;
}
$options = new stdClass;
$options->overflowdiv = true;
$contentcell = new html_table_cell();
$contentcell->attributes['class'] = 'content';
$contentcell->text = format_text($group->description, $group->descriptionformat, $options);
$groupinforow[] = $contentcell;
}
// Check if we have something to show.
if (!empty($groupinforow)) {
$groupinfotable = new html_table();
$groupinfotable->attributes['class'] = 'groupinfobox';
$groupinfotable->data[] = new html_table_row($groupinforow);
echo html_writer::table($groupinfotable);
$grouprenderer = $PAGE->get_renderer('core_group');
$groupdetailpage = new \core_group\output\group_details($groupid);
echo $grouprenderer->group_details($groupdetailpage);
}
/// Print the editing form
+53
View File
@@ -0,0 +1,53 @@
{{!
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 core_group/group_details
Template for the Groups page.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* name string Group Name
* pictureurl string Group image url
* description string Group description
* edit string edit link to edit the group
Example context (json):
{
"name": "Group Name",
"pictureurl": "https://raw.githubusercontent.com/moodle/moodle/master/pix/g/f1.png",
"description": "This is the description for Group Name",
"editurl": "http://www.moodle.org"
}
}}
{{#name}}
<div class="groupinfobox container-fluid p-x-1 p-y-1">
{{#pictureurl}}
<div class="group-image"><img class="grouppicture" src="{{{pictureurl}}}" alt="{{{name}}}" title="{{{name}}}"/></div>
{{/pictureurl}}
{{#editurl}}
<div class="group-edit"><a href="{{editurl}}">{{#pix}}t/edit, core, {{#str}}editgroupprofile{{/str}}{{/pix}}</a></div>
{{/editurl}}
<h3 class="">{{{name}}}</h3>
<div class="group-description">{{{description}}}</div>
</div>
{{/name}}
+104
View File
@@ -0,0 +1,104 @@
@core @core_group
Feature: The description of a group can be viewed by students and teachers
In order to view the description of a group
As a teacher
I need to create groups and add descriptions to them.
Background:
Given the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
| student2 | Student | 2 | student2@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
@javascript
Scenario: A student can see the group description when visible groups are set. Teachers can see group details.
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| Group mode | Visible groups |
And I press "Save and display"
And I navigate to "Users > Groups" in current page administration
And I press "Create group"
And I set the following fields to these values:
| Group name | Group A |
| Group description | Description for Group A |
And I press "Save changes"
And I press "Create group"
And I set the following fields to these values:
| Group name | Group B |
And I press "Save changes"
And I add "Student 1 (student1@example.com)" user to "Group A" group members
And I add "Student 2 (student2@example.com)" user to "Group B" group members
And I am on "Course 1" course homepage
And I navigate to course participants
And I open the autocomplete suggestions list
And I click on "Group: Group A" item in the autocomplete list
And I should see "Description for Group A"
And ".groupinfobox" "css_element" should exist
And I should see "Description for Group A"
And I click on "Group: Group A" "autocomplete_selection"
And I open the autocomplete suggestions list
And I click on "Group: Group B" item in the autocomplete list
And ".groupinfobox" "css_element" should not exist
And I log out
When I log in as "student1"
And I am on "Course 1" course homepage
And I navigate to course participants
Then I should see "Description for Group A"
And I log out
And I log in as "student2"
And I am on "Course 1" course homepage
And I navigate to course participants
And ".groupinfobox" "css_element" should not exist
@javascript
Scenario: A student can not see the group description when separate groups are set. Teachers can see group details.
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| Group mode | Separate groups |
And I press "Save and display"
And I navigate to "Users > Groups" in current page administration
And I press "Create group"
And I set the following fields to these values:
| Group name | Group A |
| Group description | Description for Group A |
And I press "Save changes"
And I press "Create group"
And I set the following fields to these values:
| Group name | Group B |
And I press "Save changes"
And I add "Student 1 (student1@example.com)" user to "Group A" group members
And I add "Student 2 (student2@example.com)" user to "Group B" group members
And I am on "Course 1" course homepage
And I navigate to course participants
And I open the autocomplete suggestions list
And I click on "Group: Group A" item in the autocomplete list
And I should see "Description for Group A"
And ".groupinfobox" "css_element" should exist
And I click on "Group: Group A" "autocomplete_selection"
And I open the autocomplete suggestions list
And I click on "Group: Group B" item in the autocomplete list
And ".groupinfobox" "css_element" should not exist
And I log out
When I log in as "student1"
And I am on "Course 1" course homepage
And I navigate to course participants
Then I should not see "Description for Group A"
And ".groupinfobox" "css_element" should not exist
And I log out
And I log in as "student2"
And I am on "Course 1" course homepage
And I navigate to course participants
And ".groupinfobox" "css_element" should not exist
+1 -1
View File
@@ -36,7 +36,7 @@ $string['chooselanguagehead'] = 'Sprache wählen';
$string['chooselanguagesub'] = 'Wählen Sie eine Sprache, die Sie während der Installation verwenden wollen. Die ausgewählte Sprache wird nach der Installation als Standardsprache der Instanz benutzt, aber Sie dürfen die Sprache jederzeit ändern.';
$string['clialreadyconfigured'] = 'Die Datei config.php existiert bereits. Bitte benutzen Sie admin/cli/install_database.php, wenn Sie diese Site installieren möchten.';
$string['clialreadyinstalled'] = 'Die Datei config.php existiert bereits. Bitte benutzen Sie admin/cli/install_database.php, wenn Sie diese Site aktualisieren möchten.';
$string['cliinstallheader'] = 'Installation von Moodle {$a} über die Kommandozeile';
$string['cliinstallheader'] = 'Installation von Moodle {$a} über die Befehlszeile';
$string['databasehost'] = 'Datenbank-Server';
$string['databasename'] = 'Datenbank-Name';
$string['databasetypehead'] = 'Datenbank-Treiber wählen';
+3
View File
@@ -37,6 +37,9 @@ $string['cliincorrectvalueerror'] = 'Грешка, погрешна вредно
$string['cliincorrectvalueretry'] = 'Неточни вредност, ве молиме обидете се повторно';
$string['clitypevalue'] = 'впишете вредност';
$string['clitypevaluedefault'] = 'впишете вредност, притиснете Enter за да го користите стандардната вредност ({$a})';
$string['cliunknowoption'] = 'Непрепознаена опција:
{$a}
Ве молиме употребете --help опција.';
$string['cliyesnoprompt'] = 'впишете y (значи да) или n (значи не)';
$string['environmentrequireinstall'] = 'потребно е да се инсталира/овозможи';
$string['environmentrequireversion'] = 'верзијата {$a->needed} е потребна, а вие извршувате {$a->current}';
+6
View File
@@ -72,5 +72,11 @@ $string['welcomep20'] = 'Vedeți această pagină deoarece ați instalat și lan
$string['welcomep30'] = 'Lansarea <strong>{$a->installername}</strong> include aplicațiile
pentru a crea un mediu în care <strong>Moodle</strong> va funcționa, și anume:';
$string['welcomep40'] = 'Pachetul include și <strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong>.';
$string['welcomep50'] = 'Utilizarea tuturor aplicațiilor din acest pachet este determinată de respectivele lor
licențe. Pachetul complet <strong>{$a->installername}</strong> este
<a href="http://www.opensource.org/docs/definition_plain.html">open source</a> și este distribuit sub licența <a href="http://www.gnu.org/copyleft/gpl.html">GPL</a>.';
$string['welcomep60'] = 'Următoarele pagini oferă pași ușor de urmat pentru a
configura și seta <strong>Moodle</strong> în computerul dumneavoastră. Puteți accepta setările implicite
sau, opțional, le modificați pentru a corespunde nevoilor dumneavoastră.';
$string['welcomep70'] = 'Click pe butonul "Next" de mai jos pentru a continua setarea <strong>Moodle</strong>.';
$string['wwwroot'] = 'Adresă Web';
+4 -10
View File
@@ -581,7 +581,7 @@ $string['gravatardefaulturl_help'] = 'Gravatar needs a default image to display
$string['gradeexport'] = 'Primary grade export methods';
$string['guestroleid'] = 'Role for guest';
$string['guestroleid_help'] = 'This role is automatically assigned to the guest user. It is also temporarily assigned to not enrolled users that enter the course via guest enrolment plugin.';
$string['helpadminseesall'] = 'Do admins see all calendar events or just those that apply to themselves?';
$string['helpadminseesall'] = 'In the site calendar, do admins see and filter events from all course calendars or just those from courses they are enrolled in? Regardless of the chosen option, admins will always be able to manage events for each course calendar by navigating to the course first, and then accessing the course calendar directly.';
$string['helpcalendarcustomexport'] = 'Enable custom date range export option in calendar exports. Calendar exports must be enabled before this is effective.';
$string['helpexportlookahead'] = 'How many days in the future does the calendar look for events during export for the custom export option?';
$string['helpexportlookback'] = 'How many days in the past does the calendar look for events during export for the custom export option?';
@@ -616,6 +616,7 @@ $string['installhijacked'] = 'Installation must be finished from the original IP
$string['installsessionerror'] = 'Can not initialise PHP session, please verify that your browser accepts cookies.';
$string['intlrecommended'] = 'Intl extension is used to improve internationalization support, such as locale aware sorting.';
$string['intlrequired'] = 'Intl extension is required to improve internationalization support, such as locale aware sorting and international domain names.';
$string['invalidforgottenpasswordurl'] = 'The forgotten password URL is not a valid URL.';
$string['invalidsection'] = 'Invalid section.';
$string['invaliduserchangeme'] = 'Username "changeme" is reserved -- you cannot create an account with it.';
$string['ipblocked'] = 'This site is not available currently.';
@@ -939,15 +940,7 @@ $string['quizattemptsupgradedmessage'] = 'In Moodle 2.1 there was a major upgrad
$string['recaptchaprivatekey'] = 'ReCAPTCHA secret key';
$string['recaptchapublickey'] = 'ReCAPTCHA site key';
$string['register'] = 'Register your site';
$string['registermoodlenet'] = 'We\'d love to stay in touch for important things for your Moodle site!
By registering,
* You are contributing to our collective knowledge about the users of Moodle which helps us improve Moodle and all our community services
* Youll be one of the first to find out about important notifications such as security alerts and new Moodle releases.
* You can access and activate mobile push notifications from your Moodle site through our free [Moodle Mobile app](https://download.moodle.org/mobile/)
* Optionally, your site can be included as a proud member and supporter of the Moodle community on the [list of registered sites](https://moodle.net/stats).
';
$string['registermoodlenet'] = '<p>We\'d love to stay in touch for important things for your Moodle site!</p><p>By registering,</p><ul><li>You are contributing to our collective knowledge about the users of Moodle which helps us improve Moodle and all our community services.</li><li>Youll be one of the first to find out about important notifications such as security alerts and new Moodle releases.</li><li>You can access and activate mobile push notifications from your Moodle site through our free <a href="https://download.moodle.org/mobile/">Moodle Mobile app</a>.</li><li>Optionally, your site can be included as a proud member and supporter of the Moodle community on the <a href="https://moodle.net/stats">list of registered sites</a>.</li></ul>';
$string['registermoodleorg'] = 'When you register your site';
$string['registermoodleorgli1'] = 'You are added to a low-volume mailing list for important notifications such as security alerts and new releases of Moodle.';
$string['registermoodleorgli2'] = 'Statistics about your site will be added to the {$a} of the worldwide Moodle community.';
@@ -1175,6 +1168,7 @@ $string['unsupporteddbstorageengine'] = 'The database storage engine being used
$string['unsupporteddbtablerowformat'] = 'Your database has tables using Antelope as the file format. You are recommended to convert the tables to the Barracuda file format. See the documentation <a href="https://docs.moodle.org/en/cli">Administration via command line</a> for details of a tool for converting InnoDB tables to Barracuda.';
$string['unsupportedphpversion7'] = 'PHP version 7 is not supported.';
$string['unsupportedphpversion71'] = 'PHP version 7.1 is not supported.';
$string['unsupportedphpversion72'] = 'PHP version 7.2 is not supported.';
$string['unsuspenduser'] = 'Activate user account';
$string['updateaccounts'] = 'Update existing accounts';
$string['updatecomponent'] = 'Update component';
+1
View File
@@ -108,6 +108,7 @@ $string['eventrepeat'] = 'Repeats';
$string['eventsall'] = 'All events';
$string['eventsdeleted'] = 'Events deleted';
$string['eventsimported'] = 'Events imported: {$a}';
$string['eventsource'] = 'Event source';
$string['eventsupdated'] = 'Events updated: {$a}';
$string['eventsfor'] = '{$a} events';
$string['eventskey'] = 'Events key';
+4
View File
@@ -159,3 +159,7 @@ privacy,core_hub
privacy_help,core_hub
configloginhttps,core_admin
loginhttps,core_admin
groupaddnewnote,core_notes
selectnotestate,core_notes
extendenrol,core
groupextendenrol,core
+4
View File
@@ -111,6 +111,10 @@ $string['send'] = 'Send';
$string['sendingvia'] = 'Sending "{$a->provider}" via "{$a->processor}"';
$string['sendingviawhen'] = 'Sending "{$a->provider}" via "{$a->processor}" when {$a->state}';
$string['sendmessage'] = 'Send message';
$string['sendbulkmessage'] = 'Send message to {$a} people';
$string['sendbulkmessagesingle'] = 'Send message to 1 person';
$string['sendbulkmessagesent'] = 'Message sent to {$a} people';
$string['sendbulkmessagesentsingle'] = 'Message sent to 1 person';
$string['sendmessageto'] = 'Send message to {$a}';
$string['sendmessagetopopup'] = 'Send message to {$a} - new window';
$string['settings'] = 'Settings';

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