Merge branch 'MDL-59478-master' of git://github.com/junpataleta/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 11 Sep 2017 22:21:48 +0000 (00:21 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 11 Sep 2017 22:21:48 +0000 (00:21 +0200)
383 files changed:
admin/index.php
admin/registration/forms.php
admin/registration/lib.php
admin/registration/register.php
admin/renderer.php
admin/roles/classes/capability_table_with_risks.php
admin/settings/analytics.php
admin/tool/analytics/classes/output/models_list.php
admin/tool/analytics/cli/guess_course_start_and_end.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/model.php
admin/tool/analytics/templates/models_list.mustache
admin/tool/lp/templates/form-user-selector-suggestion.mustache
admin/tool/lp/tests/behat/plan_crud.feature
admin/tool/mobile/lang/en/tool_mobile.php
analytics/classes/classifier.php [new file with mode: 0644]
analytics/classes/course.php
analytics/classes/local/analyser/base.php
analytics/classes/local/indicator/base.php
analytics/classes/local/indicator/binary.php
analytics/classes/local/indicator/community_of_inquiry_activity.php
analytics/classes/local/indicator/discrete.php
analytics/classes/local/indicator/linear.php
analytics/classes/local/target/base.php
analytics/classes/local/target/binary.php
analytics/classes/local/target/discrete.php
analytics/classes/local/target/linear.php
analytics/classes/local/time_splitting/base.php
analytics/classes/model.php
analytics/classes/predictor.php
analytics/classes/regressor.php [new file with mode: 0644]
analytics/classes/site.php
analytics/tests/model_test.php
backup/moodle2/backup_course_task.class.php
backup/moodle2/restore_stepslib.php
backup/util/helper/tests/backup_encode_content_test.php
blocks/activity_results/block_activity_results.php
blocks/calendar_month/block_calendar_month.php
blocks/calendar_month/tests/behat/block_calendar_month.feature
blocks/myoverview/templates/course-event-list.mustache
blocks/participants/block_participants.php
calendar/amd/build/calendar.min.js
calendar/amd/build/calendar_filter.min.js [new file with mode: 0644]
calendar/amd/build/calendar_mini.min.js [new file with mode: 0644]
calendar/amd/build/drag_drop.min.js [deleted file]
calendar/amd/build/drag_drop_data_store.min.js [new file with mode: 0644]
calendar/amd/build/event_form.min.js
calendar/amd/build/events.min.js
calendar/amd/build/modal_event_form.min.js
calendar/amd/build/month_navigation_drag_drop.min.js [new file with mode: 0644]
calendar/amd/build/month_view_drag_drop.min.js [new file with mode: 0644]
calendar/amd/build/selectors.min.js [new file with mode: 0644]
calendar/amd/build/summary_modal.min.js
calendar/amd/build/view_manager.min.js
calendar/amd/src/calendar.js
calendar/amd/src/calendar_filter.js [new file with mode: 0644]
calendar/amd/src/calendar_mini.js [new file with mode: 0644]
calendar/amd/src/drag_drop_data_store.js [new file with mode: 0644]
calendar/amd/src/event_form.js
calendar/amd/src/events.js
calendar/amd/src/modal_event_form.js
calendar/amd/src/month_navigation_drag_drop.js [new file with mode: 0644]
calendar/amd/src/month_view_drag_drop.js [moved from calendar/amd/src/drag_drop.js with 64% similarity]
calendar/amd/src/selectors.js [new file with mode: 0644]
calendar/amd/src/summary_modal.js
calendar/amd/src/view_manager.js
calendar/classes/external/calendar_event_exporter.php
calendar/classes/external/day_exporter.php
calendar/classes/external/month_exporter.php
calendar/classes/external/week_day_exporter.php [new file with mode: 0644]
calendar/classes/external/week_exporter.php
calendar/classes/local/event/forms/create.php
calendar/classes/local/event/mappers/create_update_form_mapper.php
calendar/lib.php
calendar/renderer.php
calendar/set.php [deleted file]
calendar/templates/calendar_mini.mustache [new file with mode: 0644]
calendar/templates/calendar_month.mustache [new file with mode: 0644]
calendar/templates/event_filter.mustache [new file with mode: 0644]
calendar/templates/event_filter_key.mustache [new file with mode: 0644]
calendar/templates/event_summary_modal.mustache
calendar/templates/minicalendar_day_link.mustache
calendar/templates/modal_event_form.mustache
calendar/templates/month_detailed.mustache
calendar/templates/month_mini.mustache [new file with mode: 0644]
calendar/templates/month_navigation.mustache
calendar/tests/behat/behat_calendar.php
calendar/tests/behat/calendar.feature
calendar/tests/externallib_test.php
calendar/view.php
course/classes/analytics/indicator/completion_enabled.php [new file with mode: 0644]
course/classes/analytics/indicator/no_teacher.php
course/classes/analytics/indicator/potential_cognitive_depth.php [new file with mode: 0644]
course/classes/analytics/indicator/potential_social_breadth.php [new file with mode: 0644]
course/classes/search/mycourse.php
course/lib.php
course/recent_form.php
course/tests/courselib_test.php
course/tests/indicators_test.php [new file with mode: 0644]
enrol/externallib.php
enrol/ldap/lang/en/enrol_ldap.php
enrol/locallib.php
enrol/manual/templates/form-user-selector-suggestion.mustache
enrol/meta/tests/behat/enrol_meta.feature
enrol/self/tests/behat/self_enrolment.feature
group/classes/output/user_groups_editable.php
install/lang/da/install.php
install/lang/dsb/langconfig.php [new file with mode: 0644]
install/lang/kn/install.php [new file with mode: 0644]
install/lang/sk/moodle.php
install/lang/sl/error.php
lang/en/access.php
lang/en/admin.php
lang/en/analytics.php
lang/en/calendar.php
lang/en/completion.php
lang/en/deprecated.txt
lang/en/enrol.php
lang/en/error.php
lang/en/hub.php
lang/en/moodle.php
lang/en/repository.php
lang/en/webservice.php
lib/amd/build/form-autocomplete.min.js
lib/amd/build/inplace_editable.min.js
lib/amd/build/modal_confirm.min.js
lib/amd/build/modal_events.min.js
lib/amd/build/modal_factory.min.js
lib/amd/build/permissionmanager.min.js
lib/amd/src/form-autocomplete.js
lib/amd/src/inplace_editable.js
lib/amd/src/modal_confirm.js
lib/amd/src/modal_events.js
lib/amd/src/modal_factory.js
lib/amd/src/permissionmanager.js
lib/classes/analytics/target/course_dropout.php
lib/classes/analytics/target/no_teaching.php
lib/classes/oauth2/api.php
lib/classes/oauth2/client.php
lib/classes/oauth2/refresh_system_tokens_task.php
lib/classes/oauth2/rest.php
lib/classes/task/manager.php
lib/coursecatlib.php
lib/db/access.php
lib/db/install.xml
lib/db/upgrade.php
lib/ddl/tests/ddl_test.php
lib/dml/moodle_database.php
lib/dml/tests/dml_test.php
lib/filebrowser/file_browser.php
lib/filebrowser/file_info_context_course.php
lib/filebrowser/file_info_context_coursecat.php
lib/filebrowser/file_info_context_module.php
lib/filebrowser/file_info_context_system.php
lib/filebrowser/tests/file_browser_test.php [new file with mode: 0644]
lib/filelib.php
lib/filestorage/file_system.php
lib/filestorage/file_system_filedir.php
lib/form/datetimeselector.php
lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector-debug.js
lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector-min.js
lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector.js
lib/form/yui/src/dateselector/js/dateselector.js
lib/formslib.php
lib/mlbackend/php/classes/processor.php
lib/mlbackend/php/lang/en/mlbackend_php.php
lib/mlbackend/python/classes/processor.php
lib/navigationlib.php
lib/templates/loginform.mustache
lib/templates/overlay_loading.mustache [moved from lib/templates/modal_confirm.mustache with 60% similarity]
lib/templates/url_select.mustache
lib/tests/adhoc_task_test.php
lib/tests/behat/behat_forms.php
lib/upgrade.txt
message/amd/build/message_area_messages.min.js
message/amd/src/message_area_messages.js
message/classes/api.php
message/classes/search/base_message.php
message/output/airnotifier/lang/en/message_airnotifier.php
message/tests/behat/delete_all_messages.feature
message/tests/search_test_received.php
message/tests/search_test_sent.php
mod/assign/backup/moodle2/restore_assign_stepslib.php
mod/assign/classes/analytics/indicator/cognitive_depth.php
mod/assign/classes/analytics/indicator/social_breadth.php
mod/assign/classes/search/activity.php
mod/assign/db/upgrade.php
mod/assign/feedback/editpdf/classes/combined_document.php
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/submission/comments/lib.php
mod/assign/submission/comments/locallib.php
mod/assign/tests/behat/allow_another_attempt.feature
mod/assign/tests/behat/assign_comments_no_error.feature [new file with mode: 0644]
mod/assign/tests/behat/rescale_grades.feature
mod/assign/tests/lib_test.php
mod/assign/tests/locallib_test.php
mod/assign/upgrade.txt
mod/assign/upgradelib.php
mod/assign/version.php
mod/book/classes/analytics/indicator/activity_base.php
mod/book/classes/analytics/indicator/cognitive_depth.php
mod/book/classes/analytics/indicator/social_breadth.php
mod/book/classes/search/activity.php
mod/book/classes/search/chapter.php
mod/book/lang/en/book.php
mod/book/lib.php
mod/chat/classes/analytics/indicator/cognitive_depth.php
mod/chat/classes/analytics/indicator/social_breadth.php
mod/chat/classes/search/activity.php
mod/chat/lang/en/chat.php
mod/choice/classes/analytics/indicator/cognitive_depth.php
mod/choice/classes/analytics/indicator/social_breadth.php
mod/choice/classes/search/activity.php
mod/choice/lang/en/choice.php
mod/data/classes/analytics/indicator/cognitive_depth.php
mod/data/classes/analytics/indicator/social_breadth.php
mod/data/classes/search/activity.php
mod/data/lang/en/data.php
mod/feedback/classes/analytics/indicator/cognitive_depth.php
mod/feedback/classes/analytics/indicator/social_breadth.php
mod/feedback/classes/search/activity.php
mod/feedback/lang/en/feedback.php
mod/feedback/show_nonrespondents.php
mod/folder/classes/analytics/indicator/activity_base.php
mod/folder/classes/analytics/indicator/cognitive_depth.php
mod/folder/classes/analytics/indicator/social_breadth.php
mod/folder/classes/search/activity.php
mod/folder/lang/en/folder.php
mod/forum/classes/analytics/indicator/cognitive_depth.php
mod/forum/classes/analytics/indicator/social_breadth.php
mod/forum/classes/search/activity.php
mod/forum/classes/search/post.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/tests/behat/forum_subscriptions_availability.feature
mod/forum/tests/behat/forum_subscriptions_management.feature
mod/glossary/classes/analytics/indicator/cognitive_depth.php
mod/glossary/classes/analytics/indicator/social_breadth.php
mod/glossary/classes/search/activity.php
mod/glossary/classes/search/entry.php
mod/glossary/lang/en/glossary.php
mod/imscp/classes/analytics/indicator/activity_base.php
mod/imscp/classes/analytics/indicator/cognitive_depth.php
mod/imscp/classes/analytics/indicator/social_breadth.php
mod/imscp/classes/search/activity.php
mod/imscp/lang/en/imscp.php
mod/label/classes/analytics/indicator/activity_base.php
mod/label/classes/analytics/indicator/cognitive_depth.php
mod/label/classes/analytics/indicator/social_breadth.php
mod/label/classes/search/activity.php
mod/label/lang/en/label.php
mod/lesson/classes/analytics/indicator/cognitive_depth.php
mod/lesson/classes/analytics/indicator/social_breadth.php
mod/lesson/classes/search/activity.php
mod/lesson/lang/en/lesson.php
mod/lti/classes/analytics/indicator/cognitive_depth.php
mod/lti/classes/analytics/indicator/social_breadth.php
mod/lti/classes/search/activity.php
mod/lti/lang/en/lti.php
mod/lti/locallib.php
mod/lti/tests/locallib_test.php
mod/page/classes/analytics/indicator/activity_base.php
mod/page/classes/analytics/indicator/cognitive_depth.php
mod/page/classes/analytics/indicator/social_breadth.php
mod/page/classes/search/activity.php
mod/page/lang/en/page.php
mod/quiz/classes/analytics/indicator/cognitive_depth.php
mod/quiz/classes/analytics/indicator/social_breadth.php
mod/quiz/classes/search/activity.php
mod/quiz/lang/en/quiz.php
mod/resource/classes/analytics/indicator/activity_base.php
mod/resource/classes/analytics/indicator/cognitive_depth.php
mod/resource/classes/analytics/indicator/social_breadth.php
mod/resource/classes/search/activity.php
mod/resource/lang/en/resource.php
mod/resource/tests/search_test.php
mod/scorm/classes/analytics/indicator/cognitive_depth.php
mod/scorm/classes/analytics/indicator/social_breadth.php
mod/scorm/classes/search/activity.php
mod/scorm/lang/en/scorm.php
mod/survey/classes/analytics/indicator/activity_base.php
mod/survey/classes/analytics/indicator/cognitive_depth.php
mod/survey/classes/analytics/indicator/social_breadth.php
mod/survey/classes/search/activity.php
mod/survey/lang/en/survey.php
mod/url/classes/analytics/indicator/activity_base.php
mod/url/classes/analytics/indicator/cognitive_depth.php
mod/url/classes/analytics/indicator/social_breadth.php
mod/url/classes/search/activity.php
mod/url/lang/en/url.php
mod/wiki/classes/analytics/indicator/cognitive_depth.php
mod/wiki/classes/analytics/indicator/social_breadth.php
mod/wiki/classes/search/activity.php
mod/wiki/classes/search/collaborative_page.php
mod/wiki/lang/en/wiki.php
mod/workshop/classes/analytics/indicator/cognitive_depth.php
mod/workshop/classes/analytics/indicator/social_breadth.php
mod/workshop/classes/search/activity.php
mod/workshop/lang/en/workshop.php
notes/delete.php
notes/edit.php
notes/index.php
phpunit.xml.dist
question/engine/questionusage.php
report/completion/index.php
report/completion/lib.php
report/completion/user.php
report/insights/action.php
report/insights/classes/output/insight.php
report/insights/classes/output/insights_list.php
report/insights/classes/output/renderer.php
report/insights/insights.php
report/insights/lang/en/report_insights.php
report/insights/lib.php
report/insights/prediction.php
report/insights/settings.php [new file with mode: 0644]
report/insights/templates/insight.mustache
report/insights/templates/insight_details.mustache
report/insights/templates/insights_list.mustache
report/insights/version.php
report/log/index.php
report/log/lang/en/report_log.php
report/log/lib.php
report/log/user.php
report/outline/lang/en/report_outline.php
report/outline/lib.php
report/outline/user.php
report/security/lang/en/report_security.php
report/security/locallib.php
report/stats/lib.php
report/stats/user.php
repository/filepicker.js
repository/local/lib.php
search/classes/base.php
search/classes/base_activity.php
search/classes/base_mod.php
search/tests/base_activity_test.php [new file with mode: 0644]
search/tests/base_test.php [new file with mode: 0644]
theme/boost/scss/moodle/calendar.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/user.scss
theme/boost/templates/core/form_autocomplete_selection.mustache
theme/boost/templates/core_calendar/minicalendar_day_link.mustache
theme/bootstrapbase/less/moodle.less
theme/bootstrapbase/less/moodle/bs4-compat.less
theme/bootstrapbase/less/moodle/calendar.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/forms.less
theme/bootstrapbase/less/moodle/modal.less
theme/bootstrapbase/less/moodle/user.less
theme/bootstrapbase/less/moodle/variables.less [deleted file]
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/block_myoverview/course-event-list-item.mustache
theme/bootstrapbase/templates/block_myoverview/course-event-list.mustache
theme/bootstrapbase/templates/block_myoverview/course-summary.mustache
theme/bootstrapbase/templates/block_myoverview/courses-view-course-item.mustache
theme/bootstrapbase/templates/block_myoverview/event-list-item.mustache
theme/bootstrapbase/templates/core_calendar/minicalendar_day_link.mustache
theme/bootstrapbase/templates/report_insights/insight.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/report_insights/insight_details.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/report_insights/insights_list.mustache [new file with mode: 0644]
user/action_redir.php
user/amd/build/status_field.min.js
user/amd/src/status_field.js
user/classes/output/user_roles_editable.php
user/classes/participants_table.php
user/classes/search/user.php
user/externallib.php
user/index.php
user/lib.php
user/messageselect.php
user/renderer.php
user/templates/status_field.mustache
user/templates/unified_filter.mustache
user/tests/behat/edit_user_enrolment.feature
user/tests/behat/view_participants.feature
user/tests/userlib_test.php
version.php
webservice/lib.php
webservice/rest/simpleserver.php

index d0da26b..ba80a96 100644 (file)
@@ -864,10 +864,17 @@ $cachewarnings = cache_helper::warnings();
 $eventshandlers = $DB->get_records_sql('SELECT DISTINCT component FROM {events_handlers}');
 $themedesignermode = !empty($CFG->themedesignermode);
 
+// Check if a directory with development libraries exists.
+if (is_dir($CFG->dirroot.'/vendor') || is_dir($CFG->dirroot.'/node_modules')) {
+    $devlibdir = true;
+} else {
+    $devlibdir = false;
+}
+
 admin_externalpage_setup('adminnotifications');
 
 $output = $PAGE->get_renderer('core', 'admin');
 
 echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, $cronoverdue, $dbproblems,
                                        $maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
-                                       $registered, $cachewarnings, $eventshandlers, $themedesignermode);
+                                       $registered, $cachewarnings, $eventshandlers, $themedesignermode, $devlibdir);
index 4cb0e40..0d70879 100644 (file)
@@ -257,7 +257,7 @@ class site_registration_form extends moodleform {
         $modulenumberaveragecfg = get_config('hub', 'site_modulenumberaverage_' . $cleanhuburl);
         // Mobile related information.
         $mobileservicesenabled = get_config('hub', 'site_mobileservicesenabled_' . $cleanhuburl);
-        $mobilenotificacionsenabled = get_config('hub', 'site_mobilenotificacionsenabled_' . $cleanhuburl);
+        $mobilenotificationsenabled = get_config('hub', 'site_mobilenotificationsenabled_' . $cleanhuburl);
         $registereduserdevices = get_config('hub', 'site_registereduserdevices_' . $cleanhuburl);
         $registeredactiveuserdevices = get_config('hub', 'site_registeredactiveuserdevices_' . $cleanhuburl);
 
@@ -474,10 +474,10 @@ class site_registration_form extends moodleform {
             $mform->setType('mobileservicesenabled', PARAM_INT);
 
             $mobilenotificationsstatus = $aremobilenotificationsenabled ? 'yes' : 'no';
-            $mform->addElement('checkbox', 'mobilenotificacionsenabled', '',
-                    " " . get_string('mobilenotificacionsenabled', 'hub', $mobilenotificationsstatus));
-            $mform->setDefault('mobilenotificacionsenabled', $mobilenotificacionsenabled != -1);
-            $mform->setType('mobilenotificacionsenabled', PARAM_INT);
+            $mform->addElement('checkbox', 'mobilenotificationsenabled', '',
+                    " " . get_string('mobilenotificationsenabled', 'hub', $mobilenotificationsstatus));
+            $mform->setDefault('mobilenotificationsenabled', $mobilenotificationsenabled != -1);
+            $mform->setType('mobilenotificationsenabled', PARAM_INT);
 
             $mform->addElement('checkbox', 'registereduserdevices', '',
                     " " . get_string('registereduserdevices', 'hub', $registereduserdevicescount));
@@ -547,10 +547,10 @@ class site_registration_form extends moodleform {
             $mform->setType('mobileservicesenabled', PARAM_INT);
 
             $mobilenotificationsstatus = $aremobilenotificationsenabled ? 'yes' : 'no';
-            $mform->addElement('static', 'mobilenotificacionsenabledlabel', '',
-                    " " . get_string('mobilenotificacionsenabled', 'hub', $mobilenotificationsstatus));
-            $mform->addElement('hidden', 'mobilenotificacionsenabled', 1);
-            $mform->setType('mobilenotificacionsenabled', PARAM_INT);
+            $mform->addElement('static', 'mobilenotificationsenabledlabel', '',
+                    " " . get_string('mobilenotificationsenabled', 'hub', $mobilenotificationsstatus));
+            $mform->addElement('hidden', 'mobilenotificationsenabled', 1);
+            $mform->setType('mobilenotificationsenabled', PARAM_INT);
 
             $mform->addElement('static', 'registereduserdeviceslabel', '',
                     " " . get_string('registereduserdevices', 'hub', $registereduserdevicescount));
index 1e48bc8..8d1f8db 100644 (file)
@@ -282,7 +282,7 @@ class registration_manager {
         $siteinfo['url'] = $CFG->wwwroot;
         // Mobile related information.
         $siteinfo['mobileservicesenabled'] = 0;
-        $siteinfo['mobilenotificacionsenabled'] = 0;
+        $siteinfo['mobilenotificationsenabled'] = 0;
         $siteinfo['registereduserdevices'] = 0;
         $siteinfo['registeredactiveuserdevices'] = 0;
         if (!empty($CFG->enablewebservices) && !empty($CFG->enablemobilewebservice)) {
@@ -291,7 +291,7 @@ class registration_manager {
             $airnotifierextpath = $CFG->dirroot . '/message/output/airnotifier/externallib.php';
             if (file_exists($airnotifierextpath)) { // Maybe some one uninstalled the plugin.
                 require_once($airnotifierextpath);
-                $siteinfo['mobilenotificacionsenabled'] = message_airnotifier_external::is_system_configured();
+                $siteinfo['mobilenotificationsenabled'] = message_airnotifier_external::is_system_configured();
                 $siteinfo['registeredactiveuserdevices'] = $DB->count_records('message_airnotifier_devices', array('enable' => 1));
             }
         }
index 43d2e42..2b7cf96 100644 (file)
@@ -69,7 +69,7 @@ if (!empty($fromform) and confirm_sesskey()) {
     // The function get_site_info() will not calculate the optional data if config is set to -1.
     $inputnames = array('courses', 'users', 'roleassignments', 'posts', 'questions', 'resources',
         'badges', 'issuedbadges', 'modulenumberaverage', 'participantnumberaverage',
-        'mobileservicesenabled', 'mobilenotificacionsenabled', 'registereduserdevices', 'registeredactiveuserdevices');
+        'mobileservicesenabled', 'mobilenotificationsenabled', 'registereduserdevices', 'registeredactiveuserdevices');
     foreach ($inputnames as $inputname) {
         if (empty($fromform->{$inputname})) {
             $fromform->{$inputname} = -1;
@@ -103,7 +103,7 @@ if (!empty($fromform) and confirm_sesskey()) {
     set_config('site_modulenumberaverage_' . $cleanhuburl, $fromform->modulenumberaverage, 'hub');
     set_config('site_participantnumberaverage_' . $cleanhuburl, $fromform->participantnumberaverage, 'hub');
     set_config('site_mobileservicesenabled_' . $cleanhuburl, $fromform->mobileservicesenabled, 'hub');
-    set_config('site_mobilenotificacionsenabled_' . $cleanhuburl, $fromform->mobilenotificacionsenabled, 'hub');
+    set_config('site_mobilenotificationsenabled_' . $cleanhuburl, $fromform->mobilenotificationsenabled, 'hub');
     set_config('site_registereduserdevices_' . $cleanhuburl, $fromform->registereduserdevices, 'hub');
     set_config('site_registeredactiveuserdevices_' . $cleanhuburl, $fromform->registeredactiveuserdevices, 'hub');
 }
@@ -149,7 +149,7 @@ if (!empty($fromform) and empty($update) and confirm_sesskey()) {
         $fromform->participantnumberaverage = $siteinfo['participantnumberaverage'];
         $fromform->street = $siteinfo['street'];
         $fromform->mobileservicesenabled = $siteinfo['mobileservicesenabled'];
-        $fromform->mobilenotificacionsenabled = $siteinfo['mobilenotificacionsenabled'];
+        $fromform->mobilenotificationsenabled = $siteinfo['mobilenotificationsenabled'];
         $fromform->registereduserdevices = $siteinfo['registereduserdevices'];
         $fromform->registeredactiveuserdevices = $siteinfo['registeredactiveuserdevices'];
 
index 63cc1ad..561b24a 100644 (file)
@@ -276,12 +276,15 @@ class core_admin_renderer extends plugin_renderer_base {
      * @param int|null $availableupdatesfetch timestamp of the most recent updates fetch or null (unknown)
      * @param string[] $cachewarnings An array containing warnings from the Cache API.
      * @param array $eventshandlers Events 1 API handlers.
+     * @param bool $themedesignermode Warn about the theme designer mode.
+     * @param bool $devlibdir Warn about development libs directory presence.
      *
      * @return string HTML to output.
      */
     public function admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed,
             $cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch,
-            $buggyiconvnomb, $registered, array $cachewarnings = array(), $eventshandlers = 0, $themedesignermode = false) {
+            $buggyiconvnomb, $registered, array $cachewarnings = array(), $eventshandlers = 0,
+            $themedesignermode = false, $devlibdir = false) {
         global $CFG;
         $output = '';
 
@@ -290,6 +293,7 @@ class core_admin_renderer extends plugin_renderer_base {
         $output .= $this->legacy_log_store_writing_error();
         $output .= empty($CFG->disableupdatenotifications) ? $this->available_updates($availableupdates, $availableupdatesfetch) : '';
         $output .= $this->insecure_dataroot_warning($insecuredataroot);
+        $output .= $this->development_libs_directories_warning($devlibdir);
         $output .= $this->themedesignermode_warning($themedesignermode);
         $output .= $this->display_errors_warning($errorsdisplayed);
         $output .= $this->buggy_iconv_warning($buggyiconvnomb);
@@ -520,6 +524,24 @@ class core_admin_renderer extends plugin_renderer_base {
         }
     }
 
+    /**
+     * Render a warning that a directory with development libs is present.
+     *
+     * @param bool $devlibdir True if the warning should be displayed.
+     * @return string
+     */
+    protected function development_libs_directories_warning($devlibdir) {
+
+        if ($devlibdir) {
+            $moreinfo = new moodle_url('/report/security/index.php');
+            $warning = get_string('devlibdirpresent', 'core_admin', ['moreinfourl' => $moreinfo->out()]);
+            return $this->warning($warning, 'error');
+
+        } else {
+            return '';
+        }
+    }
+
     /**
      * Render an appropriate message if dataroot is insecure.
      * @param bool $errorsdisplayed
index b62ad19..db7e16b 100644 (file)
@@ -183,9 +183,14 @@ abstract class core_role_capability_table_with_risks extends core_role_capabilit
     public function get_risk_icon($type) {
         global $OUTPUT;
 
-        $text = $OUTPUT->pix_icon('i/' . str_replace('risk', 'risk_', $type), get_string($type . 'short', 'admin'));
+        $alt = get_string("{$type}short", "admin");
+        $title = get_string($type, "admin");
+
+        $text = $OUTPUT->pix_icon('i/' . str_replace('risk', 'risk_', $type), $alt, 'moodle', [
+                'title' => $title,
+            ]);
         $action = new popup_action('click', $this->risksurl, 'docspopup');
-        $riskicon = $OUTPUT->action_link($this->risksurl, $text, $action, array('title'=>get_string($type, 'admin')));
+        $riskicon = $OUTPUT->action_link($this->risksurl, $text, $action);
 
         return $riskicon;
     }
index 17724a3..3d28719 100644 (file)
@@ -79,7 +79,7 @@ if ($hassiteconfig) {
 
         $timesplittingoptions = array();
         $timesplittingdefaults = array('\core\analytics\time_splitting\quarters_accum',
-            '\core\analytics\time_splitting\quarters', '\core\analytics\time_splitting\no_splitting');
+            '\core\analytics\time_splitting\quarters', '\core\analytics\time_splitting\single_range');
         foreach ($alltimesplittings as $key => $timesplitting) {
             $timesplittingoptions[$key] = $timesplitting->get_name();
         }
@@ -97,5 +97,9 @@ if ($hassiteconfig) {
         }
         $settings->add(new admin_setting_configdirectory('analytics/modeloutputdir', new lang_string('modeloutputdir', 'analytics'),
             new lang_string('modeloutputdirinfo', 'analytics'), $defaultmodeloutputdir));
+
+        // Disable web interface evaluation and get predictions.
+        $settings->add(new admin_setting_configcheckbox('analytics/onlycli', new lang_string('onlycli', 'analytics'),
+            new lang_string('onlycliinfo', 'analytics'), 1));
     }
 }
index c068b18..b05e102 100644 (file)
@@ -62,6 +62,12 @@ class models_list implements \renderable, \templatable {
 
         $data = new \stdClass();
 
+        $onlycli = get_config('analytics', 'onlycli');
+        if ($onlycli === false) {
+            // Default applied if no config found.
+            $onlycli = 1;
+        }
+
         $data->models = array();
         foreach ($this->models as $model) {
             $modeldata = $model->export();
@@ -115,7 +121,9 @@ class models_list implements \renderable, \templatable {
             }
 
             // Model predictions list.
-            if ($model->uses_insights()) {
+            if (!$model->is_enabled()) {
+                $modeldata->noinsights = get_string('disabledmodel', 'analytics');
+            } else if ($model->uses_insights()) {
                 $predictioncontexts = $model->get_predictions_contexts();
                 if ($predictioncontexts) {
 
@@ -180,7 +188,7 @@ class models_list implements \renderable, \templatable {
             $actionsmenu->add($icon);
 
             // Evaluate machine-learning-based models.
-            if ($model->get_indicators() && !$model->is_static()) {
+            if (!$onlycli && $model->get_indicators() && !$model->is_static()) {
                 $url = new \moodle_url('model.php', array('action' => 'evaluate', 'id' => $model->get_id()));
                 $icon = new \action_menu_link_secondary($url, new \pix_icon('i/calc', get_string('evaluate', 'tool_analytics')),
                     get_string('evaluate', 'tool_analytics'));
@@ -188,7 +196,7 @@ class models_list implements \renderable, \templatable {
             }
 
             // Get predictions.
-            if ($modeldata->enabled && !empty($modeldata->timesplitting)) {
+            if (!$onlycli && $modeldata->enabled && !empty($modeldata->timesplitting)) {
                 $url = new \moodle_url('model.php', array('action' => 'getpredictions', 'id' => $model->get_id()));
                 $icon = new \action_menu_link_secondary($url, new \pix_icon('i/notifications',
                     get_string('getpredictions', 'tool_analytics')), get_string('getpredictions', 'tool_analytics'));
@@ -216,9 +224,18 @@ class models_list implements \renderable, \templatable {
             $data->models[] = $modeldata;
         }
 
-        $data->warnings = array(
-            (object)array('message' => get_string('bettercli', 'tool_analytics'), 'closebutton' => true)
-        );
+        if (!$onlycli) {
+            $data->warnings = array(
+                (object)array('message' => get_string('bettercli', 'tool_analytics'), 'closebutton' => true)
+            );
+        } else {
+            $url = new \moodle_url('/admin/settings.php', array('section' => 'analyticssettings'),
+                'id_s_analytics_onlycli');
+            $data->infos = array(
+                (object)array('message' => get_string('clievaluationandpredictions', 'tool_analytics', $url->out()),
+                    'closebutton' => true)
+            );
+        }
 
         return $data;
     }
index f8273e1..404b010 100644 (file)
@@ -32,6 +32,8 @@ require_once($CFG->dirroot . '/course/format/weeks/lib.php');
 
 $help = "Guesses course start and end dates based on activity logs.
 
+IMPORTANT: Don't use this script if you keep previous academic years users enrolled in courses. Guesses would not be accurate.
+
 Options:
 --guessstart           Guess the course start date (default to true)
 --guessend             Guess the course end date (default to true)
@@ -183,10 +185,14 @@ function tool_analytics_calculate_course_dates($course, $options) {
 
                 $course->enddate = $guessedenddate;
 
-                if ($course->enddate > $course->startdate) {
-                    $notification .= PHP_EOL . '  ' . get_string('enddate') . ': ' . userdate($course->enddate);
-                } else {
+                $updateit = false;
+                if ($course->enddate < $course->startdate) {
                     $notification .= PHP_EOL . '  ' . get_string('errorendbeforestart', 'analytics', userdate($course->enddate));
+                } else if ($course->startdate + (YEARSECS + (WEEKSECS * 4)) > $course->enddate) {
+                    $notification .= PHP_EOL . '  ' . get_string('coursetoolong', 'analytics');
+                } else {
+                    $notification .= PHP_EOL . '  ' . get_string('enddate') . ': ' . userdate($course->enddate);
+                    $updateit = true;
                 }
 
                 if ($options['update']) {
index 4922a51..df38a14 100644 (file)
@@ -26,24 +26,26 @@ $string['accuracy'] = 'Accuracy';
 $string['allpredictions'] = 'All predictions';
 $string['analysingsitedata'] = 'Analysing the site';
 $string['analyticmodels'] = 'Analytic models';
-$string['bettercli'] = 'Evaluating models and generating predictions may involve heavy processing. It is advised that you run these actions via the command line interface';
+$string['bettercli'] = 'Evaluating models and generating predictions may involve heavy processing. It is recommended to run these actions from the command line.';
 $string['cantguessstartdate'] = 'Can\'t guess the start date';
 $string['cantguessenddate'] = 'Can\'t guess the end date';
-$string['clienablemodel'] = 'You can enable the model by selecting a time splitting method by its id. Note that you can also enable it later using the web interface (\'none\' to exit)';
+$string['clienablemodel'] = 'You can enable the model by selecting a time-splitting method by its ID. Note that you can also enable it later using the web interface (\'none\' to exit).';
+$string['clievaluationandpredictions'] = 'A cron task iterates through enabled models and gets predictions. Models evaluation via command line is disabled. You can allow these processes to be executed manually via web interface by enabling <a href="{$a}">\'onlycli\' analytics setting</a>';
 $string['editmodel'] = 'Edit "{$a}" model';
-$string['edittrainedwarning'] = 'This model has already been trained, note that changing its indicators or its time splitting method will delete its previous predictions and start generating the new ones';
+$string['edittrainedwarning'] = 'This model has already been trained. Note that changing its indicators or its time-splitting method will delete its previous predictions and start generating new predictions.';
 $string['enabled'] = 'Enabled';
-$string['errorcantenablenotimesplitting'] = 'You need to select a time splitting method before enabling the model';
-$string['errornoenabledandtrainedmodels'] = 'There are not enabled and trained models to predict';
-$string['errornoenabledmodels'] = 'There are not enabled models to train';
+$string['errorcantenablenotimesplitting'] = 'You need to select a time-splitting method before enabling the model';
+$string['errornoenabledandtrainedmodels'] = 'There are no enabled and trained models to predict.';
+$string['errornoenabledmodels'] = 'There are no enabled models to train.';
 $string['errornoexport'] = 'Only trained models can be exported';
-$string['errornostaticedit'] = 'Models based on assumptions can not be edited';
-$string['errornostaticevaluated'] = 'Models based on assumptions can not be evaluated, they are always 100% correct according to how they were defined';
-$string['errornostaticlog'] = 'Models based on assumptions can not be evaluated, there is no preformance log';
+$string['errornostaticedit'] = 'Models based on assumptions cannot be edited.';
+$string['errornostaticevaluated'] = 'Models based on assumptions cannot be evaluated. They are always 100% correct according to how they were defined.';
+$string['errornostaticlog'] = 'Models based on assumptions cannot be evaluated because there is no performance log.';
+$string['erroronlycli'] = 'Execution only allowed via command line';
 $string['errortrainingdataexport'] = 'The model training data could not be exported';
 $string['evaluate'] = 'Evaluate';
 $string['evaluatemodel'] = 'Evaluate model';
-$string['evaluationinbatches'] = 'The site contents are calculated and stored in batches, during evaluation you can stop the process at any moment, the next time you run it it will continue from the point you stopped it.';
+$string['evaluationinbatches'] = 'The site contents are calculated and stored in batches. The evaluation process may be stopped at any time. The next time it is run, it will continue from the point when it was stopped.';
 $string['export'] = 'Export';
 $string['exporttrainingdata'] = 'Export training data';
 $string['getpredictionsresultscli'] = 'Results using {$a->name} (id: {$a->id}) course duration splitting';
@@ -51,7 +53,7 @@ $string['getpredictionsresults'] = 'Results using {$a->name} course duration spl
 $string['extrainfo'] = 'Info';
 $string['generalerror'] = 'Evaluation error. Status code {$a}';
 $string['getpredictions'] = 'Get predictions';
-$string['goodmodel'] = 'This is a good model and it can be used to predict, enable it to start getting predictions.';
+$string['goodmodel'] = 'This is a good model for using to obtain predictions. Enable it to start obtaining predictions.';
 $string['indicators'] = 'Indicators';
 $string['info'] = 'Info';
 $string['insights'] = 'Insights';
@@ -77,4 +79,4 @@ $string['trainingresults'] = 'Training results';
 $string['trainmodels'] = 'Train models';
 $string['viewlog'] = 'Log';
 $string['weeksenddateautomaticallyset'] = 'End date automatically set based on start date and the number of sections';
-$string['weeksenddatedefault'] = 'End date would be automatically calculated from the course start date';
+$string['weeksenddatedefault'] = 'End date automatically calculated from the course start date.';
index daf62cf..6d64c42 100644 (file)
@@ -71,6 +71,12 @@ $PAGE->set_pagelayout('report');
 $PAGE->set_title($title);
 $PAGE->set_heading($title);
 
+$onlycli = get_config('analytics', 'onlycli');
+if ($onlycli === false) {
+    // Default applied if no config found.
+    $onlycli = 1;
+}
+
 switch ($action) {
 
     case 'enable':
@@ -131,6 +137,10 @@ switch ($action) {
             throw new moodle_exception('errornostaticevaluate', 'tool_analytics');
         }
 
+        if ($onlycli) {
+            throw new moodle_exception('erroronlycli', 'tool_analytics');
+        }
+
         // Web interface is used by people who can not use CLI nor code stuff, always use
         // cached stuff as they will change the model through the web interface as well
         // which invalidates the previously analysed stuff.
@@ -142,13 +152,22 @@ switch ($action) {
     case 'getpredictions':
         echo $OUTPUT->header();
 
+        if ($onlycli) {
+            throw new moodle_exception('erroronlycli', 'tool_analytics');
+        }
+
         $trainresults = $model->train();
         $trainlogs = $model->get_analyser()->get_logs();
 
         // Looks dumb to get a new instance but better be conservative.
         $model = new \core_analytics\model($model->get_model_obj());
-        $predictresults = $model->predict();
-        $predictlogs = $model->get_analyser()->get_logs();
+        if ($model->is_trained()) {
+            $predictresults = $model->predict();
+            $predictlogs = $model->get_analyser()->get_logs();
+        } else {
+            $predictresults = false;
+            $predictlogs = array();
+        }
 
         $renderer = $PAGE->get_renderer('tool_analytics');
         echo $renderer->render_get_predictions_results($trainresults, $trainlogs, $predictresults, $predictlogs);
index 49e4a91..9330c54 100644 (file)
 {{#warnings}}
     {{> core/notification_warning}}
 {{/warnings}}
+{{#infos}}
+    {{> core/notification_info}}
+{{/infos}}
+
 <div class="box">
     <table class="generaltable fullwidth">
         <caption>{{#str}}modelslist, tool_analytics{{/str}}</caption>
index 0946f78..61557f9 100644 (file)
@@ -49,7 +49,7 @@
     }
 }}
 <span>
-    <img height="18" src="{{profileimageurlsmall}}" alt="" role="presentation">
+    <img height="12" src="{{profileimageurlsmall}}" alt="" role="presentation">
     <span>{{fullname}}</span>
     {{#hasidentity}}
     <span><small>{{identity}}</small></span>
index 7e9fecc..f1ff7dd 100644 (file)
@@ -29,7 +29,7 @@ Feature: Manage plearning plan
     And I navigate to "Competencies > Learning plan templates" in site administration
     And I click on ".template-userplans" "css_element" in the "Science template" "table_row"
     And I open the autocomplete suggestions list
-    And I click on "Admin" item in the autocomplete list
+    And I click on "Admin User" item in the autocomplete list
     And I press key "27" in the field "Select users to create learning plans for"
     When I click on "Create learning plans" "button"
     Then I should see "A learning plan was created"
index 9955cb6..c8b207c 100644 (file)
@@ -37,18 +37,20 @@ mm.user.student|Aprendiz|es
 </pre>
 For a complete list of string identifiers, see the documentation.';
 $string['custommenuitems'] = 'Custom menu items';
-$string['custommenuitems_desc'] = 'Additional items can be added to the app\'s main menu by specifying them here. Enter each custom menu item on a new line with format: item text, link URL, link-opening method (inappbrowser, browser or embedded) and language code (optional, for displaying the item to users of the specified language only), separated by pipe characters. For example:
+$string['custommenuitems_desc'] = 'Additional items can be added to the app\'s main menu by specifying them here. Enter each custom menu item on a new line with format: item text, link URL, link-opening method and language code (optional, for displaying the item to users of the specified language only), separated by pipe characters.
+
+Link-opening methods are: app (for linking to an activity supported by the app), inappbrowser (for opening a link in a browser without leaving the app), browser (for opening the link in the device default browser outside the app) and embedded (for displaying the link in an iframe in a new page in the app).
+
+For example:
 <pre>
-App\'s help | https://someurl.xyz/help | inappbrowser | en
-Visit our SIS | https://someurl.xyz | browser | en
-My grades | https://someurl.xyz/local/mygrades/index.php | embedded | en
-Mis calificaciones | https://someurl.xyz/local/mygrades/index.php | embedded | es
-</pre>
-Use inappbrowser when you want to open the link in a browser without leaving the app, use browser for opening the link in the device default browser and embedded if you want to display the link embedded in a new page in the app.';
+App help|https://someurl.xyz/help|inappbrowser
+My grades|https://someurl.xyz/local/mygrades/index.php|embedded|en
+Mis calificaciones|https://someurl.xyz/local/mygrades/index.php|embedded|es
+</pre>';
 $string['disabledfeatures'] = 'Disabled features';
 $string['disabledfeatures_desc'] = 'Select here the features you want to disable in the Mobile app for your site. Please note that some features listed here could be already disabled via other site settings. You will have to log out and log in again in the app to see the changes.';
 $string['enablesmartappbanners'] = 'Enable App Banners';
-$string['enablesmartappbanners_desc'] = 'This will display a banner promoting the Moodle Mobile app when visiting the site in a Mobile browser.';
+$string['enablesmartappbanners_desc'] = 'If enabled, a banner promoting the mobile app will be displayed when accessing the site using a mobile browser.';
 $string['forcedurlscheme'] = 'If you want to allow only your custom branded app to be opened via a browser window, then specify its URL scheme here; otherwise leave the field empty.';
 $string['forcedurlscheme_key'] = 'URL scheme';
 $string['forcelogout'] = 'Force log out';
@@ -77,4 +79,4 @@ $string['pluginnotenabledorconfigured'] = 'Plugin not enabled or configured.';
 $string['remoteaddons'] = 'Remote add-ons';
 $string['typeoflogin'] = 'Type of login';
 $string['typeoflogin_desc'] = 'If the site uses a SSO authentication method, then select via a browser window or via an embedded browser. An embedded browser provides a better user experience, though it doesn\'t work with all SSO plugins. If using SSO, autologinguests should be disabled.';
-$string['getmoodleonyourmobile'] = 'Get Moodle on your mobile';
+$string['getmoodleonyourmobile'] = 'Get the mobile app';
diff --git a/analytics/classes/classifier.php b/analytics/classes/classifier.php
new file mode 100644 (file)
index 0000000..be0d3a0
--- /dev/null
@@ -0,0 +1,69 @@
+<?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/>.
+
+/**
+ * Classifier interface.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Classifier interface.
+ *
+ * @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
+ */
+interface classifier extends predictor {
+
+    /**
+     * Train this processor classification model using the provided supervised learning dataset.
+     *
+     * @param string $uniqueid
+     * @param \stored_file $dataset
+     * @param string $outputdir
+     * @return \stdClass
+     */
+    public function train_classification($uniqueid, \stored_file $dataset, $outputdir);
+
+    /**
+     * Classifies the provided dataset samples.
+     *
+     * @param string $uniqueid
+     * @param \stored_file $dataset
+     * @param string $outputdir
+     * @return \stdClass
+     */
+    public function classify($uniqueid, \stored_file $dataset, $outputdir);
+
+    /**
+     * Evaluates this processor classification model using the provided supervised learning dataset.
+     *
+     * @param string $uniqueid
+     * @param float $maxdeviation
+     * @param int $niterations
+     * @param \stored_file $dataset
+     * @param string $outputdir
+     * @return \stdClass
+     */
+    public function evaluate_classification($uniqueid, $maxdeviation, $niterations, \stored_file $dataset, $outputdir);
+}
index aae5748..791d7c2 100644 (file)
@@ -140,10 +140,18 @@ class course implements \core_analytics\analysable {
         $this->now = time();
 
         // Get the course users, including users assigned to student and teacher roles at an higher context.
-        $studentroles = array_keys(get_archetype_roles('student'));
+        $cache = \cache::make_from_params(\cache_store::MODE_REQUEST, 'core_analytics', 'rolearchetypes');
+
+        if (!$studentroles = $cache->get('student')) {
+            $studentroles = array_keys(get_archetype_roles('student'));
+            $cache->set('student', $studentroles);
+        }
         $this->studentids = $this->get_user_ids($studentroles);
 
-        $teacherroles = array_keys(get_archetype_roles('editingteacher') + get_archetype_roles('teacher'));
+        if (!$teacherroles = $cache->get('teacher')) {
+            $teacherroles = array_keys(get_archetype_roles('editingteacher') + get_archetype_roles('teacher'));
+            $cache->set('teacher', $teacherroles);
+        }
         $this->teacherids = $this->get_user_ids($teacherroles);
     }
 
index 9bf10c8..ab2f52e 100644 (file)
@@ -469,6 +469,9 @@ abstract class base {
             return $result;
         }
 
+        // Add extra metadata.
+        $this->add_model_metadata($data);
+
         // Write all calculated data to a file.
         $file = $dataset->store($data);
 
@@ -636,4 +639,32 @@ abstract class base {
             $DB->insert_record('analytics_predict_samples', $predictionrange);
         }
     }
+
+    /**
+     * Adds target metadata to the dataset.
+     *
+     * @param array $data
+     * @return void
+     */
+    protected function add_model_metadata(&$data) {
+        global $CFG;
+
+        $metadata = array(
+            'moodleversion' => $CFG->version,
+            'targetcolumn' => $this->analysabletarget->get_id()
+        );
+        if ($this->analysabletarget->is_linear()) {
+            $metadata['targettype'] = 'linear';
+            $metadata['targetmin'] = $this->analysabletarget::get_min_value();
+            $metadata['targetmax'] = $this->analysabletarget::get_max_value();
+        } else {
+            $metadata['targettype'] = 'discrete';
+            $metadata['targetclasses'] = json_encode($this->analysabletarget::get_classes());
+        }
+
+        foreach ($metadata as $varname => $value) {
+            $data[0][] = $varname;
+            $data[1][] = $value;
+        }
+    }
 }
index 30fdf38..7c6aa3a 100644 (file)
@@ -121,6 +121,22 @@ abstract class base extends \core_analytics\calculable {
         return self::MIN_VALUE;
     }
 
+    /**
+     * Hook to allow indicators to pre-fill data that is shared accross time range calculations.
+     *
+     * Useful to fill analysable-dependant data that does not depend on the time ranges. Use
+     * instance vars to cache data that can be re-used across samples calculations but changes
+     * between time ranges (indicator instances are reset between time ranges to avoid unexpected
+     * problems).
+     *
+     * You are also responsible of emptying previous analysable caches.
+     *
+     * @param \core_analytics\analysable $analysable
+     * @return void
+     */
+    public function fill_per_analysable_caches(\core_analytics\analysable $analysable) {
+    }
+
     /**
      * Calculates the indicator.
      *
index 7f4c346..a730996 100644 (file)
@@ -46,6 +46,23 @@ abstract class binary extends discrete {
         return array(0);
     }
 
+    /**
+     * It should always be displayed.
+     *
+     * Binary values have no subtypes by default, please overwrite if
+     * your indicator is adding extra features.
+     *
+     * @param float $value
+     * @param string $subtype
+     * @return bool
+     */
+    public function should_be_displayed($value, $subtype) {
+        if ($subtype != false) {
+            return false;
+        }
+        return true;
+    }
+
     /**
      * get_display_value
      *
index 60d5e0f..8a886eb 100644 (file)
@@ -60,12 +60,36 @@ abstract class community_of_inquiry_activity extends linear {
      */
     const INDICATOR_SOCIAL = "social";
 
+    /**
+     * Max cognitive depth level accepted.
+     */
+    const MAX_COGNITIVE_LEVEL = 5;
+
+    /**
+     * Max social breadth level accepted.
+     */
+    const MAX_SOCIAL_LEVEL = 5;
+
+    /**
+     * Fetch the course grades of this activity type instances.
+     *
+     * @param \core_analytics\analysable $analysable
+     * @return void
+     */
+    public function fill_per_analysable_caches(\core_analytics\analysable $analysable) {
+
+        // Better to check it, we can not be 100% it will be a \core_analytics\course object.
+        if ($analysable instanceof \core_analytics\course) {
+            $this->fetch_student_grades($analysable);
+        }
+    }
+
     /**
      * Returns the activity type. No point in changing this class in children classes.
      *
      * @var string The activity name (e.g. assign or quiz)
      */
-    protected final function get_activity_type() {
+    public final function get_activity_type() {
         $class = get_class($this);
         $package = stristr($class, "\\", true);
         $type = str_replace("mod_", "", $package);
@@ -81,7 +105,7 @@ abstract class community_of_inquiry_activity extends linear {
      * @param \cm_info $cm
      * @return int
      */
-    protected function get_cognitive_depth_level(\cm_info $cm) {
+    public function get_cognitive_depth_level(\cm_info $cm) {
         throw new \coding_exception('Overwrite get_cognitive_depth_level method to set your activity potential cognitive ' .
             'depth level');
     }
@@ -92,7 +116,7 @@ abstract class community_of_inquiry_activity extends linear {
      * @param \cm_info $cm
      * @return int
      */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         throw new \coding_exception('Overwrite get_social_breadth_level method to set your activity potential social ' .
             'breadth level');
     }
@@ -396,8 +420,8 @@ abstract class community_of_inquiry_activity extends linear {
         }
 
         if ($this->grades === null) {
-            $courseactivities = $this->course->get_all_activities($this->get_activity_type());
-            $this->grades = $this->course->get_student_grades($courseactivities);
+            // Even if this is probably already filled during fill_per_analysable_caches.
+            $this->fetch_student_grades($this->course);
         }
 
         if ($cm = $this->retrieve('cm', $sampleid)) {
@@ -506,7 +530,7 @@ 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 > 5 || $potentiallevel < 1) {
+            if (!is_int($potentiallevel) || $potentiallevel > self::MAX_COGNITIVE_LEVEL || $potentiallevel < 1) {
                 throw new \coding_exception('Activities\' potential cognitive depth go from 1 to 5.');
             }
             $scoreperlevel = $scoreperactivity / $potentiallevel;
@@ -593,12 +617,19 @@ 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 > 2 || $potentiallevel < 1) {
-                throw new \coding_exception('Activities\' potential social breadth go from 1 to 2.');
+            if (!is_int($potentiallevel) || $potentiallevel > self::MAX_SOCIAL_LEVEL || $potentiallevel < 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:
+                    // 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.
+
                     // Social breadth level 2 is to view feedback. (Same as cognitive level 3).
 
                     if ($this->any_feedback('viewed', $cm, $contextid, $user)) {
@@ -645,10 +676,21 @@ abstract class community_of_inquiry_activity extends linear {
         throw new \coding_exception("Indicator type is invalid.");
     }
 
+    /**
+     * Gets the course student grades.
+     *
+     * @param \core_analytics\course $course
+     * @return void
+     */
+    protected function fetch_student_grades(\core_analytics\course $course) {
+        $courseactivities = $course->get_all_activities($this->get_activity_type());
+        $this->grades = $course->get_student_grades($courseactivities);
+    }
+
     /**
      * Defines indicator type.
      *
      * @return string
      */
-    abstract protected function get_indicator_type();
+    abstract public function get_indicator_type();
 }
index 0b51e0d..8a8aa66 100644 (file)
@@ -85,7 +85,7 @@ abstract class discrete extends base {
      */
     public function get_display_value($value, $subtype = false) {
 
-        $displayvalue = array_search($subtype, static::get_classes());
+        $displayvalue = array_search($subtype, static::get_classes(), false);
 
         debugging('Please overwrite \core_analytics\local\indicator\discrete::get_display_value to show something ' .
             'different than the default "' . $displayvalue . '"', DEBUG_DEVELOPER);
index 691aa8a..56902c0 100644 (file)
@@ -63,7 +63,7 @@ abstract class linear extends base {
     }
 
     /**
-     * should_be_displayed
+     * Show only the main feature.
      *
      * @param float $value
      * @param string $subtype
index 466255e..1a4f978 100644 (file)
@@ -130,6 +130,8 @@ abstract class base extends \core_analytics\calculable {
     /**
      * Callback to execute once a prediction has been returned from the predictions processor.
      *
+     * Note that the analytics_predictions db record is not yet inserted.
+     *
      * @param int $modelid
      * @param int $sampleid
      * @param int $rangeindex
@@ -231,7 +233,7 @@ abstract class base extends \core_analytics\calculable {
      */
     protected function min_prediction_score() {
         // The default minimum discards predictions with a low score.
-        return \core_analytics\model::MIN_SCORE;
+        return \core_analytics\model::PREDICTION_MIN_SCORE;
     }
 
     /**
index ea04410..f8c3b04 100644 (file)
@@ -78,7 +78,7 @@ abstract class binary extends discrete {
             throw new \moodle_exception('errorpredictionformat', 'analytics');
         }
 
-        if (in_array($value, $this->ignored_predicted_classes())) {
+        if (in_array($value, $this->ignored_predicted_classes(), false)) {
             // Just in case, if it is ignored the prediction should not even be recorded but if it would, it is ignored now,
             // which should mean that is it nothing serious.
             return self::OUTCOME_VERY_POSITIVE;
index 0799b4d..cbd8fe0 100644 (file)
@@ -42,17 +42,18 @@ abstract class discrete extends base {
      */
     public function is_linear() {
         // Not supported yet.
-        throw new \coding_exception('Sorry, this version\'s prediction processors only support targets with binary values.');
+        throw new \coding_exception('Sorry, this version\'s prediction processors only support targets with binary values.' .
+            ' You can write your own and overwrite this method though.');
     }
 
     /**
      * Is the provided class one of this target valid classes?
      *
-     * @param string $class
+     * @param mixed $class
      * @return bool
      */
     protected static function is_a_class($class) {
-        return (in_array($class, static::get_classes()));
+        return (in_array($class, static::get_classes(), false));
     }
 
     /**
@@ -99,7 +100,7 @@ abstract class discrete extends base {
             throw new \moodle_exception('errorpredictionformat', 'analytics');
         }
 
-        if (in_array($value, $this->ignored_predicted_classes())) {
+        if (in_array($value, $this->ignored_predicted_classes(), false)) {
             // Just in case, if it is ignored the prediction should not even be recorded.
             return self::OUTCOME_OK;
         }
@@ -138,15 +139,16 @@ abstract class discrete extends base {
      * Returns the predicted classes that will be ignored.
      *
      * Better be keen to add more than less classes here, the callback is always able to discard some classes. As an example
-     * a target with classes 'grade 0-3', 'grade 3-6', 'grade 6-8' and 'grade 8-10' is interested in flagging both 'grade 0-3'
-     * and 'grade 3-6'. On the other hand, a target like dropout risk with classes 'yes', 'no' may just be interested in 'yes'.
+     * a target with classes 'grade 0-3', 'grade 3-6', 'grade 6-8' and 'grade 8-10' is interested in flagging both 'grade 6-8'
+     * and 'grade 8-10' as ignored. On the other hand, a target like dropout risk with classes 'yes', 'no' may just be
+     * interested in 'yes'.
      *
      * @return array List of values that will be ignored (array keys are ignored).
      */
     protected function ignored_predicted_classes() {
         // Coding exception as this will only be called if this target have non-linear values.
-        throw new \coding_exception('Overwrite ignored_predicted_classes() and return an array with the classes that triggers ' .
-            'the callback');
+        throw new \coding_exception('Overwrite ignored_predicted_classes() and return an array with the classes that should not ' .
+            'trigger the callback');
     }
 
     /**
@@ -162,10 +164,8 @@ abstract class discrete extends base {
             return false;
         }
 
-        if (!$this->is_linear()) {
-            if (in_array($predictedvalue, $this->ignored_predicted_classes())) {
-                return false;
-            }
+        if (in_array($predictedvalue, $this->ignored_predicted_classes())) {
+            return false;
         }
 
         return true;
index 8d8e258..d16ad96 100644 (file)
@@ -42,7 +42,8 @@ abstract class linear extends base {
      */
     public function is_linear() {
         // Not supported yet.
-        throw new \coding_exception('Sorry, this version\'s prediction processors only support targets with binary values.');
+        throw new \coding_exception('Sorry, this version\'s prediction processors only support targets with binary values.' .
+            ' You can write your own and overwrite this method though.');
     }
 
     /**
@@ -52,7 +53,7 @@ abstract class linear extends base {
      * @param string $ignoredsubtype
      * @return int
      */
-    public function get_calculated_outcome($value, $ignoredsubtype = false) {
+    public function get_calculation_outcome($value, $ignoredsubtype = false) {
 
         // This is very generic, targets will probably be interested in overwriting this.
         $diff = static::get_max_value() - static::get_min_value();
@@ -67,7 +68,7 @@ abstract class linear extends base {
      *
      * @return float
      */
-    protected static function get_max_value() {
+    public static function get_max_value() {
         // Coding exception as this will only be called if this target have linear values.
         throw new \coding_exception('Overwrite get_max_value() and return the target max value');
     }
@@ -77,11 +78,33 @@ abstract class linear extends base {
      *
      * @return float
      */
-    protected static function get_min_value() {
+    public static function get_min_value() {
         // Coding exception as this will only be called if this target have linear values.
         throw new \coding_exception('Overwrite get_min_value() and return the target min value');
     }
 
+    /**
+     * Should the model callback be triggered?
+     *
+     * @param mixed $predictedvalue
+     * @param float $predictionscore
+     * @return bool
+     */
+    public function triggers_callback($predictedvalue, $predictionscore) {
+
+        if (!parent::triggers_callback($predictedvalue, $predictionscore)) {
+            return false;
+        }
+
+        // People may not want to set a boundary.
+        $boundary = $this->get_callback_boundary();
+        if (!empty($boundary) && floatval($predictedvalue) < $boundary) {
+            return false;
+        }
+
+        return true;
+    }
+
     /**
      * Returns the minimum value that triggers the callback.
      *
index 109084e..f45934a 100644 (file)
@@ -235,6 +235,9 @@ abstract class base {
         $newcalculations = array();
         foreach ($indicators as $indicator) {
 
+            // Hook to allow indicators to store analysable-dependant data.
+            $indicator->fill_per_analysable_caches($this->analysable);
+
             // Per-range calculations.
             foreach ($ranges as $rangeindex => $range) {
 
@@ -371,12 +374,9 @@ abstract class base {
         $metadata = array(
             'timesplitting' => $this->get_id(),
             // If no target the first column is the sampleid, if target the last column is the target.
+            // This will need to be updated when we support unsupervised learning models.
             'nfeatures' => count(current($dataset)) - 1
         );
-        if ($target) {
-            $metadata['targetclasses'] = json_encode($target::get_classes());
-            $metadata['targettype'] = ($target->is_linear()) ? 'linear' : 'discrete';
-        }
 
         // The first 2 samples will be used to store metadata about the dataset.
         $metadatacolumns = [];
index 04a95f1..8120dd6 100644 (file)
@@ -80,6 +80,11 @@ class model {
      */
     const MIN_SCORE = 0.7;
 
+    /**
+     * Minimum prediction confidence (from 0 to 1) to accept a prediction as reliable enough.
+     */
+    const PREDICTION_MIN_SCORE = 0.6;
+
     /**
      * Maximum standard deviation between different evaluation repetitions to consider that evaluation results are stable.
      */
@@ -438,7 +443,12 @@ class model {
 
             // Reset trained flag.
             $this->model->trained = 0;
+
+        } else if ($this->model->enabled != $enabled) {
+            // We purge the cached contexts with insights as some will not be visible anymore.
+            $this->purge_insights_cache();
         }
+
         $this->model->enabled = intval($enabled);
         $this->model->indicators = $indicatorsstr;
         $this->model->timesplitting = $timesplittingid;
@@ -524,8 +534,13 @@ class model {
             $outputdir = $this->get_output_dir(array('evaluation', $dashestimesplittingid));
 
             // Evaluate the dataset, the deviation we accept in the results depends on the amount of iterations.
-            $predictorresult = $predictor->evaluate($this->model->id, self::ACCEPTED_DEVIATION,
+            if ($this->get_target()->is_linear()) {
+                $predictorresult = $predictor->evaluate_regression($this->get_unique_id(), self::ACCEPTED_DEVIATION,
+                self::EVALUATION_ITERATIONS, $dataset, $outputdir);
+            } else {
+                $predictorresult = $predictor->evaluate_classification($this->get_unique_id(), self::ACCEPTED_DEVIATION,
                 self::EVALUATION_ITERATIONS, $dataset, $outputdir);
+            }
 
             $result->status = $predictorresult->status;
             $result->info = $predictorresult->info;
@@ -599,7 +614,11 @@ class model {
         $samplesfile = $datasets[$this->model->timesplitting];
 
         // Train using the dataset.
-        $predictorresult = $predictor->train($this->get_unique_id(), $samplesfile, $outputdir);
+        if ($this->get_target()->is_linear()) {
+            $predictorresult = $predictor->train_regression($this->get_unique_id(), $samplesfile, $outputdir);
+        } else {
+            $predictorresult = $predictor->train_classification($this->get_unique_id(), $samplesfile, $outputdir);
+        }
 
         $result = new \stdClass();
         $result->status = $predictorresult->status;
@@ -678,8 +697,12 @@ class model {
             $result->predictions = $this->get_static_predictions($indicatorcalculations);
 
         } else {
-            // Prediction process runs on the machine learning backend.
-            $predictorresult = $predictor->predict($this->get_unique_id(), $samplesfile, $outputdir);
+            // Estimation and classification processes run on the machine learning backend side.
+            if ($this->get_target()->is_linear()) {
+                $predictorresult = $predictor->estimate($this->get_unique_id(), $samplesfile, $outputdir);
+            } else {
+                $predictorresult = $predictor->classify($this->get_unique_id(), $samplesfile, $outputdir);
+            }
             $result->status = $predictorresult->status;
             $result->info = $predictorresult->info;
             $result->predictions = $this->format_predictor_predictions($predictorresult);
@@ -757,9 +780,12 @@ class model {
                 list($sampleid, $rangeindex) = $this->get_time_splitting()->infer_sample_info($uniquesampleid);
 
                 // Store the predicted values.
-                $samplecontext = $this->save_prediction($sampleid, $rangeindex, $prediction->prediction,
+                list($record, $samplecontext) = $this->prepare_prediction_record($sampleid, $rangeindex, $prediction->prediction,
                     $prediction->predictionscore, json_encode($indicatorcalculations[$uniquesampleid]));
 
+                // We will later bulk-insert them all.
+                $records[$uniquesampleid] = $record;
+
                 // Also store all samples context to later generate insights or whatever action the target wants to perform.
                 $samplecontexts[$samplecontext->id] = $samplecontext;
 
@@ -768,6 +794,8 @@ class model {
             }
         }
 
+        $this->save_predictions($records);
+
         return $samplecontexts;
     }
 
@@ -894,7 +922,7 @@ class model {
      * @param string $calculations
      * @return \context
      */
-    protected function save_prediction($sampleid, $rangeindex, $prediction, $predictionscore, $calculations) {
+    protected function prepare_prediction_record($sampleid, $rangeindex, $prediction, $predictionscore, $calculations) {
         global $DB;
 
         $context = $this->get_analyser()->sample_access_context($sampleid);
@@ -908,9 +936,18 @@ class model {
         $record->predictionscore = $predictionscore;
         $record->calculations = $calculations;
         $record->timecreated = time();
-        $DB->insert_record('analytics_predictions', $record);
 
-        return $context;
+        return array($record, $context);
+    }
+
+    /**
+     * Save the prediction objects.
+     *
+     * @param \stdClass[] $records
+     */
+    protected function save_predictions($records) {
+        global $DB;
+        $DB->insert_records('analytics_predictions', $records);
     }
 
     /**
@@ -939,6 +976,13 @@ class model {
             $this->model->timesplitting = $timesplittingid;
             $this->model->version = $now;
         }
+
+        // Purge pages with insights as this may change things.
+        if ($timesplittingid && $timesplittingid !== $this->model->timesplitting ||
+                $this->model->enabled != 1) {
+            $this->purge_insights_cache();
+        }
+
         $this->model->enabled = 1;
         $this->model->timemodified = $now;
 
@@ -1194,7 +1238,7 @@ class model {
 
         // Generate a unique id for this site, this model and this time splitting method, considering the last time
         // that the model target and indicators were updated.
-        $ids = array($CFG->wwwroot, $CFG->dirroot, $CFG->prefix, $this->model->id, $this->model->version);
+        $ids = array($CFG->wwwroot, $CFG->prefix, $this->model->id, $this->model->version);
         $this->uniqueid = sha1(implode('$$', $ids));
 
         return $this->uniqueid;
@@ -1343,6 +1387,13 @@ class model {
 
         // We don't expect people to clear models regularly and the cost of filling the cache is
         // 1 db read per context.
+        $this->purge_insights_cache();
+    }
+
+    /**
+     * Purges the insights cache.
+     */
+    private function purge_insights_cache() {
         $cache = \cache::make('core', 'contextwithinsights');
         $cache->purge();
     }
index 944c76e..4b4548b 100644 (file)
@@ -41,36 +41,4 @@ interface predictor {
      * @return bool
      */
     public function is_ready();
-
-    /**
-     * Train the provided dataset.
-     *
-     * @param int $modelid
-     * @param \stored_file $dataset
-     * @param string $outputdir
-     * @return \stdClass
-     */
-    public function train($modelid, \stored_file $dataset, $outputdir);
-
-    /**
-     * Predict the provided dataset samples.
-     *
-     * @param int $modelid
-     * @param \stored_file $dataset
-     * @param string $outputdir
-     * @return \stdClass
-     */
-    public function predict($modelid, \stored_file $dataset, $outputdir);
-
-    /**
-     * evaluate
-     *
-     * @param int $modelid
-     * @param float $maxdeviation
-     * @param int $niterations
-     * @param \stored_file $dataset
-     * @param string $outputdir
-     * @return \stdClass
-     */
-    public function evaluate($modelid, $maxdeviation, $niterations, \stored_file $dataset, $outputdir);
 }
diff --git a/analytics/classes/regressor.php b/analytics/classes/regressor.php
new file mode 100644 (file)
index 0000000..c2d2a89
--- /dev/null
@@ -0,0 +1,69 @@
+<?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/>.
+
+/**
+ * Regressors interface.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Regressors interface.
+ *
+ * @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
+ */
+interface regressor extends predictor {
+
+    /**
+     * Train this processor regression model using the provided supervised learning dataset.
+     *
+     * @param string $uniqueid
+     * @param \stored_file $dataset
+     * @param string $outputdir
+     * @return \stdClass
+     */
+    public function train_regression($uniqueid, \stored_file $dataset, $outputdir);
+
+    /**
+     * Estimates linear values for the provided dataset samples.
+     *
+     * @param string $uniqueid
+     * @param \stored_file $dataset
+     * @param mixed $outputdir
+     * @return void
+     */
+    public function estimate($uniqueid, \stored_file $dataset, $outputdir);
+
+    /**
+     * Evaluates this processor regression model using the provided supervised learning dataset.
+     *
+     * @param string $uniqueid
+     * @param float $maxdeviation
+     * @param int $niterations
+     * @param \stored_file $dataset
+     * @param string $outputdir
+     * @return \stdClass
+     */
+    public function evaluate_regression($uniqueid, $maxdeviation, $niterations, \stored_file $dataset, $outputdir);
+}
index d616d4b..69fe722 100644 (file)
@@ -73,21 +73,14 @@ class site implements \core_analytics\analysable {
             return $this->start;
         }
 
-        if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
-            $this->start = 0;
-            return $this->start;
-        }
-
-        // Basically a SELECT MIN(timecreated) FROM ...
-        $events = $logstore->get_events_select("", array(), "timecreated ASC", 0, 1);
-        if ($events) {
-            // There should be just 1 event.
-            $event = reset($events);
-            $this->start = intval($event->timecreated);
-        } else {
-            $this->start = 0;
+        // Much faster than reading the first log in the site.
+        $admins = get_admins();
+        $this->start = 9999999999;
+        foreach ($admins as $admin) {
+            if ($admin->firstaccess < $this->start) {
+                $this->start = $admin->firstaccess;
+            }
         }
-
         return $this->start;
     }
 
@@ -101,21 +94,7 @@ class site implements \core_analytics\analysable {
             return $this->end;
         }
 
-        if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
-            $this->end = time();
-            return $this->end;
-        }
-
-        // Basically a SELECT MAX(timecreated) FROM ...
-        $events = $logstore->get_events_select("", array(), "timecreated DESC", 0, 1);
-        if ($events) {
-            // There should be just 1 event.
-            $event = reset($events);
-            $this->end = intval($event->timecreated);
-        } else {
-            $this->end = time();
-        }
-
+        $this->end = time();
         return $this->end;
     }
 }
index d4cf05a..b8e1768 100644 (file)
@@ -150,12 +150,11 @@ class analytics_model_testcase extends advanced_testcase {
 
         global $DB;
 
-        // 2 built-in models + the testing one.
-        $this->assertCount(3, $DB->get_records('analytics_models'));
+        $count = $DB->count_records('analytics_models');
 
         // No new models added if the builtin ones already exist.
         \core_analytics\manager::add_builtin_models();
-        $this->assertCount(3, $DB->get_records('analytics_models'));
+        $this->assertCount($count, $DB->get_records('analytics_models'));
 
         $target = \core_analytics\manager::get_target('\core\analytics\target\no_teaching');
         $this->assertTrue(\core_analytics\model::exists($target));
index f9ae5dc..d9060e3 100644 (file)
@@ -167,15 +167,25 @@ class backup_course_task extends backup_task {
     /**
      * Helper method, used by encode_content_links.
      * @param string $content content in which to encode links.
-     * @param unknown_type $name the name of this type of encoded link.
-     * @param unknown_type $path the path that identifies this type of link, up
+     * @param string $name the name of this type of encoded link.
+     * @param string $path the path that identifies this type of link, up
      *      to the ?paramname= bit.
      * @return string content with one type of link encoded.
      */
     static private function encode_links_helper($content, $name, $path) {
         global $CFG;
-        $base = preg_quote($CFG->wwwroot . $path, '/');
-        return preg_replace('/(' . $base . ')([0-9]+)/', '$@' . $name . '*$2@$', $content);
+        // We want to convert both http and https links.
+        $root = $CFG->wwwroot;
+        $httpsroot = str_replace('http://', 'https://', $root);
+        $httproot = str_replace('https://', 'http://', $root);
+
+        $httpsbase = preg_quote($httpsroot . $path, '/');
+        $httpbase = preg_quote($httproot . $path, '/');
+
+        $return = preg_replace('/(' . $httpsbase . ')([0-9]+)/', '$@' . $name . '*$2@$', $content);
+        $return = preg_replace('/(' . $httpbase . ')([0-9]+)/', '$@' . $name . '*$2@$', $return);
+
+        return $return;
     }
 
 // Protected API starts here
index dfcf545..10872f8 100644 (file)
@@ -5560,6 +5560,6 @@ class restore_calendar_action_events extends restore_execution_step {
         // The task will automatically populate all data.
         $task = new \core\task\refresh_mod_calendar_events_task();
         $task->set_custom_data(array('courseid' => $this->get_courseid()));
-        \core\task\manager::queue_adhoc_task($task);
+        \core\task\manager::queue_adhoc_task($task, true);
     }
 }
index 26299d9..63554d3 100644 (file)
@@ -44,13 +44,33 @@ class backup_course_task_testcase extends basic_testcase {
      */
     public function test_course_encode_content_links() {
         global $CFG;
+        $httpsroot = "https://moodle.org";
+        $httproot = "http://moodle.org";
+        $oldroot = $CFG->wwwroot;
+
+        // HTTPS root and links of both types in content.
+        $CFG->wwwroot = $httpsroot;
         $encoded = backup_course_task::encode_content_links(
-                $CFG->wwwroot . '/course/view.php?id=123, ' .
-                $CFG->wwwroot . '/grade/index.php?id=123, ' .
-                $CFG->wwwroot . '/grade/report/index.php?id=123, ' .
-                $CFG->wwwroot . '/badges/view.php?type=2&id=123 and ' .
-                $CFG->wwwroot . '/user/index.php?id=123.');
-        $this->assertEquals('$@COURSEVIEWBYID*123@$, $@GRADEINDEXBYID*123@$, ' .
+                $httproot . '/course/view.php?id=123, ' .
+                $httpsroot . '/course/view.php?id=123, ' .
+                $httpsroot . '/grade/index.php?id=123, ' .
+                $httpsroot . '/grade/report/index.php?id=123, ' .
+                $httpsroot . '/badges/view.php?type=2&id=123 and ' .
+                $httpsroot . '/user/index.php?id=123.');
+        $this->assertEquals('$@COURSEVIEWBYID*123@$, $@COURSEVIEWBYID*123@$, $@GRADEINDEXBYID*123@$, ' .
                 '$@GRADEREPORTINDEXBYID*123@$, $@BADGESVIEWBYID*123@$ and $@USERINDEXVIEWBYID*123@$.', $encoded);
+
+        // HTTP root and links of both types in content.
+        $CFG->wwwroot = $httproot;
+        $encoded = backup_course_task::encode_content_links(
+            $httproot . '/course/view.php?id=123, ' .
+            $httpsroot . '/course/view.php?id=123, ' .
+            $httproot . '/grade/index.php?id=123, ' .
+            $httproot . '/grade/report/index.php?id=123, ' .
+            $httproot . '/badges/view.php?type=2&id=123 and ' .
+            $httproot . '/user/index.php?id=123.');
+        $this->assertEquals('$@COURSEVIEWBYID*123@$, $@COURSEVIEWBYID*123@$, $@GRADEINDEXBYID*123@$, ' .
+            '$@GRADEREPORTINDEXBYID*123@$, $@BADGESVIEWBYID*123@$ and $@USERINDEXVIEWBYID*123@$.', $encoded);
+        $CFG->wwwroot = $oldroot;
     }
 }
index de9b9ed..52fb8e9 100644 (file)
@@ -26,6 +26,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 require_once($CFG->dirroot . '/lib/grade/constants.php');
+require_once($CFG->dirroot . '/course/lib.php');
 
 define('B_ACTIVITYRESULTS_NAME_FORMAT_FULL', 1);
 define('B_ACTIVITYRESULTS_NAME_FORMAT_ID',   2);
@@ -328,7 +329,7 @@ class block_activity_results extends block_base {
                 if ($nameformat == B_ACTIVITYRESULTS_NAME_FORMAT_FULL) {
                     if (has_capability('moodle/course:managegroups', $context)) {
                         $grouplink = $CFG->wwwroot.'/group/overview.php?id='.$courseid.'&amp;group=';
-                    } else if (has_capability('moodle/course:viewparticipants', $context)) {
+                    } else if (course_can_view_participants($context)) {
                         $grouplink = $CFG->wwwroot.'/user/index.php?id='.$courseid.'&amp;group=';
                     } else {
                         $grouplink = '';
index cc3d6f6..18a57d7 100644 (file)
@@ -70,29 +70,26 @@ class block_calendar_month extends block_base {
         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.
-            $filtercourse = calendar_get_default_courses();
+            $course = get_site();
+            $courses = calendar_get_default_courses();
         } else {
             // Forcibly filter events to include only those from the particular course we are in.
-            $filtercourse = array($courseid => $this->page->course);
+            $course = $this->page->course;
+            $courses = [$course->id => $course];
         }
 
-        list($courses, $group, $user) = calendar_set_filters($filtercourse);
-        if ($issite) {
-            // For the front page.
-            $this->content->text .= calendar_get_mini($courses, $group, $user, false, false,
-                'frontpage', $courseid, $time);
-            // No filters for now.
-        } else {
-            // For any other course.
-            $this->content->text .= calendar_get_mini($courses, $group, $user, false, false,
-                'course', $courseid, $time);
-            $this->content->text .= '<h3 class="eventskey">'.get_string('eventskey', 'calendar').'</h3>';
-            $this->content->text .= '<div class="filters calendar_filters">' .
-                calendar_filter_controls($this->page->url) . '</div>';
+        $renderer = $this->page->get_renderer('core_calendar');
+
+        $calendar = new calendar_information(0, 0, 0, $time);
+        $calendar->prepare_for_view($course, $courses);
+
+        list($data, $template) = calendar_get_view($calendar, 'mini');
+        $this->content->text .= $renderer->render_from_template($template, $data);
+
+        if (!$issite) {
+            $this->content->text .= $renderer->event_filter();
         }
 
         return $this->content;
     }
 }
-
-
index 8753167..86baea3 100644 (file)
@@ -23,7 +23,7 @@ Feature: Enable the calendar block in a course and test it's functionality
     Given I log in as "teacher1"
     And I am on "Course 1" course homepage with editing mode on
     When I add the "Calendar" block
-    Then I should see "Events key" in the "Calendar" "block"
+    Then "Calendar" "block" should exist
 
   @javascript
   Scenario: View a global event in the calendar block
@@ -136,9 +136,14 @@ Feature: Enable the calendar block in a course and test it's functionality
     And I press "Save and display"
     And I turn editing mode on
     And I add the "Calendar" block
-    And I create a calendar event with form data:
+    And I click on "This month" "link"
+    And I click on "New event" "button"
+    And I set the following fields to these values:
       | id_eventtype | Group |
       | id_name | Group Event |
+    And I set the following fields to these values:
+      | Group | Group 1 |
+    And I press "Save"
     And I log out
     Then I log in as "student1"
     And I am on "Course 1" course homepage
@@ -150,7 +155,7 @@ Feature: Enable the calendar block in a course and test it's functionality
     And I hover over today in the calendar
     And I should not see "Group Event"
 
-  @javascript
+  @javascript @arn
   Scenario: Filter group events in the calendar block
     Given the following "groups" exist:
       | name    | course | idnumber |
@@ -173,9 +178,14 @@ Feature: Enable the calendar block in a course and test it's functionality
       | id_eventtype | Course |
       | id_name | Course Event 1 |
     And I am on "Course 1" course homepage
-    And I create a calendar event with form data:
+    And I click on "This month" "link"
+    And I click on "New event" "button"
+    And I set the following fields to these values:
       | id_eventtype | Group |
       | id_name | Group Event 1 |
+    And I set the following fields to these values:
+      | Group | Group 1 |
+    And I press "Save"
     And I log out
     Then I log in as "student1"
     And I am on "Course 1" course homepage
index d9e729a..d7f9fb2 100644 (file)
@@ -97,7 +97,7 @@
              role="presentation">
         <p class="text-muted m-t-1">{{#str}} noevents, block_myoverview {{/str}}</p>
         <a href="{{viewurl}}" class="btn btn-secondary {{#visible}}text-primary{{/visible}}"
-           aria-label="{{#str}} viewcoursename, block_myoverview, {{fullnamedisplay}} {{/str}}">
+           aria-label="{{#str}} viewcoursename, block_myoverview, {{{fullnamedisplay}}} {{/str}}">
             {{#str}} viewcourse, block_myoverview {{/str}}
         </a>
     </div>
index f8214df..b1b433b 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/course/lib.php');
+
+/**
+ * Participants block
+ *
+ * @package    block_participants
+ * @copyright  1999 onwards Martin Dougiamas (http://dougiamas.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
 class block_participants extends block_list {
     function init() {
         $this->title = get_string('pluginname', 'block_participants');
@@ -48,12 +59,12 @@ class block_participants extends block_list {
             $this->content = '';
             return $this->content;
         } else if ($this->page->course->id == SITEID) {
-            if (!has_capability('moodle/site:viewparticipants', context_system::instance())) {
+            if (!course_can_view_participants(context_system::instance())) {
                 $this->content = '';
                 return $this->content;
             }
         } else {
-            if (!has_capability('moodle/course:viewparticipants', $currentcontext)) {
+            if (!course_can_view_participants($currentcontext)) {
                 $this->content = '';
                 return $this->content;
             }
index b0336c4..1999702 100644 (file)
Binary files a/calendar/amd/build/calendar.min.js and b/calendar/amd/build/calendar.min.js differ
diff --git a/calendar/amd/build/calendar_filter.min.js b/calendar/amd/build/calendar_filter.min.js
new file mode 100644 (file)
index 0000000..ef94d15
Binary files /dev/null and b/calendar/amd/build/calendar_filter.min.js differ
diff --git a/calendar/amd/build/calendar_mini.min.js b/calendar/amd/build/calendar_mini.min.js
new file mode 100644 (file)
index 0000000..979e92e
Binary files /dev/null and b/calendar/amd/build/calendar_mini.min.js differ
diff --git a/calendar/amd/build/drag_drop.min.js b/calendar/amd/build/drag_drop.min.js
deleted file mode 100644 (file)
index 249c6d6..0000000
Binary files a/calendar/amd/build/drag_drop.min.js and /dev/null differ
diff --git a/calendar/amd/build/drag_drop_data_store.min.js b/calendar/amd/build/drag_drop_data_store.min.js
new file mode 100644 (file)
index 0000000..e2cc7de
Binary files /dev/null and b/calendar/amd/build/drag_drop_data_store.min.js differ
index 49c3a88..1f6e3ff 100644 (file)
Binary files a/calendar/amd/build/event_form.min.js and b/calendar/amd/build/event_form.min.js differ
index 762a824..77b2153 100644 (file)
Binary files a/calendar/amd/build/events.min.js and b/calendar/amd/build/events.min.js differ
index 78b804a..4d52804 100644 (file)
Binary files a/calendar/amd/build/modal_event_form.min.js and b/calendar/amd/build/modal_event_form.min.js differ
diff --git a/calendar/amd/build/month_navigation_drag_drop.min.js b/calendar/amd/build/month_navigation_drag_drop.min.js
new file mode 100644 (file)
index 0000000..a8888bf
Binary files /dev/null and b/calendar/amd/build/month_navigation_drag_drop.min.js differ
diff --git a/calendar/amd/build/month_view_drag_drop.min.js b/calendar/amd/build/month_view_drag_drop.min.js
new file mode 100644 (file)
index 0000000..b2ee13d
Binary files /dev/null and b/calendar/amd/build/month_view_drag_drop.min.js differ
diff --git a/calendar/amd/build/selectors.min.js b/calendar/amd/build/selectors.min.js
new file mode 100644 (file)
index 0000000..8dbeb18
Binary files /dev/null and b/calendar/amd/build/selectors.min.js differ
index 358bad0..0a9f53a 100644 (file)
Binary files a/calendar/amd/build/summary_modal.min.js and b/calendar/amd/build/summary_modal.min.js differ
index dc730d3..301ef42 100644 (file)
Binary files a/calendar/amd/build/view_manager.min.js and b/calendar/amd/build/view_manager.min.js differ
index 86fd106..2958c83 100644 (file)
@@ -58,10 +58,15 @@ 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"]'
     };
 
     /**
@@ -137,7 +142,9 @@ define([
                 body: Templates.render('core_calendar/event_summary_body', eventData),
                 templateContext: {
                     canedit: eventData.canedit,
-                    candelete: eventData.candelete
+                    candelete: eventData.candelete,
+                    isactionevent: eventData.isactionevent,
+                    url: eventData.url
                 }
             };
 
@@ -165,24 +172,30 @@ define([
      * updated.
      *
      * @param {event} e The calendar move event
-     * @param {object} eventElement The jQuery element with the event id
-     * @param {object} originElement The jQuery element for where the event is moving from
+     * @param {int} eventId The event id being moved
+     * @param {object|null} originElement The jQuery element for where the event is moving from
      * @param {object} destinationElement The jQuery element for where the event is moving to
      */
-    var handleMoveEvent = function(e, eventElement, originElement, destinationElement) {
-        var eventId = eventElement.attr('data-event-id');
-        var originTimestamp = originElement.attr('data-day-timestamp');
+    var handleMoveEvent = function(e, eventId, originElement, destinationElement) {
+        var originTimestamp = null;
         var destinationTimestamp = destinationElement.attr('data-day-timestamp');
 
+        if (originElement) {
+            originTimestamp = originElement.attr('data-day-timestamp');
+        }
+
         // If the event has actually changed day.
-        if (originTimestamp != destinationTimestamp) {
+        if (!originElement || originTimestamp != destinationTimestamp) {
             Templates.render('core/loading', {})
                 .then(function(html, js) {
                     // First we show some loading icons in each of the days being affected.
-                    originElement.find(SELECTORS.DAY_CONTENT).addClass('hidden');
                     destinationElement.find(SELECTORS.DAY_CONTENT).addClass('hidden');
-                    Templates.appendNodeContents(originElement, html, js);
                     Templates.appendNodeContents(destinationElement, html, js);
+
+                    if (originElement) {
+                        originElement.find(SELECTORS.DAY_CONTENT).addClass('hidden');
+                        Templates.appendNodeContents(originElement, html, js);
+                    }
                     return;
                 })
                 .then(function() {
@@ -192,19 +205,21 @@ define([
                 .then(function() {
                     // If the update was successful then broadcast an event letting the calendar
                     // know that an event has been moved.
-                    $('body').trigger(CalendarEvents.eventMoved, [eventElement, originElement, destinationElement]);
+                    $('body').trigger(CalendarEvents.eventMoved, [eventId, originElement, destinationElement]);
                     return;
                 })
                 .always(function() {
                     // Always remove the loading icons regardless of whether the update
                     // request was successful or not.
-                    var originLoadingElement = originElement.find(SELECTORS.LOADING_ICON);
                     var destinationLoadingElement = destinationElement.find(SELECTORS.LOADING_ICON);
-                    originElement.find(SELECTORS.DAY_CONTENT).removeClass('hidden');
                     destinationElement.find(SELECTORS.DAY_CONTENT).removeClass('hidden');
-
-                    Templates.replaceNode(originLoadingElement, '', '');
                     Templates.replaceNode(destinationLoadingElement, '', '');
+
+                    if (originElement) {
+                        var originLoadingElement = originElement.find(SELECTORS.LOADING_ICON);
+                        originElement.find(SELECTORS.DAY_CONTENT).removeClass('hidden');
+                        Templates.replaceNode(originLoadingElement, '', '');
+                    }
                     return;
                 })
                 .fail(Notification.exception);
@@ -243,16 +258,17 @@ define([
      * @param {object} eventFormModalPromise A promise reolved with the event form modal
      */
     var registerCalendarEventListeners = function(root, eventFormModalPromise) {
-        var body = $('body');
+        var body = $('body'),
+            courseId = $(root).find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('courseid');
 
         body.on(CalendarEvents.created, function() {
-            CalendarViewManager.reloadCurrentMonth();
+            CalendarViewManager.reloadCurrentMonth(root);
         });
         body.on(CalendarEvents.deleted, function() {
-            CalendarViewManager.reloadCurrentMonth();
+            CalendarViewManager.reloadCurrentMonth(root);
         });
         body.on(CalendarEvents.updated, function() {
-            CalendarViewManager.reloadCurrentMonth();
+            CalendarViewManager.reloadCurrentMonth(root);
         });
         body.on(CalendarEvents.editActionEvent, function(e, url) {
             // Action events needs to be edit directly on the course module.
@@ -262,7 +278,7 @@ define([
         body.on(CalendarEvents.moveEvent, handleMoveEvent);
         // When an event is successfully moved we should updated the UI.
         body.on(CalendarEvents.eventMoved, function() {
-            window.location.reload();
+            CalendarViewManager.reloadCurrentMonth(root);
         });
 
         eventFormModalPromise.then(function(modal) {
@@ -272,7 +288,7 @@ define([
                 modal.setEventId(eventId);
                 modal.show();
             });
-
+            modal.setCourseId(courseId);
             return;
         });
     };
@@ -280,24 +296,63 @@ define([
     /**
      * Register event listeners for the module.
      */
-    var registerEventListeners = function() {
-        var root = $(SELECTORS.ROOT);
-
+    var registerEventListeners = function(root) {
         // Bind click events to event links.
-        root.on('click', SELECTORS.EVENT_LINK, function(e) {
+        root.on('click', SELECTORS.EVENT_ITEM, function(e) {
             e.preventDefault();
-            var eventId = $(this).attr('data-event-id');
+            // 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;
+
+            if (target.is(SELECTORS.EVENT_LINK)) {
+                eventId = target.attr('data-event-id');
+            } else {
+                eventId = target.find(SELECTORS.EVENT_LINK).attr('data-event-id');
+            }
+
             renderEventSummaryModal(eventId);
         });
 
+        root.on('change', SELECTORS.COURSE_SELECTOR, function() {
+            var selectElement = $(this);
+            var courseId = selectElement.val();
+            CalendarViewManager.reloadCurrentMonth(root, courseId)
+                .then(function() {
+                    // We need to get the selector again because the content has changed.
+                    return root.find(SELECTORS.COURSE_SELECTOR).val(courseId);
+                })
+                .fail(Notification.exception);
+        });
+
         var eventFormPromise = registerEventFormModal(root);
         registerCalendarEventListeners(root, eventFormPromise);
+
+        // Bind click events to calendar days.
+        root.on('click', SELECTORS.DAY, function(e) {
+            var target = $(e.target);
+
+            if (!target.is(SELECTORS.VIEW_DAY_LINK)) {
+                var startTime = $(this).attr('data-new-event-timestamp');
+                eventFormPromise.then(function(modal) {
+                    modal.setStartTime(startTime);
+                    modal.show();
+                    return;
+                });
+
+                e.preventDefault();
+            }
+        });
     };
 
     return {
-        init: function() {
-            CalendarViewManager.init();
-            registerEventListeners();
+        init: function(root) {
+            root = $(root);
+
+            CalendarViewManager.init(root);
+            registerEventListeners(root);
         }
     };
 });
diff --git a/calendar/amd/src/calendar_filter.js b/calendar/amd/src/calendar_filter.js
new file mode 100644 (file)
index 0000000..5352a7c
--- /dev/null
@@ -0,0 +1,119 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This module is responsible for the calendar filter.
+ *
+ * @module     core_calendar/calendar_filter
+ * @package    core_calendar
+ * @copyright  2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([
+    'jquery',
+    'core_calendar/selectors',
+    'core_calendar/events',
+    'core/str',
+    'core/templates',
+],
+function(
+    $,
+    CalendarSelectors,
+    CalendarEvents,
+    Str,
+    Templates
+) {
+
+    var registerEventListeners = function(root) {
+        root.on('click', CalendarSelectors.eventFilterItem, function(e) {
+            var target = $(e.currentTarget);
+
+            toggleFilter(target);
+
+            e.preventDefault();
+        });
+
+        $('body').on(CalendarEvents.viewUpdated, function() {
+            var filters = root.find(CalendarSelectors.eventFilterItem);
+
+            filters.each(function(i, filter) {
+                filter = $(filter);
+                if (filter.data('eventtype-hidden')) {
+                    var data = getFilterData(filter);
+                    fireFilterChangedEvent(data);
+                }
+            });
+        });
+    };
+
+    var toggleFilter = function(target) {
+        var data = getFilterData(target);
+
+        // Toggle the hidden. We need to render the template before we change the value.
+        data.hidden = !data.hidden;
+
+        return Str.get_string('eventtype' + data.type, 'calendar')
+        .then(function(nameStr) {
+            data.name = nameStr;
+
+            return data;
+        })
+        .then(function(context) {
+            return Templates.render('core_calendar/event_filter_key', context);
+        })
+        .then(function(html, js) {
+            return Templates.replaceNode(target, html, js);
+        })
+        .then(function() {
+            fireFilterChangedEvent(data);
+            return;
+        });
+    };
+
+    /**
+     * Fire the filterChanged event for the specified data.
+     *
+     * @param   {object} data The data to include
+     */
+    var fireFilterChangedEvent = function(data) {
+        M.util.js_pending("month-mini-filterChanged");
+        $('body').trigger(CalendarEvents.filterChanged, {
+            type: data.type,
+            hidden: data.hidden,
+        });
+        M.util.js_complete("month-mini-filterChanged");
+    };
+
+    /**
+     * Get the filter data for the specified target.
+     *
+     * @param   {jQuery} target The target node
+     * @return  {Object}
+     */
+    var getFilterData = function(target) {
+        return {
+            type: target.data('eventtype'),
+            hidden: target.data('eventtype-hidden'),
+        };
+    };
+
+    return {
+        init: function(root) {
+            root = $(root);
+
+            registerEventListeners(root);
+        }
+    };
+});
diff --git a/calendar/amd/src/calendar_mini.js b/calendar/amd/src/calendar_mini.js
new file mode 100644 (file)
index 0000000..48a4f44
--- /dev/null
@@ -0,0 +1,57 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This module is the highest level module for the calendar. It is
+ * responsible for initialising all of the components required for
+ * the calendar to run. It also coordinates the interaction between
+ * components by listening for and responding to different events
+ * triggered within the calendar UI.
+ *
+ * @module     core_calendar/calendar
+ * @package    core_calendar
+ * @copyright  2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([
+    'jquery',
+    'core_calendar/selectors',
+    'core_calendar/events',
+    'core_calendar/view_manager',
+],
+function(
+    $,
+    CalendarSelectors,
+    CalendarEvents,
+    CalendarViewManager
+) {
+
+    var registerEventListeners = function(root) {
+        $('body').on(CalendarEvents.filterChanged, function(e, data) {
+            var daysWithEvent = root.find(CalendarSelectors.eventType[data.type]);
+
+            daysWithEvent.toggleClass('calendar_event_' + data.type, !data.hidden);
+        });
+    };
+
+    return {
+        init: function(root) {
+            root = $(root);
+
+            CalendarViewManager.init(root);
+            registerEventListeners(root);
+        }
+    };
+});
diff --git a/calendar/amd/src/drag_drop_data_store.js b/calendar/amd/src/drag_drop_data_store.js
new file mode 100644 (file)
index 0000000..6414f8e
--- /dev/null
@@ -0,0 +1,95 @@
+// 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/>.
+
+/**
+ * A javascript module to store calendar drag and drop data.
+ *
+ * This module is unfortunately required because of the limitations
+ * of the HTML5 drag and drop API and it's ability to provide data
+ * between the different stages of the drag/drop lifecycle.
+ *
+ * @module     core_calendar/drag_drop_data_store
+ * @package    core_calendar
+ * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([], function() {
+    /* @var {int|null} eventId The id of the event being dragged */
+    var eventId = null;
+    /* @var {int|null} durationDays How many days the event spans */
+    var durationDays = null;
+
+    /**
+     * Store the id of the event being dragged.
+     *
+     * @param {int} id The event id
+     */
+    var setEventId = function(id) {
+        eventId = id;
+    };
+
+    /**
+     * Get the stored event id.
+     *
+     * @return {int|null}
+     */
+    var getEventId = function() {
+        return eventId;
+    };
+
+    /**
+     * Check if the store has an event id.
+     *
+     * @return {bool}
+     */
+    var hasEventId = function() {
+        return eventId !== null;
+    };
+
+    /**
+     * Store the duration (in days) of the event being dragged.
+     *
+     * @param {int} days Number of days the event spans
+     */
+    var setDurationDays = function(days) {
+        durationDays = days;
+    };
+
+    /**
+     * Get the stored number of days.
+     *
+     * @return {int|null}
+     */
+    var getDurationDays = function() {
+        return durationDays;
+    };
+
+    /**
+     * Reset all of the stored values.
+     */
+    var clearAll = function() {
+        setEventId(null);
+        setDurationDays(null);
+    };
+
+    return {
+        setEventId: setEventId,
+        getEventId: getEventId,
+        hasEventId: hasEventId,
+        setDurationDays: setDurationDays,
+        getDurationDays: getDurationDays,
+        clearAll: clearAll
+    };
+});
index 0a4e33e..f537f6d 100644 (file)
  * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/templates'], function($, Templates) {
+define(['jquery'], function($) {
 
     var SELECTORS = {
-        EVENT_TYPE: '[name="eventtype"]',
-        EVENT_COURSE_ID: '[name="courseid"]',
         EVENT_GROUP_COURSE_ID: '[name="groupcourseid"]',
         EVENT_GROUP_ID: '[name="groupid"]',
-        FORM_GROUP: '.form-group',
         SELECT_OPTION: 'option',
-        ADVANCED_ELEMENT: '.fitem.advanced',
-        FIELDSET_ADVANCED_ELEMENTS: 'fieldset.containsadvancedelements',
-        MORELESS_TOGGLE: '.moreless-actions'
-    };
-
-    var EVENT_TYPES = {
-        USER: 'user',
-        SITE: 'site',
-        COURSE: 'course',
-        GROUP: 'group'
-    };
-
-    var EVENTS = {
-        SHOW_ADVANCED: 'event_form-show-advanced',
-        HIDE_ADVANCED: 'event_form-hide-advanced',
-        ADVANCED_SHOWN: 'event_form-advanced-shown',
-        ADVANCED_HIDDEN: 'event_form-advanced-hidden',
-    };
-
-    /**
-     * Find the old show more / show less toggle added by the mform and destroy it.
-     * We are handling the visibility of the advanced fields with the more/less button
-     * in the footer of the modal that this form is rendered within.
-     *
-     * @method destroyOldMoreLessToggle
-     * @param {object} formElement The root form element
-     */
-    var destroyOldMoreLessToggle = function(formElement) {
-        formElement.find(SELECTORS.FIELDSET_ADVANCED_ELEMENTS).removeClass('containsadvancedelements');
-        var element = formElement.find(SELECTORS.MORELESS_TOGGLE);
-        Templates.replaceNode(element, '', '');
-    };
-
-    /**
-     * Find each of the advanced form elements and make them visible.
-     *
-     * This function triggers the ADVANCED_SHOWN event for any other
-     * component to handle (e.g. the event form modal).
-     *
-     * @method destroyOldMoreLessToggle
-     * @param {object} formElement The root form element
-     */
-    var showAdvancedElements = function(formElement) {
-        formElement.find(SELECTORS.ADVANCED_ELEMENT).removeClass('hidden');
-        formElement.trigger(EVENTS.ADVANCED_SHOWN);
-    };
-
-    /**
-     * Find each of the advanced form elements and hide them.
-     *
-     * This function triggers the ADVANCED_HIDDEN event for any other
-     * component to handle (e.g. the event form modal).
-     *
-     * @method hideAdvancedElements
-     * @param {object} formElement The root form element
-     */
-    var hideAdvancedElements = function(formElement) {
-        formElement.find(SELECTORS.ADVANCED_ELEMENT).addClass('hidden');
-        formElement.trigger(EVENTS.ADVANCED_HIDDEN);
-    };
-
-    /**
-     * Listen for any events telling this module to show or hide it's
-     * advanced elements.
-     *
-     * This function listens for SHOW_ADVANCED and HIDE_ADVANCED.
-     *
-     * @method listenForShowHideEvents
-     * @param {object} formElement The root form element
-     */
-    var listenForShowHideEvents = function(formElement) {
-        formElement.on(EVENTS.SHOW_ADVANCED, function() {
-            showAdvancedElements(formElement);
-        });
-
-        formElement.on(EVENTS.HIDE_ADVANCED, function() {
-            hideAdvancedElements(formElement);
-        });
     };
 
     /**
@@ -140,75 +59,6 @@ define(['jquery', 'core/templates'], function($, Templates) {
             });
     };
 
-    /**
-     * Toggle the visibility of the secondary select elements based on
-     * the event type the user has selected.
-     *
-     * There are 3 secondary select elements within the form:
-     *      - course: a list of all courses a user can add course events to
-     *      - group course: a list of all courses a user can add group events to.
-     *                      this list can be different from the course list above.
-     *      - group: a list of all groups a user can add an event to. This list will
-     *               be filtered further based on the group course selected.
-     *
-     *  There are 4 event types:
-     *      - user: none of the secondary selects should be visible.
-     *      - site: none of the secondary selects should be visible.
-     *      - course: "course" select should be visible and both "group course"
-     *                and "group" should be hidden.
-     *      - group: "group course" and "group" should be visible and "course"
-     *               should be hidden.
-     *
-     * @method hideTypeSubSelects
-     * @param {object} formElement The root form element
-     */
-    var hideTypeSubSelects = function(formElement) {
-        var typeSelect = formElement.find(SELECTORS.EVENT_TYPE);
-        var eventType = typeSelect.val();
-        var courseIdSelect = formElement.find(SELECTORS.EVENT_COURSE_ID)
-            .closest(SELECTORS.FORM_GROUP)
-            .removeClass('hidden');
-        var groupCourseIdSelect = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID)
-            .closest(SELECTORS.FORM_GROUP)
-            .removeClass('hidden');
-        var groupIdSelect = formElement.find(SELECTORS.EVENT_GROUP_ID)
-            .closest(SELECTORS.FORM_GROUP)
-            .removeClass('hidden');
-
-        // Hide the unreleated selectors for the given event type.
-        switch (eventType) {
-            case EVENT_TYPES.COURSE:
-                groupCourseIdSelect.addClass('hidden');
-                groupIdSelect.addClass('hidden');
-                break;
-            case EVENT_TYPES.GROUP:
-                courseIdSelect.addClass('hidden');
-                break;
-            default:
-                courseIdSelect.addClass('hidden');
-                groupCourseIdSelect.addClass('hidden');
-                groupIdSelect.addClass('hidden');
-        }
-    };
-
-    /**
-     * Listen for when the user changes the event type select in the
-     * form and then toggle the visibility of the appropriate secondary
-     * select elements.
-     *
-     * See: hideTypeSubSelects.
-     *
-     * @method addTypeSelectListeners
-     * @param {object} formElement The root form element
-     */
-    var addTypeSelectListeners = function(formElement) {
-        var typeSelect = formElement.find(SELECTORS.EVENT_TYPE);
-
-        typeSelect.on('change', function() {
-            hideTypeSubSelects(formElement);
-        });
-    };
-
     /**
      * Listen for when the user changes the group course when configuring
      * a group event and filter the options in the group select to only
@@ -232,7 +82,7 @@ define(['jquery', 'core/templates'], function($, Templates) {
                     element.removeClass('hidden');
                     element.prop('disabled', false);
 
-                    if (selectedIndex === null) {
+                    if (selectedIndex === null || element.attr('selected')) {
                         selectedIndex = index;
                     }
                 } else {
@@ -253,30 +103,15 @@ define(['jquery', 'core/templates'], function($, Templates) {
      *
      * @method init
      * @param {string} formId The value of the form's id attribute
-     * @param {bool} hasError If the form has errors rendered form the server.
      */
-    var init = function(formId, hasError) {
+    var init = function(formId) {
         var formElement = $('#' + formId);
 
-        listenForShowHideEvents(formElement);
-        destroyOldMoreLessToggle(formElement);
-        hideTypeSubSelects(formElement);
         parseGroupSelect(formElement);
-        addTypeSelectListeners(formElement);
         addCourseGroupSelectListeners(formElement);
-
-        // If we know that the form has been rendered with server side
-        // errors then we need to display all of the elements in the form
-        // in case one of those elements has the error.
-        if (hasError) {
-            showAdvancedElements(formElement);
-        } else {
-            hideAdvancedElements(formElement);
-        }
     };
 
     return {
         init: init,
-        events: EVENTS,
     };
 });
index 95e0968..29b5fd2 100644 (file)
@@ -31,6 +31,8 @@ define([], function() {
         editActionEvent: 'calendar-events:edit_action_event',
         eventMoved: 'calendar-events:event_moved',
         monthChanged: 'calendar-events:month_changed',
-        moveEvent: 'calendar-events:move_event'
+        moveEvent: 'calendar-events:move_event',
+        filterChanged: 'calendar-events:filter_changed',
+        viewUpdated: 'calendar-events:view_updated',
     };
 });
index bad5ca4..24f7b84 100644 (file)
@@ -33,8 +33,7 @@ define([
             'core/modal_registry',
             'core/fragment',
             'core_calendar/events',
-            'core_calendar/repository',
-            'core_calendar/event_form'
+            'core_calendar/repository'
         ],
         function(
             $,
@@ -47,13 +46,11 @@ define([
             ModalRegistry,
             Fragment,
             CalendarEvents,
-            Repository,
-            EventForm
+            Repository
         ) {
 
     var registered = false;
     var SELECTORS = {
-        MORELESS_BUTTON: '[data-action="more-less-toggle"]',
         SAVE_BUTTON: '[data-action="save"]',
         LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]',
     };
@@ -66,16 +63,47 @@ define([
     var ModalEventForm = function(root) {
         Modal.call(this, root);
         this.eventId = null;
+        this.startTime = null;
+        this.courseId = null;
         this.reloadingBody = false;
         this.reloadingTitle = false;
         this.saveButton = this.getFooter().find(SELECTORS.SAVE_BUTTON);
-        this.moreLessButton = this.getFooter().find(SELECTORS.MORELESS_BUTTON);
     };
 
     ModalEventForm.TYPE = 'core_calendar-modal_event_form';
     ModalEventForm.prototype = Object.create(Modal.prototype);
     ModalEventForm.prototype.constructor = ModalEventForm;
 
+    /**
+     * Set the course id to the given value.
+     *
+     * @method setCourseId
+     * @param {int} id The event id
+     */
+    ModalEventForm.prototype.setCourseId = function(id) {
+        this.courseId = id;
+    };
+
+    /**
+     * Retrieve the current course id, if any.
+     *
+     * @method getCourseId
+     * @return {int|null} The event id
+     */
+    ModalEventForm.prototype.getCourseId = function() {
+        return this.courseId;
+    };
+
+    /**
+     * Check if the modal has an course id.
+     *
+     * @method hasCourseId
+     * @return {bool}
+     */
+    ModalEventForm.prototype.hasCourseId = function() {
+        return this.courseId !== null;
+    };
+
     /**
      * Set the event id to the given value.
      *
@@ -107,79 +135,61 @@ define([
     };
 
     /**
-     * Get the form element from the modal.
+     * Set the start time to the given value.
      *
-     * @method getForm
-     * @return {object}
+     * @method setStartTime
+     * @param {int} time The start time
      */
-    ModalEventForm.prototype.getForm = function() {
-        return this.getBody().find('form');
+    ModalEventForm.prototype.setStartTime = function(time) {
+        this.startTime = time;
     };
 
     /**
-     * Disable the buttons in the footer.
+     * Retrieve the current start time, if any.
      *
-     * @method disableButtons
+     * @method getStartTime
+     * @return {int|null} The start time
      */
-    ModalEventForm.prototype.disableButtons = function() {
-        this.saveButton.prop('disabled', true);
-        this.moreLessButton.prop('disabled', true);
+    ModalEventForm.prototype.getStartTime = function() {
+        return this.startTime;
     };
 
     /**
-     * Enable the buttons in the footer.
+     * Check if the modal has start time.
      *
-     * @method enableButtons
+     * @method hasStartTime
+     * @return {bool}
      */
-    ModalEventForm.prototype.enableButtons = function() {
-        this.saveButton.prop('disabled', false);
-        this.moreLessButton.prop('disabled', false);
+    ModalEventForm.prototype.hasStartTime = function() {
+        return this.startTime !== null;
     };
 
     /**
-     * Set the more/less button in the footer to the "more"
-     * state.
+     * Get the form element from the modal.
      *
-     * @method setMoreButton
+     * @method getForm
+     * @return {object}
      */
-    ModalEventForm.prototype.setMoreButton = function() {
-        this.moreLessButton.attr('data-collapsed', 'true');
-        Str.get_string('more', 'calendar').then(function(string) {
-            this.moreLessButton.text(string);
-            return;
-        }.bind(this));
+    ModalEventForm.prototype.getForm = function() {
+        return this.getBody().find('form');
     };
 
     /**
-     * Set the more/less button in the footer to the "less"
-     * state.
+     * Disable the buttons in the footer.
      *
-     * @method setLessButton
+     * @method disableButtons
      */
-    ModalEventForm.prototype.setLessButton = function() {
-        this.moreLessButton.attr('data-collapsed', 'false');
-        Str.get_string('less', 'calendar').then(function(string) {
-            this.moreLessButton.text(string);
-            return;
-        }.bind(this));
+    ModalEventForm.prototype.disableButtons = function() {
+        this.saveButton.prop('disabled', true);
     };
 
     /**
-     * Toggle the more/less button in the footer from the current
-     * state to it's opposite state.
+     * Enable the buttons in the footer.
      *
-     * @method toggleMoreLessButton
+     * @method enableButtons
      */
-    ModalEventForm.prototype.toggleMoreLessButton = function() {
-        var form = this.getForm();
-
-        if (this.moreLessButton.attr('data-collapsed') == 'true') {
-            form.trigger(EventForm.events.SHOW_ADVANCED);
-            this.setLessButton();
-        } else {
-            form.trigger(EventForm.events.HIDE_ADVANCED);
-            this.setMoreButton();
-        }
+    ModalEventForm.prototype.enableButtons = function() {
+        this.saveButton.prop('disabled', false);
     };
 
     /**
@@ -226,10 +236,9 @@ define([
      *
      * @method reloadBodyContent
      * @param {string} formData The serialised form data
-     * @param {bool} hasError True if we know the form data is erroneous
      * @return {object} A promise resolved with the fragment html and js from
      */
-    ModalEventForm.prototype.reloadBodyContent = function(formData, hasError) {
+    ModalEventForm.prototype.reloadBodyContent = function(formData) {
         if (this.reloadingBody) {
             return this.bodyPromise;
         }
@@ -244,12 +253,18 @@ define([
             args.eventid = this.getEventId();
         }
 
+        if (this.hasStartTime()) {
+            args.starttime = this.getStartTime();
+        }
+
+        if (this.hasCourseId()) {
+            args.courseid = this.getCourseId();
+        }
+
         if (typeof formData !== 'undefined') {
             args.formdata = formData;
         }
 
-        args.haserror = (typeof hasError == 'undefined') ? false : hasError;
-
         this.bodyPromise = Fragment.loadFragment('calendar', 'event_form', contextId, args);
 
         this.setBody(this.bodyPromise);
@@ -305,6 +320,7 @@ define([
     ModalEventForm.prototype.hide = function() {
         Modal.prototype.hide.call(this);
         this.setEventId(null);
+        this.setStartTime(null);
     };
 
     /**
@@ -345,7 +361,7 @@ define([
                     // If there was a server side validation error then
                     // we need to re-request the rendered form from the server
                     // in order to display the error for the user.
-                    return this.reloadBodyContent(formData, true);
+                    return this.reloadBodyContent(formData);
                 } else {
                     // No problemo! Our work here is done.
                     this.hide();
@@ -398,28 +414,6 @@ define([
             e.preventDefault();
             e.stopPropagation();
         }.bind(this));
-
-        // Toggle the state of the more/less button in the footer.
-        this.getModal().on(CustomEvents.events.activate, SELECTORS.MORELESS_BUTTON, function(e, data) {
-            this.toggleMoreLessButton();
-
-            data.originalEvent.preventDefault();
-            e.stopPropagation();
-        }.bind(this));
-
-        // When the event form tells us that the advanced fields are shown
-        // then the more/less button should be set to less to allow the user
-        // to hide the advanced fields.
-        this.getModal().on(EventForm.events.ADVANCED_SHOWN, function() {
-            this.setLessButton();
-        }.bind(this));
-
-        // When the event form tells us that the advanced fields are hidden
-        // then the more/less button should be set to more to allow the user
-        // to show the advanced fields.
-        this.getModal().on(EventForm.events.ADVANCED_HIDDEN, function() {
-            this.setMoreButton();
-        }.bind(this));
     };
 
     // Automatically register with the modal registry the first time this module is imported so that you can create modals
diff --git a/calendar/amd/src/month_navigation_drag_drop.js b/calendar/amd/src/month_navigation_drag_drop.js
new file mode 100644 (file)
index 0000000..0e192df
--- /dev/null
@@ -0,0 +1,221 @@
+// 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/>.
+
+/**
+ * A javascript module to handle calendar drag and drop in the calendar
+ * month view navigation.
+ *
+ * This code is run each time the calendar month view is re-rendered. We
+ * only register the event handlers once per page load so that the in place
+ * DOM updates that happen on month change don't continue to register handlers.
+ *
+ * @module     core_calendar/month_navigation_drag_drop
+ * @class      month_navigation_drag_drop
+ * @package    core_calendar
+ * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([
+            'jquery',
+            'core_calendar/drag_drop_data_store',
+        ],
+        function(
+            $,
+            DataStore
+        ) {
+
+    var SELECTORS = {
+        DRAGGABLE: '[draggable="true"][data-region="event-item"]',
+        DROP_ZONE: '[data-drop-zone="nav-link"]',
+    };
+    var HOVER_CLASS = 'bg-primary text-white';
+    var TARGET_CLASS = 'drop-target';
+    var HOVER_TIME = 1000; // 1 second hover to change month.
+
+    // We store some static variables at the module level because this
+    // module is called each time the calendar month view is reloaded but
+    // we want some actions to only occur ones.
+
+    /* @var {bool} registered If the event listeners have been added */
+    var registered = false;
+    /* @var {int} hoverTimer The timeout id of any timeout waiting for hover */
+    var hoverTimer = null;
+    /* @var {object} root The root nav element we're operating on */
+    var root = null;
+
+    /**
+     * Add or remove the appropriate styling to indicate whether
+     * the drop target is being hovered over.
+     *
+     * @param {object} target The target drop zone element
+     * @param {bool} hovered If the element is hovered over ot not
+     */
+    var updateHoverState = function(target, hovered) {
+        if (hovered) {
+            target.addClass(HOVER_CLASS);
+        } else {
+            target.removeClass(HOVER_CLASS);
+        }
+    };
+
+    /**
+     * Add some styling to the UI to indicate that the nav links
+     * are an acceptable drop target.
+     */
+    var addDropZoneIndicator = function() {
+        root.find(SELECTORS.DROP_ZONE).addClass(TARGET_CLASS);
+    };
+
+    /**
+     * Remove the styling from the nav links.
+     */
+    var removeDropZoneIndicator = function() {
+        root.find(SELECTORS.DROP_ZONE).removeClass(TARGET_CLASS);
+    };
+
+    /**
+     * Get the drop zone target from the event, if one is found.
+     *
+     * @param {event} e Javascript event
+     * @return {object|null}
+     */
+    var getTargetFromEvent = function(e) {
+        var target = $(e.target).closest(SELECTORS.DROP_ZONE);
+        return (target.length) ? target : null;
+    };
+
+    /**
+     * This will add a visual indicator to the calendar UI to
+     * indicate which nav link is a valid drop zone.
+     */
+    var dragstartHandler = function(e) {
+        // Make sure the drag event is for a calendar event.
+        var eventElement = $(e.target).closest(SELECTORS.DRAGGABLE);
+
+        if (eventElement.length) {
+            addDropZoneIndicator();
+        }
+    };
+
+    /**
+     * Update the hover state of the target nav element when
+     * the user is dragging an event over it.
+     *
+     * This will add a visual indicator to the calendar UI to
+     * indicate which nav link is being hovered.
+     *
+     * @param {event} e The dragover event
+     */
+    var dragoverHandler = function(e) {
+        e.preventDefault();
+        var target = getTargetFromEvent(e);
+
+        if (!target) {
+            return;
+        }
+
+        // If we're not draggin a calendar event then
+        // ignore it.
+        if (!DataStore.hasEventId()) {
+            return;
+        }
+
+        if (!hoverTimer) {
+            hoverTimer = setTimeout(function() {
+                target.click();
+                hoverTimer = null;
+            }, HOVER_TIME);
+        }
+
+        updateHoverState(target, true);
+        removeDropZoneIndicator();
+    };
+
+    /**
+     * Update the hover state of the target nav element that was
+     * previously dragged over but has is no longer a drag target.
+     *
+     * This will remove the visual indicator from the calendar UI
+     * that was added by the dragoverHandler.
+     *
+     * @param {event} e The dragstart event
+     */
+    var dragleaveHandler = function(e) {
+        var target = getTargetFromEvent(e);
+
+        if (!target) {
+            return;
+        }
+
+        if (hoverTimer) {
+            clearTimeout(hoverTimer);
+            hoverTimer = null;
+        }
+
+        updateHoverState(target, false);
+        addDropZoneIndicator();
+        e.preventDefault();
+    };
+
+    /**
+     * Remove the visual indicator from the calendar UI that was
+     * added by the dragoverHandler.
+     *
+     * @param {event} e The drop event
+     */
+    var dropHandler = function(e) {
+        removeDropZoneIndicator();
+        var target = getTargetFromEvent(e);
+
+        if (!target) {
+            return;
+        }
+
+        updateHoverState(target, false);
+        e.preventDefault();
+    };
+
+    return {
+        /**
+         * Initialise the event handlers for the drag events.
+         *
+         * @param {object} rootElement The element containing calendar nav links
+         */
+        init: function(rootElement) {
+            // Only register the handlers once on the first load.
+            if (!registered) {
+                // These handlers are only added the first time the module
+                // is loaded because we don't want to have a new listener
+                // added each time the "init" function is called otherwise we'll
+                // end up with lots of stale handlers.
+                document.addEventListener('dragstart', dragstartHandler, false);
+                document.addEventListener('dragover', dragoverHandler, false);
+                document.addEventListener('dragleave', dragleaveHandler, false);
+                document.addEventListener('drop', dropHandler, false);
+                document.addEventListener('dragend', removeDropZoneIndicator, false);
+                registered = true;
+            }
+
+            // Update the module variable to operate on the given
+            // root element.
+            root = $(rootElement);
+
+            // If we're currently dragging then add the indicators.
+            if (DataStore.hasEventId()) {
+                addDropZoneIndicator();
+            }
+        },
+    };
+});
similarity index 64%
rename from calendar/amd/src/drag_drop.js
rename to calendar/amd/src/month_view_drag_drop.js
index de085f0..05b3733 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * A javascript module to handle calendar drag and drop. This module
- * unfortunately requires some state to be maintained because of the
- * limitations of the HTML5 drag and drop API which means it can't
- * be used multiple times with the current implementation.
+ * A javascript module to handle calendar drag and drop in the calendar
+ * month view.
  *
- * @module     core_calendar/drag_drop
- * @class      drag_drop
+ * @module     core_calendar/month_view_drag_drop
+ * @class      month_view_drag_drop
  * @package    core_calendar
  * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 define([
             'jquery',
-            'core_calendar/events'
+            'core_calendar/events',
+            'core_calendar/drag_drop_data_store'
         ],
         function(
             $,
-            CalendarEvents
+            CalendarEvents,
+            DataStore
         ) {
 
     var SELECTORS = {
         ROOT: "[data-region='calendar']",
-        DRAGGABLE: '[draggable="true"]',
-        DROP_ZONE: '[data-drop-zone="true"]',
+        DRAGGABLE: '[draggable="true"][data-region="event-item"]',
+        DROP_ZONE: '[data-drop-zone="month-view-day"]',
         WEEK: '[data-region="month-view-week"]',
     };
-    var HOVER_CLASS = 'bg-primary';
+    var HOVER_CLASS = 'bg-primary text-white';
+    /* @var {bool} registered If the event listeners have been added */
+    var registered = false;
 
-    // Unfortunately we are required to maintain some module
-    // level state due to the limitations of the HTML5 drag
-    // and drop API. Specifically the inability to pass data
-    // between the dragstate and dragover events handlers
-    // using the DataTransfer object in the event.
-
-    /** @var int eventId The event id being moved. */
-    var eventId = null;
-    /** @var int duration The number of days the event spans */
-    var duration = null;
+    /**
+     * Get the correct drop zone element from the given javascript
+     * event.
+     *
+     * @param {event} e The javascript event
+     * @return {object|null}
+     */
+    var getDropZoneFromEvent = function(e) {
+        var dropZone = $(e.target).closest(SELECTORS.DROP_ZONE);
+        return (dropZone.length) ? dropZone : null;
+    };
 
     /**
      * Update the hover state for the event in the calendar to reflect
@@ -70,11 +73,10 @@ define([
      * @param {bool} hovered If the target is hovered or not
      * @param {int} count How many days to highlight (default to duration)
      */
-    var updateHoverState = function(target, hovered, count) {
-        var dropZone = $(target).closest(SELECTORS.DROP_ZONE);
+    var updateHoverState = function(dropZone, hovered, count) {
         if (typeof count === 'undefined') {
             // This is how many days we need to highlight.
-            count = duration;
+            count = DataStore.getDurationDays();
         }
 
         if (hovered) {
@@ -115,16 +117,20 @@ define([
      * @param {event} e The dragstart event
      */
     var dragstartHandler = function(e) {
-        var eventElement = $(e.target);
+        var eventElement = $(e.target).closest(SELECTORS.DRAGGABLE);
 
-        if (!eventElement.is('[data-event-id]')) {
-            eventElement = eventElement.find('[data-event-id]');
+        if (!eventElement.length) {
+            return;
         }
 
-        eventId = eventElement.attr('data-event-id');
+        eventElement = eventElement.find('[data-event-id]');
 
+        var eventId = eventElement.attr('data-event-id');
         var eventsSelector = SELECTORS.ROOT + ' [data-event-id="' + eventId + '"]';
-        duration = $(eventsSelector).length;
+        var duration = $(eventsSelector).length;
+
+        DataStore.setEventId(eventId);
+        DataStore.setDurationDays(duration);
 
         e.dataTransfer.effectAllowed = "move";
         e.dataTransfer.dropEffect = "move";
@@ -145,7 +151,14 @@ define([
      */
     var dragoverHandler = function(e) {
         e.preventDefault();
-        updateHoverState(e.target, true);
+
+        var dropZone = getDropZoneFromEvent(e);
+
+        if (!dropZone) {
+            return;
+        }
+
+        updateHoverState(dropZone, true);
     };
 
     /**
@@ -158,8 +171,14 @@ define([
      * @param {event} e The dragstart event
      */
     var dragleaveHandler = function(e) {
+        var dropZone = getDropZoneFromEvent(e);
+
+        if (!dropZone) {
+            return;
+        }
+
+        updateHoverState(dropZone, false);
         e.preventDefault();
-        updateHoverState(e.target, false);
     };
 
     /**
@@ -174,35 +193,46 @@ define([
      * @param {event} e The dragstart event
      */
     var dropHandler = function(e) {
-        e.preventDefault();
+        var dropZone = getDropZoneFromEvent(e);
+
+        if (!dropZone) {
+            DataStore.clearAll();
+            return;
+        }
 
+        var eventId = DataStore.getEventId();
         var eventElementSelector = SELECTORS.ROOT + ' [data-event-id="' + eventId + '"]';
         var eventElement = $(eventElementSelector);
-        var origin = eventElement.closest(SELECTORS.DROP_ZONE);
+        var origin = null;
         var destination = $(e.target).closest(SELECTORS.DROP_ZONE);
 
-        updateHoverState(e.target, false);
-        $('body').trigger(CalendarEvents.moveEvent, [eventElement, origin, destination]);
+        if (eventElement.length) {
+            origin = eventElement.closest(SELECTORS.DROP_ZONE);
+        }
+
+        updateHoverState(dropZone, false);
+        $('body').trigger(CalendarEvents.moveEvent, [eventId, origin, destination]);
+        DataStore.clearAll();
+
+        e.preventDefault();
     };
 
     return {
         /**
          * Initialise the event handlers for the drag events.
-         *
-         * @param {object} root The root calendar element that containers the drag drop elements
          */
-        init: function(root) {
-            root = $(root);
-
-            root.find(SELECTORS.DRAGGABLE).each(function(index, element) {
-                element.addEventListener('dragstart', dragstartHandler, true);
-            });
-
-            root.find(SELECTORS.DROP_ZONE).each(function(index, element) {
-                element.addEventListener('dragover', dragoverHandler, true);
-                element.addEventListener('dragleave', dragleaveHandler, true);
-                element.addEventListener('drop', dropHandler, true);
-            });
+        init: function() {
+            if (!registered) {
+                // These handlers are only added the first time the module
+                // is loaded because we don't want to have a new listener
+                // added each time the "init" function is called otherwise we'll
+                // end up with lots of stale handlers.
+                document.addEventListener('dragstart', dragstartHandler, false);
+                document.addEventListener('dragover', dragoverHandler, false);
+                document.addEventListener('dragleave', dragleaveHandler, false);
+                document.addEventListener('drop', dropHandler, false);
+                registered = true;
+            }
         },
     };
 });
diff --git a/calendar/amd/src/selectors.js b/calendar/amd/src/selectors.js
new file mode 100644 (file)
index 0000000..89bb65a
--- /dev/null
@@ -0,0 +1,40 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This module is responsible for the calendar filter.
+ *
+ * @module     core_calendar/calendar_selectors
+ * @package    core_calendar
+ * @copyright  2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([], function() {
+    return {
+        eventFilterItem: "[data-action='filter-event-type']",
+        eventType: {
+            site: "[data-eventtype-site]",
+            course: "[data-eventtype-course]",
+            group: "[data-eventtype-group]",
+            user: "[data-eventtype-user]",
+        },
+        popoverType: {
+            site: "[data-popover-eventtype-site]",
+            course: "[data-popover-eventtype-course]",
+            group: "[data-popover-eventtype-group]",
+            user: "[data-popover-eventtype-user]",
+        },
+    };
+});
index 4a668be..da138c3 100644 (file)
@@ -118,33 +118,11 @@ define(['jquery', 'core/str', 'core/notification', 'core/custom_interaction_even
         // Apply parent event listeners.
         Modal.prototype.registerEventListeners.call(this);
 
-        var confirmPromise = ModalFactory.create(
-            {
-                type: ModalFactory.types.CONFIRM
-            },
-            this.getDeleteButton()
-        ).then(function(modal) {
-            modal.getRoot().on(ModalEvents.yes, function() {
-                var eventId = this.getEventId();
-
-                CalendarRepository.deleteEvent(eventId)
-                    .then(function() {
-                        $('body').trigger(CalendarEvents.deleted, [eventId]);
-                        this.hide();
-                    }.bind(this))
-                    .catch(Notification.exception);
-            }.bind(this));
-
-            return modal;
-        }.bind(this));
-
         // We have to wait for the modal to finish rendering in order to ensure that
         // the data-event-title property is available to use as the modal title.
         this.getRoot().on(ModalEvents.bodyRendered, function() {
             var eventTitle = this.getBody().find(SELECTORS.ROOT).attr('data-event-title');
-            confirmPromise.then(function(modal) {
-                modal.setBody(Str.get_string('confirmeventdelete', 'core_calendar', eventTitle));
-            });
+            prepareDeleteAction(this, eventTitle);
         }.bind(this));
 
         CustomEvents.define(this.getEditButton(), [
@@ -173,6 +151,48 @@ define(['jquery', 'core/str', 'core/notification', 'core/custom_interaction_even
         }.bind(this));
     };
 
+    /**
+     * Prepares the action for the summary modal's delete action.
+     *
+     * @param {ModalEventSummary} summaryModal The summary modal instance.
+     * @param {string} eventTitle The event title.
+     */
+    function prepareDeleteAction(summaryModal, eventTitle) {
+        var deleteStrings = [
+            {
+                key: 'deleteevent',
+                component: 'calendar'
+            },
+            {
+                key: 'confirmeventdelete',
+                component: 'calendar',
+                param: eventTitle
+            }
+        ];
+        var eventId = summaryModal.getEventId();
+        var stringsPromise = Str.get_strings(deleteStrings);
+        var deletePromise = ModalFactory.create(
+            {
+                type: ModalFactory.types.SAVE_CANCEL
+            },
+            summaryModal.getDeleteButton()
+        );
+
+        $.when(stringsPromise, deletePromise).then(function(strings, deleteModal) {
+            deleteModal.setTitle(strings[0]);
+            deleteModal.setBody(strings[1]);
+            deleteModal.setSaveButtonText(strings[0]);
+            deleteModal.getRoot().on(ModalEvents.save, function() {
+                CalendarRepository.deleteEvent(eventId).then(function() {
+                    $('body').trigger(CalendarEvents.deleted, [eventId]);
+                    summaryModal.hide();
+                    return;
+                }).catch(Notification.exception);
+            });
+            return deleteModal;
+        }).fail(Notification.exception);
+    }
+
     // Automatically register with the modal registry the first time this module is imported so that you can create modals
     // of this type using the modal factory.
     if (!registered) {
index eccb0a3..f8636ef 100644 (file)
@@ -26,8 +26,9 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
 
         var SELECTORS = {
             ROOT: "[data-region='calendar']",
-            CALENDAR_NAV_LINK: "span.calendarwrapper .arrow_link",
+            CALENDAR_NAV_LINK: ".calendarwrapper .arrow_link",
             CALENDAR_MONTH_WRAPPER: ".calendarwrapper",
+            LOADING_ICON_CONTAINER: '[data-region="overlay-icon-container"]'
         };
 
         /**
@@ -41,7 +42,7 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
             root.on('click', SELECTORS.CALENDAR_NAV_LINK, function(e) {
                 var courseId = $(root).find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('courseid');
                 var link = $(e.currentTarget);
-                changeMonth(link.attr('href'), link.data('time'), courseId);
+                changeMonth(root, link.attr('href'), link.data('time'), courseId);
 
                 e.preventDefault();
             });
@@ -54,13 +55,22 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
          * @param {Number} courseid The id of the course whose events are shown
          * @return {promise}
          */
-        var refreshMonthContent = function(time, courseid) {
+        var refreshMonthContent = function(root, time, courseid) {
+            startLoading(root);
+
             return CalendarRepository.getCalendarMonthData(time, courseid)
                 .then(function(context) {
-                    return Templates.render('core_calendar/month_detailed', context);
+                    return Templates.render(root.attr('data-template'), context);
                 })
                 .then(function(html, js) {
-                    return Templates.replaceNode(SELECTORS.CALENDAR_MONTH_WRAPPER, html, js);
+                    return Templates.replaceNode(root.find(SELECTORS.CALENDAR_MONTH_WRAPPER), html, js);
+                })
+                .then(function() {
+                    $('body').trigger(CalendarEvents.viewUpdated);
+                    return;
+                })
+                .always(function() {
+                    return stopLoading(root);
                 })
                 .fail(Notification.exception);
         };
@@ -73,10 +83,12 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
          * @param {Number} courseid The id of the course whose events are shown
          * @return {promise}
          */
-        var changeMonth = function(url, time, courseid) {
-            return refreshMonthContent(time, courseid)
+        var changeMonth = function(root, url, time, courseid) {
+            return refreshMonthContent(root, time, courseid)
                 .then(function() {
-                    window.history.pushState({}, '', url);
+                    if (url.length && url !== '#') {
+                        window.history.pushState({}, '', url);
+                    }
                     return arguments;
                 })
                 .then(function() {
@@ -88,19 +100,46 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
         /**
          * Reload the current month view data.
          *
+         * @param {object} root The container element.
+         * @param {Number} courseId The course id.
          * @return {promise}
          */
-        var reloadCurrentMonth = function() {
-            var root = $(SELECTORS.ROOT),
-                courseid = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('courseid'),
-                time = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('current-time');
+        var reloadCurrentMonth = function(root, courseId) {
+            var time = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('current-time');
+
+            if (!courseId) {
+                courseId = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('courseid');
+            }
+            return refreshMonthContent(root, time, courseId);
+        };
+
+        /**
+         * Set the element state to loading.
+         *
+         * @param {object} root The container element
+         * @method startLoading
+         */
+        var startLoading = function(root) {
+            var loadingIconContainer = root.find(SELECTORS.LOADING_ICON_CONTAINER);
+
+            loadingIconContainer.removeClass('hidden');
+        };
+
+        /**
+         * Remove the loading state from the element.
+         *
+         * @param {object} root The container element
+         * @method stopLoading
+         */
+        var stopLoading = function(root) {
+            var loadingIconContainer = root.find(SELECTORS.LOADING_ICON_CONTAINER);
 
-            return refreshMonthContent(time, courseid);
+            loadingIconContainer.addClass('hidden');
         };
 
         return {
-            init: function() {
-                registerEventListeners(SELECTORS.ROOT);
+            init: function(root) {
+                registerEventListeners(root);
             },
             reloadCurrentMonth: reloadCurrentMonth,
             changeMonth: changeMonth,
index 7ba5805..f83f273 100644 (file)
@@ -47,6 +47,16 @@ class calendar_event_exporter extends event_exporter_base {
 
         $values = parent::define_other_properties();
         $values['url'] = ['type' => PARAM_URL];
+        $values['islastday'] = [
+            'type' => PARAM_BOOL,
+            'default' => false,
+        ];
+        $values['calendareventtype'] = [
+            'type' => PARAM_TEXT,
+        ];
+        $values['popupname'] = [
+            'type' => PARAM_RAW,
+        ];
 
         return $values;
     }
@@ -58,6 +68,8 @@ class calendar_event_exporter extends event_exporter_base {
      * @return array Keys are the property names, values are their values.
      */
     protected function get_other_values(renderer_base $output) {
+        global $CFG;
+
         $values = parent::get_other_values($output);
 
         $eventid = $this->event->get_id();
@@ -65,6 +77,55 @@ class calendar_event_exporter extends event_exporter_base {
         $url = new \moodle_url($this->related['daylink'], [], "event_{$eventid}");
         $values['url'] = $url->out(false);
 
+        $values['islastday'] = false;
+        $today = $this->related['type']->timestamp_to_date_array($this->related['today']);
+
+        $values['popupname'] = $this->event->get_name();
+
+        $times = $this->event->get_times();
+        if ($duration = $times->get_duration()) {
+            $enddate = $this->related['type']->timestamp_to_date_array($times->get_end_time()->getTimestamp());
+            $values['islastday'] = true;
+            $values['islastday'] = $values['islastday'] && $enddate['year'] == $today['year'];
+            $values['islastday'] = $values['islastday'] && $enddate['mon'] == $today['mon'];
+            $values['islastday'] = $values['islastday'] && $enddate['mday'] == $today['mday'];
+        }
+
+        $subscription = $this->event->get_subscription();
+        if ($subscription && !empty($subscription->get('id')) && $CFG->calendar_showicalsource) {
+            $a = (object) [
+                'name' => $values['popupname'],
+                'source' => $subscription->get('name'),
+            ];
+            $values['popupname'] = get_string('namewithsource', 'calendar', $a);
+        } else {
+            if ($values['islastday']) {
+                $startdate = $this->related['type']->timestamp_to_date_array($times->get_start_time()->getTimestamp());
+                $samedate = true;
+                $samedate = $samedate && $startdate['mon'] == $enddate['mon'];
+                $samedate = $samedate && $startdate['year'] == $enddate['year'];
+                $samedate = $samedate && $startdate['mday'] == $enddate['mday'];
+
+                if (!$samedate) {
+                    $values['popupname'] = get_string('eventendtimewrapped', 'calendar', $values['popupname']);
+                }
+            }
+        }
+
+        // Include course's shortname into the event name, if applicable.
+        $course = $this->event->get_course();
+        if ($course && $course->get('id') && $course->get('id') !== SITEID) {
+            $eventnameparams = (object) [
+                'name' => $values['popupname'],
+                'course' => format_string($course->get('shortname'), true, [
+                        'context' => $this->related['context'],
+                    ])
+            ];
+            $values['popupname'] = get_string('eventnameandcourse', 'calendar', $eventnameparams);
+        }
+
+        $values['calendareventtype'] = $this->get_calendar_event_type();
+
         return $values;
     }
 
@@ -76,7 +137,24 @@ class calendar_event_exporter extends event_exporter_base {
     protected static function define_related() {
         $related = parent::define_related();
         $related['daylink'] = \moodle_url::class;
+        $related['type'] = '\core_calendar\type_base';
+        $related['today'] = 'int';
 
         return $related;
     }
+
+    /**
+     * Return the normalised event type.
+     * Activity events are normalised to be course events.
+     *
+     * @return string
+     */
+    public function get_calendar_event_type() {
+        $type = $this->event->get_type();
+        if ($type == 'open' || $type == 'close') {
+            $type = 'course';
+        }
+
+        return $type;
+    }
 }
index 5cac152..0f5b92b 100644 (file)
@@ -39,6 +39,24 @@ use moodle_url;
  */
 class day_exporter extends exporter {
 
+    /**
+     * @var \calendar_information $calendar The calendar being displayed.
+     */
+    protected $calendar;
+
+    /**
+     * Constructor.
+     *
+     * @param \calendar_information $calendar The calendar information for the period being displayed
+     * @param mixed $data Either an stdClass or an array of values.
+     * @param array $related Related objects.
+     */
+    public function __construct(\calendar_information $calendar, $data, $related) {
+        $this->calendar = $calendar;
+
+        parent::__construct($data, $related);
+    }
+
     /**
      * Return the list of properties.
      *
@@ -69,6 +87,15 @@ class day_exporter extends exporter {
             'yday' => [
                 'type' => PARAM_INT,
             ],
+            // These are additional params.
+            'istoday' => [
+                'type' => PARAM_BOOL,
+                'default' => false,
+            ],
+            'isweekend' => [
+                'type' => PARAM_BOOL,
+                'default' => false,
+            ],
         ];
     }
 
@@ -82,13 +109,8 @@ class day_exporter extends exporter {
             'timestamp' => [
                 'type' => PARAM_INT,
             ],
-            'istoday' => [
-                'type' => PARAM_BOOL,
-                'default' => false,
-            ],
-            'isweekend' => [
-                'type' => PARAM_BOOL,
-                'default' => false,
+            'neweventtimestamp' => [
+                'type' => PARAM_INT,
             ],
             'viewdaylink' => [
                 'type' => PARAM_URL,
@@ -97,7 +119,19 @@ class day_exporter extends exporter {
             'events' => [
                 'type' => calendar_event_exporter::read_properties_definition(),
                 'multiple' => true,
-            ]
+            ],
+            'calendareventtypes' => [
+                'type' => PARAM_RAW,
+                'multiple' => true,
+            ],
+            'popovertitle' => [
+                'type' => PARAM_RAW,
+                'default' => '',
+            ],
+            'haslastdayofevent' => [
+                'type' => PARAM_BOOL,
+                'default' => false,
+            ],
         ];
     }
 
@@ -108,29 +142,66 @@ class day_exporter extends exporter {
      * @return array Keys are the property names, values are their values.
      */
     protected function get_other_values(renderer_base $output) {
+        $timestamp = $this->data[0];
+        // Need to account for user's timezone.
+        $usernow = usergetdate(time());
+        $today = new \DateTimeImmutable();
+        // The start time should use the day's date but the current
+        // time of the day (adjusted for user's timezone).
+        $neweventstarttime = $today->setTimestamp($timestamp)->setTime(
+            $usernow['hours'],
+            $usernow['minutes'],
+            $usernow['seconds']
+        );
+
         $return = [
-            'timestamp' => $this->data[0],
+            'timestamp' => $timestamp,
+            'neweventtimestamp' => $neweventstarttime->getTimestamp()
         ];
 
         $url = new moodle_url('/calendar/view.php', [
                 'view' => 'day',
-                'time' => $this->data[0],
+                'time' => $timestamp,
+                'course' => $this->calendar->course->id,
             ]);
         $return['viewdaylink'] = $url->out(false);
 
         $cache = $this->related['cache'];
-        $return['events'] = array_map(function($event) use ($cache, $output, $url) {
+        $eventexporters = array_map(function($event) use ($cache, $output, $url) {
             $context = $cache->get_context($event);
             $course = $cache->get_course($event);
             $exporter = new calendar_event_exporter($event, [
                 'context' => $context,
                 'course' => $course,
                 'daylink' => $url,
+                'type' => $this->related['type'],
+                'today' => $this->data[0],
             ]);
 
-            return $exporter->export($output);
+            return $exporter;
         }, $this->related['events']);
 
+        $return['events'] = array_map(function($exporter) use ($output) {
+            return $exporter->export($output);
+        }, $eventexporters);
+
+        if ($popovertitle = $this->get_popover_title()) {
+            $return['popovertitle'] = $popovertitle;
+        }
+
+        $return['calendareventtypes'] = array_map(function($exporter) {
+            return $exporter->get_calendar_event_type();
+        }, $eventexporters);
+        $return['calendareventtypes'] = array_values(array_unique($return['calendareventtypes']));
+
+        $return['haslastdayofevent'] = false;
+        foreach ($return['events'] as $event) {
+            if ($event->islastday) {
+                $return['haslastdayofevent'] = true;
+                break;
+            }
+        }
+
         return $return;
     }
 
@@ -146,4 +217,26 @@ class day_exporter extends exporter {
             'type' => '\core_calendar\type_base',
         ];
     }
+
+    /**
+     * Get the title for this popover.
+     *
+     * @return string
+     */
+    protected function get_popover_title() {
+        $title = null;
+
+        $userdate = userdate($this->data[0], get_string('strftimedayshort'));
+        if (count($this->related['events'])) {
+            $title = get_string('eventsfor', 'calendar', $userdate);
+        } else if ($this->data['istoday']) {
+            $title = $userdate;
+        }
+
+        if ($this->data['istoday']) {
+            $title = get_string('todayplustitle', 'calendar', $userdate);
+        }
+
+        return $title;
+    }
 }
index 9b99498..6d180aa 100644 (file)
@@ -76,7 +76,19 @@ class month_exporter extends exporter {
 
         $related['type'] = $type;
 
-        parent::__construct([], $related);
+        $data = [
+            'url' => $this->url->out(false),
+        ];
+
+        parent::__construct($data, $related);
+    }
+
+    protected static function define_properties() {
+        return [
+            'url' => [
+                'type' => PARAM_URL,
+            ],
+        ];
     }
 
     /**
@@ -106,15 +118,38 @@ class month_exporter extends exporter {
             'view' => [
                 'type' => PARAM_ALPHA,
             ],
+            'time' => [
+                'type' => PARAM_INT,
+            ],
+            'periodname' => [
+                // Note: We must use RAW here because the calendar type returns the formatted month name based on a
+                // calendar format.
+                'type' => PARAM_RAW,
+            ],
             'previousperiod' => [
                 'type' => PARAM_INT,
             ],
+            'previousperiodname' => [
+                // Note: We must use RAW here because the calendar type returns the formatted month name based on a
+                // calendar format.
+                'type' => PARAM_RAW,
+            ],
             'nextperiod' => [
                 'type' => PARAM_INT,
             ],
-            'time' => [
-                'type' => PARAM_INT,
-            ]
+            'nextperiodname' => [
+                // Note: We must use RAW here because the calendar type returns the formatted month name based on a
+                // calendar format.
+                'type' => PARAM_RAW,
+            ],
+            'larrow' => [
+                // The left arrow defined by the theme.
+                'type' => PARAM_RAW,
+            ],
+            'rarrow' => [
+                // The right arrow defined by the theme.
+                'type' => PARAM_RAW,
+            ],
         ];
     }
 
@@ -125,16 +160,24 @@ class month_exporter extends exporter {
      * @return array Keys are the property names, values are their values.
      */
     protected function get_other_values(renderer_base $output) {
+        $previousperiod = $this->get_previous_month_timestamp();
+        $nextperiod = $this->get_next_month_timestamp();
+
         return [
             'courseid' => $this->calendar->courseid,
-            'view' => 'month',
-            'previousperiod' => $this->get_previous_month_timestamp(),
-            'nextperiod' => $this->get_next_month_timestamp(),
             'filter_selector' => $this->get_course_filter_selector($output),
             'navigation' => $this->get_navigation($output),
             'weeks' => $this->get_weeks($output),
             'daynames' => $this->get_day_names($output),
-            'time' => $this->calendar->time
+            'view' => 'month',
+            'time' => $this->calendar->time,
+            'periodname' => userdate($this->calendar->time, get_string('strftimemonthyear')),
+            'previousperiod' => $previousperiod,
+            'previousperiodname' => userdate($previousperiod, get_string('strftimemonthyear')),
+            'nextperiod' => $nextperiod,
+            'nextperiodname' => userdate($nextperiod, get_string('strftimemonthyear')),
+            'larrow' => $output->larrow(),
+            'rarrow' => $output->rarrow(),
         ];
     }
 
@@ -213,13 +256,13 @@ class month_exporter extends exporter {
         $prepadding = ($firstdayno + $daysinweek - 1) % $daysinweek;
         $daysinfirstweek = $daysinweek - $prepadding;
         $days = array_slice($alldays, 0, $daysinfirstweek);
-        $week = new week_exporter($days, $prepadding, ($daysinweek - count($days) - $prepadding), $this->related);
+        $week = new week_exporter($this->calendar, $days, $prepadding, ($daysinweek - count($days) - $prepadding), $this->related);
         $weeks[] = $week->export($output);
 
         // Now chunk up the remaining day. and turn them into weeks.
         $daychunks = array_chunk(array_slice($alldays, $daysinfirstweek), $daysinweek);
         foreach ($daychunks as $days) {
-            $week = new week_exporter($days, 0, ($daysinweek - count($days)), $this->related);
+            $week = new week_exporter($this->calendar, $days, 0, ($daysinweek - count($days)), $this->related);
             $weeks[] = $week->export($output);
         }
 
@@ -279,7 +322,7 @@ class month_exporter extends exporter {
      */
     protected function get_next_month_timestamp() {
         $date = $this->related['type']->timestamp_to_date_array($this->calendar->time);
-        $month = calendar_sub_month($date['mon'], $date['year']);
+        $month = calendar_add_month($date['mon'], $date['year']);
         $monthtime = $this->related['type']->convert_to_gregorian($month[1], $month[0], 1);
 
         return make_timestamp($monthtime['year'], $monthtime['month'], $monthtime['day'], $monthtime['hour'], $monthtime['minute']);
diff --git a/calendar/classes/external/week_day_exporter.php b/calendar/classes/external/week_day_exporter.php
new file mode 100644 (file)
index 0000000..67442f5
--- /dev/null
@@ -0,0 +1,180 @@
+<?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/>.
+
+/**
+ * Contains event class for displaying the day on month view.
+ *
+ * @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\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+use renderer_base;
+use moodle_url;
+
+/**
+ * Class for displaying the day on month view.
+ *
+ * @package   core_calendar
+ * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class week_day_exporter extends day_exporter {
+
+    /**
+     * Return the list of additional properties.
+     *
+     * @return array
+     */
+    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;
+    }
+
+    /**
+     * Get the additional values to inject while exporting.
+     *
+     * @param renderer_base $output The renderer.
+     * @return array Keys are the property names, values are their values.
+     */
+    protected function get_other_values(renderer_base $output) {
+        $timestamp = $this->data[0];
+        // Need to account for user's timezone.
+        $usernow = usergetdate(time());
+        $today = new \DateTimeImmutable();
+        // The start time should use the day's date but the current
+        // time of the day (adjusted for user's timezone).
+        $neweventstarttime = $today->setTimestamp($timestamp)->setTime(
+            $usernow['hours'],
+            $usernow['minutes'],
+            $usernow['seconds']
+        );
+
+        $return = [
+            'timestamp' => $timestamp,
+            'neweventtimestamp' => $neweventstarttime->getTimestamp()
+        ];
+
+        $url = new moodle_url('/calendar/view.php', [
+                'view' => 'day',
+                'time' => $timestamp,
+                'course' => $this->calendar->course->id,
+            ]);
+        $return['viewdaylink'] = $url->out(false);
+
+        $cache = $this->related['cache'];
+        $eventexporters = array_map(function($event) use ($cache, $output, $url) {
+            $context = $cache->get_context($event);
+            $course = $cache->get_course($event);
+            $exporter = new calendar_event_exporter($event, [
+                'context' => $context,
+                'course' => $course,
+                'daylink' => $url,
+                'type' => $this->related['type'],
+                'today' => $this->data[0],
+            ]);
+
+            return $exporter;
+        }, $this->related['events']);
+
+        $return['events'] = array_map(function($exporter) use ($output) {
+            return $exporter->export($output);
+        }, $eventexporters);
+
+        if ($popovertitle = $this->get_popover_title()) {
+            $return['popovertitle'] = $popovertitle;
+        }
+
+        $return['calendareventtypes'] = array_map(function($exporter) {
+            return $exporter->get_calendar_event_type();
+        }, $eventexporters);
+        $return['calendareventtypes'] = array_values(array_unique($return['calendareventtypes']));
+
+        $return['haslastdayofevent'] = false;
+        foreach ($return['events'] as $event) {
+            if ($event->islastday) {
+                $return['haslastdayofevent'] = true;
+                break;
+            }
+        }
+
+        return $return;
+    }
+
+    /**
+     * Returns a list of objects that are related.
+     *
+     * @return array
+     */
+    protected static function define_related() {
+        return [
+            'events' => '\core_calendar\local\event\entities\event_interface[]',
+            'cache' => '\core_calendar\external\events_related_objects_cache',
+            'type' => '\core_calendar\type_base',
+        ];
+    }
+
+    /**
+     * Get the title for this popover.
+     *
+     * @return string
+     */
+    protected function get_popover_title() {
+        $title = null;
+
+        $userdate = userdate($this->data[0], get_string('strftimedayshort'));
+        if (count($this->related['events'])) {
+            $title = get_string('eventsfor', 'calendar', $userdate);
+        } else if ($this->data['istoday']) {
+            $title = $userdate;
+        }
+
+        if ($this->data['istoday']) {
+            $title = get_string('todayplustitle', 'calendar', $userdate);
+        }
+
+        return $title;
+    }
+}
index afc92b2..ff75a35 100644 (file)
@@ -53,18 +53,25 @@ class week_exporter extends exporter {
      */
     protected $postpadding = 0;
 
+    /**
+     * @var \calendar_information $calendar The calendar being displayed.
+     */
+    protected $calendar;
+
     /**
      * Constructor.
      *
+     * @param \calendar_information $calendar The calendar information for the period being displayed
      * @param mixed $days An array of day_exporter objects.
      * @param int $prepadding The number of pre-padding days at the start of the week.
      * @param int $postpadding The number of post-padding days at the start of the week.
      * @param array $related Related objects.
      */
-    public function __construct($days, $prepadding, $postpadding, $related) {
+    public function __construct(\calendar_information $calendar, $days, $prepadding, $postpadding, $related) {
         $this->days = $days;
         $this->prepadding = $prepadding;
         $this->postpadding = $postpadding;
+        $this->calendar = $calendar;
 
         parent::__construct([], $related);
     }
@@ -85,7 +92,7 @@ class week_exporter extends exporter {
                 'multiple' => true,
             ],
             'days' => [
-                'type' => day_exporter::read_properties_definition(),
+                'type' => week_day_exporter::read_properties_definition(),
                 'multiple' => true,
             ],
         ];
@@ -98,6 +105,7 @@ class week_exporter extends exporter {
      * @return array Keys are the property names, values are their values.
      */
     protected function get_other_values(renderer_base $output) {
+        global $CFG;
         $return = [
             'prepadding' => [],
             'postpadding' => [],
@@ -112,6 +120,14 @@ class week_exporter extends exporter {
         }
 
         $return['days'] = [];
+        $today = $this->related['type']->timestamp_to_date_array(time());
+
+        $weekend = CALENDAR_DEFAULT_WEEKEND;
+        if (isset($CFG->calendar_weekend)) {
+            $weekend = intval($CFG->calendar_weekend);
+        }
+        $numberofdaysinweek = $this->related['type']->get_num_weekdays();
+
         foreach ($this->days as $daydata) {
             $events = [];
             foreach ($this->related['events'] as $event) {
@@ -132,7 +148,14 @@ class week_exporter extends exporter {
                 $events[] = $event;
             }
 
-            $day = new day_exporter($daydata, [
+            $istoday = true;
+            $istoday = $istoday && $today['year'] == $daydata['year'];
+            $istoday = $istoday && $today['yday'] == $daydata['yday'];
+            $daydata['istoday'] = $istoday;
+
+            $daydata['isweekend'] = !!($weekend & (1 << ($daydata['wday'] % $numberofdaysinweek)));
+
+            $day = new week_day_exporter($this->calendar, $daydata, [
                 'events' => $events,
                 'cache' => $this->related['cache'],
                 'type' => $this->related['type'],
index 3e8ed78..8459913 100644 (file)
@@ -41,9 +41,13 @@ class create extends \moodleform {
         global $PAGE;
 
         $mform = $this->_form;
-        $haserror = !empty($this->_customdata['haserror']);
+        $starttime = isset($this->_customdata['starttime']) ? $this->_customdata['starttime'] : 0;
         $eventtypes = calendar_get_all_allowed_types();
 
+        if (empty($eventtypes)) {
+            print_error('nopermissiontoupdatecalendar');
+        }
+
         $mform->setDisableShortforms();
         $mform->disable_form_change_checker();
 
@@ -58,7 +62,7 @@ class create extends \moodleform {
         $mform->setType('name', PARAM_TEXT);
 
         // Event time start field.
-        $mform->addElement('date_time_selector', 'timestart', get_string('date'));
+        $mform->addElement('date_time_selector', 'timestart', get_string('date'), ['defaulttime' => $starttime]);
 
         // Add the select elements for the available event types.
         $this->add_event_type_elements($mform, $eventtypes);
@@ -77,8 +81,7 @@ class create extends \moodleform {
         $this->add_event_repeat_elements($mform);
 
         // Add the javascript required to enhance this mform.
-        // Including the show/hide of advanced elements and the display of the correct select elements for event types.
-        $PAGE->requires->js_call_amd('core_calendar/event_form', 'init', [$mform->getAttribute('id'), $haserror]);
+        $PAGE->requires->js_call_amd('core_calendar/event_form', 'init', [$mform->getAttribute('id')]);
     }
 
     /**
@@ -93,6 +96,12 @@ class create 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 (isset($data[$coursekey]) && $data[$coursekey] > 0) {
             if ($course = $DB->get_record('course', ['id' => $data[$coursekey]])) {
@@ -203,7 +212,7 @@ class create extends \moodleform {
             }
 
             $mform->addElement('select', 'courseid', get_string('course'), $courseoptions);
-            $mform->disabledIf('courseid', 'eventtype', 'noteq', 'course');
+            $mform->hideIf('courseid', 'eventtype', 'noteq', 'course');
         }
 
         if (isset($eventtypes['group'])) {
@@ -214,7 +223,7 @@ class create extends \moodleform {
             }
 
             $mform->addElement('select', 'groupcourseid', get_string('course'), $courseoptions);
-            $mform->disabledIf('groupcourseid', 'eventtype', 'noteq', 'group');
+            $mform->hideIf('groupcourseid', 'eventtype', 'noteq', 'group');
 
             $groupoptions = [];
             foreach ($eventtypes['group'] as $group) {
@@ -226,7 +235,7 @@ class create extends \moodleform {
             }
 
             $mform->addElement('select', 'groupid', get_string('group'), $groupoptions);
-            $mform->disabledIf('groupid', 'eventtype', 'noteq', 'group');
+            $mform->hideIf('groupid', 'eventtype', 'noteq', 'group');
         }
     }
 
index bedcccc..b0bb691 100644 (file)
@@ -90,7 +90,11 @@ class create_update_form_mapper implements create_update_form_mapper_interface {
 
         // Default course id if none is set.
         if (!isset($properties->courseid)) {
-            $properties->courseid = 0;
+            if ($properties->eventtype === 'site') {
+                $properties->courseid = SITEID;
+            } else {
+                $properties->courseid = 0;
+            }
         }
 
         // Decode the form fields back into valid event property.
index 30cbaa4..713dc0c 100644 (file)
@@ -1049,7 +1049,7 @@ class calendar_information {
     public function add_sidecalendar_blocks(core_calendar_renderer $renderer, $showfilters=false, $view=null) {
         if ($showfilters) {
             $filters = new block_contents();
-            $filters->content = $renderer->fake_block_filters($this->courseid, 0, 0, 0, $view, $this->courses);
+            $filters->content = $renderer->event_filter();
             $filters->footer = '';
             $filters->title = get_string('eventskey', 'calendar');
             $renderer->add_pretend_calendar_block($filters, BLOCK_POS_RIGHT);
@@ -1478,7 +1478,7 @@ function calendar_get_mini($courses, $groups, $users, $calmonth = false, $calyea
 
             // Class and cell content.
             if (isset($typesbyday[$day]['startglobal'])) {
-                $class .= ' calendar_event_global';
+                $class .= ' calendar_event_site';
             } else if (isset($typesbyday[$day]['startcourse'])) {
                 $class .= ' calendar_event_course';
             } else if (isset($typesbyday[$day]['startgroup'])) {
@@ -1981,88 +1981,6 @@ function calendar_top_controls($type, $data) {
     return $content;
 }
 
-/**
- * Formats a filter control element.
- *
- * @param moodle_url $url of the filter
- * @param int $type constant defining the type filter
- * @return string html content of the element
- */
-function calendar_filter_controls_element(moodle_url $url, $type) {
-    global $OUTPUT;
-
-    switch ($type) {
-        case CALENDAR_EVENT_GLOBAL:
-            $typeforhumans = 'global';
-            $class = 'calendar_event_global';
-            break;
-        case CALENDAR_EVENT_COURSE:
-            $typeforhumans = 'course';
-            $class = 'calendar_event_course';
-            break;
-        case CALENDAR_EVENT_GROUP:
-            $typeforhumans = 'groups';
-            $class = 'calendar_event_group';
-            break;
-        case CALENDAR_EVENT_USER:
-            $typeforhumans = 'user';
-            $class = 'calendar_event_user';
-            break;
-    }
-
-    if (calendar_show_event_type($type)) {
-        $icon = $OUTPUT->pix_icon('t/hide', get_string('hide'));
-        $str = get_string('hide' . $typeforhumans . 'events', 'calendar');
-    } else {
-        $icon = $OUTPUT->pix_icon('t/show', get_string('show'));
-        $str = get_string('show' . $typeforhumans . 'events', 'calendar');
-    }
-    $content = \html_writer::start_tag('li', array('class' => 'calendar_event'));
-    $content .= \html_writer::start_tag('a', array('href' => $url, 'rel' => 'nofollow'));
-    $content .= \html_writer::tag('span', $icon, array('class' => $class));
-    $content .= \html_writer::tag('span', $str, array('class' => 'eventname'));
-    $content .= \html_writer::end_tag('a');
-    $content .= \html_writer::end_tag('li');
-
-    return $content;
-}
-
-/**
- * Get the controls filter for calendar.
- *
- * Filter is used to hide calendar info from the display page.
- *
-
- * @param moodle_url $returnurl return-url for filter controls
- * @return string $content return filter controls in html
- */
-function calendar_filter_controls(moodle_url $returnurl) {
-    $groupevents = true;
-
-    $seturl = new \moodle_url('/calendar/set.php', array('return' => base64_encode($returnurl->out_as_local_url(false)),
-        'sesskey' => sesskey()));
-    $content = \html_writer::start_tag('ul');
-
-    $seturl->param('var', 'showglobal');
-    $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_GLOBAL);
-
-    $seturl->param('var', 'showcourses');
-    $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_COURSE);
-
-    if (isloggedin() && !isguestuser()) {
-        if ($groupevents) {
-            // This course MIGHT have group events defined, so show the filter.
-            $seturl->param('var', 'showgroups');
-            $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_GROUP);
-        }
-        $seturl->param('var', 'showuser');
-        $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_USER);
-    }
-    $content .= \html_writer::end_tag('ul');
-
-    return $content;
-}
-
 /**
  * Return the representation day.
  *
@@ -2175,6 +2093,7 @@ function calendar_get_link_previous($text, $linkbase, $d, $m, $y, $accesshide =
 
     $attrs = [
         'data-time' => calendar_get_timestamp($d, $m, $y, $time),
+        'data-drop-zone' => 'nav-link',
     ];
 
     return link_arrow_left($text, $href->out(false), $accesshide, 'previous', $attrs);
@@ -2202,6 +2121,7 @@ function calendar_get_link_next($text, $linkbase, $d, $m, $y, $accesshide = fals
 
     $attrs = [
         'data-time' => calendar_get_timestamp($d, $m, $y, $time),
+        'data-drop-zone' => 'nav-link',
     ];
 
     return link_arrow_right($text, $href->out(false), $accesshide, 'next', $attrs);
@@ -3462,6 +3382,11 @@ function calendar_get_view(\calendar_information $calendar, $view) {
         $monthdays = $type->get_num_days_in_month($date['year'], $date['mon']);
         $tend = $tstart + ($monthdays * DAYSECS) - 1;
         $selectortitle = get_string('detailedmonthviewfor', 'calendar');
+        if ($view === 'mini') {
+            $template = 'core_calendar/calendar_mini';
+        } else {
+            $template = 'core_calendar/calendar_month';
+        }
     }
 
     list($userparam, $groupparam, $courseparam) = array_map(function($param) {
@@ -3516,7 +3441,6 @@ function calendar_get_view(\calendar_information $calendar, $view) {
 
     $month = new \core_calendar\external\month_exporter($calendar, $type, $related);
     $data = $month->export($renderer);
-    $template = 'core_calendar/month_detailed';
 
     return [$data, $template];
 }
@@ -3534,6 +3458,8 @@ function calendar_output_fragment_event_form($args) {
     $html = '';
     $data = null;
     $eventid = isset($args['eventid']) ? clean_param($args['eventid'], PARAM_INT) : null;
+    $starttime = isset($args['starttime']) ? clean_param($args['starttime'], PARAM_INT) : null;
+    $courseid = isset($args['courseid']) ? clean_param($args['courseid'], PARAM_INT) : null;
     $event = null;
     $hasformdata = isset($args['formdata']) && !empty($args['formdata']);
     $formoptions = [];
@@ -3542,8 +3468,8 @@ function calendar_output_fragment_event_form($args) {
         parse_str(clean_param($args['formdata'], PARAM_TEXT), $data);
     }
 
-    if (isset($args['haserror'])) {
-        $formoptions['haserror'] = clean_param($args['haserror'], PARAM_BOOL);
+    if ($starttime) {
+        $formoptions['starttime'] = $starttime;
     }
 
     if (is_null($eventid)) {
@@ -3556,6 +3482,12 @@ function calendar_output_fragment_event_form($args) {
             true,
             $data
         );
+        if ($courseid != SITEID) {
+            $data['eventtype'] = 'course';
+            $data['courseid'] = $courseid;
+            $data['groupcourseid'] = $courseid;
+        }
+        $mform->set_data($data);
     } else {
         $event = calendar_event::load($eventid);
         $event->count_repeats();
@@ -3640,3 +3572,25 @@ function calendar_get_footer_options($calendar) {
 
     return [$data, $template];
 }
+
+/**
+ * Get the list of potential calendar filter types as a type => name
+ * combination.
+ *
+ * @return array
+ */
+function calendar_get_filter_types() {
+    $types = [
+        'site',
+        'course',
+        'group',
+        'user',
+    ];
+
+    return array_map(function($type) {
+        return [
+            'type' => $type,
+            'name' => get_string("eventtype{$type}", "calendar"),
+        ];
+    }, $types);
+}
index 6e7cab8..dc597ab 100644 (file)
@@ -50,24 +50,6 @@ class core_calendar_renderer extends plugin_renderer_base {
         return html_writer::end_tag('div');
     }
 
-    /**
-     * Produces the content for the filters block (pretend block)
-     *
-     * @param int $courseid
-     * @param int $day
-     * @param int $month
-     * @param int $year
-     * @param int $view
-     * @param int $courses
-     * @return string
-     */
-    public function fake_block_filters($courseid, $day, $month, $year, $view, $courses) {
-        $returnurl = $this->page->url;
-        $returnurl->param('course', $courseid);
-        return html_writer::tag('div', calendar_filter_controls($returnurl),
-            array('class' => 'calendar_filters filters'));
-    }
-
     /**
      * Produces the content for the three months block (pretend block)
      *
@@ -281,7 +263,7 @@ class core_calendar_renderer extends plugin_renderer_base {
             $output .= html_writer::tag('div', $source, array('class' => 'subscription'));
         }
         if (!empty($event->courselink)) {
-            $output .= html_writer::tag('div', $event->courselink, array('class' => 'course'));
+            $output .= html_writer::tag('div', $event->courselink);
         }
         if (!empty($event->time)) {
             $output .= html_writer::tag('span', $event->time, array('class' => 'date pull-xs-right m-r-1'));
@@ -291,7 +273,8 @@ class core_calendar_renderer extends plugin_renderer_base {
         }
 
         if (!empty($event->actionurl)) {
-            $output .= html_writer::tag('div', html_writer::link(new moodle_url($event->actionurl), $event->actionname));
+            $actionlink = html_writer::link(new moodle_url($event->actionurl), $event->actionname);
+            $output .= html_writer::tag('div', $actionlink, ['class' => 'action']);
         }
 
         $output .= $this->output->box_end();
@@ -388,14 +371,15 @@ class core_calendar_renderer extends plugin_renderer_base {
         }
         $courseurl = new moodle_url($returnurl);
         $courseurl->remove_params('course');
-        $select = new single_select($courseurl, 'course', $courseoptions, $selected, null);
-        $select->class = 'cal_courses_flt m-r-1';
-        if ($label !== null) {
-            $select->set_label($label);
-        } else {
-            $select->set_label(get_string('listofcourses'), array('class' => 'accesshide'));
+
+        if ($label === null) {
+            $label = get_string('listofcourses');
         }
-        return $this->output->render($select);
+
+        $select = html_writer::label($label, 'course', false, ['class' => 'm-r-1']);
+        $select .= html_writer::select($courseoptions, 'course', $selected, false, ['class' => 'cal_courses_flt']);
+
+        return $select;
     }
 
     /**
@@ -500,4 +484,16 @@ class core_calendar_renderer extends plugin_renderer_base {
         $html .= html_writer::end_tag('form');
         return $html;
     }
+
+    /**
+     * Render the event filter region.
+     *
+     * @return  string
+     */
+    public function event_filter() {
+        $data = [
+            'eventtypes' => calendar_get_filter_types(),
+        ];
+        return $this->render_from_template('core_calendar/event_filter', $data);
+    }
 }
diff --git a/calendar/set.php b/calendar/set.php
deleted file mode 100644 (file)
index c69de93..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Sets the events filter for the calendar view.
- *
- * @package   core_calendar
- * @copyright 2003 Jon Papaioannou (pj@moodle.org)
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-require_once('../config.php');
-require_once($CFG->dirroot.'/calendar/lib.php');
-
-$var = required_param('var', PARAM_ALPHA);
-$return = clean_param(base64_decode(required_param('return', PARAM_RAW)), PARAM_LOCALURL);
-$courseid = optional_param('id', -1, PARAM_INT);
-if ($courseid != -1) {
-    $return = new moodle_url($return, array('course' => $courseid));
-} else {
-    $return = new moodle_url($return);
-}
-
-if (!confirm_sesskey()) {
-    // Do not call require_sesskey() since this page may be accessed without session (for example by bots).
-    redirect($return);
-}
-
-$url = new moodle_url('/calendar/set.php', array('return'=>base64_encode($return->out_as_local_url(false)), 'course' => $courseid, 'var'=>$var, 'sesskey'=>sesskey()));
-$PAGE->set_url($url);
-$PAGE->set_context(context_system::instance());
-
-switch($var) {
-    case 'showgroups':
-        calendar_set_event_type_display(CALENDAR_EVENT_GROUP);
-        break;
-    case 'showcourses':
-        calendar_set_event_type_display(CALENDAR_EVENT_COURSE);
-        break;
-    case 'showglobal':
-        calendar_set_event_type_display(CALENDAR_EVENT_GLOBAL);
-        break;
-    case 'showuser':
-        calendar_set_event_type_display(CALENDAR_EVENT_USER);
-        break;
-}
-
-redirect($return);
diff --git a/calendar/templates/calendar_mini.mustache b/calendar/templates/calendar_mini.mustache
new file mode 100644 (file)
index 0000000..6e28cbf
--- /dev/null
@@ -0,0 +1,45 @@
+{{!
+    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 calendar/calendar_mini
+
+    Calendar month view for blocks.
+
+    The purpose of this template is to render the month view for blocks.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<div{{!
+  }} id="calendar-month-{{uniqid}}-{{time}}" {{!
+    }} data-template="core_calendar/month_mini" {{!
+    }} data-includenavigation="{{#includenavigation}}true{{/includenavigation}}{{^includenavigation}}false{{/includenavigation}}"{{!
+    }}>
+    {{> core_calendar/month_mini}}
+</div>
+{{#js}}
+require(['jquery', 'core_calendar/calendar_mini'], function($, CalendarMini) {
+    CalendarMini.init($("#calendar-month-{{uniqid}}-{{time}}"));
+});
+{{/js}}
diff --git a/calendar/templates/calendar_month.mustache b/calendar/templates/calendar_month.mustache
new file mode 100644 (file)
index 0000000..9e5866b
--- /dev/null
@@ -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 calendar/calendar_month
+
+    Calendar month view for blcoks.
+
+    The purpose of this template is to render the main, detailed monthly view.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<div id="calendar-month-{{uniqid}}" data-template="core_calendar/month_detailed">
+    {{> core_calendar/month_detailed}}
+</div>
+{{#js}}
+require(['jquery', 'core_calendar/calendar'], function($, Calendar) {
+    Calendar.init($("#calendar-month-{{uniqid}}"));
+});
+{{/js}}
diff --git a/calendar/templates/event_filter.mustache b/calendar/templates/event_filter.mustache
new file mode 100644 (file)
index 0000000..4e6e789
--- /dev/null
@@ -0,0 +1,45 @@
+{{!
+    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 calendar/event_filter
+
+    Filter to select which event types displayed on this page.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<div id="event-filter-{{uniqid}}" class="calendar_filters m-t-2">
+    <ul>
+        {{#eventtypes}}
+            <li>
+                {{> core_calendar/event_filter_key }}
+            </li>
+        {{/eventtypes}}
+    </ul>
+</div>
+{{#js}}
+require(['jquery', 'core_calendar/calendar_filter'], function($, CalendarFilter) {
+    CalendarFilter.init($("#event-filter-{{uniqid}}"));
+});
+{{/js}}
diff --git a/calendar/templates/event_filter_key.mustache b/calendar/templates/event_filter_key.mustache
new file mode 100644 (file)
index 0000000..be1154b
--- /dev/null
@@ -0,0 +1,45 @@
+{{!
+    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 calendar/event_filter_key
+
+    Key item for the event filter.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<a href="#" data-action="filter-event-type" data-eventtype="{{type}}" data-eventtype-hidden="{{hidden}}">
+    {{^hidden}}
+        <span class="calendar_event_{{type}}">
+            {{#pix}}i/hide, core, {{#str}}hideeventtype, calendar, {{name}}{{/str}}{{/pix}}
+        </span>
+        <span class="eventname">{{#str}}hideeventtype, calendar, {{name}}{{/str}}</span>
+    {{/hidden}}
+    {{#hidden}}
+        <span class="calendar_event_{{type}}">
+            {{#pix}}i/show, core, {{#str}}showeventtype, calendar, {{name}}{{/str}}{{/pix}}
+        </span>
+        <span class="eventname">{{#str}}showeventtype, calendar, {{name}}{{/str}}</span>
+    {{/hidden}}
+</a>