Merge branch 'MDL-60025-master' of git://github.com/jleyva/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 11 Sep 2017 21:50:36 +0000 (23:50 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 11 Sep 2017 21:50:36 +0000 (23:50 +0200)
229 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/mobile/lang/en/tool_mobile.php
analytics/classes/local/analyser/base.php
analytics/classes/model.php
backup/moodle2/restore_stepslib.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/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/event_form.min.js
calendar/amd/build/events.min.js
calendar/amd/build/modal_event_form.min.js
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/event_form.js
calendar/amd/src/events.js
calendar/amd/src/modal_event_form.js
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/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 [moved from lib/templates/modal_confirm.mustache with 58% similarity]
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/tests/behat/behat_calendar.php
calendar/tests/behat/calendar.feature
calendar/view.php
course/classes/search/mycourse.php
course/lib.php
course/recent_form.php
course/tests/courselib_test.php
enrol/externallib.php
enrol/ldap/lang/en/enrol_ldap.php
enrol/meta/tests/behat/enrol_meta.feature
enrol/self/tests/behat/self_enrolment.feature
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/hub.php
lang/en/moodle.php
lang/en/repository.php
lang/en/webservice.php
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/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/task/manager.php
lib/coursecatlib.php
lib/db/access.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/filestorage/file_system.php
lib/filestorage/file_system_filedir.php
lib/mlbackend/php/classes/processor.php
lib/mlbackend/php/lang/en/mlbackend_php.php
lib/navigationlib.php
lib/templates/loginform.mustache
lib/templates/url_select.mustache
lib/tests/adhoc_task_test.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/classes/search/activity.php
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/tests/behat/allow_another_attempt.feature
mod/assign/upgrade.txt
mod/book/classes/search/activity.php
mod/book/classes/search/chapter.php
mod/book/lang/en/book.php
mod/chat/classes/search/activity.php
mod/chat/lang/en/chat.php
mod/choice/classes/search/activity.php
mod/choice/lang/en/choice.php
mod/data/classes/search/activity.php
mod/data/lang/en/data.php
mod/feedback/classes/search/activity.php
mod/feedback/lang/en/feedback.php
mod/feedback/show_nonrespondents.php
mod/folder/classes/search/activity.php
mod/folder/lang/en/folder.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/search/activity.php
mod/glossary/classes/search/entry.php
mod/glossary/lang/en/glossary.php
mod/imscp/classes/search/activity.php
mod/imscp/lang/en/imscp.php
mod/label/classes/search/activity.php
mod/label/lang/en/label.php
mod/lesson/classes/search/activity.php
mod/lesson/lang/en/lesson.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/search/activity.php
mod/page/lang/en/page.php
mod/quiz/classes/search/activity.php
mod/quiz/lang/en/quiz.php
mod/resource/classes/search/activity.php
mod/resource/lang/en/resource.php
mod/resource/tests/search_test.php
mod/scorm/classes/search/activity.php
mod/scorm/lang/en/scorm.php
mod/survey/classes/search/activity.php
mod/survey/lang/en/survey.php
mod/url/classes/search/activity.php
mod/url/lang/en/url.php
mod/wiki/classes/search/activity.php
mod/wiki/classes/search/collaborative_page.php
mod/wiki/lang/en/wiki.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
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/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/templates/core_calendar/minicalendar_day_link.mustache
theme/bootstrapbase/less/moodle/calendar.less
theme/bootstrapbase/less/moodle/forms.less
theme/bootstrapbase/style/moodle.css
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/amd/build/status_field.min.js
user/amd/src/status_field.js
user/classes/search/user.php
user/externallib.php
user/index.php
user/lib.php
user/messageselect.php
user/tests/behat/edit_user_enrolment.feature
user/tests/userlib_test.php
version.php
webservice/lib.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 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';
index b179dcb..ab2f52e 100644 (file)
@@ -469,8 +469,8 @@ abstract class base {
             return $result;
         }
 
-        // Add target metadata.
-        $this->add_target_metadata($data);
+        // Add extra metadata.
+        $this->add_model_metadata($data);
 
         // Write all calculated data to a file.
         $file = $dataset->store($data);
@@ -646,21 +646,25 @@ abstract class base {
      * @param array $data
      * @return void
      */
-    protected function add_target_metadata(&$data) {
-        $data[0][] = 'targetcolumn';
-        $data[1][] = $this->analysabletarget->get_id();
+    protected function add_model_metadata(&$data) {
+        global $CFG;
+
+        $metadata = array(
+            'moodleversion' => $CFG->version,
+            'targetcolumn' => $this->analysabletarget->get_id()
+        );
         if ($this->analysabletarget->is_linear()) {
-            $data[0][] = 'targettype';
-            $data[1][] = 'linear';
-            $data[0][] = 'targetmin';
-            $data[1][] = $this->analysabletarget::get_min_value();
-            $data[0][] = 'targetmax';
-            $data[1][] = $this->analysabletarget::get_max_value();
+            $metadata['targettype'] = 'linear';
+            $metadata['targetmin'] = $this->analysabletarget::get_min_value();
+            $metadata['targetmax'] = $this->analysabletarget::get_max_value();
         } else {
-            $data[0][] = 'targettype';
-            $data[1][] = 'discrete';
-            $data[0][] = 'targetclasses';
-            $data[1][] = json_encode($this->analysabletarget::get_classes());
+            $metadata['targettype'] = 'discrete';
+            $metadata['targetclasses'] = json_encode($this->analysabletarget::get_classes());
+        }
+
+        foreach ($metadata as $varname => $value) {
+            $data[0][] = $varname;
+            $data[1][] = $value;
         }
     }
 }
index 568dcbf..8120dd6 100644 (file)
@@ -443,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;
@@ -971,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;
 
@@ -1375,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 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 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 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 9d29445..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
index d90e7c8..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 f29703f..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/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 cb96bd8..301ef42 100644 (file)
Binary files a/calendar/amd/build/view_manager.min.js and b/calendar/amd/build/view_manager.min.js differ
index ea10975..2958c83 100644 (file)
@@ -64,7 +64,9 @@ define([
         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']"
+        VIEW_DAY_LINK: "[data-action='view-day-link']",
+        CALENDAR_MONTH_WRAPPER: ".calendarwrapper",
+        COURSE_SELECTOR: 'select[name="course"]'
     };
 
     /**
@@ -140,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
                 }
             };
 
@@ -254,7 +258,8 @@ 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(root);
@@ -283,7 +288,7 @@ define([
                 modal.setEventId(eventId);
                 modal.show();
             });
-
+            modal.setCourseId(courseId);
             return;
         });
     };
@@ -291,9 +296,7 @@ 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_ITEM, function(e) {
             e.preventDefault();
@@ -313,6 +316,17 @@ define([
             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);
 
@@ -334,9 +348,11 @@ define([
     };
 
     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);
+        }
+    };
+});
index e6abfd8..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
@@ -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 cee039a..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"]',
     };
@@ -67,16 +64,46 @@ define([
         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.
      *
@@ -154,7 +181,6 @@ define([
      */
     ModalEventForm.prototype.disableButtons = function() {
         this.saveButton.prop('disabled', true);
-        this.moreLessButton.prop('disabled', true);
     };
 
     /**
@@ -164,53 +190,6 @@ define([
      */
     ModalEventForm.prototype.enableButtons = function() {
         this.saveButton.prop('disabled', false);
-        this.moreLessButton.prop('disabled', false);
-    };
-
-    /**
-     * Set the more/less button in the footer to the "more"
-     * state.
-     *
-     * @method setMoreButton
-     */
-    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));
-    };
-
-    /**
-     * Set the more/less button in the footer to the "less"
-     * state.
-     *
-     * @method setLessButton
-     */
-    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));
-    };
-
-    /**
-     * Toggle the more/less button in the footer from the current
-     * state to it's opposite state.
-     *
-     * @method toggleMoreLessButton
-     */
-    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();
-        }
     };
 
     /**
@@ -257,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;
         }
@@ -279,12 +257,14 @@ define([
             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);
@@ -381,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();
@@ -434,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/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 836b011..f8636ef 100644 (file)
@@ -42,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();
             });
@@ -55,17 +55,19 @@ 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 root = $(SELECTORS.ROOT);
-
+        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);
@@ -81,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() {
@@ -96,13 +100,17 @@ 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(root) {
-            var 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');
 
-            return refreshMonthContent(time, courseid);
+            if (!courseId) {
+                courseId = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('courseid');
+            }
+            return refreshMonthContent(root, time, courseId);
         };
 
         /**
@@ -130,8 +138,8 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
         };
 
         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 4a713bc..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,
+            ],
         ];
     }
 
@@ -85,14 +112,6 @@ class day_exporter extends exporter {
             'neweventtimestamp' => [
                 'type' => PARAM_INT,
             ],
-            'istoday' => [
-                'type' => PARAM_BOOL,
-                'default' => false,
-            ],
-            'isweekend' => [
-                'type' => PARAM_BOOL,
-                'default' => false,
-            ],
             'viewdaylink' => [
                 'type' => PARAM_URL,
                 'optional' => true,
@@ -100,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,
+            ],
         ];
     }
 
@@ -131,22 +162,46 @@ class day_exporter extends exporter {
         $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'];
-        $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;
     }
 
@@ -162,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 e6c025f..8459913 100644 (file)
@@ -41,7 +41,6 @@ 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();
 
@@ -82,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')]);
     }
 
     /**
@@ -214,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'])) {
@@ -225,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) {
@@ -237,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 3ad809b..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.
  *
@@ -3464,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) {
@@ -3518,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];
 }
@@ -3537,6 +3459,7 @@ function calendar_output_fragment_event_form($args) {
     $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 = [];
@@ -3545,10 +3468,6 @@ 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;
     }
@@ -3563,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();
@@ -3647,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 c5b8531..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 = '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}}
similarity index 58%
rename from lib/templates/modal_confirm.mustache
rename to calendar/templates/calendar_month.mustache
index 549a3c6..9e5866b 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template core/modal_confirm
+    @template calendar/calendar_month
 
-    Moodle template for a confirmation modal.
+    Calendar month view for blcoks.
 
-    The purpose of this template is to render a modal.
+    The purpose of this template is to render the main, detailed monthly view.
 
     Classes required for JS:
     * none
     Data attributes required for JS:
     * none
 
-    Context variables required for this template:
-    * body HTML content for the boday
-
     Example context (json):
     {
     }
 }}
-
-{{< core/modal }}
-    {{$title}}{{#str}} confirm {{/str}}{{/title}}
-    {{$body}}{{#str}} areyousure {{/str}}{{/body}}
-    {{$footer}}
-        <button type="button" class="btn btn-primary" data-action="yes">{{#str}} yes {{/str}}</button>
-        <button type="button" class="btn btn-secondary" data-action="no">{{#str}} no {{/str}}</button>
-    {{/footer}}
-{{/ core/modal }}
+<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>
index 845331a..583f500 100644 (file)
@@ -32,5 +32,8 @@
     {{/candelete}}
     <button type="button" class="btn btn-primary" data-action="edit">{{#str}} edit {{/str}}</button>
 {{/canedit}}
+{{#isactionevent}}
+    <a href="{{url}}">{{#str}} gotoactivity, core_calendar {{/str}}</a>
+{{/isactionevent}}
 {{/footer}}
 {{/ core/modal }}
index bdb5e67..d6c91af 100644 (file)
 }}
 {{< core/hover_tooltip }}
     {{$anchor}}
-        <a href="{{url}}">{{day}}</a>
+        <a href="{{url}}">{{$day}}{{day}}{{/day}}</a>
     {{/anchor}}
     {{$tooltip}}
-        <b>{{title}}</b>
-        {{{content}}}
+        <b>{{$title}}{{title}}{{/title}}</b>
+        {{$content}}{{{content}}}{{/content}}
     {{/tooltip}}
 {{/ core/hover_tooltip }}
index e99c491..99a2f59 100644 (file)
 
 {{< core/modal }}
     {{$footer}}
-        <button type="button"
-                class="btn btn-secondary"
-                data-collapsed="true"
-                data-action="more-less-toggle">
-
-            {{#str}} more, calendar {{/str}}
-        </button>
         <button type="button"
                 class="btn btn-primary"
                 data-context-id="{{contextid}}"
index 80005dc..463b687 100644 (file)
@@ -77,8 +77,9 @@
                                             <li class="events-underway">[{{name}}]</li>
                                         {{/underway}}
                                         {{^underway}}
-                                            <li class="calendar_event_{{eventtype}}"
+                                            <li class="calendar_event_{{calendareventtype}}"
                                                 data-region="event-item"
+                                                data-eventtype-{{calendareventtype}}="1"
                                                 {{#canedit}}
                                                     draggable="true"
                                                     data-drag-type="move"
     </table>
 </div>
 {{#js}}
-require(['jquery', 'core_calendar/month_view_drag_drop'], function($, DragDrop) {
+require([
+    'jquery',
+    'core_calendar/month_view_drag_drop',
+    'core_calendar/selectors',
+    'core_calendar/events',
+], function(
+    $,
+    DragDrop,
+    CalendarSelectors,
+    CalendarEvents
+) {
     var root = $('#month-detailed-{{uniqid}}');
     DragDrop.init(root);
+
+    $('body').on(CalendarEvents.filterChanged, function(e, data) {
+        M.util.js_pending("month-detailed-{{uniqid}}-filterChanged");
+        // A filter value has been changed.
+        // Find all matching cells in the popover data, and hide them.
+        $("#month-detailed-{{uniqid}}")
+            .find(CalendarSelectors.eventType[data.type])
+            .toggleClass('hidden', !!data.hidden);
+        M.util.js_complete("month-detailed-{{uniqid}}-filterChanged");
+    });
 });
 {{/js}}
diff --git a/calendar/templates/month_mini.mustache b/calendar/templates/month_mini.mustache
new file mode 100644 (file)
index 0000000..b14afc9
--- /dev/null
@@ -0,0 +1,154 @@
+{{!
+    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/month_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="month-mini-{{uniqid}}-{{time}}" class="calendarwrapper" data-courseid="{{courseid}}" data-current-time="{{time}}">
+    {{> core/overlay_loading}}
+    <table class="minicalendar calendartable">
+        <caption class="calendar-controls">
+            <a href="#" class="arrow_link previous" title="{{#str}}monthprev, calendar{{/str}}" data-time="{{previousperiod}}"><span class="arrow">{{{larrow}}}</span></a>
+            <span class="hide"> | </span>
+            <span class="current">
+                <a href="{{{url}}}" title="{{#str}}monththis, calendar{{/str}}" data-time="">{{periodname}}</a>
+            </span>
+            <span class="hide"> | </span>
+            <a href="#" class="arrow_link next" title="{{#str}}monthnext, calendar{{/str}}" data-time="{{nextperiod}}"><span class="arrow">{{{rarrow}}}</span></a>
+        </caption>
+        <thead>
+          <tr>
+                {{# daynames }}
+                <th class="header text-xs-center" scope="col">
+                    <abbr title="{{fullname}}">{{shortname}}</abbr>
+                </th>
+                {{/ daynames }}
+            </tr>
+        </thead>
+        <tbody>
+{{#weeks}}
+            <tr data-region="month-view-week">
+                {{#prepadding}}
+                    <td class="dayblank">&nbsp;</td>
+                {{/prepadding}}
+                {{#days}}
+                    <td class="day text-center{{!
+                            This is the list of additional classes to display.
+
+                            This cell is for today.
+                            }}{{#istoday}} today{{/istoday}}{{!
+
+                            This day falls on a weekend.
+                            }}{{#isweekend}} weekend{{/isweekend}}{{!
+
+                            There is at least one event.
+                            }}{{#events.0}} hasevent{{/events.0}}{{!
+
+                            There are events on this day which span multiple days.
+                            }}{{#durationevents.0}} duration{{/durationevents.0}}{{!
+                            }}{{#durationevents}} duration_{{.}}{{/durationevents}}{{!
+                            }}{{#islastday}} duraction_finish{{/islastday}}{{!
+
+                            These event types fall on this day.
+                            }}{{#calendareventtypes}}{{!
+                            }} calendar_event_{{.}}{{!
+                            }}{{/calendareventtypes}}{{!
+
+                            This day contains the last day of an event.
+                            }}{{#haslastdayofevent}}{{!
+                            }} duration_finish{{!
+                            }}{{/haslastdayofevent}}{{!
+                        }}"{{!
+
+                        Add data-eventtype-[eventtype] data attributes.
+                        These are used to show and hide events using the filter.
+                        }}{{#calendareventtypes}}{{!
+                        }} data-eventtype-{{.}}="1"{{!
+                        }}{{/calendareventtypes}}{{!
+
+                        This is the timestamp for this month.
+                        }} data-day-timestamp="{{timestamp}}"{{!
+                    }}>{{!
+                        }}{{#popovertitle}}
+                            {{< core_calendar/minicalendar_day_link }}
+                                {{$day}}{{mday}}{{/day}}
+                                {{$url}}{{viewdaylink}}{{/url}}
+                                {{$title}}{{popovertitle}}{{/title}}
+                                {{$nocontent}}{{#str}}eventnone, calendar{{/str}}{{/nocontent}}
+                                {{$content}}
+                                    {{#events}}
+                                        <div data-popover-eventtype-{{calendareventtype}}="1">
+                                            {{#modulename}}
+                                                {{#pix}} icon, {{modulename}} {{/pix}}
+                                            {{/modulename}}
+                                            {{^modulename}}
+                                                {{#pix}} i/{{eventtype}}event, core {{/pix}}
+                                            {{/modulename}}
+                                            {{{popupname}}}
+                                        </div>
+                                    {{/events}}
+                                {{/content}}
+                            {{/ core_calendar/minicalendar_day_link }}
+                        {{/popovertitle}}{{!
+                        }}{{^popovertitle}}
+                            {{mday}}
+                        {{/popovertitle}}{{!
+                    }}</td>
+                {{/days}}
+                {{#postpadding}}
+                    <td class="dayblank">&nbsp;</td>
+                {{/postpadding}}
+            </tr>
+{{/weeks}}
+        </tbody>
+    </table>
+</div>
+{{#js}}
+require([
+    'jquery',
+    'core_calendar/selectors',
+    'core_calendar/events',
+], function(
+    $,
+    CalendarSelectors,
+    CalendarEvents
+) {
+
+    $('body').on(CalendarEvents.filterChanged, function(e, data) {
+        M.util.js_pending("month-mini-{{uniqid}}-filterChanged");
+        // A filter value has been changed.
+        // Find all matching cells in the popover data, and hide them.
+        $("#month-mini-{{uniqid}}-{{time}}")
+            .find(CalendarSelectors.popoverType[data.type])
+            .toggleClass('hidden', !!data.hidden);
+        M.util.js_complete("month-mini-{{uniqid}}-filterChanged");
+    });
+});
+{{/js}}
index ef8fa66..0bd835c 100644 (file)
@@ -84,9 +84,9 @@ class behat_calendar extends behat_base {
      * @param int $day The day of the current month
      */
     public function i_hover_over_day_of_this_month_in_calendar($day) {
-        $summarytitle = get_string('calendarheading', 'calendar', userdate(time(), get_string('strftimemonthyear')));
+        $summarytitle = userdate(time(), get_string('strftimemonthyear'));
         // The current month table.
-        $currentmonth = "table[contains(concat(' ', normalize-space(@summary), ' '), ' {$summarytitle} ')]";
+        $currentmonth = "table[descendant::*[self::caption[contains(concat(' ', normalize-space(.), ' '), ' {$summarytitle} ')]]]";
 
         // Strings for the class cell match.
         $cellclasses  = "contains(concat(' ', normalize-space(@class), ' '), ' day ')";
@@ -95,6 +95,7 @@ class behat_calendar extends behat_base {
         $dayofmonth   = "a[{$daycontains}]";
 
         $xpath = '//' . $currentmonth . '/descendant::' . $daycell . '/' . $dayofmonth;
+        $this->execute("behat_general::wait_until_the_page_is_ready");
         $this->execute("behat_general::i_hover", array($xpath, "xpath_element"));
 
     }
index 5f2cf5e..f1d6153 100644 (file)
@@ -104,7 +104,7 @@ Feature: Perform basic calendar functionality
     When I follow "This month"
     And I click on "Really awesome event!" "link"
     And I click on "Delete" "button"
-    And I click on "Yes" "button"
+    And I click on "Delete event" "button"
     And I wait to be redirected
     Then I should not see "Really awesome event!"
 
@@ -124,3 +124,30 @@ Feature: Perform basic calendar functionality
       | Description | Wait, this event isn't that great. |
     And I press "Save"
     Then I should see "Mediocre event"
+
+  @javascript
+  Scenario: Module events editing
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    And the following "activities" exist:
+      | activity | course | idnumber | name        | intro                   | timeopen      | timeclose     |
+      | choice   | C1     | choice1  | Test choice | Test choice description | ##today## | ##today##  |
+    When I follow "This month"
+    Then I should see "Choice Test choice open"
+    And I should see "Choice Test choice close"
+    When I click on "Choice Test choice open" "link"
+    Then "Delete" "button" should not exist
+    And "Edit" "button" should not exist
+    And I should see "Open event"
+    When I click on "Go to activity" "link"
+    And I wait to be redirected
+    Then I should see "Test choice"
+    And I am on "Course 1" course homepage
+    And I follow "This month"
+    When I click on "Choice Test choice close" "link"
+    Then "Delete" "button" should not exist
+    And "Edit" "button" should not exist
+    And I should see "Close event"
+    When I click on "Go to activity" "link"
+    And I wait to be redirected
+    Then I should see "Test choice"
index ba0bc94..6447c8d 100644 (file)
@@ -161,5 +161,4 @@ echo html_writer::end_tag('div');
 list($data, $template) = calendar_get_footer_options($calendar);
 echo $renderer->render_from_template($template, $data);
 
-$PAGE->requires->js_call_amd('core_calendar/calendar', 'init');
 echo $OUTPUT->footer();
index 833f7fb..8972162 100644 (file)
@@ -124,4 +124,41 @@ class mycourse extends \core_search\base {
     public function get_context_url(\core_search\document $doc) {
         return new \moodle_url('/course/view.php', array('id' => $doc->get('courseid')));
     }
+
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
+
+    /**
+     * Return the context info required to index files for
+     * this search area.
+     *
+     * Should be overridden by each search area.
+     *
+     * @return array
+     */
+    public function get_search_fileareas() {
+        $fileareas = array(
+                'overviewfiles',
+                'summary'// Fileareas.
+        );
+
+        return $fileareas;
+    }
+
+    /**
+     * Returns the moodle component name.
+     *
+     * It might be the plugin name (whole frankenstyle name) or the core subsystem name.
+     *
+     * @return string
+     */
+    public function get_component_name() {
+        return 'course';
+    }
 }
index 2ccc6f1..b38c641 100644 (file)
@@ -3894,16 +3894,14 @@ function course_get_user_navigation_options($context, $course = null) {
     // Frontpage settings?
     if ($isfrontpage) {
         // We are on the front page, so make sure we use the proper capability (site:viewparticipants).
-        $options->participants = has_capability('moodle/site:viewparticipants', $sitecontext) ||
-            has_capability('moodle/course:enrolreview', $sitecontext);
+        $options->participants = course_can_view_participants($sitecontext);
         $options->badges = !empty($CFG->enablebadges) && has_capability('moodle/badges:viewbadges', $sitecontext);
         $options->tags = !empty($CFG->usetags) && $isloggedin;
         $options->search = !empty($CFG->enableglobalsearch) && has_capability('moodle/search:query', $sitecontext);
         $options->calendar = $isloggedin;
     } else {
         // We are in a course, so make sure we use the proper capability (course:viewparticipants).
-        $options->participants = has_capability('moodle/course:viewparticipants', $context) ||
-            has_capability('moodle/course:enrolreview', $context);
+        $options->participants = course_can_view_participants($context);
         $options->badges = !empty($CFG->enablebadges) && !empty($CFG->badges_allowcoursebadges) &&
                             has_capability('moodle/badges:viewbadges', $context);
         // Add view grade report is permitted.
@@ -4237,3 +4235,34 @@ function course_check_module_updates_since($cm, $from, $fileareas = array(), $fi
 
     return $updates;
 }
+
+/**
+ * Returns true if the user can view the participant page, false otherwise,
+ *
+ * @param context $context The context we are checking.
+ * @return bool
+ */
+function course_can_view_participants($context) {
+    $viewparticipantscap = 'moodle/course:viewparticipants';
+    if ($context->contextlevel == CONTEXT_SYSTEM) {
+        $viewparticipantscap = 'moodle/site:viewparticipants';
+    }
+
+    return has_any_capability([$viewparticipantscap, 'moodle/course:enrolreview'], $context);
+}
+
+/**
+ * Checks if a user can view the participant page, if not throws an exception.
+ *
+ * @param context $context The context we are checking.
+ * @throws required_capability_exception
+ */
+function course_require_view_participants($context) {
+    if (!course_can_view_participants($context)) {
+        $viewparticipantscap = 'moodle/course:viewparticipants';
+        if ($context->contextlevel == CONTEXT_SYSTEM) {
+            $viewparticipantscap = 'moodle/site:viewparticipants';
+        }
+        throw new required_capability_exception($context, $viewparticipantscap, 'nopermissions', '');
+    }
+}
index cc2750a..d3056ea 100644 (file)
@@ -27,6 +27,7 @@ if (!defined('MOODLE_INTERNAL')) {
     die('Direct access to this script is forbidden.');    ///  It must be included from a Moodle page
 }
 
+require_once($CFG->dirroot . '/course/lib.php');
 require_once($CFG->libdir.'/formslib.php');
 
 class recent_form extends moodleform {
@@ -64,9 +65,9 @@ class recent_form extends moodleform {
         }
 
         if ($COURSE->id == SITEID) {
-            $viewparticipants = has_capability('moodle/site:viewparticipants', context_system::instance());
+            $viewparticipants = course_can_view_participants(context_system::instance());
         } else {
-            $viewparticipants = has_capability('moodle/course:viewparticipants', $context);
+            $viewparticipants = course_can_view_participants($context);
         }
 
         if ($viewparticipants) {
index 702771f..6d53ba5 100644 (file)
@@ -3904,4 +3904,174 @@ class core_course_courselib_testcase extends advanced_testcase {
         // Update the assign instances for this course.
         $this->assertTrue(course_module_bulk_update_calendar_events('assign', $course->id));
     }
+
+    /**
+     * Test that a student can view participants in a course they are enrolled in.
+     */
+    public function test_course_can_view_participants_as_student() {
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+
+        $this->setUser($user);
+
+        $this->assertTrue(course_can_view_participants($coursecontext));
+    }
+
+    /**
+     * Test that a student in a course can not view participants on the site.
+     */
+    public function test_course_can_view_participants_as_student_on_site() {
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+
+        $this->setUser($user);
+
+        $this->assertFalse(course_can_view_participants(context_system::instance()));
+    }
+
+    /**
+     * Test that an admin can view participants on the site.
+     */
+    public function test_course_can_view_participants_as_admin_on_site() {
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+
+        $this->assertTrue(course_can_view_participants(context_system::instance()));
+    }
+
+    /**
+     * Test teachers can view participants in a course they are enrolled in.
+     */
+    public function test_course_can_view_participants_as_teacher() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        $user = $this->getDataGenerator()->create_user();
+        $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
+
+        $this->setUser($user);
+
+        $this->assertTrue(course_can_view_participants($coursecontext));
+    }
+
+    /**
+     * Check the teacher can still view the participants page without the 'viewparticipants' cap.
+     */
+    public function test_course_can_view_participants_as_teacher_without_view_participants_cap() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        $user = $this->getDataGenerator()->create_user();
+        $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
+
+        $this->setUser($user);
+
+        // Disable one of the capabilties.
+        assign_capability('moodle/course:viewparticipants', CAP_PROHIBIT, $roleid, $coursecontext);
+
+        // Should still be able to view the page as they have the 'moodle/course:enrolreview' cap.
+        $this->assertTrue(course_can_view_participants($coursecontext));
+    }
+
+    /**
+     * Check the teacher can still view the participants page without the 'moodle/course:enrolreview' cap.
+     */
+    public function test_course_can_view_participants_as_teacher_without_enrol_review_cap() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        $user = $this->getDataGenerator()->create_user();
+        $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
+
+        $this->setUser($user);
+
+        // Disable one of the capabilties.
+        assign_capability('moodle/course:enrolreview', CAP_PROHIBIT, $roleid, $coursecontext);
+
+        // Should still be able to view the page as they have the 'moodle/course:viewparticipants' cap.
+        $this->assertTrue(course_can_view_participants($coursecontext));
+    }
+
+    /**
+     * Check the teacher can not view the participants page without the required caps.
+     */
+    public function test_course_can_view_participants_as_teacher_without_required_caps() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        $user = $this->getDataGenerator()->create_user();
+        $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
+
+        $this->setUser($user);
+
+        // Disable the capabilities.
+        assign_capability('moodle/course:viewparticipants', CAP_PROHIBIT, $roleid, $coursecontext);
+        assign_capability('moodle/course:enrolreview', CAP_PROHIBIT, $roleid, $coursecontext);
+
+        $this->assertFalse(course_can_view_participants($coursecontext));
+    }
+
+    /**
+     * Check that an exception is not thrown if we can view the participants page.
+     */
+    public function test_course_require_view_participants() {
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+
+        $this->setUser($user);
+
+        course_require_view_participants($coursecontext);
+    }
+
+    /**
+     * Check that an exception is thrown if we can't view the participants page.
+     */
+    public function test_course_require_view_participants_as_student_on_site() {
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+
+        $this->setUser($user);
+
+        $this->expectException('required_capability_exception');
+        course_require_view_participants(context_system::instance());
+    }
 }
index 370141a..d11bfab 100644 (file)
@@ -86,6 +86,8 @@ class core_enrol_external extends external_api {
      */
     public static function get_enrolled_users_with_capability($coursecapabilities, $options) {
         global $CFG, $DB;
+
+        require_once($CFG->dirroot . '/course/lib.php');
         require_once($CFG->dirroot . "/user/lib.php");
 
         if (empty($coursecapabilities)) {
@@ -145,11 +147,8 @@ class core_enrol_external extends external_api {
                 throw new moodle_exception(get_string('errorcoursecontextnotvalid' , 'webservice', $exceptionparam));
             }
 
-            if ($courseid == SITEID) {
-                require_capability('moodle/site:viewparticipants', $context);
-            } else {
-                require_capability('moodle/course:viewparticipants', $context);
-            }
+            course_require_view_participants($context);
+
             // The accessallgroups capability is needed to use this option.
             if (!empty($groupid) && groups_is_member($groupid)) {
                 require_capability('moodle/site:accessallgroups', $coursecontext);
@@ -293,7 +292,9 @@ class core_enrol_external extends external_api {
      * @return array of courses
      */
     public static function get_users_courses($userid) {
-        global $USER, $DB;
+        global $CFG, $USER, $DB;
+
+        require_once($CFG->dirroot . '/course/lib.php');
 
         // Do basic automatic PARAM checks on incoming data, using params description
         // If any problems are found then exceptions are thrown with helpful error messages
@@ -312,7 +313,7 @@ class core_enrol_external extends external_api {
                 continue;
             }
 
-            if ($userid != $USER->id and !has_capability('moodle/course:viewparticipants', $context)) {
+            if ($userid != $USER->id and !course_can_view_participants($context)) {
                 // we need capability to view participants
                 continue;
             }
@@ -520,6 +521,8 @@ class core_enrol_external extends external_api {
      */
     public static function get_enrolled_users($courseid, $options = array()) {
         global $CFG, $USER, $DB;
+
+        require_once($CFG->dirroot . '/course/lib.php');
         require_once($CFG->dirroot . "/user/lib.php");
 
         $params = self::validate_parameters(
@@ -600,11 +603,8 @@ class core_enrol_external extends external_api {
             throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam);
         }
 
-        if ($courseid == SITEID) {
-            require_capability('moodle/site:viewparticipants', $context);
-        } else {
-            require_capability('moodle/course:viewparticipants', $context);
-        }
+        course_require_view_participants($context);
+
         // to overwrite this parameter, you need role:review capability
         if ($withcapability) {
             require_capability('moodle/role:review', $coursecontext);
index 50724df..e0834e2 100644 (file)
@@ -82,13 +82,13 @@ $string['group_memberofattribute'] = 'Name of the attribute that specifies which
 $string['group_memberofattribute_key'] = '\'Member of\' attribute';
 $string['host_url'] = 'Specify LDAP host in URL-form like \'ldap://ldap.myorg.com/\' or \'ldaps://ldap.myorg.com/\'';
 $string['host_url_key'] = 'Host URL';
-$string['idnumber_attribute'] = 'If the group membership contains distinguised names, specify the same attribute you have used for the user \'ID Number\' mapping in the LDAP authentication settings';
+$string['idnumber_attribute'] = 'If the group membership contains distinguished names, specify the same attribute you have used for the user \'ID Number\' mapping in the LDAP authentication settings.';
 $string['idnumber_attribute_key'] = 'ID number attribute';
 $string['ldap_encoding'] = 'Specify encoding used by LDAP server. Most probably utf-8, MS AD v2 uses default platform encoding such as cp1252, cp1250, etc.';
 $string['ldap_encoding_key'] = 'LDAP encoding';
 $string['ldap:manage'] = 'Manage LDAP enrol instances';
 $string['memberattribute'] = 'LDAP member attribute';
-$string['memberattribute_isdn'] = 'If the group membership contains distinguised names, you need to specify it here. If it does, you also need to configure the remaining settings of this section';
+$string['memberattribute_isdn'] = 'If the group membership contains distinguished names, you need to specify them here. If so, you also need to configure the remaining settings in this section.';
 $string['memberattribute_isdn_key'] = 'Member attribute uses dn';
 $string['nested_groups'] = 'Do you want to use nested groups (groups of groups) for enrolment?';
 $string['nested_groups_key'] = 'Nested groups';
@@ -97,7 +97,7 @@ $string['nosuchrole'] = "No such role: '{\$a}'\n";
 $string['objectclass'] = 'objectClass used to search courses. Usually \'group\' or \'posixGroup\'';
 $string['objectclass_key'] = 'Object class';
 $string['ok'] = "OK!\n";
-$string['opt_deref'] = 'If the group membership contains distinguised names, specify how aliases are handled during search. Select one of the following values: \'No\' (LDAP_DEREF_NEVER) or \'Yes\' (LDAP_DEREF_ALWAYS)';
+$string['opt_deref'] = 'If the group membership contains distinguished names, specify how aliases are handled during a search. Select one of the following values: \'No\' (LDAP_DEREF_NEVER) or \'Yes\' (LDAP_DEREF_ALWAYS).';
 $string['opt_deref_key'] = 'Dereference aliases';
 $string['phpldap_noextension'] = '<em>The PHP LDAP module does not seem to be present. Please ensure it is installed and enabled if you want to use this enrolment plugin.</em>';
 $string['pluginname'] = 'LDAP enrolments';
@@ -117,11 +117,11 @@ $string['unassignrole']  = "Unassigning role '{\$a->role_shortname}' to user '{\
 $string['unassignroleid']  = "Unassigning role id '{\$a->role_id}' to user id '{\$a->user_id}'\n";
 $string['unassignrolefailed'] = "Failed to unassign role '{\$a->role_shortname}' to user '{\$a->user_username}' from course '{\$a->course_shortname}' (id {\$a->course_id})\n";
 $string['updatelocal'] = 'Update local data';
-$string['user_attribute'] =  'If the group membership contains distinguised names, specify the attribute used to name/search users. If you are using LDAP authentication, this value should match the attribute specified in the \'ID Number\' mapping in the LDAP authentication plugin';
+$string['user_attribute'] = 'If the group membership contains distinguished names, specify the attribute used to name/search for users. If you are using LDAP authentication, this value should match the attribute specified in the \'ID Number\' mapping in the LDAP authentication plugin.';
 $string['user_attribute_key'] = 'ID number attribute';
-$string['user_contexts'] = 'If the group membership contains distinguised names, specify the list of contexts where users are located. Separate different contexts with \';\'. For example: \'ou=users,o=org; ou=others,o=org\'';
+$string['user_contexts'] = 'If the group membership contains distinguished names, specify the list of contexts where users are located. Separate different contexts with a semi-colon (;). For example: \'ou=users,o=org; ou=others,o=org\'.';
 $string['user_contexts_key'] = 'Contexts';
-$string['user_search_sub'] = 'If the group membership contains distinguised names, specify if the search for users is done in subcontexts too';
+$string['user_search_sub'] = 'If the group membership contains distinguished names, specify if the search for users is done in sub-contexts too.';
 $string['user_search_sub_key'] = 'Search subcontexts';
 $string['user_settings'] = 'User lookup settings';
 $string['user_type'] = 'If the group membership contains distinguished names, specify how users are stored in LDAP';
index cc65fcf..3a135be 100644 (file)
@@ -114,5 +114,5 @@ Feature: Enrolments are synchronised with meta courses
     And I navigate to course participants
     # Suspended users can be unenrolled.
     When I click on "//a[@data-action='unenrol']" "xpath_element" in the "student2" "table_row"
-    And I click on "Yes" "button"
+    And I click on "Unenrol" "button" in the "[data-region='modal']" "css_element"
     Then I should not see "Student 2" in the "participants" "table"
index 332e3af..feb67bf 100644 (file)
@@ -123,5 +123,5 @@ Feature: Users can auto-enrol themself in courses where self enrolment is allowe
     And I am on "Course 1" course homepage
     And I navigate to course participants
     When I click on "//a[@data-action='unenrol']" "xpath_element" in the "student1" "table_row"
-    And I click on "Yes" "button"
+    And I click on "Unenrol" "button" in the "[data-region='modal']" "css_element"
     Then I should not see "Student 1" in the "participants" "table"
index 6e8789e..9eb6d21 100644 (file)
@@ -30,8 +30,6 @@ $string['activitynext'] = 'Next activity';
 $string['activityprev'] = 'Previous activity';
 $string['breadcrumb'] = 'Breadcrumb trail';
 $string['hideblocka'] = 'Hide {$a} block';
-$string['monthnext'] = 'Next month';
-$string['monthprev'] = 'Previous month';
 $string['showblocka'] = 'Show {$a} block';
 $string['sitemap'] = 'Site map';
 $string['skipa'] = 'Skip {$a}';
@@ -41,3 +39,7 @@ $string['skipto'] = 'Skip to {$a}';
 $string['tocontent'] = 'Skip to main content';
 $string['tonavigation'] = 'Go to navigation';
 $string['youarehere'] = 'You are here';
+
+// Deprecated since Moodle 3.4.
+$string['monthnext'] = 'Next month';
+$string['monthprev'] = 'Previous month';
index 13267b8..dc90581 100644 (file)
@@ -440,6 +440,7 @@ $string['deleteunconfirmed'] = 'Delete not fully setup users after';
 $string['deleteuser'] = 'Delete user';
 $string['density'] = 'Density';
 $string['denyemailaddresses'] = 'Denied email domains';
+$string['devlibdirpresent'] = 'Directories with development libraries such as <em>vendor</em> or <em>node_modules</em> should not be present on public sites. See the <a href="{$a->moreinfourl}">security overview report</a> for more details.';
 $string['development'] = 'Development';
 $string['devicedetectregex'] = 'Device detection regular expressions';
 $string['devicedetectregex_desc'] = '<p>By default, Moodle can detect devices of the type default (desktop PCs, laptops, etc), mobile (phones and small hand held devices), tablet (iPads, Android tablets) and legacy (Internet Explorer 6 users).  The theme selector can be used to apply separate themes to all of these.  This setting allows regular expressions that allow the detection of extra device types (these take precedence over the default types).</p>
@@ -1141,7 +1142,7 @@ $string['timezonephpdefault'] = 'Default PHP timezone ({$a})';
 $string['timezoneserver'] = 'Server timezone ({$a})';
 $string['tlswarning'] = 'No PHP/cURL extension with TLSv1.2 support has been detected. Some services may not work. It is strongly recommended to upgrade your TLS libraries.';
 $string['tokenduration'] = 'User created token duration';
-$string['tokenduration_desc'] = 'New tokens created by users (for example via the mobile app) will be valid for the specified time.';
+$string['tokenduration_desc'] = 'Length of time for which a web services token created by a user (for example via the mobile app) is valid.';
 $string['tokenizerrecommended'] = 'Installing the optional PHP Tokenizer extension is recommended -- it improves Moodle Networking functionality.';
 $string['tools'] = 'Admin tools';
 $string['toolsmanage'] = 'Manage admin tools';
index 81fa1bc..b127fcb 100644 (file)
@@ -30,53 +30,55 @@ $string['analyticslogstore_help'] = 'The log store that will be used by the anal
 $string['analyticssettings'] = 'Analytics settings';
 $string['coursetoolong'] = 'The course is too long';
 $string['enabledtimesplittings'] = 'Time splitting methods';
-$string['erroralreadypredict'] = '{$a} file has already been used to predict';
+$string['disabledmodel'] = 'Disabled model';
+$string['erroralreadypredict'] = 'File {$a} has already been used to generate predictions.';
 $string['errorcannotreaddataset'] = 'Dataset file {$a} can not be read';
-$string['errorcannotwritedataset'] = 'Dataset file {$a} can not be written';
-$string['errorendbeforestart'] = 'The guessed end date ({$a}) is before the course start date.';
+$string['errorcannotwritedataset'] = 'Dataset file {$a} cannot be written';
+$string['errorendbeforestart'] = 'The end date ({$a}) is before the course start date.';
 $string['errorinvalidindicator'] = 'Invalid {$a} indicator';
-$string['errorinvalidtimesplitting'] = 'Invalid time splitting, please ensure you added the class fully qualified class name';
-$string['errornoindicators'] = 'This model does not have any indicator';
-$string['errornopredictresults'] = 'No results returned from the predictions processor, check the output directory contents for more info';
-$string['errornotimesplittings'] = 'This model does not have any time splitting method';
-$string['errornoroles'] = 'Student or teacher roles have not been defined. Define them in analytics settings page.';
-$string['errornotarget'] = 'This model does not have any target';
-$string['errorpredictioncontextnotavailable'] = 'This prediction context is not available anymore';
+$string['errorinvalidtimesplitting'] = 'Invalid time splitting; please ensure you add the class fully qualified class name.';
+$string['errornoindicators'] = 'This model does not have any indicators.';
+$string['errornopredictresults'] = 'No results returned from the predictions processor. Check the output directory contents for more information.';
+$string['errornotimesplittings'] = 'This model does not have any time-splitting method.';
+$string['errornoroles'] = 'Student or teacher roles have not been defined. Define them in the analytics settings page.';
+$string['errornotarget'] = 'This model does not have any target.';
+$string['errorpredictioncontextnotavailable'] = 'This prediction context is not available anymore.';
 $string['errorpredictionformat'] = 'Wrong prediction calculations format';
 $string['errorpredictionnotfound'] = 'Prediction not found';
 $string['errorpredictionsprocessor'] = 'Predictions processor error: {$a}';
-$string['errorpredictwrongformat'] = 'The predictions processor return can not be decoded: "{$a}"';
+$string['errorpredictwrongformat'] = 'The predictions processor return cannot be decoded: "{$a}"';
 $string['errorprocessornotready'] = 'The selected predictions processor is not ready: {$a}';
 $string['errorsamplenotavailable'] = 'The predicted sample is not available anymore';
-$string['errorunexistingtimesplitting'] = 'The selected time splitting method is not available';
-$string['errorunexistingmodel'] = 'Unexisting model {$a}';
+$string['errorunexistingtimesplitting'] = 'The selected time-splitting method is not available.';
+$string['errorunexistingmodel'] = 'Non-existing model {$a}';
 $string['errorunknownaction'] = 'Unknown action';
-$string['eventpredictionactionstarted'] = 'Prediction action started';
+$string['eventpredictionactionstarted'] = 'Prediction process started';
 $string['insightmessagesubject'] = 'New insight for "{$a->contextname}": {$a->insightname}';
-$string['insightinfo'] = '{$a->insightname} - {$a->contextname}';
 $string['insightinfomessage'] = 'The system generated some insights for you: {$a}';
 $string['insightinfomessagehtml'] = 'The system generated some insights for you: <a href="{$a}">{$a}</a>.';
-$string['invalidtimesplitting'] = 'Model with id {$a} needs a time splitting method before it can be used to train';
-$string['invalidanalysablefortimesplitting'] = 'It can not be analysed using {$a} time splitting method';
+$string['invalidtimesplitting'] = 'Model with ID {$a} needs a time-splitting method before it can be used to train.';
+$string['invalidanalysablefortimesplitting'] = 'It cannot be analysed using {$a} time-splitting method.';
 $string['nocourses'] = 'No courses to analyse';
 $string['modeloutputdir'] = 'Models output directory';
 $string['modeloutputdirinfo'] = 'Directory where prediction processors store all evaluation info. Useful for debugging and research.';
-$string['noevaluationbasedassumptions'] = 'Models based on assumptions can not be evaluated';
+$string['noevaluationbasedassumptions'] = 'Models based on assumptions cannot be evaluated.';
 $string['nodata'] = 'No data to analyse';
 $string['noinsightsmodel'] = 'This model does not generate insights';
 $string['noinsights'] = 'No insights reported';
 $string['nonewdata'] = 'No new data available';
 $string['nonewranges'] = 'No new predictions yet';
-$string['nonewtimeranges'] = 'No new time ranges, nothing to predict';
+$string['nonewtimeranges'] = 'No new time ranges; nothing to predict.';
 $string['nopredictionsyet'] = 'No predictions available yet';
 $string['noranges'] = 'No predictions yet';
 $string['notrainingbasedassumptions'] = 'Models based on assumptions do not need training';
 $string['novaliddata'] = 'No valid data available';
 $string['novalidsamples'] = 'No valid samples available';
+$string['onlycli'] = 'Analytics processes execution via command line only';
+$string['onlycliinfo'] = 'Analytics processes like evaluating models, training machine learning algorithms or getting predictions can take some time, they will run as cron tasks and they can be forced via command line. Disable this setting if you want your site managers to be able to run these processes manually via web interface';
 $string['predictionsprocessor'] = 'Predictions processor';
 $string['predictionsprocessor_help'] = 'Prediction processors are the machine learning backends that process the datasets generated by calculating models\' indicators and targets.';
 $string['processingsitecontents'] = 'Processing site contents';
 $string['successfullyanalysed'] = 'Successfully analysed';
-$string['timesplittingmethod'] = 'Time splitting method';
-$string['timesplittingmethod_help'] = 'The time splitting method divides the course duration in parts, the predictions engine will run at the end of these parts. It is recommended that you only enable the time splitting methods you could be interested on using; the evaluation process will iterate through all of them so the more time splitting methods to go through the slower the evaluation process will be.';
+$string['timesplittingmethod'] = 'Time-splitting method';
+$string['timesplittingmethod_help'] = 'The time-splitting method divides the course duration into parts; the predictions engine will run at the end of these parts. It is recommended that you only enable the time-splitting methods you may be interested in using. The evaluation process will iterate through all enabled time-spitting methods, so the more time-splitting methods enabled, the slower the evaluation process will be.';
 $string['viewprediction'] = 'View prediction details';
index eccd233..30a936c 100644 (file)
@@ -90,6 +90,7 @@ $string['eventdate'] = 'Date';
 $string['eventdescription'] = 'Description';
 $string['eventduration'] = 'Duration';
 $string['eventendtime'] = 'End time';
+$string['eventendtimewrapped'] = '{$a} (End time)';
 $string['eventinstanttime'] = 'Time';
 $string['eventkind'] = 'Type of event';
 $string['eventname'] = 'Event title';
@@ -130,14 +131,19 @@ $string['generateurlbutton'] = 'Get calendar URL';
 $string['global'] = 'Global';
 $string['globalevent'] = 'Global event';
 $string['globalevents'] = 'Global events';
+$string['gotoactivity'] = 'Go to activity';
 $string['gotocalendar'] = 'Go to calendar';
 $string['group'] = 'Group';
 $string['groupevent'] = 'Group event';
 $string['groupevents'] = 'Group events';
-$string['hidecourseevents'] = 'Hide course events';
-$string['hideglobalevents'] = 'Hide global events';
-$string['hidegroupsevents'] = 'Hide group events';
-$string['hideuserevents'] = 'Hide user events';
+$string['eventtypeglobal'] = 'global';
+$string['eventtypesite'] = 'global';
+$string['eventtypecourse'] = 'course';
+$string['eventtypemodule'] = 'module';
+$string['eventtypegroup'] = 'group';
+$string['eventtypeuser'] = 'user';
+$string['hideeventtype'] = 'Hide {$a} events';
+$string['showeventtype'] = 'Show {$a} events';
 $string['hourly'] = 'Hourly';
 $string['ical'] = 'iCal';
 $string['importcalendar'] = 'Import calendar';
@@ -158,9 +164,10 @@ $string['monday'] = 'Monday';
 $string['monthly'] = 'Monthly';
 $string['monthlyview'] = 'Monthly view';
 $string['monthnext'] = 'Next month';
+$string['monthprev'] = 'Previous month';
 $string['monththis'] = 'This month';
 $string['more'] = 'More';
-$string['namewithsource'] = '{$a->name}({$a->source})';
+$string['namewithsource'] = '{$a->name} ({$a->source})';
 $string['never'] = 'Never';
 $string['newevent'] = 'New event';
 $string['notitle'] = 'no title';
@@ -193,10 +200,6 @@ $string['repeatweeksl'] = 'Repeat weekly, creating altogether';
 $string['repeatweeksr'] = 'events';
 $string['sat'] = 'Sat';
 $string['saturday'] = 'Saturday';
-$string['showcourseevents'] = 'Show course events';
-$string['showglobalevents'] = 'Show global events';
-$string['showgroupsevents'] = 'Show group events';
-$string['showuserevents'] = 'Show user events';
 $string['shown'] = 'shown';
 $string['siteevents'] = 'Site events';
 $string['spanningevents'] = 'Events underway';
@@ -213,17 +216,10 @@ $string['timeformat_12'] = '12-hour (am/pm)';
 $string['timeformat_24'] = '24-hour';
 $string['timeperiod'] = 'Time period';
 $string['today'] = 'Today';
+$string['todayplustitle'] = 'Today {$a}';
 $string['tomorrow'] = 'Tomorrow';
 $string['tt_deleteevent'] = 'Delete event';
 $string['tt_editevent'] = 'Edit event';
-$string['tt_hidecourse'] = 'Course events are shown (click to hide)';
-$string['tt_hideglobal'] = 'Global events are shown (click to hide)';
-$string['tt_hidegroups'] = 'Group events are shown (click to hide)';
-$string['tt_hideuser'] = 'User events are shown (click to hide)';
-$string['tt_showcourse'] = 'Course events are hidden (click to show)';
-$string['tt_showglobal'] = 'Global events are hidden (click to show)';
-$string['tt_showgroups'] = 'Group events are hidden (click to show)';
-$string['tt_showuser'] = 'User events are hidden (click to show)';
 $string['tue'] = 'Tue';
 $string['tuesday'] = 'Tuesday';
 $string['typeclose'] = 'Close event';
@@ -251,3 +247,21 @@ $string['youcandeleteallrepeats'] = 'This event is part of a repeating event ser
 
 // Deprecated since Moodle 3.2.
 $string['for'] = 'for';
+
+// Deprecated since Moodle 3.4.
+$string['tt_hidecourse'] = 'Course events are shown (click to hide)';
+$string['tt_hideglobal'] = 'Global events are shown (click to hide)';
+$string['tt_hidegroups'] = 'Group events are shown (click to hide)';
+$string['tt_hideuser'] = 'User events are shown (click to hide)';
+$string['tt_showcourse'] = 'Course events are hidden (click to show)';
+$string['tt_showglobal'] = 'Global events are hidden (click to show)';
+$string['tt_showgroups'] = 'Group events are hidden (click to show)';
+$string['tt_showuser'] = 'User events are hidden (click to show)';
+$string['hidecourseevents'] = 'Hide course events';
+$string['hideglobalevents'] = 'Hide global events';
+$string['hidegroupsevents'] = 'Hide group events';
+$string['hideuserevents'] = 'Hide user events';
+$string['showcourseevents'] = 'Show course events';
+$string['showglobalevents'] = 'Show global events';
+$string['showgroupsevents'] = 'Show group events';
+$string['showuserevents'] = 'Show user events';
index fbb11b4..4bd4742 100644 (file)
@@ -89,7 +89,7 @@ $string['completionicons_help'] = 'A tick next to an activity name may be used t
 
 If a box with a dotted border is shown, a tick will appear automatically when you have completed the activity according to conditions set by the teacher.
 
-If a box with a solid border is shown, you can click it to tick the box when you think you have completed the activity. (Clicking it again removes the tick if you change your mind.) The tick is optional and is simply a way of tracking your progress through the course.';
+If a box with a solid border is shown, you can click it to tick the box when you think you have completed the activity. (Clicking it again removes the tick if you change your mind.)';
 $string['completionmenuitem'] = 'Completion';
 $string['completionnotenabled'] = 'Completion is not enabled';
 $string['completionnotenabledforcourse'] = 'Completion is not enabled for this course';
index 55a0e70..cd7bb58 100644 (file)
@@ -61,3 +61,21 @@ error:backpackloginfailed,core_badges
 signinwithyouremail,core_badges
 sectionusedefaultname,core
 registermoochtips,core_hub
+monthnext,core_access
+monthprev,core_access
+tt_hidecourse,core_calendar
+tt_hideglobal,core_calendar
+tt_hidegroups,core_calendar
+tt_hideuser,core_calendar
+tt_showcourse,core_calendar
+tt_showglobal,core_calendar
+tt_showgroups,core_calendar
+tt_showuser,core_calendar
+hidecourseevents,core_calendar
+hideglobalevents,core_calendar
+hidegroupsevents,core_calendar
+hideuserevents,core_calendar
+showcourseevents,core_calendar
+showglobalevents,core_calendar
+showgroupsevents,core_calendar
+showuserevents,core_calendar
index 0d168b8..e179229 100644 (file)
@@ -117,7 +117,7 @@ $string['licence'] = 'Licence';
 $string['licence_help'] = 'Select the licence you want to distribute your course under.';
 $string['licence_link'] = 'licenses';
 $string['logourl'] = 'Logo URL';
-$string['mobilenotificacionsenabled'] = 'Mobile notifications enabled ({$a})';
+$string['mobilenotificationsenabled'] = 'Mobile notifications enabled ({$a})';
 $string['mobileservicesenabled'] = 'Mobile services enabled ({$a})';
 $string['modulenumberaverage'] = 'Average number of course modules ({$a})';
 $string['moodleorg'] = 'Moodle';
index 6c18422..addc4f7 100644 (file)
@@ -1024,25 +1024,25 @@ $string['includesitefiles'] = 'Include site files used in this course';
 $string['includeuserfiles'] = 'Include user files';
 $string['increasesections'] = 'Increase the number of sections';
 $string['indicator:accessesafterend'] = 'Accesses after the end date';
-$string['indicator:accessesafterend_help'] = '';
+$string['indicator:accessesafterend_help'] = 'This indicator reflects the accesses by the student after the end date of the course.';
 $string['indicator:accessesbeforestart'] = 'Accesses before the start date';
-$string['indicator:accessesbeforestart_help'] = '';
+$string['indicator:accessesbeforestart_help'] = 'This indicator reflects the accesses by the student before the start date of the course.';
 $string['indicator:anywrite'] = 'Any write action';
-$string['indicator:anywrite_help'] = '';
+$string['indicator:anywrite_help'] = 'This indicator represents any write (submit) action taken by the student.';
 $string['indicator:completeduserprofile'] = 'User profile is completed';
-$string['indicator:completeduserprofile_help'] = '';
+$string['indicator:completeduserprofile_help'] = 'This indicator represents that the student has completed their user profile.';
 $string['indicator:completionenabled'] = 'Completion enabled';
-$string['indicator:completionenabled_help'] = '';
+$string['indicator:completionenabled_help'] = 'This indicator represents that completion tracking has been enabled.';
 $string['indicator:noteacher'] = 'There are no teachers';
-$string['indicator:noteacher_help'] = '';
+$string['indicator:noteacher_help'] = 'This indicator reflects that this course has no teachers.';
 $string['indicator:potentialcognitive'] = 'Potential cognitive depth';
-$string['indicator:potentialcognitive_help'] = '';
+$string['indicator:potentialcognitive_help'] = 'This indicator is based on the potential cognitive depth that could be reached by a student in this activity.';
 $string['indicator:potentialsocial'] = 'Potential social breadth';
-$string['indicator:potentialsocial_help'] = '';
+$string['indicator:potentialsocial_help'] = 'This indicator is based on the potential social breadth that could be reached by the student in the activity.';
 $string['indicator:readactions'] = 'Read actions amount';
-$string['indicator:readactions_help'] = '';
+$string['indicator:readactions_help'] = 'This indicator represents the number of read (view) actions taken by the student.';
 $string['indicator:userforumstracking'] = 'User is tracking forums';
-$string['indicator:userforumstracking_help'] = '';
+$string['indicator:userforumstracking_help'] = 'This indicator represents whether or not the student has tracking turned on in the forums.';
 $string['info'] = 'Information';
 $string['institution'] = 'Institution';
 $string['instudentview'] = 'in student view';
@@ -1900,9 +1900,9 @@ $string['tagalready'] = 'This tag already exists';
 $string['tagmanagement'] = 'Add/delete tags ...';
 $string['tags'] = 'Tags';
 $string['target:coursedropout'] = 'Students at risk of dropping out';
-$string['target:coursedropout_help'] = '';
+$string['target:coursedropout_help'] = 'This target describes whether the student is considered at risk of dropping out.';
 $string['target:noteachingactivity'] = 'No teaching';
-$string['target:noteachingactivity_help'] = '';
+$string['target:noteachingactivity_help'] = 'This target describes whether or not this teacher is likely to engage in any activity in the course.';
 $string['targetlabelstudentdropoutyes'] = 'Student at risk of dropping out';
 $string['targetlabelstudentdropoutno'] = 'Not at risk';
 $string['targetlabelteachingyes'] = 'Users with teaching capabilities have access to the course';
@@ -1919,22 +1919,22 @@ If \'plain text area\' is selected, a format for text input areas such as HTML o
 The list of available text editors is determined by the site administrator.';
 $string['texteditor'] = 'Use standard web forms';
 $string['textformat'] = 'Plain text format';
-$string['timesplitting:deciles'] = 'Deciles';
-$string['timesplitting:deciles_help'] = '';
-$string['timesplitting:decilesaccum'] = 'Deciles accumulative';
-$string['timesplitting:decilesaccum_help'] = '';
+$string['timesplitting:deciles'] = 'Tenths';
+$string['timesplitting:deciles_help'] = 'This time-splitting method divides the course into tenths (10 equal parts), with each prediction being based on the data of only the most recent previous tenth.';
+$string['timesplitting:decilesaccum'] = 'Tenths accumulative';
+$string['timesplitting:decilesaccum_help'] = 'This time-splitting method divides the course into tenths (10 equal parts), with each prediction being based on the data of all previous tenths.';
 $string['timesplitting:nosplitting'] = 'No time splitting';
-$string['timesplitting:nosplitting_help'] = '';
+$string['timesplitting:nosplitting_help'] = 'No time-splitting method is defined for this model.';
 $string['timesplitting:quarters'] = 'Quarters';
-$string['timesplitting:quarters_help'] = '';
+$string['timesplitting:quarters_help'] = 'This time-splitting method divides the course into quarters (4 equal parts), with each prediction being based on the data of only the most recent previous quarter.';
 $string['timesplitting:quartersaccum'] = 'Quarters accumulative';
-$string['timesplitting:quartersaccum_help'] = '';
+$string['timesplitting:quartersaccum_help'] = 'This time-splitting method divides the course into quarters (4 equal parts), with each prediction being based on the data of all previous quarters.';
 $string['timesplitting:singlerange'] = 'Single range';
-$string['timesplitting:singlerange_help'] = '';
+$string['timesplitting:singlerange_help'] = 'This time-splitting method considers the entire course as a single span.';
 $string['timesplitting:weekly'] = 'Weekly';
-$string['timesplitting:weekly_help'] = '';
+$string['timesplitting:weekly_help'] = 'This time-splitting method divides the course into weeks (7 days), with each prediction being based on the data of only the most recent previous week.';
 $string['timesplitting:weeklyaccum'] = 'Weekly accumulative';
-$string['timesplitting:weeklyaccum_help'] = '';
+$string['timesplitting:weeklyaccum_help'] = 'This time-splitting method divides the course into weeks (7 days), with each prediction being based on the data of all previous weeks.';
 $string['thanks'] = 'Thanks';
 $string['theme'] = 'Theme';
 $string['themes'] = 'Themes';
index 53825f5..a978f52 100644 (file)
@@ -154,8 +154,8 @@ $string['license'] = 'License';
 $string['linkexternal'] = 'Link external';
 $string['listview'] = 'View as list';
 $string['loading'] = 'Loading...';
-$string['login'] = 'Login to your account';
-$string['logintoaccount'] = 'Login to your {$a} account';
+$string['login'] = 'Log in to your account';
+$string['logintoaccount'] = 'Log in to your {$a} account';
 $string['logout'] = 'Logout';
 $string['lostsource'] = 'Error. Source is missing. {$a}';
 $string['makefileinternal'] = 'Make a copy of the file';
@@ -190,7 +190,7 @@ $string['overwriteall'] = 'Overwrite all';
 $string['path'] = 'Path';
 $string['plugin'] = 'Repository plug-ins';
 $string['pluginerror'] = 'Errors in repository plugin.';
-$string['popup'] = 'Click "Login" button to login';
+$string['popup'] = 'Click "Login" button to log in';
 $string['popupblockeddownload'] = 'The downloading window is blocked, please allow the popup window, and try again.';
 $string['preview'] = 'Preview';
 $string['privatefilesof'] = '{$a} Private files';
index 057f9f6..aeb331f 100644 (file)
@@ -131,7 +131,7 @@ $string['norequiredcapability'] = 'No required capability';
 $string['notoken'] = 'The token list is empty.';
 $string['onesystemcontrolling'] = 'Allow an external system to control Moodle';
 $string['onesystemcontrollingdescription'] = 'The following steps help you to set up the Moodle web services to allow an external system to interact with Moodle. This includes setting up a token (security key) authentication method.';
-$string['onlyseecreatedtokens'] = 'Only tokens you own or created can be seen. You can still delete other tokens.';
+$string['onlyseecreatedtokens'] = 'Any token can be deleted, though you can only view tokens that you created.';
 $string['operation'] = 'Operation';
 $string['optional'] = 'Optional';
 $string['passwordisexpired'] = 'Password is expired.';
index 905b621..68df031 100644 (file)
Binary files a/lib/amd/build/modal_confirm.min.js and b/lib/amd/build/modal_confirm.min.js differ
index 9b2eaab..5fc8341 100644 (file)
Binary files a/lib/amd/build/modal_events.min.js and b/lib/amd/build/modal_events.min.js differ
index 02e0604..04cb7e1 100644 (file)
Binary files a/lib/amd/build/modal_factory.min.js and b/lib/amd/build/modal_factory.min.js differ
index 2464ba2..4a6c31b 100644 (file)
Binary files a/lib/amd/build/permissionmanager.min.js and b/lib/amd/build/permissionmanager.min.js differ
index 171c433..f396c8e 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Contain the logic for the save/cancel modal.
+ * Contain the logic for the yes/no confirmation modal.
+ * This has been deprecated and should not be used anymore. Please use core/modal_save_cancel instead.
+ * See MDL-59759.
  *
- * @module     core/modal_save_cancel
- * @class      modal_save_cancel
+ * @deprecated Since Moodle 3.4
+ * @module     core/modal_confirm
+ * @class      modal_confirm
  * @package    core
  * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/notification', 'core/custom_interaction_events', 'core/modal', 'core/modal_events'],
-        function($, Notification, CustomEvents, Modal, ModalEvents) {
-
-    var SELECTORS = {
-        YES_BUTTON: '[data-action="yes"]',
-        NO_BUTTON: '[data-action="no"]',
-    };
+define(['jquery', 'core/custom_interaction_events', 'core/modal_events', 'core/modal_save_cancel', 'core/log'],
+        function($, CustomEvents, ModalEvents, ModalSaveCancel, Log) {
 
     /**
      * Constructor for the Modal.
@@ -36,57 +34,13 @@ define(['jquery', 'core/notification', 'core/custom_interaction_events', 'core/m
      * @param {object} root The root jQuery element for the modal
      */
     var ModalConfirm = function(root) {
-        Modal.call(this, root);
-
-        if (!this.getFooter().find(SELECTORS.YES_BUTTON).length) {
-            Notification.exception({message: 'No "yes" button found'});
-        }
-
-        if (!this.getFooter().find(SELECTORS.NO_BUTTON).length) {
-            Notification.exception({message: 'No "no" button found'});
-        }
+        Log.warn("The CONFIRM modal type has been deprecated and should not be used anymore." +
+            " Please use the SAVE_CANCEL modal type instead.");
+        ModalSaveCancel.call(this, root);
     };
 
-    ModalConfirm.prototype = Object.create(Modal.prototype);
+    ModalConfirm.prototype = Object.create(ModalSaveCancel.prototype);
     ModalConfirm.prototype.constructor = ModalConfirm;
 
-    /**
-     * Override parent implementation to prevent changing the footer content.
-     */
-    ModalConfirm.prototype.setFooter = function() {
-        Notification.exception({message: 'Can not change the footer of a confirm modal'});
-        return;
-    };
-
-    /**
-     * Set up all of the event handling for the modal.
-     *
-     * @method registerEventListeners
-     */
-    ModalConfirm.prototype.registerEventListeners = function() {
-        // Apply parent event listeners.
-        Modal.prototype.registerEventListeners.call(this);
-
-        this.getModal().on(CustomEvents.events.activate, SELECTORS.YES_BUTTON, function(e, data) {
-            var yesEvent = $.Event(ModalEvents.yes);
-            this.getRoot().trigger(yesEvent, this);
-
-            if (!yesEvent.isDefaultPrevented()) {
-                this.hide();
-                data.originalEvent.preventDefault();
-            }
-        }.bind(this));
-
-        this.getModal().on(CustomEvents.events.activate, SELECTORS.NO_BUTTON, function(e, data) {
-            var noEvent = $.Event(ModalEvents.no);
-            this.getRoot().trigger(noEvent, this);
-
-            if (!noEvent.isDefaultPrevented()) {
-                this.hide();
-                data.originalEvent.preventDefault();
-            }
-        }.bind(this));
-    };
-
     return ModalConfirm;
 });
index bbd3979..9c26179 100644 (file)
@@ -32,8 +32,9 @@ define([], function() {
         // ModalSaveCancel events.
         save: 'modal-save-cancel:save',
         cancel: 'modal-save-cancel:cancel',
-        // ModalConfirm events.
-        yes: 'modal-confirm:yes',
-        no: 'modal-confirm:no',
+        // ModalConfirm events. Deprecated since Moodle 3.4. See MDL-59759.
+        // Point core/modal_confirm events to save/cancel events of core/modal_save_cancel.
+        yes: 'modal-save-cancel:save',
+        no: 'modal-save-cancel:cancel',
     };
 });
index f9db208..770ecdf 100644 (file)
@@ -32,7 +32,8 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
     var TEMPLATES = {
         DEFAULT: 'core/modal',
         SAVE_CANCEL: 'core/modal_save_cancel',
-        CONFIRM: 'core/modal_confirm',
+        // Deprecated since Moodle 3.4. Point template to core/modal_save_cancel instead. See MDL-59759.
+        CONFIRM: 'core/modal_save_cancel',
         CANCEL: 'core/modal_cancel',
     };
 
@@ -40,6 +41,7 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
     var TYPES = {
         DEFAULT: 'DEFAULT',
         SAVE_CANCEL: 'SAVE_CANCEL',
+        // Deprecated since Moodle 3.4. See MDL-59759.
         CONFIRM: 'CONFIRM',
         CANCEL: 'CANCEL',
     };
@@ -47,6 +49,7 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
     // Register the common set of modals.
     ModalRegistry.register(TYPES.DEFAULT, Modal, TEMPLATES.DEFAULT);
     ModalRegistry.register(TYPES.SAVE_CANCEL, ModalSaveCancel, TEMPLATES.SAVE_CANCEL);
+    // Deprecated since Moodle 3.4. See MDL-59759.
     ModalRegistry.register(TYPES.CONFIRM, ModalConfirm, TEMPLATES.CONFIRM);
     ModalRegistry.register(TYPES.CANCEL, ModalCancel, TEMPLATES.CANCEL);
 
index 6c76ec4..dcd8712 100644 (file)
@@ -104,11 +104,13 @@ define(['jquery', 'core/config', 'core/notification', 'core/templates', 'core/yu
                         templatedata.spanclass = 'allowed';
                         templatedata.linkclass = 'preventlink';
                         templatedata.action = 'prevent';
+                        templatedata.icon = 't/delete';
                         break;
                     case 'prohibit':
                         templatedata.spanclass = 'forbidden';
                         templatedata.linkclass = 'unprohibitlink';
                         templatedata.action = 'unprohibit';
+                        templatedata.icon = 't/delete';
                         break;
                     case 'prevent':
                         row.find('a[data-role-id="' + roleid + '"]').first().closest('.allowed').remove();
index 855be38..b773632 100644 (file)
@@ -115,7 +115,7 @@ class course_dropout extends \core_analytics\local\target\binary {
     }
 
     /**
-     * is_valid_analysable
+     * Discards courses that are not yet ready to be used for training or prediction.
      *
      * @param \core_analytics\analysable $course
      * @param bool $fortraining
@@ -147,13 +147,22 @@ class course_dropout extends \core_analytics\local\target\binary {
             return get_string('nocourseendtime');
         }
 
+        if ($course->get_end() < $course->get_start()) {
+            return get_string('errorendbeforestart', 'analytics');
+        }
+
+        // A course that lasts longer than 1 year probably have wrong start or end dates.
+        if ($course->get_end() - $course->get_start() > (YEARSECS + (WEEKSECS * 4))) {
+            return get_string('coursetoolong', 'analytics');
+        }
+
         // Ongoing courses data can not be used to train.
         if ($fortraining && !$course->is_finished()) {
             return get_string('coursenotyetfinished');
         }
 
         if ($fortraining) {
-            // Not a valid target for training if there are not enough course accesses.
+            // Not a valid target for training if there are not enough course accesses between the course start and end dates.
 
             $params = array('courseid' => $course->get_id(), 'anonymous' => 0, 'start' => $course->get_start(),
                 'end' => $course->get_end());
@@ -178,14 +187,39 @@ class course_dropout extends \core_analytics\local\target\binary {
     }
 
     /**
-     * All student enrolments are valid.
+     * Discard student enrolments that are invalid.
      *
      * @param int $sampleid
      * @param \core_analytics\analysable $course
      * @param bool $fortraining
-     * @return true|string
+     * @return bool
      */
     public function is_valid_sample($sampleid, \core_analytics\analysable $course, $fortraining = true) {
+
+        $userenrol = $this->retrieve('user_enrolments', $sampleid);
+        if ($userenrol->timeend && $course->get_start() > $userenrol->timeend) {
+            // Discard enrolments which time end is prior to the course start. This should get rid of
+            // old user enrolments that remain on the course.
+            return false;
+        }
+
+        $limit = $course->get_start() - (YEARSECS + (WEEKSECS * 4));
+        if (($userenrol->timestart && $userenrol->timestart < $limit) ||
+                (!$userenrol->timestart && $userenrol->timecreated < $limit)) {
+            // Following what we do in is_valid_sample, we will discard enrolments that last more than 1 academic year
+            // because they have incorrect start and end dates or because they are reused along multiple years
+            // without removing previous academic years students. This may not be very accurate because some courses
+            // can last just some months, but it is better than nothing and they will be flagged as drop out anyway
+            // in most of the cases.
+            return false;
+        }
+
+        if (($userenrol->timestart && $userenrol->timestart > $course->get_end()) ||
+                (!$userenrol->timestart && $userenrol->timecreated > $course->get_end())) {
+            // Discard user enrolments that starts after the analysable official end.
+            return false;
+        }
+
         return true;
     }
 
index ef54503..bacd39a 100644 (file)
@@ -63,6 +63,9 @@ class no_teaching extends \core_analytics\local\target\binary {
      * @return \core_analytics\prediction_action[]
      */
     public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false) {
+        global $CFG;
+
+        require_once($CFG->dirroot . '/course/lib.php');
 
         // No need to call the parent as the parent's action is view details and this target only have 1 feature.
         $actions = array();
@@ -75,7 +78,7 @@ class no_teaching extends \core_analytics\local\target\binary {
         $actions['viewcourse'] = new \core_analytics\prediction_action('viewcourse', $prediction,
             $url, $pix, get_string('view'));
 
-        if (has_any_capability(['moodle/course:viewparticipants', 'moodle/course:enrolreview'], $sampledata['context'])) {
+        if (course_can_view_participants($sampledata['context'])) {
             $url = new \moodle_url('/user/index.php', array('id' => $course->id));
             $pix = new \pix_icon('i/cohort', get_string('participants'));
             $actions['viewparticipants'] = new \core_analytics\prediction_action('viewparticipants', $prediction,
@@ -134,7 +137,7 @@ class no_teaching extends \core_analytics\local\target\binary {
      * @param int $sampleid
      * @param \core_analytics\analysable $analysable
      * @param bool $fortraining
-     * @return true|string
+     * @return bool
      */
     public function is_valid_sample($sampleid, \core_analytics\analysable $analysable, $fortraining = true) {
 
index 106878b..b19b689 100644 (file)
@@ -125,13 +125,30 @@ class manager {
         $DB->delete_records_select('task_scheduled', $sql, $params);
     }
 
+    /**
+     * Checks if the task with the same classname, component and customdata is already scheduled
+     *
+     * @param adhoc_task $task
+     * @return bool
+     */
+    protected static function task_is_scheduled($task) {
+        global $DB;
+        $record = self::record_from_adhoc_task($task);
+        $params = [$record->classname, $record->component, $record->customdata];
+        $sql = 'classname = ? AND component = ? AND ' .
+            $DB->sql_compare_text('customdata', \core_text::strlen($record->customdata) + 1) . ' = ?';
+        return $DB->record_exists_select('task_adhoc', $sql, $params);
+    }
+
     /**
      * Queue an adhoc task to run in the background.
      *
      * @param \core\task\adhoc_task $task - The new adhoc task information to store.
+     * @param bool $checkforexisting - If set to true and the task with the same classname, component and customdata
+     *     is already scheduled then it will not schedule a new task. Can be used only for ASAP tasks.
      * @return boolean - True if the config was saved.
      */
-    public static function queue_adhoc_task(adhoc_task $task) {
+    public static function queue_adhoc_task(adhoc_task $task, $checkforexisting = false) {
         global $DB;
 
         $record = self::record_from_adhoc_task($task);
@@ -139,6 +156,13 @@ class manager {
         if (!$task->get_next_run_time()) {
             $record->nextruntime = time() - 1;
         }
+
+        // Check if the same task is already scheduled.
+        if ($checkforexisting && self::task_is_scheduled($task)) {
+            return false;
+        }
+
+        // Queue the task.
         $result = $DB->insert_record('task_adhoc', $record);
 
         return $result;
index e5edde1..d8c8ef1 100644 (file)
@@ -1839,6 +1839,7 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
         }
 
         if ($coursesids) {
+            require_once($CFG->dirroot.'/course/lib.php');
             if (!move_courses($coursesids, $newparentid)) {
                 if ($showfeedback) {
                     echo $OUTPUT->notification("Error moving courses");
index 86c450c..7b3a37a 100644 (file)
@@ -500,7 +500,7 @@ $capabilities = array(
 
     'moodle/user:delete' => array(
 
-        'riskbitmask' => RISK_PERSONAL, RISK_DATALOSS,
+        'riskbitmask' => RISK_PERSONAL | RISK_DATALOSS,
 
         'captype' => 'write',
         'contextlevel' => CONTEXT_SYSTEM,
index edf6258..1fbec98 100644 (file)
@@ -2115,6 +2115,55 @@ class core_ddl_testcase extends database_driver_testcase {
         }
     }
 
+    /**
+     * Data provider for test_get_enc_quoted().
+     *
+     * @return array The type-value pair fixture.
+     */
+    public function test_get_enc_quoted_provider() {
+        return array(
+            // Reserved: some examples from SQL-92.
+            [true, 'from'],
+            [true, 'table'],
+            [true, 'where'],
+            // Not reserved.
+            [false, 'my_awesome_column_name']
+        );
+    }
+
+    /**
+     * This is a test for sql_generator::getEncQuoted().
+     *
+     * @dataProvider test_get_enc_quoted_provider
+     * @param string $reserved Whether the column name is reserved or not.
+     * @param string $columnname The column name to be quoted, according to the value of $reserved.
+     **/
+    public function test_get_enc_quoted($reserved, $columnname) {
+        $DB = $this->tdb;
+        $gen = $DB->get_manager()->generator;
+
+        if (!$reserved) {
+            // No need to quote the column name.
+            $this->assertSame($columnname, $gen->getEncQuoted($columnname));
+        } else {
+            // Column name should be quoted.
+            $dbfamily = $DB->get_dbfamily();
+
+            switch ($dbfamily) {
+                case 'mysql':
+                    $this->assertSame("`$columnname`", $gen->getEncQuoted($columnname));
+                    break;
+                case 'mssql': // The Moodle connection runs under 'QUOTED_IDENTIFIER ON'.
+                case 'oracle':
+                case 'postgres':
+                case 'sqlite':
+                default:
+                    $this->assertSame('"' . $columnname . '"', $gen->getEncQuoted($columnname));
+                    break;
+            }
+        }
+    }
+
     // Following methods are not supported == Do not test.
     /*
         public function testRenameIndex() {
index 3dd3586..8741823 100644 (file)
@@ -2398,7 +2398,8 @@ abstract class moodle_database {
         // NOTE: override this methods if following standard compliant SQL
         //       does not work for your driver.
 
-        $columnname = $column->name;
+        // Enclose the column name by the proper quotes if it's a reserved word.
+        $columnname = $this->get_manager()->generator->getEncQuoted($column->name);
         $sql = "UPDATE {".$table."}
                        SET $columnname = REPLACE($columnname, ?, ?)
                      WHERE $columnname IS NOT NULL";
index 849fbf3..8786585 100644 (file)
@@ -4573,19 +4573,55 @@ class core_dml_testcase extends database_driver_testcase {
         $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
         $table->add_field('name', XMLDB_TYPE_CHAR, '20', null, null);
         $table->add_field('intro', XMLDB_TYPE_TEXT, 'big', null, null);
+        // Add a CHAR field named using a word reserved for all the supported DB servers.
+        $table->add_field('where', XMLDB_TYPE_CHAR, '20', null, null, null, 'localhost');
+        // Add a TEXT field named using a word reserved for all the supported DB servers.
+        $table->add_field('from', XMLDB_TYPE_TEXT, 'big', null, null);
         $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
         $dbman->create_table($table);
 
-        $id1 = (string)$DB->insert_record($tablename, array('name' => null, 'intro' => null));
-        $id2 = (string)$DB->insert_record($tablename, array('name' => '', 'intro' => ''));
-        $id3 = (string)$DB->insert_record($tablename, array('name' => 'xxyy', 'intro' => 'vvzz'));
-        $id4 = (string)$DB->insert_record($tablename, array('name' => 'aa bb aa bb', 'intro' => 'cc dd cc aa'));
-        $id5 = (string)$DB->insert_record($tablename, array('name' => 'kkllll', 'intro' => 'kkllll'));
+        $fromfield = $dbman->generator->getEncQuoted('from');
+        $DB->execute("INSERT INTO {".$tablename."} (name,intro,$fromfield) VALUES (NULL,NULL,'localhost')");
+        $DB->execute("INSERT INTO {".$tablename."} (name,intro,$fromfield) VALUES ('','','localhost')");
+        $DB->execute("INSERT INTO {".$tablename."} (name,intro,$fromfield) VALUES ('xxyy','vvzz','localhost')");
+        $DB->execute("INSERT INTO {".$tablename."} (name,intro,$fromfield) VALUES ('aa bb aa bb','cc dd cc aa','localhost')");
+        $DB->execute("INSERT INTO {".$tablename."} (name,intro,$fromfield) VALUES ('kkllll','kkllll','localhost')");
 
         $expected = $DB->get_records($tablename, array(), 'id ASC');
+        $idx = 1;
+        $id1 = $id2 = $id3 = $id4 = $id5 = 0;
+        foreach (array_keys($expected) as $identifier) {
+            ${"id$idx"} = (string)$identifier;
+            $idx++;
+        }
 
         $columns = $DB->get_columns($tablename);
 
+        // Replace should work even with columns named using a reserved word.
+        $this->assertEquals('C', $columns['where']->meta_type);
+        $this->assertEquals('localhost', $expected[$id1]->where);
+        $this->assertEquals('localhost', $expected[$id2]->where);
+        $this->assertEquals('localhost', $expected[$id3]->where);
+        $this->assertEquals('localhost', $expected[$id4]->where);
+        $this->assertEquals('localhost', $expected[$id5]->where);
+        $DB->replace_all_text($tablename, $columns['where'], 'localhost', '::1');
+        $result = $DB->get_records($tablename, array(), 'id ASC');
+        $expected[$id1]->where = '::1';
+        $expected[$id2]->where = '::1';
+        $expected[$id3]->where = '::1';
+        $expected[$id4]->where = '::1';
+        $expected[$id5]->where = '::1';
+        $this->assertEquals($expected, $result);
+        $this->assertEquals('X', $columns['from']->meta_type);
+        $DB->replace_all_text($tablename, $columns['from'], 'localhost', '127.0.0.1');
+        $result = $DB->get_records($tablename, array(), 'id ASC');
+        $expected[$id1]->from = '127.0.0.1';
+        $expected[$id2]->from = '127.0.0.1';
+        $expected[$id3]->from = '127.0.0.1';
+        $expected[$id4]->from = '127.0.0.1';
+        $expected[$id5]->from = '127.0.0.1';
+        $this->assertEquals($expected, $result);
+
         $DB->replace_all_text($tablename, $columns['name'], 'aa', 'o');
         $result = $DB->get_records($tablename, array(), 'id ASC');
         $expected[$id4]->name = 'o bb o bb';
index 8f99d9d..11b68c8 100644 (file)
@@ -60,6 +60,9 @@ require_once("$CFG->libdir/filebrowser/file_info_context_module.php");
 */
 class file_browser {
 
+    /** @var array cached list of enrolled courses.  */
+    protected $enrolledcourses = null;
+
     /**
      * Looks up file_info instance
      *
@@ -190,7 +193,7 @@ class file_browser {
     /**
      * Returns info about the files at Course category context
      *
-     * @param stdClass $context context object
+     * @param context $context context object
      * @param string $component component
      * @param string $filearea file area
      * @param int $itemid item ID
@@ -199,39 +202,36 @@ class file_browser {
      * @return file_info|null file_info instance or null if not found or access not allowed
      */
     private function get_file_info_context_module($context, $component, $filearea, $itemid, $filepath, $filename) {
-        global $COURSE, $DB, $CFG;
-
-        static $cachedmodules = array();
-
-        if (!array_key_exists($context->instanceid, $cachedmodules)) {
-            $cachedmodules[$context->instanceid] = get_coursemodule_from_id('', $context->instanceid);
-        }
-
-        if (!($cm = $cachedmodules[$context->instanceid])) {
+        if (!($context instanceof context_module)) {
             return null;
         }
+        $coursecontext = $context->get_course_context();
+        $modinfo = get_fast_modinfo($coursecontext->instanceid);
+        $cm = $modinfo->get_cm($context->instanceid);
 
-        if ($cm->course == $COURSE->id) {
-            $course = $COURSE;
-        } else if (!$course = $DB->get_record('course', array('id'=>$cm->course))) {
+        if (empty($cm->uservisible)) {
             return null;
         }
 
-        $modinfo = get_fast_modinfo($course);
-        if (empty($modinfo->cms[$cm->id]->uservisible)) {
-            return null;
-        }
-
-        $modname = $modinfo->cms[$cm->id]->modname;
-
-        if (!file_exists("$CFG->dirroot/mod/$modname/lib.php")) {
-            return null;
-        }
-
-        // ok, we know that module exists, and user may access it
-
-        $level = new file_info_context_module($this, $context, $course, $cm, $modname);
+        $level = new file_info_context_module($this, $context, $cm->get_course(), $cm, $cm->modname);
         return $level->get_file_info($component, $filearea, $itemid, $filepath, $filename);
     }
 
+    /**
+     * Check if user is enrolled into the course
+     *
+     * This function keeps a cache of enrolled courses because it may be called multiple times for many courses in one request
+     *
+     * @param int $courseid
+     * @return bool
+     */
+    public function is_enrolled($courseid) {
+        if ($this->enrolledcourses === null || PHPUNIT_TEST) {
+            // Since get_file_browser() returns a statically cached object we can't rely on cache
+            // inside the file_browser class in the unittests.
+            // TODO MDL-59964 remove this caching when it's implemented inside enrol_get_my_courses().
+            $this->enrolledcourses = enrol_get_my_courses(['id']);
+        }
+        return array_key_exists($courseid, $this->enrolledcourses);
+    }
 }
index 67f842c..3e9ffa3 100644 (file)
@@ -36,6 +36,9 @@ class file_info_context_course extends file_info {
     /** @var stdClass course object */
     protected $course;
 
+    /** @var file_info_context_module[] cached child modules. See {@link get_child_module()} */
+    protected $childrenmodules = [];
+
     /**
      * Constructor
      *
@@ -68,7 +71,7 @@ class file_info_context_course extends file_info {
             return null;
         }
 
-        if (!is_viewing($this->context) and !is_enrolled($this->context)) {
+        if (!is_viewing($this->context) and !$this->browser->is_enrolled($this->course->id)) {
             // no peaking here if not enrolled or inspector
             return null;
         }
@@ -86,6 +89,41 @@ class file_info_context_course extends file_info {
         return null;
     }
 
+    /**
+     * Returns list of areas inside this course
+     *
+     * @param string $extensions Only return areas that have files with these extensions
+     * @param bool $returnemptyfolders return all areas always, if true it will ignore the previous argument
+     * @return array
+     */
+    protected function get_course_areas($extensions = '*', $returnemptyfolders = false) {
+        global $DB;
+
+        $allareas = [
+            'course_summary',
+            'course_overviewfiles',
+            'course_section',
+            'backup_section',
+            'backup_course',
+            'backup_automated',
+            'course_legacy'
+        ];
+
+        if ($returnemptyfolders) {
+            return $allareas;
+        }
+
+        $params1 = ['contextid' => $this->context->id, 'emptyfilename' => '.'];
+        $sql1 = "SELECT " . $DB->sql_concat('f.component', "'_'", 'f.filearea') . "
+            FROM {files} f
+            WHERE f.filename <> :emptyfilename AND f.contextid = :contextid ";
+        $sql3 = ' GROUP BY f.component, f.filearea';
+        list($sql2, $params2) = $this->build_search_files_sql($extensions);
+        $areaswithfiles = $DB->get_fieldset_sql($sql1 . $sql2 . $sql3, array_merge($params1, $params2));
+
+        return array_intersect($allareas, $areaswithfiles);
+    }
+
     /**
      * Gets a stored file for the course summary filearea directory
      *
@@ -387,6 +425,28 @@ class file_info_context_course extends file_info {
         return $this->get_filtered_children('*', false, true);
     }
 
+    /**
+     * Returns the child module if it is accessible by the current user
+     *
+     * @param cm_info|int $cm
+     * @return file_info_context_module|null
+     */
+    protected function get_child_module($cm) {
+        $cmid = is_object($cm) ? $cm->id : $cm;
+        if (!array_key_exists($cmid, $this->childrenmodules)) {
+            $this->childrenmodules[$cmid] = null;
+            if (!($cm instanceof cm_info)) {
+                $cms = get_fast_modinfo($this->course)->cms;
+                $cm = array_key_exists($cmid, $cms) ? $cms[$cmid] : null;
+            }
+            if ($cm && $cm->uservisible) {
+                $this->childrenmodules[$cmid] = new file_info_context_module($this->browser,
+                    $cm->context, $this->course, $cm, $cm->modname);
+            }
+        }
+        return $this->childrenmodules[$cmid];
+    }
+
     /**
      * Help function to return files matching extensions or their count
      *
@@ -397,46 +457,52 @@ class file_info_context_course extends file_info {
      * @return array|int array of file_info instances or the count
      */
     private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) {
-        $areas = array(
-            array('course', 'summary'),
-            array('course', 'overviewfiles'),
-            array('course', 'section'),
-            array('backup', 'section'),
-            array('backup', 'course'),
-            array('backup', 'automated'),
-            array('course', 'legacy')
-        );
         $children = array();
-        foreach ($areas as $area) {
+
+        $courseareas = $this->get_course_areas($extensions, $returnemptyfolders);
+        foreach ($courseareas as $areaname) {
+            $area = explode('_', $areaname, 2);
             if ($child = $this->get_file_info($area[0], $area[1], 0, '/', '.')) {
-                if ($returnemptyfolders || $child->count_non_empty_children($extensions)) {
-                    $children[] = $child;
-                    if (($countonly !== false) && count($children) >= $countonly) {
-                        return $countonly;
-                    }
+                $children[] = $child;
+                if (($countonly !== false) && count($children) >= $countonly) {
+                    return $countonly;
                 }
             }
         }
 
+        $cnt = count($children);
         if (!has_capability('moodle/course:managefiles', $this->context)) {
             // 'managefiles' capability is checked in every activity module callback.
             // Don't even waste time on retrieving the modules if we can't browse the files anyway
         } else {
-            // now list all modules
-            $modinfo = get_fast_modinfo($this->course);
-            foreach ($modinfo->cms as $cminfo) {
-                if (empty($cminfo->uservisible)) {
-                    continue;
-                }
-                $modcontext = context_module::instance($cminfo->id, IGNORE_MISSING);
-                if ($child = $this->browser->get_file_info($modcontext)) {
-                    if ($returnemptyfolders || $child->count_non_empty_children($extensions)) {
+            if ($returnemptyfolders) {
+                $modinfo = get_fast_modinfo($this->course);
+                foreach ($modinfo->cms as $cminfo) {
+                    if ($child = $this->get_child_module($cminfo)) {
                         $children[] = $child;
-                        if (($countonly !== false) && count($children) >= $countonly) {
-                            return $countonly;
+                        $cnt++;
+                    }
+                }
+            } else if ($moduleareas = $this->get_module_areas_with_files($extensions)) {
+                // We found files in some of the modules.
+                // Create array of children modules ordered with the same way as cms in modinfo.
+                $modulechildren = array_fill_keys(array_keys(get_fast_modinfo($this->course)->get_cms()), null);
+                foreach ($moduleareas as $area) {
+                    if ($modulechildren[$area->cmid]) {
+                        // We already found non-empty area within the same module, do not analyse other areas.
+                        continue;
+                    }
+                    if ($child = $this->get_child_module($area->cmid)) {
+                        if ($child->get_file_info($area->component, $area->filearea, $area->itemid, null, null)) {
+                            $modulechildren[$area->cmid] = $child;
+                            $cnt++;
+                            if (($countonly !== false) && $cnt >= $countonly) {
+                                return $cnt;
+                            }
                         }
                     }
                 }
+                $children = array_merge($children, array_values(array_filter($modulechildren)));
             }
         }
 
@@ -446,6 +512,52 @@ class file_info_context_course extends file_info {
         return $children;
     }
 
+    /**
+     * Returns list of areas inside the course modules that have files with the given extension
+     *
+     * @param string $extensions
+     * @return array
+     */
+    protected function get_module_areas_with_files($extensions = '*') {
+        global $DB;
+
+        $params1 = ['contextid' => $this->context->id,
+            'emptyfilename' => '.',
+            'contextlevel' => CONTEXT_MODULE,
+            'depth' => $this->context->depth + 1,
+            'pathmask' => $this->context->path . '/%'];
+        $sql1 = "SELECT ctx.id AS contextid, f.component, f.filearea, f.itemid, ctx.instanceid AS cmid, " .
+                context_helper::get_preload_record_columns_sql('ctx') . "
+            FROM {files} f
+            INNER JOIN {context} ctx ON ctx.id = f.contextid
+            WHERE f.filename <> :emptyfilename
+              AND ctx.contextlevel = :contextlevel
+              AND ctx.depth = :depth
+              AND " . $DB->sql_like('ctx.path', ':pathmask') . " ";
+        $sql3 = ' GROUP BY ctx.id, f.component, f.filearea, f.itemid, ctx.instanceid,
+              ctx.path, ctx.depth, ctx.contextlevel
+            ORDER BY ctx.id, f.component, f.filearea, f.itemid';
+        list($sql2, $params2) = $this->build_search_files_sql($extensions);
+        $areas = [];
+        if ($rs = $DB->get_recordset_sql($sql1. $sql2 . $sql3, array_merge($params1, $params2))) {
+            foreach ($rs as $record) {
+                context_helper::preload_from_record($record);
+                $areas[] = $record;
+            }
+            $rs->close();
+        }
+
+        // Sort areas so 'backup' and 'intro' are in the beginning of the list, they are the easiest to check access to.
+        usort($areas, function($a, $b) {
+            $aeasy = ($a->filearea === 'intro' && substr($a->component, 0, 4) === 'mod_') ||
+                ($a->filearea === 'activity' && $a->component === 'backup');
+            $beasy = ($b->filearea === 'intro' && substr($b->component, 0, 4) === 'mod_') ||
+                ($b->filearea === 'activity' && $b->component === 'backup');
+            return $aeasy == $beasy ? 0 : ($aeasy ? -1 : 1);
+        });
+        return $areas;
+    }
+
     /**
      * Returns list of children which are either files matching the specified extensions
      * or folders that contain at least one such file.
index 7c354bd..d0b4746 100644 (file)
@@ -97,6 +97,10 @@ class file_info_context_coursecat extends file_info {
     protected function get_area_coursecat_description($itemid, $filepath, $filename) {
         global $CFG;
 
+        if (!$this->category->id) {
+            // No coursecat description area for "system".
+            return null;
+        }
         if (!$this->category->visible and !has_capability('moodle/category:viewhiddencategories', $this->context)) {
             return null;
         }
@@ -158,37 +162,92 @@ class file_info_context_coursecat extends file_info {
      * @return array of file_info instances
      */
     public function get_children() {
-        global $DB;
-
         $children = array();
 
         if ($child = $this->get_area_coursecat_description(0, '/', '.')) {
             $children[] = $child;
         }
 
-        $course_cats = $DB->get_records('course_categories', array('parent'=>$this->category->id), 'sortorder', 'id,visible');
-        foreach ($course_cats as $category) {
+        list($coursecats, $hiddencats) = $this->get_categories();
+        foreach ($coursecats as $category) {
             $context = context_coursecat::instance($category->id);
-            if (!$category->visible and !has_capability('moodle/category:viewhiddencategories', $context)) {
-                continue;
-            }
-            if ($child = $this->browser->get_file_info($context)) {
-                $children[] = $child;
-            }
+            $children[] = new self($this->browser, $context, $category);
         }
 
-        $courses = $DB->get_records('course', array('category'=>$this->category->id), 'sortorder', 'id,visible');
+        $courses = $this->get_courses($hiddencats);
         foreach ($courses as $course) {
-            $context = context_course::instance($course->id);
-            if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $context)) {
-                continue;
-            }
-            if ($child = $this->browser->get_file_info($context)) {
-                $children[] = $child;
+            $children[] = $this->get_child_course($course);
+        }
+
+        return array_filter($children);
+    }
+
+    /**
+     * List of courses in this category and in hidden subcategories
+     *
+     * @param array $hiddencats list of categories that are hidden from current user and returned by {@link get_categories()}
+     * @return array list of courses
+     */
+    protected function get_courses($hiddencats) {
+        global $DB, $CFG;
+        require_once($CFG->libdir.'/modinfolib.php');
+
+        $params = array('category' => $this->category->id, 'contextlevel' => CONTEXT_COURSE);
+        $sql = 'c.category = :category';
+
+        foreach ($hiddencats as $category) {
+            $catcontext = context_coursecat::instance($category->id);
+            $sql .= ' OR ' . $DB->sql_like('ctx.path', ':path' . $category->id);
+            $params['path' . $category->id] = $catcontext->path . '/%';
+        }
+
+        // Let's retrieve only minimum number of fields from course table -
+        // what is needed to check access or call get_fast_modinfo().
+        $coursefields = array_merge(['id', 'visible'], course_modinfo::$cachedfields);
+        $fields = 'c.' . join(',c.', $coursefields) . ', ' .
+            context_helper::get_preload_record_columns_sql('ctx');
+        return $DB->get_records_sql('SELECT ' . $fields . ' FROM {course} c
+                JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)
+                WHERE ('.$sql.') ORDER BY c.sortorder', $params);
+    }
+
+    /**
+     * Finds accessible and non-accessible direct subcategories
+     *
+     * @return array [$coursecats, $hiddencats] - child categories that are visible to the current user and not visible
+     */
+    protected function get_categories() {
+        global $DB;
+        $fields = 'c.*, ' . context_helper::get_preload_record_columns_sql('ctx');
+        $coursecats = $DB->get_records_sql('SELECT ' . $fields . ' FROM {course_categories} c
+                LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)
+                WHERE c.parent = :parent ORDER BY c.sortorder',
+            array('parent' => $this->category->id, 'contextlevel' => CONTEXT_COURSECAT));
+
+        $hiddencats = [];
+
+        foreach ($coursecats as $id => &$category) {
+            context_helper::preload_from_record($category);
+            $context = context_coursecat::instance($category->id);
+            if (!$category->visible && !has_capability('moodle/category:viewhiddencategories', $context)) {
+                $hiddencats[$id] = $coursecats[$id];
+                unset($coursecats[$id]);
             }
         }
+        return [$coursecats, $hiddencats];
+    }
 
-        return $children;
+    /**
+     * Returns the file info element for a given course or null if course is not accessible
+     *
+     * @param stdClass $course may contain context fields for preloading
+     * @return file_info_context_course|null
+     */
+    protected function get_child_course($course) {
+        context_helper::preload_from_record($course);
+        $context = context_course::instance($course->id);
+        $child = new file_info_context_course($this->browser, $context, $course);
+        return $child->get_file_info(null, null, null, null, null);
     }
 
     /**
@@ -200,53 +259,33 @@ class file_info_context_coursecat extends file_info {
      * @return int
      */
     public function count_non_empty_children($extensions = '*', $limit = 1) {
-        global $DB;
         $cnt = 0;
-        if (($child = $this->get_area_coursecat_description(0, '/', '.'))
-                && $child->count_non_empty_children($extensions) && (++$cnt) >= $limit) {
-            return $cnt;
+        if ($child = $this->get_area_coursecat_description(0, '/', '.')) {
+            $cnt += $child->count_non_empty_children($extensions) ? 1 : 0;
+            if ($cnt >= $limit) {
+                return $cnt;
+            }
         }
 
-        $rs = $DB->get_recordset_sql('SELECT ctx.id AS contextid, c.visible
-                FROM {context} ctx, {course} c
-                WHERE ctx.instanceid = c.id
-                AND ctx.contextlevel = :courselevel
-                AND c.category = :categoryid
-                ORDER BY c.visible DESC', // retrieve visible courses first
-                array('categoryid' => $this->category->id, 'courselevel' => CONTEXT_COURSE));
-        foreach ($rs as $record) {
-            $context = context::instance_by_id($record->contextid);
-            if (!$record->visible and !has_capability('moodle/course:viewhiddencourses', $context)) {
-                continue;
-            }
-            if (($child = $this->browser->get_file_info($context))
-                    && $child->count_non_empty_children($extensions) && (++$cnt) >= $limit) {
-                break;
+        list($coursecats, $hiddencats) = $this->get_categories();
+        foreach ($coursecats as $category) {
+            $context = context_coursecat::instance($category->id);
+            $child = new file_info_context_coursecat($this->browser, $context, $category);
+            $cnt += $child->count_non_empty_children($extensions) ? 1 : 0;
+            if ($cnt >= $limit) {
+                return $cnt;
             }
         }
-        $rs->close();
-        if ($cnt >= $limit) {
-            return $cnt;
-        }
 
-        $rs = $DB->get_recordset_sql('SELECT ctx.id AS contextid, cat.visible
-                FROM {context} ctx, {course_categories} cat
-                WHERE ctx.instanceid = cat.id
-                AND ctx.contextlevel = :catlevel
-                AND cat.parent = :categoryid
-                ORDER BY cat.visible DESC', // retrieve visible categories first
-                array('categoryid' => $this->category->id, 'catlevel' => CONTEXT_COURSECAT));
-        foreach ($rs as $record) {
-            $context = context::instance_by_id($record->contextid);
-            if (!$record->visible and !has_capability('moodle/category:viewhiddencategories', $context)) {
-                continue;
-            }
-            if (($child = $this->browser->get_file_info($context))
-                    && $child->count_non_empty_children($extensions) && (++$cnt) >= $limit) {
-                break;
+        $courses = $this->get_courses($hiddencats);
+        foreach ($courses as $course) {
+            if ($child = $this->get_child_course($course)) {
+                $cnt += $child->count_non_empty_children($extensions) ? 1 : 0;
+                if ($cnt >= $limit) {
+                    return $cnt;
+                }
             }
         }
-        $rs->close();
 
         return $cnt;
     }
index c62f83a..c2c068f 100644 (file)
@@ -35,7 +35,7 @@ defined('MOODLE_INTERNAL') || die();
 class file_info_context_module extends file_info {
     /** @var stdClass Course object */
     protected $course;
-    /** @var stdClass Course module object */
+    /** @var cm_info Course module object */
     protected $cm;
     /** @var string Module name */
     protected $modname;
@@ -58,23 +58,17 @@ class file_info_context_module extends file_info {
 
         parent::__construct($browser, $context);
         $this->course  = $course;
-        $this->cm      = $cm;
-        $this->modname = $modname;
+        $this->cm      = cm_info::create($cm);
+        $this->modname = $this->cm->modname;
         $this->nonemptychildren = null;
 
-        include_once("$CFG->dirroot/mod/$modname/lib.php");
-
-        //find out all supported areas
-        $functionname     = 'mod_'.$modname.'_get_file_areas';
-        $functionname_old = $modname.'_get_file_areas';
-
-        if (function_exists($functionname)) {
+        if ($functionname = component_callback_exists('mod_'.$modname, 'get_file_areas')) {
+            $cm = $this->cm->get_course_module_record();
             $this->areas = $functionname($course, $cm, $context);
-        } else if (function_exists($functionname_old)) {
-            $this->areas = $functionname_old($course, $cm, $context);
         } else {
             $this->areas = array();
         }
+
         unset($this->areas['intro']); // hardcoded, ignore attempts to override it
     }
 
@@ -99,14 +93,12 @@ class file_info_context_module extends file_info {
             return null;
         }
 
-        if (!is_viewing($this->context) and !is_enrolled($this->context)) {
+        if (!is_viewing($this->context) and !$this->browser->is_enrolled($this->course->id)) {
             // no peaking here if not enrolled or inspector
             return null;
         }
 
-        $modinfo = get_fast_modinfo($this->course);
-        $cminfo = $modinfo->get_cm($this->cm->id);
-        if (!$cminfo->uservisible) {
+        if (!$this->cm->uservisible) {
             // activity hidden sorry
             return null;
         }
@@ -121,13 +113,10 @@ class file_info_context_module extends file_info {
             return $this->get_area_backup($itemid, $filepath, $filename);
         }
 
-        $functionname     = 'mod_'.$this->modname.'_get_file_info';
-        $functionname_old = $this->modname.'_get_file_info';
-
-        if (function_exists($functionname)) {
-            return $functionname($this->browser, $this->areas, $this->course, $this->cm, $this->context, $filearea, $itemid, $filepath, $filename);
-        } else if (function_exists($functionname_old)) {
-            return $functionname_old($this->browser, $this->areas, $this->course, $this->cm, $this->context, $filearea, $itemid, $filepath, $filename);
+        if ($functionname = component_callback_exists('mod_'.$this->modname, 'get_file_info')) {
+            $cm = $this->cm->get_course_module_record();
+            return $functionname($this->browser, $this->areas, $this->course, $cm,
+                $this->context, $filearea, $itemid, $filepath, $filename);
         }
 
         return null;
@@ -206,7 +195,7 @@ class file_info_context_module extends file_info {
      * @return string
      */
     public function get_visible_name() {
-        return $this->cm->name.' ('.get_string('modulename', $this->cm->modname).')';
+        return $this->cm->get_formatted_name().' ('.$this->cm->get_module_type_name().')';
     }
 
     /**
index 27144c0..a7c4f28 100644 (file)
@@ -25,6 +25,8 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+require_once($CFG->libdir.'/filebrowser/file_info_context_coursecat.php');
+
 /**
  * Represents the system context in the tree navigated by {@link file_browser}.
  *
@@ -32,7 +34,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2008 Petr Skoda (http://skodak.org)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class file_info_context_system extends file_info {
+class file_info_context_system extends file_info_context_coursecat {
 
     /**
      * Constructor
@@ -41,7 +43,7 @@ class file_info_context_system extends file_info {
      * @param stdClass $context context object
      */
     public function __construct($browser, $context) {
-        parent::__construct($browser, $context);
+        parent::__construct($browser, $context, (object)['id' => 0, 'parent' => 0, 'visible' => 1]);
     }
 
     /**
@@ -139,71 +141,6 @@ class file_info_context_system extends file_info {
         return true;
     }
 
-    /**
-     * Returns list of children.
-     *
-     * @return array of file_info instances
-     */
-    public function get_children() {
-        global $DB;
-
-        $children = array();
-
-        // Add course categories on the top level that are either visible or user is able to view hidden categories.
-        $course_cats = $DB->get_records('course_categories', array('parent'=>0), 'sortorder', 'id,visible');
-        foreach ($course_cats as $category) {
-            $context = context_coursecat::instance($category->id);
-            if (!$category->visible and !has_capability('moodle/category:viewhiddencategories', $context)) {
-                continue;
-            }
-            if ($child = $this->browser->get_file_info($context)) {
-                $children[] = $child;
-            }
-        }
-
-        // Add courses where user is enrolled that are located in hidden course categories because they would not
-        // be present in the above tree but user may still be able to access files in them.
-        if ($hiddencontexts = $this->get_inaccessible_coursecat_contexts()) {
-            $courses = enrol_get_my_courses();
-            foreach ($courses as $course) {
-                $context = context_course::instance($course->id);
-                $parents = $context->get_parent_context_ids();
-                if (array_intersect($hiddencontexts, $parents)) {
-                    // This course has hidden parent category.
-                    if ($child = $this->browser->get_file_info($context)) {
-                        $children[] = $child;
-                    }
-                }
-            }
-        }
-
-        return $children;
-    }
-
-    /**
-     * Returns list of course categories contexts that current user can not see
-     *
-     * @return array array of course categories contexts ids
-     */
-    protected function get_inaccessible_coursecat_contexts() {
-        global $DB;
-
-        $sql = context_helper::get_preload_record_columns_sql('ctx');
-        $records = $DB->get_records_sql("SELECT ctx.id, $sql
-            FROM {course_categories} c
-            JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = ?
-            WHERE c.visible = ?", [CONTEXT_COURSECAT, 0]);
-        $hiddencontexts = [];
-        foreach ($records as $record) {
-            context_helper::preload_from_record($record);
-            $context = context::instance_by_id($record->id);
-            if (!has_capa