Merge branch 'MDL-60667_master' of git://github.com/dmonllao/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Mon, 6 Nov 2017 05:27:11 +0000 (13:27 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Mon, 6 Nov 2017 05:27:11 +0000 (13:27 +0800)
23 files changed:
blocks/html/block_html.php
calendar/classes/local/event/forms/create.php
calendar/classes/local/event/forms/eventtype.php [new file with mode: 0644]
calendar/classes/local/event/forms/managesubscriptions.php [moved from calendar/managesubscriptions_form.php with 80% similarity]
calendar/externallib.php
calendar/lib.php
calendar/managesubscriptions.php
calendar/renderer.php
calendar/tests/events_test.php
calendar/tests/externallib_test.php
calendar/tests/lib_test.php
course/classes/analytics/indicator/completion_enabled.php
lib/amd/build/modal_factory.min.js
lib/amd/src/modal_factory.js
lib/classes/event/calendar_subscription_created.php
lib/classes/event/calendar_subscription_deleted.php
lib/classes/event/calendar_subscription_updated.php
lib/db/install.xml
lib/db/upgrade.php
report/insights/classes/output/insights_list.php
report/insights/lang/en/report_insights.php
report/insights/templates/insights_list.mustache
version.php

index 2183370..505ff27 100644 (file)
@@ -37,7 +37,11 @@ class block_html extends block_base {
     }
 
     function specialization() {
-        $this->title = isset($this->config->title) ? format_string($this->config->title) : format_string(get_string('newhtmlblock', 'block_html'));
+        if (isset($this->config->title)) {
+            $this->title = $this->title = format_string($this->config->title, true, ['context' => $this->context]);
+        } else {
+            $this->title = get_string('newhtmlblock', 'block_html');
+        }
     }
 
     function instance_allow_multiple() {
index 0c7a824..1e36407 100644 (file)
@@ -37,6 +37,8 @@ require_once($CFG->dirroot.'/lib/formslib.php');
  */
 class create extends \moodleform {
 
+    use eventtype;
+
     /**
      * Build the editor options using the given context.
      *
@@ -183,102 +185,6 @@ class create extends \moodleform {
         $mform->setDefault('visible', 1);
     }
 
-    /**
-     * Add the appropriate elements for the available event types.
-     *
-     * If the only event type available is 'user' then we add a hidden
-     * element because there is nothing for the user to choose.
-     *
-     * If more than one type is available then we add the elements as
-     * follows:
-     *      - Always add the event type selector
-     *      - Elements per type:
-     *          - course: add an additional select element with each
-     *                    course as an option.
-     *          - group: add a select element for the course (different
-     *                   from the above course select) and a select
-     *                   element for the group.
-     *
-     * @param MoodleQuickForm $mform
-     * @param array $eventtypes The available event types for the user
-     */
-    protected function add_event_type_elements($mform, $eventtypes) {
-        $options = [];
-
-        if (isset($eventtypes['user'])) {
-            $options['user'] = get_string('user');
-        }
-        if (isset($eventtypes['group'])) {
-            $options['group'] = get_string('group');
-        }
-        if (isset($eventtypes['course'])) {
-            $options['course'] = get_string('course');
-        }
-        if (isset($eventtypes['category'])) {
-            $options['category'] = get_string('category');
-        }
-        if (isset($eventtypes['site'])) {
-            $options['site'] = get_string('site');
-        }
-
-        // If we only have one event type and it's 'user' event then don't bother
-        // rendering the select boxes because there is no choice for the user to
-        // make.
-        if (count(array_keys($eventtypes)) == 1 && isset($eventtypes['user'])) {
-            $mform->addElement('hidden', 'eventtype');
-            $mform->setType('eventtype', PARAM_TEXT);
-            $mform->setDefault('eventtype', 'user');
-
-            // Render a static element to tell the user what type of event will
-            // be created.
-            $mform->addElement('static', 'staticeventtype', get_string('eventkind', 'calendar'), $options['user']);
-            return;
-        } else {
-            $mform->addElement('select', 'eventtype', get_string('eventkind', 'calendar'), $options);
-        }
-
-        if (isset($eventtypes['category'])) {
-            $categoryoptions = [];
-            foreach ($eventtypes['category'] as $id => $category) {
-                $categoryoptions[$id] = $category;
-            }
-
-            $mform->addElement('select', 'categoryid', get_string('category'), $categoryoptions);
-            $mform->hideIf('categoryid', 'eventtype', 'noteq', 'category');
-        }
-
-        if (isset($eventtypes['course'])) {
-            $limit = !has_capability('moodle/calendar:manageentries', context_system::instance());
-            $mform->addElement('course', 'courseid', get_string('course'), ['limittoenrolled' => $limit]);
-            $mform->hideIf('courseid', 'eventtype', 'noteq', 'course');
-        }
-
-        if (isset($eventtypes['group'])) {
-            $options = ['limittoenrolled' => true];
-            // Exclude courses without group.
-            if (isset($eventtypes['course']) && isset($eventtypes['groupcourses'])) {
-                $options['exclude'] = array_diff(array_keys($eventtypes['course']),
-                    array_keys($eventtypes['groupcourses']));
-            }
-
-            $mform->addElement('course', 'groupcourseid', get_string('course'), $options);
-            $mform->hideIf('groupcourseid', 'eventtype', 'noteq', 'group');
-
-            $groupoptions = [];
-            foreach ($eventtypes['group'] as $group) {
-                // We are formatting it this way in order to provide the javascript both
-                // the course and group ids so that it can enhance the form for the user.
-                $index = "{$group->courseid}-{$group->id}";
-                $groupoptions[$index] = format_string($group->name, true,
-                    ['context' => \context_course::instance($group->courseid)]);
-            }
-
-            $mform->addElement('select', 'groupid', get_string('group'), $groupoptions);
-            $mform->hideIf('groupid', 'eventtype', 'noteq', 'group');
-            // We handle the group select hide/show actions on the event_form module.
-        }
-    }
-
     /**
      * Add the various elements to express the duration options available
      * for an event.
diff --git a/calendar/classes/local/event/forms/eventtype.php b/calendar/classes/local/event/forms/eventtype.php
new file mode 100644 (file)
index 0000000..59a9cd7
--- /dev/null
@@ -0,0 +1,131 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The trait for adding eventtype fields to a form.
+ *
+ * @package     core_calendar
+ * @copyright   2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_calendar\local\event\forms;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The trait for adding eventtype fields to a form.
+ *
+ * @copyright   2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+trait eventtype {
+
+    /**
+     * Add the appropriate elements for the available event types.
+     *
+     * If the only event type available is 'user' then we add a hidden
+     * element because there is nothing for the user to choose.
+     *
+     * If more than one type is available then we add the elements as
+     * follows:
+     *      - Always add the event type selector
+     *      - Elements per type:
+     *          - course: add an additional select element with each
+     *                    course as an option.
+     *          - group: add a select element for the course (different
+     *                   from the above course select) and a select
+     *                   element for the group.
+     *
+     * @param MoodleQuickForm $mform
+     * @param array $eventtypes The available event types for the user
+     */
+    protected function add_event_type_elements($mform, $eventtypes) {
+        $options = [];
+
+        if (isset($eventtypes['user'])) {
+            $options['user'] = get_string('user');
+        }
+        if (isset($eventtypes['group'])) {
+            $options['group'] = get_string('group');
+        }
+        if (isset($eventtypes['course'])) {
+            $options['course'] = get_string('course');
+        }
+        if (isset($eventtypes['category'])) {
+            $options['category'] = get_string('category');
+        }
+        if (isset($eventtypes['site'])) {
+            $options['site'] = get_string('site');
+        }
+
+        // If we only have one event type and it's 'user' event then don't bother
+        // rendering the select boxes because there is no choice for the user to
+        // make.
+        if (count(array_keys($eventtypes)) == 1 && isset($eventtypes['user'])) {
+            $mform->addElement('hidden', 'eventtype');
+            $mform->setType('eventtype', PARAM_TEXT);
+            $mform->setDefault('eventtype', 'user');
+
+            // Render a static element to tell the user what type of event will
+            // be created.
+            $mform->addElement('static', 'staticeventtype', get_string('eventkind', 'calendar'), $options['user']);
+            return;
+        } else {
+            $mform->addElement('select', 'eventtype', get_string('eventkind', 'calendar'), $options);
+        }
+
+        if (isset($eventtypes['category'])) {
+            $categoryoptions = [];
+            foreach ($eventtypes['category'] as $id => $category) {
+                $categoryoptions[$id] = $category;
+            }
+
+            $mform->addElement('select', 'categoryid', get_string('category'), $categoryoptions);
+            $mform->hideIf('categoryid', 'eventtype', 'noteq', 'category');
+        }
+
+        if (isset($eventtypes['course'])) {
+            $limit = !has_capability('moodle/calendar:manageentries', \context_system::instance());
+            $mform->addElement('course', 'courseid', get_string('course'), ['limittoenrolled' => $limit]);
+            $mform->hideIf('courseid', 'eventtype', 'noteq', 'course');
+        }
+
+        if (isset($eventtypes['group'])) {
+            $options = ['limittoenrolled' => true];
+            // Exclude courses without group.
+            if (isset($eventtypes['course']) && isset($eventtypes['groupcourses'])) {
+                $options['exclude'] = array_diff(array_keys($eventtypes['course']),
+                    array_keys($eventtypes['groupcourses']));
+            }
+
+            $mform->addElement('course', 'groupcourseid', get_string('course'), $options);
+            $mform->hideIf('groupcourseid', 'eventtype', 'noteq', 'group');
+
+            $groupoptions = [];
+            foreach ($eventtypes['group'] as $group) {
+                // We are formatting it this way in order to provide the javascript both
+                // the course and group ids so that it can enhance the form for the user.
+                $index = "{$group->courseid}-{$group->id}";
+                $groupoptions[$index] = format_string($group->name, true,
+                    ['context' => \context_course::instance($group->courseid)]);
+            }
+
+            $mform->addElement('select', 'groupid', get_string('group'), $groupoptions);
+            $mform->hideIf('groupid', 'eventtype', 'noteq', 'group');
+            // We handle the group select hide/show actions on the event_form module.
+        }
+    }
+}
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  * @package calendar
  */
+namespace core_calendar\local\event\forms;
 
-if (!defined('MOODLE_INTERNAL')) {
-    die('Direct access to this script is forbidden.');    ///  It must be included from a Moodle page
-}
+defined('MOODLE_INTERNAL') || die();
 
-require_once($CFG->libdir.'/formslib.php');
+require_once($CFG->libdir . '/formslib.php');
 
 /**
  * Form for adding a subscription to a Moodle course calendar.
  * @copyright 2012 Jonathan Harker
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class calendar_addsubscription_form extends moodleform {
+class managesubscriptions extends \moodleform {
+
+    use eventtype;
 
     /**
      * Defines the form used to add calendar subscriptions.
      */
     public function definition() {
         $mform = $this->_form;
-        $courseid = optional_param('course', 0, PARAM_INT);
+        $eventtypes = calendar_get_all_allowed_types();
+        if (empty($eventtypes)) {
+            print_error('nopermissiontoupdatecalendar');
+        }
 
         $mform->addElement('header', 'addsubscriptionform', get_string('importcalendarheading', 'calendar'));
 
@@ -77,24 +81,10 @@ class calendar_addsubscription_form extends moodleform {
         $mform->disabledIf('url',  'importfrom', 'eq', CALENDAR_IMPORT_FROM_FILE);
         $mform->disabledIf('importfile', 'importfrom', 'eq', CALENDAR_IMPORT_FROM_URL);
 
-        // Eventtype: 0 = user, 1 = global, anything else = course ID.
-        list($choices, $groups) = calendar_get_eventtype_choices($courseid);
-        $mform->addElement('select', 'eventtype', get_string('eventkind', 'calendar'), $choices);
-        $mform->addRule('eventtype', get_string('required'), 'required');
-        $mform->setType('eventtype', PARAM_ALPHA);
-
-        if (!empty($groups) and is_array($groups)) {
-            $groupoptions = array();
-            foreach ($groups as $group) {
-                $groupoptions[$group->id] = $group->name;
-            }
-            $mform->addElement('select', 'groupid', get_string('typegroup', 'calendar'), $groupoptions);
-            $mform->setType('groupid', PARAM_INT);
-            $mform->disabledIf('groupid', 'eventtype', 'noteq', 'group');
-        }
+        // Add the select elements for the available event types.
+        $this->add_event_type_elements($mform, $eventtypes);
 
-        $mform->addElement('hidden', 'course');
-        $mform->setType('course', PARAM_INT);
+        // Eventtype: 0 = user, 1 = global, anything else = course ID.
         $mform->addElement('submit', 'add', get_string('add'));
     }
 
@@ -110,6 +100,14 @@ class calendar_addsubscription_form extends moodleform {
 
         $errors = parent::validation($data, $files);
 
+        $coursekey = isset($data['groupcourseid']) ? 'groupcourseid' : 'courseid';
+        $eventtypes = calendar_get_all_allowed_types();
+        $eventtype = isset($data['eventtype']) ? $data['eventtype'] : null;
+
+        if (empty($eventtype) || !isset($eventtypes[$eventtype])) {
+            $errors['eventtype'] = get_string('invalideventtype', 'calendar');
+        }
+
         if ($data['importfrom'] == CALENDAR_IMPORT_FROM_FILE) {
             if (empty($data['importfile'])) {
                 $errors['importfile'] = get_string('errorrequiredurlorfile', 'calendar');
@@ -117,7 +115,7 @@ class calendar_addsubscription_form extends moodleform {
                 // Make sure the file area is not empty and contains only one file.
                 $draftitemid = $data['importfile'];
                 $fs = get_file_storage();
-                $usercontext = context_user::instance($USER->id);
+                $usercontext = \context_user::instance($USER->id);
                 $files = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id DESC', false);
                 if (count($files) !== 1) {
                     $errors['importfile'] = get_string('errorrequiredurlorfile', 'calendar');
@@ -140,7 +138,7 @@ class calendar_addsubscription_form extends moodleform {
     public function definition_after_data() {
         $mform =& $this->_form;
 
-        $mform->applyFilter('url', 'calendar_addsubscription_form::strip_webcal');
+        $mform->applyFilter('url', static::class . '::strip_webcal');
         $mform->applyFilter('url', 'trim');
     }
 
index ce344ee..0ccbf31 100644 (file)
@@ -203,6 +203,8 @@ class core_calendar_external extends external_api {
             $courses = $params['events']['courseids'];
             $funcparam['courses'] = $courses;
         }
+        // Now get categories we can get events from.
+        $categories = \coursecat::get_all();
 
         // Let us findout groups that we can return events from.
         if (!$hassystemcap) {
@@ -273,7 +275,8 @@ class core_calendar_external extends external_api {
             } else {
                 // Can the user actually see this event?
                 $eventobj = calendar_event::load($eventobj);
-                if (($eventobj->courseid == $SITE->id) ||
+                if ((($eventobj->courseid == $SITE->id) && (empty($eventobj->categoryid))) ||
+                            (!empty($eventobj->categoryid) && in_array($eventobj->categoryid, $categories)) ||
                             (!empty($eventobj->groupid) && in_array($eventobj->groupid, $groups)) ||
                             (!empty($eventobj->courseid) && in_array($eventobj->courseid, $courses)) ||
                             ($USER->id == $eventobj->userid) ||
@@ -723,6 +726,14 @@ class core_calendar_external extends external_api {
         $warnings = array();
 
         $legacyevent = calendar_event::load($eventid);
+        // Must check we can see this event.
+        if (!calendar_view_event_allowed($legacyevent)) {
+            // We can't return a warning in this case because the event is not optional.
+            // We don't know the context for the event and it's not worth loading it.
+            $syscontext = context_system::instance();
+            throw new \required_capability_exception($syscontext, 'moodle/course:view', 'nopermission', '');
+        }
+
         $legacyevent->count_repeats();
 
         $eventmapper = event_container::get_event_mapper();
@@ -976,7 +987,7 @@ class core_calendar_external extends external_api {
         $calendar = \calendar_information::create($time, $params['courseid'], $params['categoryid']);
         self::validate_context($calendar->context);
 
-        list($data, $template) = calendar_get_view($calendar, 'day', $params['includenavigation']);
+        list($data, $template) = calendar_get_view($calendar, 'day');
 
         return $data;
     }
index 78386e2..8e76d89 100644 (file)
@@ -2113,6 +2113,91 @@ function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false) {
     return array($courses, $group, $user);
 }
 
+/**
+ * Return the capability for viewing a calendar event.
+ *
+ * @param calendar_event $event event object
+ * @return boolean
+ */
+function calendar_view_event_allowed(calendar_event $event) {
+    global $USER;
+
+    // Anyone can see site events.
+    if ($event->courseid && $event->courseid == SITEID) {
+        return true;
+    }
+
+    // If a user can manage events at the site level they can see any event.
+    $sitecontext = \context_system::instance();
+    // If user has manageentries at site level, return true.
+    if (has_capability('moodle/calendar:manageentries', $sitecontext)) {
+        return true;
+    }
+
+    if (!empty($event->groupid)) {
+        // If it is a group event we need to be able to manage events in the course, or be in the group.
+        if (has_capability('moodle/calendar:manageentries', $event->context) ||
+                has_capability('moodle/calendar:managegroupentries', $event->context)) {
+            return true;
+        }
+
+        $mycourses = enrol_get_my_courses('id');
+        return isset($mycourses[$event->courseid]) && groups_is_member($event->groupid);
+    } else if ($event->modulename) {
+        // If this is a module event we need to be able to see the module.
+        $coursemodules = [];
+        $courseid = 0;
+        // Override events do not have the courseid set.
+        if ($event->courseid) {
+            $courseid = $event->courseid;
+            $coursemodules = get_fast_modinfo($event->courseid)->instances;
+        } else {
+            $cmraw = get_coursemodule_from_instance($event->modulename, $event->instance, 0, false, MUST_EXIST);
+            $courseid = $cmraw->course;
+            $coursemodules = get_fast_modinfo($cmraw->course)->instances;
+        }
+        $hasmodule = isset($coursemodules[$event->modulename]);
+        $hasinstance = isset($coursemodules[$event->modulename][$event->instance]);
+
+        // If modinfo doesn't know about the module, return false to be safe.
+        if (!$hasmodule || !$hasinstance) {
+            return false;
+        }
+
+        // Must be able to see the course and the module - MDL-59304.
+        $cm = $coursemodules[$event->modulename][$event->instance];
+        if (!$cm->uservisible) {
+            return false;
+        }
+        $mycourses = enrol_get_my_courses('id');
+        return isset($mycourses[$courseid]);
+    } else if ($event->categoryid) {
+        // If this is a category we need to be able to see the category.
+        $cat = \coursecat::get($event->categoryid, IGNORE_MISSING);
+        if (!$cat) {
+            return false;
+        }
+        return true;
+    } else if (!empty($event->courseid)) {
+        // If it is a course event we need to be able to manage events in the course, or be in the course.
+        if (has_capability('moodle/calendar:manageentries', $event->context)) {
+            return true;
+        }
+        $mycourses = enrol_get_my_courses('id');
+        return isset($mycourses[$event->courseid]);
+    } else if ($event->userid) {
+        if ($event->userid != $USER->id) {
+            // No-one can ever see another users events.
+            return false;
+        }
+        return true;
+    } else {
+        throw new moodle_exception('unknown event type');
+    }
+
+    return false;
+}
+
 /**
  * Return the capability for editing calendar event.
  *
@@ -2664,12 +2749,36 @@ function calendar_get_eventtype_choices($courseid) {
 function calendar_add_subscription($sub) {
     global $DB, $USER, $SITE;
 
+    // Undo the form definition work around to allow us to have two different
+    // course selectors present depending on which event type the user selects.
+    if (!empty($sub->groupcourseid)) {
+        $sub->courseid = $sub->groupcourseid;
+        unset($sub->groupcourseid);
+    }
+
+    // Pull the group id back out of the value. The form saves the value
+    // as "<courseid>-<groupid>" to allow the javascript to work correctly.
+    if (!empty($sub->groupid)) {
+        list($courseid, $groupid) = explode('-', $sub->groupid);
+        $sub->courseid = $courseid;
+        $sub->groupid = $groupid;
+    }
+
+    // Default course id if none is set.
+    if (empty($sub->courseid)) {
+        if ($sub->eventtype === 'site') {
+            $sub->courseid = SITEID;
+        } else {
+            $sub->courseid = 0;
+        }
+    }
+
     if ($sub->eventtype === 'site') {
         $sub->courseid = $SITE->id;
     } else if ($sub->eventtype === 'group' || $sub->eventtype === 'course') {
-        $sub->courseid = $sub->course;
+        $sub->courseid = $sub->courseid;
     } else if ($sub->eventtype === 'category') {
-        $sub->categoryid = $sub->category;
+        $sub->categoryid = $sub->categoryid;
     } else {
         // User events.
         $sub->courseid = 0;
@@ -2689,8 +2798,25 @@ function calendar_add_subscription($sub) {
             // Trigger event, calendar subscription added.
             $eventparams = array('objectid' => $sub->id,
                 'context' => calendar_get_calendar_context($sub),
-                'other' => array('eventtype' => $sub->eventtype, 'courseid' => $sub->courseid)
+                'other' => array(
+                    'eventtype' => $sub->eventtype,
+                )
             );
+            switch ($sub->eventtype) {
+                case 'category':
+                    $eventparams['other']['categoryid'] = $sub->categoryid;
+                    break;
+                case 'course':
+                    $eventparams['other']['courseid'] = $sub->courseid;
+                    break;
+                case 'group':
+                    $eventparams['other']['courseid'] = $sub->courseid;
+                    $eventparams['other']['groupid'] = $sub->groupid;
+                    break;
+                default:
+                    $eventparams['other']['courseid'] = $sub->courseid;
+            }
+
             $event = \core\event\calendar_subscription_created::create($eventparams);
             $event->trigger();
             return $id;
@@ -2708,13 +2834,13 @@ function calendar_add_subscription($sub) {
  * Add an iCalendar event to the Moodle calendar.
  *
  * @param stdClass $event The RFC-2445 iCalendar event
- * @param int $courseid The course ID
+ * @param int $unused Deprecated
  * @param int $subscriptionid The iCalendar subscription ID
  * @param string $timezone The X-WR-TIMEZONE iCalendar property if provided
  * @throws dml_exception A DML specific exception is thrown for invalid subscriptionids.
  * @return int Code: CALENDAR_IMPORT_EVENT_UPDATED = updated,  CALENDAR_IMPORT_EVENT_INSERTED = inserted, 0 = error
  */
-function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timezone='UTC') {
+function calendar_add_icalendar_event($event, $unused = null, $subscriptionid, $timezone='UTC') {
     global $DB;
 
     // Probably an unsupported X-MICROSOFT-CDO-BUSYSTATUS event.
@@ -2787,6 +2913,7 @@ function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timez
     $eventrecord->userid = $sub->userid;
     $eventrecord->groupid = $sub->groupid;
     $eventrecord->courseid = $sub->courseid;
+    $eventrecord->categoryid = $sub->categoryid;
     $eventrecord->eventtype = $sub->eventtype;
 
     if ($updaterecord = $DB->get_record('event', array('uuid' => $eventrecord->uuid,
@@ -2867,8 +2994,24 @@ function calendar_delete_subscription($subscription) {
     // Trigger event, calendar subscription deleted.
     $eventparams = array('objectid' => $subscription->id,
         'context' => calendar_get_calendar_context($subscription),
-        'other' => array('courseid' => $subscription->courseid)
+        'other' => array(
+            'eventtype' => $subscription->eventtype,
+        )
     );
+    switch ($subscription->eventtype) {
+        case 'category':
+            $eventparams['other']['categoryid'] = $subscription->categoryid;
+            break;
+        case 'course':
+            $eventparams['other']['courseid'] = $subscription->courseid;
+            break;
+        case 'group':
+            $eventparams['other']['courseid'] = $subscription->courseid;
+            $eventparams['other']['groupid'] = $subscription->groupid;
+            break;
+        default:
+            $eventparams['other']['courseid'] = $subscription->courseid;
+    }
     $event = \core\event\calendar_subscription_deleted::create($eventparams);
     $event->trigger();
 }
@@ -2907,7 +3050,7 @@ function calendar_get_icalendar($url) {
  * @param int $subscriptionid The subscription ID.
  * @return string A log of the import progress, including errors.
  */
-function calendar_import_icalendar_events($ical, $courseid, $subscriptionid = null) {
+function calendar_import_icalendar_events($ical, $unused = null, $subscriptionid = null) {
     global $DB;
 
     $return = '';
@@ -2934,7 +3077,7 @@ function calendar_import_icalendar_events($ical, $courseid, $subscriptionid = nu
 
     $return = '';
     foreach ($ical->components['VEVENT'] as $event) {
-        $res = calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timezone);
+        $res = calendar_add_icalendar_event($event, null, $subscriptionid, $timezone);
         switch ($res) {
             case CALENDAR_IMPORT_EVENT_UPDATED:
                 $updatecount++;
@@ -2984,7 +3127,7 @@ function calendar_update_subscription_events($subscriptionid) {
     }
 
     $ical = calendar_get_icalendar($sub->url);
-    $return = calendar_import_icalendar_events($ical, $sub->courseid, $subscriptionid);
+    $return = calendar_import_icalendar_events($ical, null, $subscriptionid);
     $sub->lastupdated = time();
 
     calendar_update_subscription($sub);
@@ -3019,8 +3162,24 @@ function calendar_update_subscription($subscription) {
     $eventparams = array('userid' => $subscription->userid,
         'objectid' => $subscription->id,
         'context' => calendar_get_calendar_context($subscription),
-        'other' => array('eventtype' => $subscription->eventtype, 'courseid' => $subscription->courseid)
+        'other' => array(
+            'eventtype' => $subscription->eventtype,
+        )
     );
+    switch ($subscription->eventtype) {
+        case 'category':
+            $eventparams['other']['categoryid'] = $subscription->categoryid;
+            break;
+        case 'course':
+            $eventparams['other']['courseid'] = $subscription->courseid;
+            break;
+        case 'group':
+            $eventparams['other']['courseid'] = $subscription->courseid;
+            $eventparams['other']['groupid'] = $subscription->groupid;
+            break;
+        default:
+            $eventparams['other']['courseid'] = $subscription->courseid;
+    }
     $event = \core\event\calendar_subscription_updated::create($eventparams);
     $event->trigger();
 }
@@ -3042,9 +3201,14 @@ function calendar_can_edit_subscription($subscriptionorid) {
 
     $allowed = new \stdClass;
     $courseid = $subscription->courseid;
+    $categoryid = $subscription->categoryid;
     $groupid = $subscription->groupid;
+    $category = null;
 
-    calendar_get_allowed_types($allowed, $courseid);
+    if (!empty($categoryid)) {
+        $category = \coursecat::get($categoryid);
+    }
+    calendar_get_allowed_types($allowed, $courseid, null, $category);
     switch ($subscription->eventtype) {
         case 'user':
             return $allowed->user;
@@ -3054,6 +3218,12 @@ function calendar_can_edit_subscription($subscriptionorid) {
             } else {
                 return false;
             }
+        case 'category':
+            if (isset($allowed->categories[$categoryid])) {
+                return $allowed->categories[$categoryid];
+            } else {
+                return false;
+            }
         case 'site':
             return $allowed->site;
         case 'group':
index f1c74d6..75ddc09 100644 (file)
@@ -26,10 +26,10 @@ require_once('../config.php');
 require_once($CFG->libdir.'/bennu/bennu.inc.php');
 require_once($CFG->dirroot.'/course/lib.php');
 require_once($CFG->dirroot.'/calendar/lib.php');
-require_once($CFG->dirroot.'/calendar/managesubscriptions_form.php');
 
 // Required use.
-$courseid = optional_param('course', SITEID, PARAM_INT);
+$courseid = optional_param('course', null, PARAM_INT);
+$categoryid = optional_param('category', null, PARAM_INT);
 // Used for processing subscription actions.
 $subscriptionid = optional_param('id', 0, PARAM_INT);
 $pollinterval  = optional_param('pollinterval', 0, PARAM_INT);
@@ -39,6 +39,9 @@ $url = new moodle_url('/calendar/managesubscriptions.php');
 if ($courseid != SITEID) {
     $url->param('course', $courseid);
 }
+if ($categoryid) {
+    $url->param('categoryid', $categoryid);
+}
 navigation_node::override_active_url(new moodle_url('/calendar/view.php', array('view' => 'month')));
 $PAGE->set_url($url);
 $PAGE->set_pagelayout('admin');
@@ -58,7 +61,7 @@ if (!calendar_user_can_add_event($course)) {
     print_error('errorcannotimport', 'calendar');
 }
 
-$form = new calendar_addsubscription_form(null);
+$form = new \core_calendar\local\event\forms\managesubscriptions();
 $form->set_data(array(
     'course' => $course->id
 ));
@@ -75,7 +78,7 @@ if (!empty($formdata)) {
         $calendar = $form->get_file_content('importfile');
         $ical = new iCalendar();
         $ical->unserialize($calendar);
-        $importresults = calendar_import_icalendar_events($ical, $courseid, $subscriptionid);
+        $importresults = calendar_import_icalendar_events($ical, null, $subscriptionid);
     } else {
         try {
             $importresults = calendar_update_subscription_events($subscriptionid);
@@ -102,11 +105,54 @@ if (!empty($formdata)) {
     }
 }
 
-$sql = 'SELECT *
-          FROM {event_subscriptions}
-         WHERE courseid = :courseid
-            OR (courseid = 0 AND userid = :userid)';
-$params = array('courseid' => $courseid, 'userid' => $USER->id);
+$types = calendar_get_all_allowed_types();
+
+$searches = [];
+$params = [];
+
+$usedefaultfilters = true;
+if (!empty($courseid) && $courseid == SITEID && isset($types['site'])) {
+    $searches[] = "(eventtype = 'site')";
+    $searches[] = "(eventtype = 'user' AND userid = :userid)";
+    $params['userid'] = $USER->id;
+    $usedefaultfilters = false;
+}
+
+if (!empty($courseid) && isset($types['course']) && array_key_exists($courseid, $types['course'])) {
+    $searches[] = "((eventtype = 'course' OR eventtype = 'group') AND courseid = :courseid)";
+    $params += ['courseid' => $courseid];
+    $usedefaultfilters = false;
+}
+
+if (!empty($categoryid) && isset($types['category']) && array_key_exists($categoryid, $types['category'])) {
+    $searches[] = "(eventtype = 'category' AND categoryid = :categoryid)";
+    $params += ['categoryid' => $categoryid];
+    $usedefaultfilters = false;
+}
+
+if ($usedefaultfilters) {
+    $searches[] = "(eventtype = 'user' AND userid = :userid)";
+    $params['userid'] = $USER->id;
+
+    if (isset($types['site'])) {
+        $searches[] = "(eventtype = 'site' AND courseid  = :siteid)";
+        $params += ['siteid' => SITEID];
+    }
+
+    if (isset($types['course'])) {
+        list($courseinsql, $courseparams) = $DB->get_in_or_equal(array_keys($types['course']), SQL_PARAMS_NAMED, 'course');
+        $searches[] = "((eventtype = 'course' OR eventtype = 'group') AND courseid {$courseinsql})";
+        $params += $courseparams;
+    }
+
+    if (isset($types['category'])) {
+        list($categoryinsql, $categoryparams) = $DB->get_in_or_equal(array_keys($types['category']), SQL_PARAMS_NAMED, 'category');
+        $searches[] = "(eventtype = 'category' AND categoryid {$categoryinsql})";
+        $params += $categoryparams;
+    }
+}
+
+$sql = "SELECT * FROM {event_subscriptions} WHERE " . implode(' OR ', $searches);;
 $subscriptions = $DB->get_records_sql($sql, $params);
 
 // Print title and header.
index 7193a97..e8dadbe 100644 (file)
@@ -283,12 +283,12 @@ class core_calendar_renderer extends plugin_renderer_base {
     /**
      * Renders a table containing information about calendar subscriptions.
      *
-     * @param int $courseid
+     * @param int $unused
      * @param array $subscriptions
      * @param string $importresults
      * @return string
      */
-    public function subscription_details($courseid, $subscriptions, $importresults = '') {
+    public function subscription_details($unused = null, $subscriptions, $importresults = '') {
         $table = new html_table();
         $table->head  = array(
             get_string('colcalendar', 'calendar'),
@@ -318,7 +318,7 @@ class core_calendar_renderer extends plugin_renderer_base {
                 $lastupdated = userdate($sub->lastupdated, get_string('strftimedatetimeshort', 'langconfig'));
             }
 
-            $cell = new html_table_cell($this->subscription_action_form($sub, $courseid));
+            $cell = new html_table_cell($this->subscription_action_form($sub));
             $cell->colspan = 2;
             $type = $sub->eventtype . 'events';
 
@@ -342,10 +342,9 @@ class core_calendar_renderer extends plugin_renderer_base {
      * Creates a form to perform actions on a given subscription.
      *
      * @param stdClass $subscription
-     * @param int $courseid
      * @return string
      */
-    protected function subscription_action_form($subscription, $courseid) {
+    protected function subscription_action_form($subscription) {
         // Assemble form for the subscription row.
         $html = html_writer::start_tag('form', array('action' => new moodle_url('/calendar/managesubscriptions.php'), 'method' => 'post'));
         if (empty($subscription->url)) {
@@ -367,7 +366,6 @@ class core_calendar_renderer extends plugin_renderer_base {
             $html .= html_writer::end_tag('div');
         }
         $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
-        $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'course', 'value' => $courseid));
         $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'id', 'value' => $subscription->id));
         $html .= html_writer::start_tag('div', array('class' => 'btn-group pull-right'));
         if (!empty($subscription->url)) {
index 6f78848..5750d50 100644 (file)
@@ -423,9 +423,9 @@ class core_calendar_events_testcase extends advanced_testcase {
     }
 
     /**
-     * Tests for calendar_subscription_added event.
+     * Tests for calendar_subscription_added event for a site subscription.
      */
-    public function test_calendar_subscription_created() {
+    public function test_calendar_subscription_created_site() {
         global $CFG;
         require_once($CFG->dirroot . '/calendar/lib.php');
         $this->resetAfterTest(true);
@@ -447,15 +447,115 @@ class core_calendar_events_testcase extends advanced_testcase {
         $this->assertEquals($id, $event->objectid);
         $this->assertEquals($subscription->courseid, $event->other['courseid']);
         $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('categoryid', $event->other);
+        $this->assertArrayNotHasKey('groupid', $event->other);
         $this->assertDebuggingNotCalled();
         $sink->close();
+    }
+
+    /**
+     * Tests for calendar_subscription_added event for a category subscription.
+     */
+    public function test_calendar_subscription_created_category() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        $categoryid = $this->course->category;
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'category';
+        $subscription->name = 'test';
+        $subscription->categoryid = $categoryid;
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $id = calendar_add_subscription($subscription);
+
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_created', $event);
+        $this->assertEquals($id, $event->objectid);
+        $this->assertEquals($categoryid, $event->other['categoryid']);
+        $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('courseid', $event->other);
+        $this->assertArrayNotHasKey('groupid', $event->other);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+    }
+
+    /**
+     * Tests for calendar_subscription_added event for a course subscription.
+     */
+    public function test_calendar_subscription_created_course() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'course';
+        $subscription->name = 'test';
+        $subscription->courseid = $this->course->id;
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $id = calendar_add_subscription($subscription);
+
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_created', $event);
+        $this->assertEquals($id, $event->objectid);
+        $this->assertEquals($subscription->courseid, $event->other['courseid']);
+        $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('categoryid', $event->other);
+        $this->assertArrayNotHasKey('groupid', $event->other);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+
+    }
+
+    /**
+     * Tests for calendar_subscription_added event for a group subscription.
+     */
+    public function test_calendar_subscription_created_group() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        $courseid = $this->course->id;
+        $groupid = 42;
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'group';
+        $subscription->name = 'test';
+        $subscription->groupid = "{$courseid}-{$groupid}";
 
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $id = calendar_add_subscription($subscription);
+
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_created', $event);
+        $this->assertEquals($id, $event->objectid);
+        $this->assertEquals($courseid, $event->other['courseid']);
+        $this->assertEquals($groupid, $event->other['groupid']);
+        $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('categoryid', $event->other);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
     }
 
     /**
-     * Tests for calendar_subscription_updated event.
+     * Tests for calendar_subscription_updated event for a site subscription.
      */
-    public function test_calendar_subscription_updated() {
+    public function test_calendar_subscription_updated_site() {
         global $CFG;
         require_once($CFG->dirroot . '/calendar/lib.php');
         $this->resetAfterTest(true);
@@ -479,15 +579,120 @@ class core_calendar_events_testcase extends advanced_testcase {
         $this->assertEquals($subscription->id, $event->objectid);
         $this->assertEquals($subscription->courseid, $event->other['courseid']);
         $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('categoryid', $event->other);
+        $this->assertArrayNotHasKey('groupid', $event->other);
         $this->assertDebuggingNotCalled();
         $sink->close();
+    }
+
+    /**
+     * Tests for calendar_subscription_updated event for a category subscription.
+     */
+    public function test_calendar_subscription_updated_category() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        $categoryid = $this->course->category;
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'category';
+        $subscription->name = 'test';
+        $subscription->categoryid = $categoryid;
+        $subscription->id = calendar_add_subscription($subscription);
+        // Now edit it.
+        $subscription->name = 'awesome';
 
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        calendar_update_subscription($subscription);
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_updated', $event);
+        $this->assertEquals($subscription->id, $event->objectid);
+        $this->assertEquals($categoryid, $event->other['categoryid']);
+        $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('courseid', $event->other);
+        $this->assertArrayNotHasKey('groupid', $event->other);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
     }
 
     /**
-     * Tests for calendar_subscription_deleted event.
+     * Tests for calendar_subscription_updated event for a group subscription.
      */
-    public function test_calendar_subscription_deleted() {
+    public function test_calendar_subscription_updated_course() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'course';
+        $subscription->name = 'test';
+        $subscription->courseid = $this->course->id;
+        $subscription->id = calendar_add_subscription($subscription);
+        // Now edit it.
+        $subscription->name = 'awesome';
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        calendar_update_subscription($subscription);
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_updated', $event);
+        $this->assertEquals($subscription->id, $event->objectid);
+        $this->assertEquals($this->course->id, $event->other['courseid']);
+        $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('categoryid', $event->other);
+        $this->assertArrayNotHasKey('groupid', $event->other);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+    }
+
+    /**
+     * Tests for calendar_subscription_updated event for a course subscription.
+     */
+    public function test_calendar_subscription_updated_group() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        $courseid = $this->course->id;
+        $groupid = 42;
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'group';
+        $subscription->name = 'test';
+        $subscription->groupid = "{$courseid}-{$groupid}";
+        $subscription->id = calendar_add_subscription($subscription);
+        // Now edit it.
+        $subscription->name = 'awesome';
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        calendar_update_subscription($subscription);
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_updated', $event);
+        $this->assertEquals($subscription->id, $event->objectid);
+        $this->assertEquals($this->course->id, $event->other['courseid']);
+        $this->assertEquals($groupid, $event->other['groupid']);
+        $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('categoryid', $event->other);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+    }
+
+    /**
+     * Tests for calendar_subscription_deleted event for a site subscription.
+     */
+    public function test_calendar_subscription_deleted_site() {
         global $CFG;
         require_once($CFG->dirroot . '/calendar/lib.php');
         $this->resetAfterTest(true);
@@ -512,4 +717,102 @@ class core_calendar_events_testcase extends advanced_testcase {
         $sink->close();
 
     }
+
+    /**
+     * Tests for calendar_subscription_deleted event for a category subscription.
+     */
+    public function test_calendar_subscription_deleted_category() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        $categoryid = $this->course->category;
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'category';
+        $subscription->name = 'test';
+        $subscription->categoryid = $categoryid;
+        $subscription->id = calendar_add_subscription($subscription);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        calendar_delete_subscription($subscription);
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_deleted', $event);
+        $this->assertEquals($subscription->id, $event->objectid);
+        $this->assertEquals($categoryid, $event->other['categoryid']);
+        $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('courseid', $event->other);
+        $this->assertArrayNotHasKey('groupid', $event->other);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+    }
+
+    /**
+     * Tests for calendar_subscription_deleted event for a course.
+     */
+    public function test_calendar_subscription_deleted_course() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'course';
+        $subscription->name = 'test';
+        $subscription->courseid = $this->course->id;
+        $subscription->id = calendar_add_subscription($subscription);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        calendar_delete_subscription($subscription);
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_deleted', $event);
+        $this->assertEquals($subscription->id, $event->objectid);
+        $this->assertEquals($this->course->id, $event->other['courseid']);
+        $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('categoryid', $event->other);
+        $this->assertArrayNotHasKey('groupid', $event->other);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+    }
+
+    /**
+     * Tests for calendar_subscription_deleted event for a group.
+     */
+    public function test_calendar_subscription_deleted_group() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        $courseid = $this->course->id;
+        $groupid = 42;
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'group';
+        $subscription->name = 'test';
+        $subscription->groupid = "{$courseid}-{$groupid}";
+        $subscription->id = calendar_add_subscription($subscription);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        calendar_delete_subscription($subscription);
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_deleted', $event);
+        $this->assertEquals($subscription->id, $event->objectid);
+        $this->assertEquals($this->course->id, $event->other['courseid']);
+        $this->assertEquals($groupid, $event->other['groupid']);
+        $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertArrayNotHasKey('categoryid', $event->other);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+    }
 }
index f60d57f..6b2ee94 100644 (file)
@@ -2201,4 +2201,208 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
 
         $this->assertTrue($result['validationerror']);
     }
+
+    /**
+     * A user should not be able load the calendar monthly view for a course they cannot access.
+     */
+    public function test_get_calendar_monthly_view_no_course_permission() {
+        global $USER;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $generator = $this->getDataGenerator();
+        $user1 = $generator->create_user();
+        $user2 = $generator->create_user();
+        $course = $generator->create_course();
+        $generator->enrol_user($user1->id, $course->id, 'student');
+        $name = 'Course Event (course' . $course->id . ')';
+        $record = new stdClass();
+        $record->courseid = $course->id;
+        $courseevent = $this->create_calendar_event($name, $USER->id, 'course', 0, time(), $record);
+
+        $timestart = new DateTime();
+        // Admin can load the course.
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_monthly_view_returns(),
+            core_calendar_external::get_calendar_monthly_view($timestart->format('n'), $timestart->format('Y'),
+                                                              $course->id, null, false)
+        );
+        $this->assertEquals($data['courseid'], $course->id);
+        // User enrolled in the course can load the course calendar.
+        $this->setUser($user1);
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_monthly_view_returns(),
+            core_calendar_external::get_calendar_monthly_view($timestart->format('n'), $timestart->format('Y'),
+                                                              $course->id, null, false)
+        );
+        $this->assertEquals($data['courseid'], $course->id);
+        // User not enrolled in the course cannot load the course calendar.
+        $this->setUser($user2);
+        $this->setExpectedException('require_login_exception');
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_monthly_view_returns(),
+            core_calendar_external::get_calendar_monthly_view($timestart->format('n'), $timestart->format('Y'),
+                                                              $course->id, null, false)
+        );
+    }
+
+    /**
+     * A user should not be able load the calendar day view for a course they cannot access.
+     */
+    public function test_get_calendar_day_view_no_course_permission() {
+        global $USER;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $generator = $this->getDataGenerator();
+        $user1 = $generator->create_user();
+        $user2 = $generator->create_user();
+        $course = $generator->create_course();
+        $generator->enrol_user($user1->id, $course->id, 'student');
+        $name = 'Course Event (course' . $course->id . ')';
+        $record = new stdClass();
+        $record->courseid = $course->id;
+        $courseevent = $this->create_calendar_event($name, $USER->id, 'course', 0, time(), $record);
+
+        $timestart = new DateTime();
+        // Admin can load the course.
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_day_view_returns(),
+            core_calendar_external::get_calendar_day_view($timestart->format('n'), $timestart->format('Y'),
+                                                          $timestart->format('j'), $course->id, null)
+        );
+        $this->assertEquals($data['courseid'], $course->id);
+        // User enrolled in the course can load the course calendar.
+        $this->setUser($user1);
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_day_view_returns(),
+            core_calendar_external::get_calendar_day_view($timestart->format('n'), $timestart->format('Y'),
+                                                          $timestart->format('j'), $course->id, null)
+        );
+        $this->assertEquals($data['courseid'], $course->id);
+        // User not enrolled in the course cannot load the course calendar.
+        $this->setUser($user2);
+        $this->setExpectedException('require_login_exception');
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_day_view_returns(),
+            core_calendar_external::get_calendar_day_view($timestart->format('n'), $timestart->format('Y'),
+                                                          $timestart->format('j'), $course->id, null)
+        );
+    }
+
+    /**
+     * A user should not be able load the calendar upcoming view for a course they cannot access.
+     */
+    public function test_get_calendar_upcoming_view_no_course_permission() {
+        global $USER;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $generator = $this->getDataGenerator();
+        $user1 = $generator->create_user();
+        $user2 = $generator->create_user();
+        $course = $generator->create_course();
+        $generator->enrol_user($user1->id, $course->id, 'student');
+        $name = 'Course Event (course' . $course->id . ')';
+        $record = new stdClass();
+        $record->courseid = $course->id;
+        $courseevent = $this->create_calendar_event($name, $USER->id, 'course', 0, time(), $record);
+
+        // Admin can load the course.
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_upcoming_view_returns(),
+            core_calendar_external::get_calendar_upcoming_view($course->id, null)
+        );
+        $this->assertEquals($data['courseid'], $course->id);
+        // User enrolled in the course can load the course calendar.
+        $this->setUser($user1);
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_upcoming_view_returns(),
+            core_calendar_external::get_calendar_upcoming_view($course->id, null)
+        );
+        $this->assertEquals($data['courseid'], $course->id);
+        // User not enrolled in the course cannot load the course calendar.
+        $this->setUser($user2);
+        $this->setExpectedException('require_login_exception');
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_upcoming_view_returns(),
+            core_calendar_external::get_calendar_upcoming_view($course->id, null)
+        );
+    }
+
+    /**
+     * A user should not be able load the calendar event for a course they cannot access.
+     */
+    public function test_get_calendar_event_by_id_no_course_permission() {
+        global $USER;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $generator = $this->getDataGenerator();
+        $user1 = $generator->create_user();
+        $user2 = $generator->create_user();
+        $course = $generator->create_course();
+        $generator->enrol_user($user1->id, $course->id, 'student');
+        $name = 'Course Event (course' . $course->id . ')';
+        $record = new stdClass();
+        $record->courseid = $course->id;
+        $courseevent = $this->create_calendar_event($name, $USER->id, 'course', 0, time(), $record);
+
+        // Admin can load the course event.
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_event_by_id_returns(),
+            core_calendar_external::get_calendar_event_by_id($courseevent->id)
+        );
+        $this->assertEquals($data['event']['id'], $courseevent->id);
+        // User enrolled in the course can load the course event.
+        $this->setUser($user1);
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_event_by_id_returns(),
+            core_calendar_external::get_calendar_event_by_id($courseevent->id)
+        );
+        $this->assertEquals($data['event']['id'], $courseevent->id);
+        // User not enrolled in the course cannot load the course event.
+        $this->setUser($user2);
+        $this->setExpectedException('required_capability_exception');
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_event_by_id_returns(),
+            core_calendar_external::get_calendar_event_by_id($courseevent->id)
+        );
+    }
+
+    /**
+     * A user should not be able load the calendar events for a category they cannot see.
+     */
+    public function test_get_calendar_events_hidden_category() {
+        global $USER;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $generator = $this->getDataGenerator();
+        $user1 = $generator->create_user();
+        $category = $generator->create_category(['visible' => 0]);
+        $name = 'Category Event (category: ' . $category->id . ')';
+        $record = new stdClass();
+        $record->categoryid = $category->id;
+        $categoryevent = $this->create_calendar_event($name, $USER->id, 'category', 0, time(), $record);
+
+        $events = [
+            'eventids' => [$categoryevent->id]
+        ];
+        $options = [];
+        // Admin can load the category event.
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_events_returns(),
+            core_calendar_external::get_calendar_events($events, $options)
+        );
+        $this->assertEquals($data['events'][0]['id'], $categoryevent->id);
+        // User with no special permission to see hidden categories will not see the event.
+        $this->setUser($user1);
+        $data = external_api::clean_returnvalue(
+            core_calendar_external::get_calendar_events_returns(),
+            core_calendar_external::get_calendar_events($events, $options)
+        );
+        $this->assertCount(0, $data['events']);
+        $this->assertEquals('nopermissions', $data['warnings'][0]['warningcode']);
+    }
 }
index ce2f1ef..081738e 100644 (file)
@@ -188,7 +188,7 @@ class core_calendar_lib_testcase extends advanced_testcase {
         $this->assertEquals($ical->parser_errors, array());
 
         $sub = calendar_get_subscription($id);
-        calendar_import_icalendar_events($ical, $sub->courseid, $sub->id);
+        calendar_import_icalendar_events($ical, null, $sub->id);
         $count = $DB->count_records('event', array('subscriptionid' => $sub->id));
         $this->assertEquals($count, 1);
 
@@ -205,7 +205,7 @@ class core_calendar_lib_testcase extends advanced_testcase {
         $this->assertEquals($ical->parser_errors, array());
 
         $sub = calendar_get_subscription($id);
-        calendar_import_icalendar_events($ical, $sub->courseid, $sub->id);
+        calendar_import_icalendar_events($ical, null, $sub->id);
         $count = $DB->count_records('event', array('subscriptionid' => $sub->id));
         $this->assertEquals($count, 1);
 
@@ -222,7 +222,7 @@ class core_calendar_lib_testcase extends advanced_testcase {
         $this->assertEquals($ical->parser_errors, array());
 
         $sub = calendar_get_subscription($id);
-        calendar_import_icalendar_events($ical, $sub->courseid, $sub->id);
+        calendar_import_icalendar_events($ical, null, $sub->id);
         $count = $DB->count_records('event', array('subscriptionid' => $sub->id));
         $this->assertEquals($count, 1);
     }
index f2598e0..87d0919 100644 (file)
@@ -69,10 +69,8 @@ class completion_enabled extends \core_analytics\local\indicator\binary {
 
         $course = $this->retrieve('course', $sampleid);
 
-        $cm = false;
-        if ($sampleorigin === 'course_modules') {
-            $cm = $this->retrieve('course_modules', $sampleid);
-        }
+        // It may not be available, but if it is the indicator checks if completion is enabled for the cm.
+        $cm = $this->retrieve('course_modules', $sampleid);
 
         $completion = new \completion_info($course);
 
index 04cb7e1..d041bf7 100644 (file)
Binary files a/lib/amd/build/modal_factory.min.js and b/lib/amd/build/modal_factory.min.js differ
index 770ecdf..29acb94 100644 (file)
@@ -63,12 +63,15 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
      */
     var setUpTrigger = function(modalPromise, triggerElement) {
         if (typeof triggerElement != 'undefined') {
+            // The element that actually shows the modal.
+            var actualTriggerElement = null;
             if (Array.isArray(triggerElement)) {
                 var selector = triggerElement[1];
                 triggerElement = triggerElement[0];
 
                 CustomEvents.define(triggerElement, [CustomEvents.events.activate]);
                 triggerElement.on(CustomEvents.events.activate, selector, function(e, data) {
+                    actualTriggerElement = e.currentTarget;
                     modalPromise.then(function(modal) {
                         modal.show();
 
@@ -79,6 +82,7 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
             } else {
                 CustomEvents.define(triggerElement, [CustomEvents.events.activate]);
                 triggerElement.on(CustomEvents.events.activate, function(e, data) {
+                    actualTriggerElement = e.currentTarget;
                     modalPromise.then(function(modal) {
                         modal.show();
 
@@ -90,7 +94,10 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
 
             modalPromise.then(function(modal) {
                 modal.getRoot().on(ModalEvents.hidden, function() {
-                    triggerElement.focus();
+                    // Focus on the trigger element that actually launched the modal.
+                    if (actualTriggerElement !== null) {
+                        actualTriggerElement.focus();
+                    }
                 });
 
                 return modal;
index 4b2c650..6749e95 100644 (file)
@@ -79,11 +79,17 @@ class calendar_subscription_created extends base
      * @return \moodle_url
      */
     public function get_url() {
-        if (($this->other['courseid'] == SITEID) || ($this->other['courseid'] == 0)) {
-            return new \moodle_url('calendar/managesubscriptions.php');
-        } else {
-            return new \moodle_url('calendar/managesubscriptions.php', array('course' => $this->other['courseid']));
+        $params = [];
+        if ($this->other['eventtype'] == 'course' || $this->other['eventtype'] == 'group') {
+            $params['course'] = $this->other['courseid'];
+            if ($this->other['eventtype'] == 'group' && isset($this->other['groupid'])) {
+                $params['group'] = $this->other['groupid'];
+            }
         }
+        if ($this->other['eventtype'] == 'category' && isset($this->other['categoryid'])) {
+            $params['category'] = $this->other['categoryid'];
+        }
+        return new \moodle_url('/calendar/managesubscriptions.php', $params);
     }
 
     /**
@@ -103,8 +109,16 @@ class calendar_subscription_created extends base
         if (!isset($this->other['eventtype'])) {
             throw new \coding_exception('The \'eventtype\' value must be set in other.');
         }
-        if (!isset($this->other['courseid'])) {
-            throw new \coding_exception('The \'courseid\' value must be set in other.');
+        if ($this->other['eventtype'] == 'course' || $this->other['eventtype'] == 'group') {
+            if (!isset($this->other['courseid'])) {
+                throw new \coding_exception('The \'courseid\' value must be set in other.');
+            }
+            if ($this->other['eventtype'] == 'group' && !isset($this->other['groupid'])) {
+                throw new \coding_exception('The \'groupid\' value must be set in other.');
+            }
+        }
+        if ($this->other['eventtype'] == 'category' && !isset($this->other['categoryid'])) {
+            throw new \coding_exception('The \'categoryid\' value must be set in other.');
         }
     }
 
index 0f7ab2a..b3bbdca 100644 (file)
@@ -78,11 +78,26 @@ class calendar_subscription_deleted extends base
      * @return \moodle_url
      */
     public function get_url() {
-        if (($this->other['courseid'] == SITEID) || ($this->other['courseid'] == 0)) {
-            return new \moodle_url('calendar/managesubscriptions.php');
+        $params = [];
+        if (isset($this->other['eventtype'])) {
+            if ($this->other['eventtype'] == 'course' || $this->other['eventtype'] == 'group') {
+                $params['course'] = $this->other['courseid'];
+                if ($this->other['eventtype'] == 'group' && isset($this->other['groupid'])) {
+                    $params['group'] = $this->other['groupid'];
+                }
+            }
+            if ($this->other['eventtype'] == 'category' && isset($this->other['categoryid'])) {
+                $params['category'] = $this->other['categoryid'];
+            }
         } else {
-            return new \moodle_url('calendar/managesubscriptions.php', array('course' => $this->other['courseid']));
+            // This is a legacy event.
+            // Prior to specification of the eventtype there were only two params.
+            if (($this->other['courseid'] != SITEID) && ($this->other['courseid'] != 0)) {
+                $params['course'] = $this->other['courseid'];
+            }
         }
+        return new \moodle_url('/calendar/managesubscriptions.php', $params);
+
     }
 
     /**
@@ -99,8 +114,19 @@ class calendar_subscription_deleted extends base
         if (!isset($this->objectid)) {
             throw new \coding_exception('The \'objectid\' must be set.');
         }
-        if (!isset($this->other['courseid'])) {
-            throw new \coding_exception('The \'courseid\' value must be set in other.');
+        if (!isset($this->other['eventtype'])) {
+            throw new \coding_exception('The \'eventtype\' value must be set in other.');
+        }
+        if ($this->other['eventtype'] == 'course' || $this->other['eventtype'] == 'group') {
+            if (!isset($this->other['courseid'])) {
+                throw new \coding_exception('The \'courseid\' value must be set in other.');
+            }
+            if ($this->other['eventtype'] == 'group' && !isset($this->other['groupid'])) {
+                throw new \coding_exception('The \'groupid\' value must be set in other.');
+            }
+        }
+        if ($this->other['eventtype'] == 'category' && !isset($this->other['categoryid'])) {
+            throw new \coding_exception('The \'categoryid\' value must be set in other.');
         }
     }
 
index eb61033..a8d6d03 100644 (file)
@@ -79,11 +79,17 @@ class calendar_subscription_updated extends base
      * @return \moodle_url
      */
     public function get_url() {
-        if (($this->other['courseid'] == SITEID) || ($this->other['courseid'] == 0)) {
-            return new \moodle_url('calendar/managesubscriptions.php');
-        } else {
-            return new \moodle_url('calendar/managesubscriptions.php', array('course' => $this->other['courseid']));
+        $params = [];
+        if ($this->other['eventtype'] == 'course' || $this->other['eventtype'] == 'group') {
+            $params['course'] = $this->other['courseid'];
+            if ($this->other['eventtype'] == 'group' && isset($this->other['groupid'])) {
+                $params['group'] = $this->other['groupid'];
+            }
         }
+        if ($this->other['eventtype'] == 'category' && isset($this->other['categoryid'])) {
+            $params['category'] = $this->other['categoryid'];
+        }
+        return new \moodle_url('/calendar/managesubscriptions.php', $params);
     }
 
     /**
@@ -103,8 +109,16 @@ class calendar_subscription_updated extends base
         if (!isset($this->other['eventtype'])) {
             throw new \coding_exception('The \'eventtype\' value must be set in other.');
         }
-        if (!isset($this->other['courseid'])) {
-            throw new \coding_exception('The \'courseid\' value must be set in other.');
+        if ($this->other['eventtype'] == 'course' || $this->other['eventtype'] == 'group') {
+            if (!isset($this->other['courseid'])) {
+                throw new \coding_exception('The \'courseid\' value must be set in other.');
+            }
+            if ($this->other['eventtype'] == 'group' && !isset($this->other['groupid'])) {
+                throw new \coding_exception('The \'groupid\' value must be set in other.');
+            }
+        }
+        if ($this->other['eventtype'] == 'category' && !isset($this->other['categoryid'])) {
+            throw new \coding_exception('The \'categoryid\' value must be set in other.');
         }
     }
 
index bbf96b4..03e5684 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20170929" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20171026" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
         <FIELD NAME="url" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="categoryid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="courseid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="groupid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
index 50599a7..09f65e7 100644 (file)
@@ -2793,5 +2793,20 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2017102100.01);
     }
 
+    if ($oldversion < 2017110300.01) {
+
+        // Define field categoryid to be added to event_subscriptions.
+        $table = new xmldb_table('event_subscriptions');
+        $field = new xmldb_field('categoryid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'url');
+
+        // Conditionally launch add field categoryid.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2017110300.01);
+    }
+
     return true;
 }
index 147721c..1ef320e 100644 (file)
@@ -95,6 +95,11 @@ class insights_list implements \renderable, \templatable {
         if ($this->model->uses_insights()) {
             $predictionsdata = $this->model->get_predictions($this->context, true, $this->page, $this->perpage);
 
+            if (!$this->model->is_static()) {
+                $notification = new \core\output\notification(get_string('justpredictions', 'report_insights'));
+                $data->nostaticmodelnotification = $notification->export_for_template($output);
+            }
+
             $data->predictions = array();
             $predictionvalues = array();
             $insights = array();
index 3556d76..68eab01 100644 (file)
@@ -28,6 +28,7 @@ $string['indicator'] = 'Indicator';
 $string['insightprediction'] = '{$a} prediction';
 $string['insight'] = 'Insight';
 $string['insights'] = 'Insights';
+$string['justpredictions'] = 'Please note that the following insights are only predictions. It is not possible to predict the future with any certainty. The insights are provided so that action can be taken as necessary to prevent any negative predictions becoming reality.';
 $string['outcome'] = 'Outcome';
 $string['outcomenegative'] = 'Negative outcome';
 $string['outcomeneutral'] = 'Neutral outcome';
index 2c6c200..b13137d 100644 (file)
@@ -31,6 +31,9 @@
     Example context (json):
     {
         "insightname": "Best insight ever",
+        "nostaticmodelnotification": {
+            "message": "This is just a prediction."
+        },
         "predictions": [
             {
                 "predictiondisplayvalue": "This dev will understand it",
 {{/modelselector}}
 
 <h2 class="m-b-2">{{{insightname}}}</h2>
+
 {{^noinsights}}
+{{#nostaticmodelnotification}}
+    <div class="m-t-2">
+        {{> core/notification_info}}
+    </div>
+{{/nostaticmodelnotification}}
+
 {{{ pagingbar }}}
 {{#predictions}}
     <table class="generaltable insights-list">
index 1a90c1f..f5ff82a 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2017110300.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2017110300.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.