From 7b1e5e6f3a99631ce19999b4d72cbe72e0de6ff2 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Mon, 18 Sep 2017 14:19:08 +0800 Subject: [PATCH 01/15] MDL-59890 calendar: Add categoryid to DB --- lib/db/install.xml | 8 +++++--- lib/db/upgrade.php | 31 +++++++++++++++++++++++++++++++ version.php | 2 +- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/lib/db/install.xml b/lib/db/install.xml index 880905b8310..bd36ac76690 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -426,6 +426,7 @@ + @@ -446,6 +447,7 @@ + @@ -453,7 +455,7 @@ - + @@ -3719,4 +3721,4 @@
-
+ \ No newline at end of file diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 4bcb77aef94..d0fb99f05b2 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2570,5 +2570,36 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2017092700.00); } + if ($oldversion < 2017092900.00) { + // Define field categoryid to be added to event. + $table = new xmldb_table('event'); + $field = new xmldb_field('categoryid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'format'); + + // Conditionally launch add field categoryid. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Add the categoryid key. + $key = new xmldb_key('categoryid', XMLDB_KEY_FOREIGN, array('categoryid'), 'course_categories', array('id')); + $dbman->add_key($table, $key); + + // Add a new index for groupid/courseid/categoryid/visible/userid. + // Do this before we remove the old index. + $index = new xmldb_index('groupid-courseid-categoryid-visible-userid', XMLDB_INDEX_NOTUNIQUE, array('groupid', 'courseid', 'categoryid', 'visible', 'userid')); + if (!$dbman->index_exists($table, $index)) { + $dbman->add_index($table, $index); + } + + // Drop the old index. + $index = new xmldb_index('groupid-courseid-visible-userid', XMLDB_INDEX_NOTUNIQUE, array('groupid', 'courseid', 'visible', 'userid')); + if ($dbman->index_exists($table, $index)) { + $dbman->drop_index($table, $index); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2017092900.00); + } + return true; } diff --git a/version.php b/version.php index 7f721bc3a3a..5c7c36ee491 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2017092800.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2017092900.00; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. From 05f184bb7f8074f9dc87b3c7520d6ccfaf86753d Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Mon, 18 Sep 2017 12:47:37 +0800 Subject: [PATCH 02/15] MDL-59890 calendar: Add ability to add category events --- calendar/amd/src/selectors.js | 2 + calendar/classes/local/event/forms/create.php | 15 +++- calendar/lib.php | 74 +++++++++++++++++-- lang/en/calendar.php | 3 + lib/coursecatlib.php | 47 ++++++++++++ 5 files changed, 132 insertions(+), 9 deletions(-) diff --git a/calendar/amd/src/selectors.js b/calendar/amd/src/selectors.js index a22a86d9963..ef7e52f90b2 100644 --- a/calendar/amd/src/selectors.js +++ b/calendar/amd/src/selectors.js @@ -26,12 +26,14 @@ define([], function() { eventFilterItem: "[data-action='filter-event-type']", eventType: { site: "[data-eventtype-site]", + category: "[data-eventtype-category]", course: "[data-eventtype-course]", group: "[data-eventtype-group]", user: "[data-eventtype-user]", }, popoverType: { site: "[data-popover-eventtype-site]", + category: "[data-popover-eventtype-category]", course: "[data-popover-eventtype-course]", group: "[data-popover-eventtype-group]", user: "[data-popover-eventtype-user]", diff --git a/calendar/classes/local/event/forms/create.php b/calendar/classes/local/event/forms/create.php index 0a6b757d6d1..8892bfb3fcd 100644 --- a/calendar/classes/local/event/forms/create.php +++ b/calendar/classes/local/event/forms/create.php @@ -55,7 +55,7 @@ class create extends \moodleform { /** * The form definition */ - public function definition () { + public function definition() { global $PAGE; $mform = $this->_form; @@ -203,6 +203,9 @@ class create extends \moodleform { 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'); } @@ -223,6 +226,16 @@ class create extends \moodleform { $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'])) { $courseoptions = []; foreach ($eventtypes['course'] as $course) { diff --git a/calendar/lib.php b/calendar/lib.php index d511497c850..fc92c73fa77 100644 --- a/calendar/lib.php +++ b/calendar/lib.php @@ -89,6 +89,10 @@ define('CALENDAR_EVENT_GROUP', 4); */ define('CALENDAR_EVENT_USER', 8); +/** + * CALENDAR_EVENT_COURSECAT - Course category calendar event types + */ +define('CALENDAR_EVENT_COURSECAT', 16); /** * CALENDAR_IMPORT_FROM_FILE - import the calendar from a file @@ -360,8 +364,7 @@ class calendar_event { if ($this->editorcontext === null) { // Switch on the event type to decide upon the appropriate context to use for this event. $this->editorcontext = $this->properties->context; - if ($this->properties->eventtype != 'user' && $this->properties->eventtype != 'course' - && $this->properties->eventtype != 'site' && $this->properties->eventtype != 'group') { + if (!calendar_is_valid_eventtype($this->properties->eventtype)) { return clean_text($this->properties->description, $this->properties->format); } } @@ -461,6 +464,11 @@ class calendar_event { $this->properties->groupid = 0; $this->properties->userid = $USER->id; break; + case 'category': + $this->properties->groupid = 0; + $this->properties->category = 0; + $this->properties->userid = $USER->id; + break; case 'group': $this->properties->userid = $USER->id; break; @@ -752,6 +760,17 @@ class calendar_event { // We have a course and are within the course context so we had // better use the courses max bytes value. $this->editoroptions['maxbytes'] = $course->maxbytes; + } else if ($properties->eventtype === 'category') { + // First check the course is valid. + $category = $DB->get_record('course_categories', array('id' => $properties->categoryid)); + if (!$category) { + print_error('invalidcourse'); + } + // Course context. + $this->editorcontext = $this->properties->context; + // We have a course and are within the course context so we had + // better use the courses max bytes value. + $this->editoroptions['maxbytes'] = $course->maxbytes; } else { // If we get here we have a custom event type as used by some // modules. In this case the event will have been added by @@ -896,8 +915,7 @@ class calendar_event { // Switch on the event type to decide upon the appropriate context to use for this event. $this->editorcontext = $this->properties->context; - if ($this->properties->eventtype != 'user' && $this->properties->eventtype != 'course' - && $this->properties->eventtype != 'site' && $this->properties->eventtype != 'group') { + if (!calendar_is_valid_eventtype($this->properties->eventtype)) { // We don't have a context here, do a normal format_text. return external_format_text($this->properties->description, $this->properties->format, $this->editorcontext->id); } @@ -2237,7 +2255,8 @@ function calendar_show_event_type($type, $user = null) { */ function calendar_set_event_type_display($type, $display = null, $user = null) { $persist = get_user_preferences('calendar_persistflt', 0, $user); - $default = CALENDAR_EVENT_GLOBAL + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP + CALENDAR_EVENT_USER; + $default = CALENDAR_EVENT_GLOBAL + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP + + CALENDAR_EVENT_USER + CALENDAR_EVENT_COURSECAT; if ($persist === 0) { global $SESSION; if (!isset($SESSION->calendarshoweventtype)) { @@ -2273,14 +2292,16 @@ function calendar_set_event_type_display($type, $display = null, $user = null) { * @param stdClass $allowed list of allowed edit for event type * @param stdClass|int $course object of a course or course id * @param array $groups array of groups for the given course + * @param stdClass|int $category object of a category */ -function calendar_get_allowed_types(&$allowed, $course = null, $groups = null) { +function calendar_get_allowed_types(&$allowed, $course = null, $groups = null, $category = null) { global $USER, $DB; $allowed = new \stdClass(); $allowed->user = has_capability('moodle/calendar:manageownentries', \context_system::instance()); $allowed->groups = false; $allowed->courses = false; + $allowed->categories = false; $allowed->site = has_capability('moodle/calendar:manageentries', \context_course::instance(SITEID)); $getgroupsfunc = function($course, $context, $user) use ($groups) { if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) { @@ -2316,6 +2337,13 @@ function calendar_get_allowed_types(&$allowed, $course = null, $groups = null) { } } } + + if (!empty($category)) { + $catcontext = \context_coursecat::instance($category->id); + if (has_capability('moodle/category:manage', $catcontext)) { + $allowed->categories = [$category->id => 1]; + } + } } /** @@ -2325,6 +2353,7 @@ function calendar_get_allowed_types(&$allowed, $course = null, $groups = null) { * The returned array will optionally have 5 keys: * 'user' : true if the logged in user can create user events * 'site' : true if the logged in user can create site events + * 'category' : array of course categories that the user can create events for * 'course' : array of courses that the user can create events for * 'group': array of groups that the user can create events for * 'groupcourses' : array of courses that the groups belong to (can @@ -2333,7 +2362,7 @@ function calendar_get_allowed_types(&$allowed, $course = null, $groups = null) { * @return array The array of allowed types. */ function calendar_get_all_allowed_types() { - global $CFG, $USER; + global $CFG, $USER, $DB; require_once($CFG->libdir . '/enrollib.php'); @@ -2349,6 +2378,10 @@ function calendar_get_all_allowed_types() { $types['site'] = true; } + if (coursecat::has_manage_capability_on_any()) { + $types['category'] = coursecat::make_categories_list('moodle/category:manage'); + } + // This function warms the context cache for the course so the calls // to load the course context in calendar_get_allowed_types don't result // in additional DB queries. @@ -2401,7 +2434,7 @@ function calendar_user_can_add_event($course) { calendar_get_allowed_types($allowed, $course); - return (bool)($allowed->user || $allowed->groups || $allowed->courses || $allowed->site); + return (bool)($allowed->user || $allowed->groups || $allowed->courses || $allowed->category || $allowed->site); } /** @@ -2426,6 +2459,8 @@ function calendar_add_event_allowed($event) { } switch ($event->eventtype) { + case 'category': + return has_capability('moodle/category:manage', $event->context); case 'course': return has_capability('moodle/calendar:manageentries', $event->context); case 'group': @@ -2485,6 +2520,9 @@ function calendar_get_eventtype_choices($courseid) { if (!empty($allowed->courses)) { $choices['course'] = get_string('courseevents', 'calendar'); } + if (!empty($allowed->categories)) { + $choices['category'] = get_string('categoryevents', 'calendar'); + } if (!empty($allowed->groups) and is_array($allowed->groups)) { $choices['group'] = get_string('group'); } @@ -2505,6 +2543,8 @@ function calendar_add_subscription($sub) { $sub->courseid = $SITE->id; } else if ($sub->eventtype === 'group' || $sub->eventtype === 'course') { $sub->courseid = $sub->course; + } else if ($sub->eventtype === 'category') { + $sub->categoryid = $sub->category; } else { // User events. $sub->courseid = 0; @@ -3262,6 +3302,7 @@ function calendar_get_footer_options($calendar) { function calendar_get_filter_types() { $types = [ 'site', + 'category', 'course', 'group', 'user', @@ -3274,3 +3315,20 @@ function calendar_get_filter_types() { ]; }, $types); } + +/** + * Check whether the specified event type is valid. + * + * @param string $type + * @return bool + */ +function calendar_is_valid_eventtype($type) { + $validtypes = [ + 'user', + 'group', + 'course', + 'category', + 'site', + ]; + return in_array($type, $validtypes); +} diff --git a/lang/en/calendar.php b/lang/en/calendar.php index a0bf7d0f4f5..28280d02cf5 100644 --- a/lang/en/calendar.php +++ b/lang/en/calendar.php @@ -44,6 +44,7 @@ $string['confirmeventseriesdelete'] = 'The "{$a->name}" event is part of a serie $string['course'] = 'Course'; $string['courseevent'] = 'Course event'; $string['courseevents'] = 'Course events'; +$string['categoryevents'] = 'Category events'; $string['courses'] = 'Courses'; $string['customexport'] = 'Custom range ({$a->timestart} - {$a->timeend})'; $string['daily'] = 'Daily'; @@ -141,6 +142,7 @@ $string['groupevent'] = 'Group event'; $string['groupevents'] = 'Group events'; $string['eventtypeglobal'] = 'global'; $string['eventtypesite'] = 'global'; +$string['eventtypecategory'] = 'category'; $string['eventtypecourse'] = 'course'; $string['eventtypemodule'] = 'module'; $string['eventtypegroup'] = 'group'; @@ -227,6 +229,7 @@ $string['tue'] = 'Tue'; $string['tuesday'] = 'Tuesday'; $string['typeclose'] = 'Close event'; $string['typecourse'] = 'Course event'; +$string['typecategory'] = 'Category event'; $string['typedue'] = 'Due event'; $string['typegradingdue'] = 'Grading due event'; $string['typegroup'] = 'Group event'; diff --git a/lib/coursecatlib.php b/lib/coursecatlib.php index d8c8ef12855..d4b56c2e936 100644 --- a/lib/coursecatlib.php +++ b/lib/coursecatlib.php @@ -291,6 +291,53 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate { return $categories; } + /** + * Load all coursecat objects. + * + * @param array $options Options: + * @param bool $options.returnhidden Return categories even if they are hidden + * @return coursecat[] + */ + public static function get_all($options = null) { + global $DB; + + if (null === $options) { + $options = []; + } + + $coursecatrecordcache = cache::make('core', 'coursecatrecords'); + + $catcontextsql = \context_helper::get_preload_record_columns_sql('ctx'); + $catsql = "SELECT cc.*, {$catcontextsql} + FROM {course_categories} cc + JOIN {context} ctx ON cc.id = ctx.instanceid"; + $catsqlwhere = "WHERE ctx.contextlevel = :contextlevel"; + $catsqlorder = "ORDER BY cc.depth ASC, cc.sortorder ASC"; + + $catrs = $DB->get_recordset_sql("{$catsql} {$catsqlwhere} {$catsqlorder}", [ + 'contextlevel' => CONTEXT_COURSECAT, + ]); + + $types['categories'] = []; + $categories = []; + $toset = []; + foreach ($catrs as $record) { + \context_helper::preload_from_record($record); + $category = new coursecat($record); + $toset[$category->id] = $category; + + if (!empty($options['returnhidden']) || $category->is_uservisible()) { + $categories[$record->id] = new coursecat($record); + } + } + $catrs->close(); + + $coursecatrecordcache->set_many($toset); + + return $categories; + + } + /** * Returns the first found category * From 7d0a866c95ecf2223ddbc6cd056a8e8291eb4daa Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 19 Sep 2017 11:12:29 +0800 Subject: [PATCH 03/15] MDL-59890 calendar: Accept categories in calendar_info --- calendar/lib.php | 73 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/calendar/lib.php b/calendar/lib.php index fc92c73fa77..f0c8dd16863 100644 --- a/calendar/lib.php +++ b/calendar/lib.php @@ -27,6 +27,8 @@ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page } +require_once($CFG->libdir . '/coursecatlib.php'); + /** * These are read by the administration component to provide default values */ @@ -762,10 +764,7 @@ class calendar_event { $this->editoroptions['maxbytes'] = $course->maxbytes; } else if ($properties->eventtype === 'category') { // First check the course is valid. - $category = $DB->get_record('course_categories', array('id' => $properties->categoryid)); - if (!$category) { - print_error('invalidcourse'); - } + \coursecat::get($properties->categoryid, MUST_EXIST, true); // Course context. $this->editorcontext = $this->properties->context; // We have a course and are within the course context so we had @@ -957,6 +956,12 @@ class calendar_information { /** @var int A course id */ public $courseid = null; + /** @var array An array of categories */ + public $categories = array(); + + /** @var int The current category */ + public $categoryid = null; + /** @var array An array of courses */ public $courses = array(); @@ -1019,17 +1024,71 @@ class calendar_information { /** * Initialize calendar information * + * @deprecated 3.4 * @param stdClass $course object * @param array $coursestoload An array of courses [$course->id => $course] * @param bool $ignorefilters options to use filter */ public function prepare_for_view(stdClass $course, array $coursestoload, $ignorefilters = false) { - $this->courseid = $course->id; + debugging('The prepare_for_view() function has been deprecated. Please update your code to use set_sources()', + DEBUG_DEVELOPER); + $this->set_sources($course, $coursestoload); + } + + /** + * Set the sources for events within the calendar. + * + * If no category is provided, then the category path for the current + * course will be used. + * + * @param stdClass $course The current course being viewed. + * @param int[] $courses The list of all courses currently accessible. + * @param stdClass $category The current category to show. + */ + public function set_sources(stdClass $course, array $courses, stdClass $category = null) { + // A cousre must always be specified. $this->course = $course; - list($courses, $group, $user) = calendar_set_filters($coursestoload, $ignorefilters); - $this->courses = $courses; + $this->courseid = $course->id; + + list($courseids, $group, $user) = calendar_set_filters($courses); + $this->courses = $courseids; $this->groups = $group; $this->users = $user; + + // Do not show category events by default. + $this->categoryid = null; + $this->categories = null; + + if (null !== $category) { + // A specific category was requested - set the current category, and include all parents of that category. + $category = \coursecat::get($category->id); + $this->categoryid = $category->id; + + $this->categories = $category->get_parents(); + $this->categories[] = $category->id; + } else if (SITEID === $this->courseid) { + // This is the site. + // Show categories for all courses the user has access to. + $this->categories = true; + $categories = []; + foreach ($courses as $course) { + $category = \coursecat::get($course->category); + $categories = array_merge($categories, $category->get_parents()); + $categories[] = $category->id; + } + + // And all categories that the user can manage. + foreach (\coursecat::get_all() as $category) { + if (!$category->has_manage_capability()) { + continue; + } + + $categories = array_merge($categories, $category->get_parents()); + $categories[] = $category->id; + } + + $this->categories = array_unique($categories); + } } /** From 0085b0ea3c64d0d3fce61d13f94c220acefd3ff0 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 19 Sep 2017 11:39:39 +0800 Subject: [PATCH 04/15] MDL-59890 calendar: Add support for the category to vault --- calendar/classes/external/event_exporter.php | 11 ++ calendar/classes/local/api.php | 2 + calendar/classes/local/event/container.php | 9 ++ .../local/event/data_access/event_vault.php | 11 ++ .../data_access/event_vault_interface.php | 1 + .../local/event/entities/action_event.php | 9 ++ .../classes/local/event/entities/event.php | 12 ++ .../local/event/entities/event_interface.php | 7 ++ .../factories/event_abstract_factory.php | 7 ++ .../local/event/mappers/event_mapper.php | 1 + .../local/event/proxies/coursecat_proxy.php | 92 ++++++++++++++ .../raw_event_retrieval_strategy.php | 32 ++++- ...raw_event_retrieval_strategy_interface.php | 2 + calendar/lib.php | 13 +- calendar/tests/action_event_test.php | 4 + calendar/tests/container_test.php | 116 ++++++++++++++++++ calendar/tests/coursecat_proxy_test.php | 64 ++++++++++ calendar/tests/event_factory_test.php | 8 ++ calendar/tests/event_mapper_test.php | 13 ++ calendar/tests/event_test.php | 3 + calendar/tests/helpers.php | 2 + .../raw_event_retrieval_strategy_test.php | 65 ++++++++++ .../tests/repeat_event_collection_test.php | 2 + lib/coursecatlib.php | 8 ++ 24 files changed, 484 insertions(+), 10 deletions(-) create mode 100644 calendar/classes/local/event/proxies/coursecat_proxy.php create mode 100644 calendar/tests/coursecat_proxy_test.php diff --git a/calendar/classes/external/event_exporter.php b/calendar/classes/external/event_exporter.php index 2642423452c..d687eb1b191 100644 --- a/calendar/classes/external/event_exporter.php +++ b/calendar/classes/external/event_exporter.php @@ -50,6 +50,11 @@ class event_exporter extends event_exporter_base { protected static function define_other_properties() { $values = parent::define_other_properties(); + + $values['isactionevent'] = ['type' => PARAM_BOOL]; + $values['iscourseevent'] = ['type' => PARAM_BOOL]; + $values['iscategoryevent'] = ['type' => PARAM_BOOL]; + $values['candelete'] = ['type' => PARAM_BOOL]; $values['url'] = ['type' => PARAM_URL]; $values['action'] = [ 'type' => event_action_exporter::read_properties_definition(), @@ -77,6 +82,9 @@ class event_exporter extends event_exporter_base { $event = $this->event; $context = $this->related['context']; + $values['isactionevent'] = false; + $values['iscourseevent'] = false; + $values['iscategoryevent'] = false; if ($moduleproxy = $event->get_course_module()) { $modulename = $moduleproxy->get('modname'); $moduleid = $moduleproxy->get('id'); @@ -86,6 +94,9 @@ class event_exporter extends event_exporter_base { $params = array('update' => $moduleid, 'return' => true, 'sesskey' => sesskey()); $editurl = new \moodle_url('/course/mod.php', $params); $values['editurl'] = $editurl->out(false); + } else if ($event->get_type() == 'category') { + $values['iscategoryevent'] = true; + $url = $event->get_category()->get_proxied_instance()->get_view_link(); } else if ($event->get_type() == 'course') { $url = \course_get_url($this->related['course'] ?: SITEID); } else { diff --git a/calendar/classes/local/api.php b/calendar/classes/local/api.php index 0ba6d54df76..00713f43f84 100644 --- a/calendar/classes/local/api.php +++ b/calendar/classes/local/api.php @@ -71,6 +71,7 @@ class api { array $usersfilter = null, array $groupsfilter = null, array $coursesfilter = null, + array $categoriesfilter = null, $withduration = true, $ignorehidden = true, callable $filter = null @@ -102,6 +103,7 @@ class api { $usersfilter, $groupsfilter, $coursesfilter, + $categoriesfilter, $withduration, $ignorehidden, $filter diff --git a/calendar/classes/local/event/container.php b/calendar/classes/local/event/container.php index 70c2cb7b4bc..398304ebc20 100644 --- a/calendar/classes/local/event/container.php +++ b/calendar/classes/local/event/container.php @@ -117,6 +117,15 @@ class container { [self::class, 'apply_component_provide_event_action'], [self::class, 'apply_component_is_event_visible'], function ($dbrow) { + if (!empty($dbrow->categoryid)) { + // This is a category event. Check that the category is visible to this user. + $category = \coursecat::get($dbrow->categoryid, IGNORE_MISSING, true); + + if (empty($category) || !$category->is_uservisible()) { + return true; + } + } + // At present we only have a bail-out check for events in course modules. if (empty($dbrow->modulename)) { return false; diff --git a/calendar/classes/local/event/data_access/event_vault.php b/calendar/classes/local/event/data_access/event_vault.php index c6c7c689c01..9111f3918c0 100644 --- a/calendar/classes/local/event/data_access/event_vault.php +++ b/calendar/classes/local/event/data_access/event_vault.php @@ -33,6 +33,8 @@ use core_calendar\local\event\factories\action_factory_interface; use core_calendar\local\event\factories\event_factory_interface; use core_calendar\local\event\strategies\raw_event_retrieval_strategy_interface; +require_once($CFG->libdir . '/coursecatlib.php'); + /** * Event vault class. * @@ -95,6 +97,7 @@ class event_vault implements event_vault_interface { array $usersfilter = null, array $groupsfilter = null, array $coursesfilter = null, + array $categoriesfilter = null, $withduration = true, $ignorehidden = true, callable $filter = null @@ -162,6 +165,7 @@ class event_vault implements event_vault_interface { $usersfilter, $groupsfilter, $coursesfilter, + $categoriesfilter, $where, $params, "COALESCE(e.timesort, e.timestart) ASC, e.id ASC", @@ -197,6 +201,10 @@ class event_vault implements event_vault_interface { event_interface $afterevent = null, $limitnum = 20 ) { + $categoryids = array_map(function($category) { + return $category->id; + }, \coursecat::get_all()); + $courseids = array_map(function($course) { return $course->id; }, enrol_get_all_users_courses($user->id)); @@ -219,6 +227,7 @@ class event_vault implements event_vault_interface { [$user->id], $groupids ? $groupids : null, $courseids ? $courseids : null, + $categoryids ? $categoryids : null, true, true, function ($event) { @@ -249,6 +258,7 @@ class event_vault implements event_vault_interface { [$user->id], $groupings[0] ? $groupings[0] : null, [$course->id], + [], true, true, function ($event) use ($course) { @@ -375,6 +385,7 @@ class event_vault implements event_vault_interface { [$userid], null, null, + null, $whereconditions, $whereparams, $ordersql, diff --git a/calendar/classes/local/event/data_access/event_vault_interface.php b/calendar/classes/local/event/data_access/event_vault_interface.php index 705b8327de0..15c5a0cad14 100644 --- a/calendar/classes/local/event/data_access/event_vault_interface.php +++ b/calendar/classes/local/event/data_access/event_vault_interface.php @@ -76,6 +76,7 @@ interface event_vault_interface { array $usersfilter = null, array $groupsfilter = null, array $coursesfilter = null, + array $categoriesfilter = null, $withduration = true, $ignorehidden = true, callable $filter = null diff --git a/calendar/classes/local/event/entities/action_event.php b/calendar/classes/local/event/entities/action_event.php index 1c76999e97a..ad1d98c6ce3 100644 --- a/calendar/classes/local/event/entities/action_event.php +++ b/calendar/classes/local/event/entities/action_event.php @@ -50,6 +50,11 @@ class action_event implements action_event_interface { */ protected $action; + /** + * @var proxy_interface $category Category for this event. + */ + protected $category; + /** * Constructor. * @@ -73,6 +78,10 @@ class action_event implements action_event_interface { return $this->event->get_description(); } + public function get_category() { + return $this->event->get_category(); + } + public function get_course() { return $this->event->get_course(); } diff --git a/calendar/classes/local/event/entities/event.php b/calendar/classes/local/event/entities/event.php index cd5a1dff68b..f68544b04cc 100644 --- a/calendar/classes/local/event/entities/event.php +++ b/calendar/classes/local/event/entities/event.php @@ -52,6 +52,11 @@ class event implements event_interface { */ protected $description; + /** + * @var proxy_interface $category Category for this event. + */ + protected $category; + /** * @var proxy_interface $course Course for this event. */ @@ -103,6 +108,7 @@ class event implements event_interface { * @param int $id The event's ID in the database. * @param string $name The event's name. * @param description_interface $description The event's description. + * @param proxy_interface $category The category associated with the event. * @param proxy_interface $course The course associated with the event. * @param proxy_interface $group The group associated with the event. * @param proxy_interface $user The user associated with the event. @@ -117,6 +123,7 @@ class event implements event_interface { $id, $name, description_interface $description, + proxy_interface $category = null, proxy_interface $course = null, proxy_interface $group = null, proxy_interface $user = null, @@ -130,6 +137,7 @@ class event implements event_interface { $this->id = $id; $this->name = $name; $this->description = $description; + $this->category = $category; $this->course = $course; $this->group = $group; $this->user = $user; @@ -153,6 +161,10 @@ class event implements event_interface { return $this->description; } + public function get_category() { + return $this->category; + } + public function get_course() { return $this->course; } diff --git a/calendar/classes/local/event/entities/event_interface.php b/calendar/classes/local/event/entities/event_interface.php index 192e75d9840..400cf78fdd7 100644 --- a/calendar/classes/local/event/entities/event_interface.php +++ b/calendar/classes/local/event/entities/event_interface.php @@ -56,6 +56,13 @@ interface event_interface { */ public function get_description(); + /** + * Get the category object associated with the event. + * + * @return proxy_interface + */ + public function get_category(); + /** * Get the course object associated with the event. * diff --git a/calendar/classes/local/event/factories/event_abstract_factory.php b/calendar/classes/local/event/factories/event_abstract_factory.php index 2695d93cf39..b0242ae4ca1 100644 --- a/calendar/classes/local/event/factories/event_abstract_factory.php +++ b/calendar/classes/local/event/factories/event_abstract_factory.php @@ -30,11 +30,14 @@ use core_calendar\local\event\entities\event; use core_calendar\local\event\entities\repeat_event_collection; use core_calendar\local\event\exceptions\invalid_callback_exception; use core_calendar\local\event\proxies\cm_info_proxy; +use core_calendar\local\event\proxies\coursecat_proxy; use core_calendar\local\event\proxies\std_proxy; use core_calendar\local\event\value_objects\event_description; use core_calendar\local\event\value_objects\event_times; use core_calendar\local\event\entities\event_interface; +require_once($CFG->libdir . '/coursecatlib.php'); + /** * Abstract factory for creating calendar events. * @@ -126,6 +129,7 @@ abstract class event_abstract_factory implements event_factory_interface { return null; } + $category = null; $course = null; $group = null; $user = null; @@ -136,6 +140,8 @@ abstract class event_abstract_factory implements event_factory_interface { $module = new cm_info_proxy($dbrow->modulename, $dbrow->instance, $dbrow->courseid); } + $category = new coursecat_proxy($dbrow->categoryid); + $course = new std_proxy($dbrow->courseid, function($id) { return calendar_get_course_cached($this->coursecachereference, $id); }); @@ -163,6 +169,7 @@ abstract class event_abstract_factory implements event_factory_interface { $dbrow->id, $dbrow->name, new event_description($dbrow->description, $dbrow->format), + $category, $course, $group, $user, diff --git a/calendar/classes/local/event/mappers/event_mapper.php b/calendar/classes/local/event/mappers/event_mapper.php index 1561e02c75c..2e338699066 100644 --- a/calendar/classes/local/event/mappers/event_mapper.php +++ b/calendar/classes/local/event/mappers/event_mapper.php @@ -69,6 +69,7 @@ class event_mapper implements event_mapper_interface { 'name' => $coalesce('name'), 'description' => $coalesce('description'), 'format' => $coalesce('format'), + 'categoryid' => $coalesce('categoryid'), 'courseid' => $coalesce('courseid'), 'groupid' => $coalesce('groupid'), 'userid' => $coalesce('userid'), diff --git a/calendar/classes/local/event/proxies/coursecat_proxy.php b/calendar/classes/local/event/proxies/coursecat_proxy.php new file mode 100644 index 00000000000..c585f3a107a --- /dev/null +++ b/calendar/classes/local/event/proxies/coursecat_proxy.php @@ -0,0 +1,92 @@ +. + +/** + * Course category proxy. + * + * @package core_calendar + * @copyright 2017 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\proxies; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/coursecatlib.php'); + +/** + * Course category proxy. + * + * This returns an instance of a coursecat rather than a stdClass. + * + * @copyright 2017 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class coursecat_proxy implements proxy_interface { + /** + * @var int $id The ID of the database record. + */ + protected $id; + + /** + * @var \stdClass $base Base class to get members from. + */ + protected $base; + + /** + * @var \coursecat $category The proxied instance. + */ + protected $category; + + /** + * coursecat_proxy constructor. + * + * @param int $id The ID of the record in the database. + */ + public function __construct($id) { + $this->id = $id; + $this->base = (object) [ + 'id' => $id, + ]; + } + + /** + * Retrieve a member of the proxied class. + * + * @param string $member The name of the member to retrieve + * @return mixed The member. + */ + public function get($member) { + if ($this->base && property_exists($this->base, $member)) { + return $this->base->{$member}; + } + + return $this->get_proxied_instance()->{$member}; + } + + /** + * Get the full instance of the proxied class. + * + * @return \coursecat + */ + public function get_proxied_instance() : \coursecat { + if (!$this->category) { + $this->category = \coursecat::get($this->id, IGNORE_MISSING, true); + } + return $this->category; + } +} diff --git a/calendar/classes/local/event/strategies/raw_event_retrieval_strategy.php b/calendar/classes/local/event/strategies/raw_event_retrieval_strategy.php index 252d37412fe..d4775ee8eb5 100644 --- a/calendar/classes/local/event/strategies/raw_event_retrieval_strategy.php +++ b/calendar/classes/local/event/strategies/raw_event_retrieval_strategy.php @@ -40,6 +40,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter array $usersfilter = null, array $groupsfilter = null, array $coursesfilter = null, + array $categoriesfilter = null, array $whereconditions = null, array $whereparams = null, $ordersql = null, @@ -51,6 +52,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter !is_null($usersfilter) ? $usersfilter : true, // True means no filter in old implementation. !is_null($groupsfilter) ? $groupsfilter : true, !is_null($coursesfilter) ? $coursesfilter : true, + !is_null($categoriesfilter) ? $categoriesfilter : true, $whereconditions, $whereparams, $ordersql, @@ -78,6 +80,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter $users, $groups, $courses, + $categories, $whereconditions, $whereparams, $ordersql, @@ -89,7 +92,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter $params = array(); // Quick test. - if (empty($users) && empty($groups) && empty($courses)) { + if (empty($users) && empty($groups) && empty($courses) && empty($categories)) { return array(); } @@ -100,11 +103,11 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter if ((is_array($users) && !empty($users)) or is_numeric($users)) { // Events from a number of users. list($insqlusers, $inparamsusers) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED); - $filters[] = "(e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0)"; + $filters[] = "(e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0 AND e.categoryid = 0)"; $params = array_merge($params, $inparamsusers); } else if ($users === true) { // Events from ALL users. - $filters[] = "(e.userid != 0 AND e.courseid = 0 AND e.groupid = 0)"; + $filters[] = "(e.userid != 0 AND e.courseid = 0 AND e.groupid = 0 AND e.categoryid = 0)"; } // Boolean false (no users at all): We don't need to do anything. @@ -130,6 +133,16 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter $filters[] = "(e.groupid = 0 AND e.courseid != 0)"; } + // Category filter. + if ((is_array($categories) && !empty($categories)) or is_numeric($categories)) { + list($insqlcategories, $inparamscategories) = $DB->get_in_or_equal($categories, SQL_PARAMS_NAMED); + $filters[] = "(e.groupid = 0 AND e.courseid = 0 AND e.categoryid $insqlcategories)"; + $params = array_merge($params, $inparamscategories); + } else if ($categories === true) { + // Events from ALL categories. + $filters[] = "(e.groupid = 0 AND e.courseid = 0 AND e.categoryid != 0)"; + } + // Security check: if, by now, we have NOTHING in $whereclause, then it means // that NO event-selecting clauses were defined. Thus, we won't be returning ANY // events no matter what. Allowing the code to proceed might return a completely @@ -168,7 +181,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter if ($user) { // Set filter condition for the user's events. - $subqueryconditions[] = "(ev.userid = :user AND ev.courseid = 0 AND ev.groupid = 0)"; + $subqueryconditions[] = "(ev.userid = :user AND ev.courseid = 0 AND ev.groupid = 0 AND ev.categoryid = 0)"; $subqueryparams['user'] = $user; foreach ($usercourses as $courseid) { @@ -210,10 +223,19 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter // Set subquery filter condition for the courses. if (!empty($subquerycourses)) { list($incourses, $incoursesparams) = $DB->get_in_or_equal($subquerycourses, SQL_PARAMS_NAMED); - $subqueryconditions[] = "(ev.groupid = 0 AND ev.courseid $incourses)"; + $subqueryconditions[] = "(ev.groupid = 0 AND ev.courseid $incourses AND ev.categoryid = 0)"; $subqueryparams = array_merge($subqueryparams, $incoursesparams); } + // Set subquery filter condition for the categories. + if ($categories === true) { + $subqueryconditions[] = "(ev.categoryid != 0 AND ev.eventtype = 'category')"; + } else if (!empty($categories)) { + list($incategories, $incategoriesparams) = $DB->get_in_or_equal($categories, SQL_PARAMS_NAMED); + $subqueryconditions[] = "(ev.groupid = 0 AND ev.courseid = 0 AND ev.categoryid $incategories)"; + $subqueryparams = array_merge($subqueryparams, $incategoriesparams); + } + // Build the WHERE condition for the sub-query. if (!empty($subqueryconditions)) { $subquerywhere = 'WHERE ' . implode(" OR ", $subqueryconditions); diff --git a/calendar/classes/local/event/strategies/raw_event_retrieval_strategy_interface.php b/calendar/classes/local/event/strategies/raw_event_retrieval_strategy_interface.php index cc670d3b05c..9225a397a01 100644 --- a/calendar/classes/local/event/strategies/raw_event_retrieval_strategy_interface.php +++ b/calendar/classes/local/event/strategies/raw_event_retrieval_strategy_interface.php @@ -39,6 +39,7 @@ interface raw_event_retrieval_strategy_interface { * @param array|null $usersfilter Array of users to retrieve events for. * @param array|null $groupsfilter Array of groups to retrieve events for. * @param array|null $coursesfilter Array of courses to retrieve events for. + * @param array|null $categoriesfilter Array of categories to retrieve events for. * @param array|null $whereconditions Array of where conditions to restrict results. * @param array|null $whereparams Array of parameters for $whereconditions. * @param string|null $ordersql SQL to order results. @@ -51,6 +52,7 @@ interface raw_event_retrieval_strategy_interface { array $usersfilter = null, array $groupsfilter = null, array $coursesfilter = null, + array $categoriesfilter = null, array $whereconditions = null, array $whereparams = null, $ordersql = null, diff --git a/calendar/lib.php b/calendar/lib.php index f0c8dd16863..6b461158f5e 100644 --- a/calendar/lib.php +++ b/calendar/lib.php @@ -3055,11 +3055,12 @@ function core_calendar_user_preferences() { * @param boolean $ignorehidden whether to select only visible events or all events * @return array $events of selected events or an empty array if there aren't any (or there was an error) */ -function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses, $withduration = true, $ignorehidden = true) { +function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses, + $withduration = true, $ignorehidden = true, $categories = []) { // Normalise the users, groups and courses parameters so that they are compliant with \core_calendar\local\api::get_events(). // Existing functions that were using the old calendar_get_events() were passing a mixture of array, int, boolean for these // parameters, but with the new API method, only null and arrays are accepted. - list($userparam, $groupparam, $courseparam) = array_map(function($param) { + list($userparam, $groupparam, $courseparam, $categoryparam) = array_map(function($param) { // If parameter is true, return null. if ($param === true) { return null; @@ -3077,7 +3078,7 @@ function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses, $ // No normalisation required. return $param; - }, [$users, $groups, $courses]); + }, [$users, $groups, $courses, $categories]); $mapper = \core_calendar\local\event\container::get_event_mapper(); $events = \core_calendar\local\api::get_events( @@ -3092,6 +3093,7 @@ function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses, $ $userparam, $groupparam, $courseparam, + $categoryparam, $withduration, $ignorehidden ); @@ -3142,7 +3144,7 @@ function calendar_get_view(\calendar_information $calendar, $view, $includenavig } } - list($userparam, $groupparam, $courseparam) = array_map(function($param) { + list($userparam, $groupparam, $courseparam, $categoryparam) = array_map(function($param) { // If parameter is true, return null. if ($param === true) { return null; @@ -3160,7 +3162,7 @@ function calendar_get_view(\calendar_information $calendar, $view, $includenavig // No normalisation required. return $param; - }, [$calendar->users, $calendar->groups, $calendar->courses]); + }, [$calendar->users, $calendar->groups, $calendar->courses, $calendar->categories]); $events = \core_calendar\local\api::get_events( $tstart, @@ -3174,6 +3176,7 @@ function calendar_get_view(\calendar_information $calendar, $view, $includenavig $userparam, $groupparam, $courseparam, + $categoryparam, true, true, function ($event) { diff --git a/calendar/tests/action_event_test.php b/calendar/tests/action_event_test.php index 70bbf906c28..acb864331a2 100644 --- a/calendar/tests/action_event_test.php +++ b/calendar/tests/action_event_test.php @@ -108,6 +108,10 @@ class core_calendar_action_event_test_event implements event_interface { return new event_description('asdf', 1); } + public function get_category() { + return new \stdClass(); + } + public function get_course() { return new \stdClass(); } diff --git a/calendar/tests/container_test.php b/calendar/tests/container_test.php index e21692eba5a..3298187ce8b 100644 --- a/calendar/tests/container_test.php +++ b/calendar/tests/container_test.php @@ -240,6 +240,84 @@ class core_calendar_container_testcase extends advanced_testcase { $this->assertNull($event); } + /** + * Test that the event factory deals with invisible categorys as an admin. + */ + public function test_event_factory_when_category_visibility_is_toggled_as_admin() { + // Create a hidden category. + $category = $this->getDataGenerator()->create_category(['visible' => 0]); + + $eventdata = [ + 'categoryid' => $category->id, + 'eventtype' => 'category', + ]; + $legacyevent = $this->create_event($eventdata); + + $dbrow = $this->get_dbrow_from_skeleton((object) $eventdata); + $dbrow->id = $legacyevent->id; + + $factory = \core_calendar\local\event\container::get_event_factory(); + $event = $factory->create_instance($dbrow); + + // Module is still visible to admins even if the category is invisible. + $this->assertInstanceOf(event_interface::class, $event); + } + + /** + * Test that the event factory deals with invisible categorys as an user. + */ + public function test_event_factory_when_category_visibility_is_toggled_as_user() { + // Create a hidden category. + $category = $this->getDataGenerator()->create_category(['visible' => 0]); + + $eventdata = [ + 'categoryid' => $category->id, + 'eventtype' => 'category', + ]; + $legacyevent = $this->create_event($eventdata); + + $dbrow = $this->get_dbrow_from_skeleton((object) $eventdata); + $dbrow->id = $legacyevent->id; + + // Use a standard user. + $user = $this->getDataGenerator()->create_user(); + + // Set the user to the student. + $this->setUser($user); + + $factory = \core_calendar\local\event\container::get_event_factory(); + $event = $factory->create_instance($dbrow); + + // Module is invisible to non-privileged users. + $this->assertNull($event); + } + + /** + * Test that the event factory deals with invisible categorys as an guest. + */ + public function test_event_factory_when_category_visibility_is_toggled_as_guest() { + // Create a hidden category. + $category = $this->getDataGenerator()->create_category(['visible' => 0]); + + $eventdata = [ + 'categoryid' => $category->id, + 'eventtype' => 'category', + ]; + $legacyevent = $this->create_event($eventdata); + + $dbrow = $this->get_dbrow_from_skeleton((object) $eventdata); + $dbrow->id = $legacyevent->id; + + // Set the user to the student. + $this->setGuestUser(); + + $factory = \core_calendar\local\event\container::get_event_factory(); + $event = $factory->create_instance($dbrow); + + // Module is invisible to guests. + $this->assertNull($event); + } + /** * Test that the event factory deals with completion related events properly. */ @@ -264,6 +342,7 @@ class core_calendar_container_testcase extends advanced_testcase { $event->userid = 1; $event->modulename = 'assign'; $event->instance = $assign->id; + $event->categoryid = 0; $event->courseid = $course->id; $event->groupid = 0; $event->timestart = time(); @@ -312,6 +391,7 @@ class core_calendar_container_testcase extends advanced_testcase { $event->userid = $user->id; $event->modulename = 'lesson'; $event->instance = $lesson->id; + $event->categoryid = 0; $event->courseid = $course->id; $event->groupid = 0; $event->timestart = time(); @@ -397,6 +477,7 @@ class core_calendar_container_testcase extends advanced_testcase { 'name' => 'Test event', 'description' => 'Hello', 'format' => 1, + 'categoryid' => 0, 'courseid' => 1, 'groupid' => 0, 'userid' => 1, @@ -418,6 +499,7 @@ class core_calendar_container_testcase extends advanced_testcase { 'name' => 'Test event', 'description' => 'Hello', 'format' => 1, + 'categoryid' => 0, 'courseid' => 1, 'groupid' => 1, 'userid' => 1, @@ -459,4 +541,38 @@ class core_calendar_container_testcase extends advanced_testcase { $event = new calendar_event($record); return $event->create($record, false); } + + /** + * Pad out a basic DB row with basic information. + * + * @param \stdClass $skeleton the current skeleton + * @return \stdClass + */ + protected function get_dbrow_from_skeleton($skeleton) { + $dbrow = (object) [ + 'name' => 'Name', + 'description' => 'Description', + 'format' => 1, + 'categoryid' => 0, + 'courseid' => 0, + 'groupid' => 0, + 'userid' => 0, + 'repeatid' => 0, + 'modulename' => '', + 'instance' => 0, + 'eventtype' => 'user', + 'timestart' => 1486396800, + 'timeduration' => 0, + 'timesort' => 1486396800, + 'visible' => 1, + 'timemodified' => 1485793098, + 'subscriptionid' => null + ]; + + foreach ((array) $skeleton as $key => $value) { + $dbrow->$key = $value; + } + + return $dbrow; + } } diff --git a/calendar/tests/coursecat_proxy_test.php b/calendar/tests/coursecat_proxy_test.php new file mode 100644 index 00000000000..c00fe47d389 --- /dev/null +++ b/calendar/tests/coursecat_proxy_test.php @@ -0,0 +1,64 @@ +. + +/** + * coursecat_proxy tests. + * + * @package core_calendar + * @copyright 2017 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\local\event\proxies\coursecat_proxy; + +/** + * coursecat_proxy testcase. + * + * @copyright 2017 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_coursecat_proxy_testcase extends advanced_testcase { + + public function test_valid_coursecat() { + global $DB; + $this->resetAfterTest(); + + $name = '2027-2028 Academic Year'; + $generator = $this->getDataGenerator(); + $category = $generator->create_category([ + 'name' => $name, + ]); + cache_helper::purge_by_event('changesincoursecat'); + + // Fetch the proxy. + $startreads = $DB->perf_get_reads(); + $proxy = new coursecat_proxy($category->id); + $this->assertInstanceOf(coursecat_proxy::class, $proxy); + $this->assertEquals(0, $DB->perf_get_reads() - $startreads); + + // Fetch the ID - this is known and doesn't require a cache read. + $this->assertEquals($category->id, $proxy->get('id')); + $this->assertEquals(0, $DB->perf_get_reads() - $startreads); + + // Fetch the name - not known, and requires a read. + $this->assertEquals($name, $proxy->get('name')); + $this->assertEquals(1, $DB->perf_get_reads() - $startreads); + + $this->assertInstanceOf('coursecat', $proxy->get_proxied_instance()); + } +} diff --git a/calendar/tests/event_factory_test.php b/calendar/tests/event_factory_test.php index 2cd22ea0516..93751263e2e 100644 --- a/calendar/tests/event_factory_test.php +++ b/calendar/tests/event_factory_test.php @@ -114,6 +114,7 @@ class core_calendar_event_factory_testcase extends advanced_testcase { 'name' => 'test', 'description' => 'Test description', 'format' => 2, + 'categoryid' => 0, 'courseid' => 1, 'groupid' => 1, 'userid' => 1, @@ -162,6 +163,7 @@ class core_calendar_event_factory_testcase extends advanced_testcase { 'name' => 'test', 'description' => 'Test description', 'format' => 2, + 'categoryid' => 0, 'courseid' => 1, 'groupid' => 1, 'userid' => 1, @@ -210,6 +212,7 @@ class core_calendar_event_factory_testcase extends advanced_testcase { 'name' => 'test', 'description' => 'Test description', 'format' => 2, + 'categoryid' => 0, 'courseid' => 1, 'groupid' => 1, 'userid' => 1, @@ -258,6 +261,7 @@ class core_calendar_event_factory_testcase extends advanced_testcase { 'name' => 'test', 'description' => 'Test description', 'format' => 2, + 'categoryid' => 0, 'courseid' => $course->id, 'groupid' => 1, 'userid' => 1, @@ -312,6 +316,7 @@ class core_calendar_event_factory_testcase extends advanced_testcase { 'name' => 'test', 'description' => 'Test description', 'format' => 2, + 'categoryid' => 0, 'courseid' => 0, 'groupid' => 1, 'userid' => 1, @@ -345,6 +350,7 @@ class core_calendar_event_factory_testcase extends advanced_testcase { 'name' => 'Test event', 'description' => 'Hello', 'format' => 1, + 'categoryid' => 0, 'courseid' => 1, 'groupid' => 1, 'userid' => 1, @@ -378,6 +384,7 @@ class core_calendar_event_factory_testcase extends advanced_testcase { 'name' => 'Test event', 'description' => 'Hello', 'format' => 1, + 'categoryid' => 0, 'courseid' => 1, 'groupid' => 1, 'userid' => 1, @@ -411,6 +418,7 @@ class core_calendar_event_factory_testcase extends advanced_testcase { 'name' => 'Test event', 'description' => 'Hello', 'format' => 1, + 'categoryid' => 0, 'courseid' => 1, 'groupid' => 1, 'userid' => 1, diff --git a/calendar/tests/event_mapper_test.php b/calendar/tests/event_mapper_test.php index 0071a500d9d..829465165be 100644 --- a/calendar/tests/event_mapper_test.php +++ b/calendar/tests/event_mapper_test.php @@ -202,6 +202,10 @@ class event_mapper_test_action_event implements action_event_interface { return $this->event->get_description(); } + public function get_category() { + return $this->event->get_category(); + } + public function get_course() { return $this->event->get_course(); } @@ -255,6 +259,11 @@ class event_mapper_test_action_event implements action_event_interface { * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class event_mapper_test_event implements event_interface { + /** + * @var proxy_interface $categoryproxy Category proxy. + */ + protected $categoryproxy; + /** * @var proxy_interface $courseproxy Course proxy. */ @@ -312,6 +321,10 @@ class event_mapper_test_event implements event_interface { return new event_description('asdf', 1); } + public function get_category() { + return $this->categoryproxy; + } + public function get_course() { return $this->courseproxy; } diff --git a/calendar/tests/event_test.php b/calendar/tests/event_test.php index b2e5d9b0c82..8b28046d2b8 100644 --- a/calendar/tests/event_test.php +++ b/calendar/tests/event_test.php @@ -26,6 +26,7 @@ defined('MOODLE_INTERNAL') || die(); use core_calendar\local\event\entities\event; use core_calendar\local\event\proxies\std_proxy; +use core_calendar\local\event\proxies\coursecat_proxy; use core_calendar\local\event\value_objects\event_description; use core_calendar\local\event\value_objects\event_times; use core_calendar\local\event\entities\event_collection_interface; @@ -48,6 +49,7 @@ class core_calendar_event_testcase extends advanced_testcase { $constructorparams['id'], $constructorparams['name'], $constructorparams['description'], + $constructorparams['category'], $constructorparams['course'], $constructorparams['group'], $constructorparams['user'], @@ -82,6 +84,7 @@ class core_calendar_event_testcase extends advanced_testcase { 'id' => 1, 'name' => 'Test event 1', 'description' => new event_description('asdf', 1), + 'category' => new coursecat_proxy(0), 'course' => new std_proxy(1, $lamecallable), 'group' => new std_proxy(1, $lamecallable), 'user' => new std_proxy(1, $lamecallable), diff --git a/calendar/tests/helpers.php b/calendar/tests/helpers.php index f8e08fbeeaf..2c3b461c70d 100644 --- a/calendar/tests/helpers.php +++ b/calendar/tests/helpers.php @@ -32,6 +32,7 @@ use core_calendar\local\event\entities\action_event; use core_calendar\local\event\entities\event; use core_calendar\local\event\entities\repeat_event_collection; use core_calendar\local\event\proxies\std_proxy; +use core_calendar\local\event\proxies\coursecat_proxy; use core_calendar\local\event\proxies\cm_info_proxy; use core_calendar\local\event\value_objects\action; use core_calendar\local\event\value_objects\event_description; @@ -108,6 +109,7 @@ class action_event_test_factory implements event_factory_interface { $record->id, $record->name, new event_description($record->description, $record->format), + new coursecat_proxy($record->categoryid), new std_proxy($record->courseid, function($id) { $course = new \stdClass(); $course->id = $id; diff --git a/calendar/tests/raw_event_retrieval_strategy_test.php b/calendar/tests/raw_event_retrieval_strategy_test.php index 82d77948ad9..9d8b1375b65 100644 --- a/calendar/tests/raw_event_retrieval_strategy_test.php +++ b/calendar/tests/raw_event_retrieval_strategy_test.php @@ -277,4 +277,69 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc $events = $retrievalstrategy->get_raw_events(); $this->assertCount(3, $events); } + + /** + * Test retrieval strategy with category specifications. + */ + public function test_get_raw_events_category() { + global $DB; + + $this->resetAfterTest(); + $retrievalstrategy = new raw_event_retrieval_strategy(); + $generator = $this->getDataGenerator(); + $category1 = $generator->create_category(); + $category2 = $generator->create_category(); + $events = [ + [ + 'name' => 'E1', + 'eventtype' => 'category', + 'description' => '', + 'format' => 1, + 'categoryid' => $category1->id, + 'userid' => 2, + 'timestart' => time(), + ], + [ + 'name' => 'E2', + 'eventtype' => 'category', + 'description' => '', + 'format' => 1, + 'categoryid' => $category2->id, + 'userid' => 2, + 'timestart' => time() + 1, + ], + ]; + + foreach ($events as $event) { + calendar_event::create($event, false); + } + + // Get all events. + $events = $retrievalstrategy->get_raw_events(null, null, null, null); + $this->assertCount(2, $events); + + $event = array_shift($events); + $this->assertEquals('E1', $event->name); + $event = array_shift($events); + $this->assertEquals('E2', $event->name); + + // Get events for C1 events. + $events = $retrievalstrategy->get_raw_events(null, null, null, [$category1->id]); + $this->assertCount(1, $events); + + $event = array_shift($events); + $this->assertEquals('E1', $event->name); + + // Get events for C2 events. + $events = $retrievalstrategy->get_raw_events(null, null, null, [$category2->id]); + $this->assertCount(1, $events); + + $event = array_shift($events); + $this->assertEquals('E2', $event->name); + + // Get events for several categories. + $events = $retrievalstrategy->get_raw_events(null, null, null, [$category1->id, $category2->id]); + $this->assertCount(2, $events); + } } + diff --git a/calendar/tests/repeat_event_collection_test.php b/calendar/tests/repeat_event_collection_test.php index f59fdd9cb00..ec6320c8de8 100644 --- a/calendar/tests/repeat_event_collection_test.php +++ b/calendar/tests/repeat_event_collection_test.php @@ -30,6 +30,7 @@ require_once($CFG->dirroot . '/calendar/lib.php'); use core_calendar\local\event\entities\event; use core_calendar\local\event\entities\repeat_event_collection; +use core_calendar\local\event\proxies\coursecat_proxy; use core_calendar\local\event\proxies\std_proxy; use core_calendar\local\event\value_objects\event_description; use core_calendar\local\event\value_objects\event_times; @@ -161,6 +162,7 @@ class core_calendar_repeat_event_collection_event_test_factory implements event_ $dbrow->id, $dbrow->name, new event_description($dbrow->description, $dbrow->format), + new coursecat_proxy($dbrow->categoryid), new std_proxy($dbrow->courseid, $identity), new std_proxy($dbrow->groupid, $identity), new std_proxy($dbrow->userid, $identity), diff --git a/lib/coursecatlib.php b/lib/coursecatlib.php index d4b56c2e936..e5af7cb12e3 100644 --- a/lib/coursecatlib.php +++ b/lib/coursecatlib.php @@ -1305,6 +1305,14 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate { array($this->id)); } + /** + */ + public function get_view_link() { + return new \moodle_url('/course/index.php', [ + 'categoryid' => $this->id, + ]); + } + /** * Searches courses * From 02d0c4355e187275d5451e5c4f42c48b8f443e91 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 19 Sep 2017 11:16:30 +0800 Subject: [PATCH 05/15] MDL-59890 calendar: Stop adding time for mini view - not used --- blocks/calendar_month/block_calendar_month.php | 18 +----------------- calendar/lib.php | 2 +- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/blocks/calendar_month/block_calendar_month.php b/blocks/calendar_month/block_calendar_month.php index 18a57d72e13..6833188c997 100644 --- a/blocks/calendar_month/block_calendar_month.php +++ b/blocks/calendar_month/block_calendar_month.php @@ -38,32 +38,16 @@ class block_calendar_month extends block_base { public function get_content() { global $CFG; - $calm = optional_param('cal_m', 0, PARAM_INT); - $caly = optional_param('cal_y', 0, PARAM_INT); - $time = optional_param('time', 0, PARAM_INT); - require_once($CFG->dirroot.'/calendar/lib.php'); if ($this->content !== null) { return $this->content; } - // If a day, month and year were passed then convert it to a timestamp. If these were passed then we can assume - // the day, month and year are passed as Gregorian, as no where in core should we be passing these values rather - // than the time. This is done for BC. - if (!empty($calm) && (!empty($caly))) { - $time = make_timestamp($caly, $calm, 1); - } else if (empty($time)) { - $time = time(); - } - $this->content = new stdClass; $this->content->text = ''; $this->content->footer = ''; - // [pj] To me it looks like this if would never be needed, but Penny added it - // when committing the /my/ stuff. Reminder to discuss and learn what it's about. - // It definitely needs SOME comment here! $courseid = $this->page->course->id; $issite = ($courseid == SITEID); @@ -80,7 +64,7 @@ class block_calendar_month extends block_base { $renderer = $this->page->get_renderer('core_calendar'); - $calendar = new calendar_information(0, 0, 0, $time); + $calendar = new calendar_information(); $calendar->prepare_for_view($course, $courses); list($data, $template) = calendar_get_view($calendar, 'mini'); diff --git a/calendar/lib.php b/calendar/lib.php index 6b461158f5e..4d86ee29143 100644 --- a/calendar/lib.php +++ b/calendar/lib.php @@ -1012,7 +1012,7 @@ class calendar_information { * @return $this */ public function set_time($time = null) { - if ($time === null) { + if (empty($time)) { $this->time = time(); } else { $this->time = $time; From e9321ad07dfdaa82afc2976d5a71500f8006d6bc Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 21 Sep 2017 13:57:12 +0800 Subject: [PATCH 06/15] MDL-59890 coursecat: Add helper to fetch nested view of coursecat --- lib/coursecatlib.php | 38 +++++++++++++++++++++++++++------ lib/tests/coursecatlib_test.php | 16 ++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/lib/coursecatlib.php b/lib/coursecatlib.php index e5af7cb12e3..916c7d830e9 100644 --- a/lib/coursecatlib.php +++ b/lib/coursecatlib.php @@ -298,13 +298,9 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate { * @param bool $options.returnhidden Return categories even if they are hidden * @return coursecat[] */ - public static function get_all($options = null) { + public static function get_all($options = []) { global $DB; - if (null === $options) { - $options = []; - } - $coursecatrecordcache = cache::make('core', 'coursecatrecords'); $catcontextsql = \context_helper::get_preload_record_columns_sql('ctx'); @@ -322,12 +318,11 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate { $categories = []; $toset = []; foreach ($catrs as $record) { - \context_helper::preload_from_record($record); $category = new coursecat($record); $toset[$category->id] = $category; if (!empty($options['returnhidden']) || $category->is_uservisible()) { - $categories[$record->id] = new coursecat($record); + $categories[$record->id] = $category; } } $catrs->close(); @@ -1306,6 +1301,9 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate { } /** + * Get the link used to view this course category. + * + * @return \moodle_url */ public function get_view_link() { return new \moodle_url('/course/index.php', [ @@ -2213,6 +2211,32 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate { } } + /** + * Get the nested name of this category, with all of it's parents. + * + * @param bool $includelinks Whether to wrap each name in the view link for that category. + * @param string $separator The string between each name. + * @param array $options Formatting options. + * @return string + */ + public function get_nested_name($includelinks = true, $separator = ' / ', $options = []) { + // Get the name of hierarchical name of this category. + $parents = $this->get_parents(); + $categories = static::get_many($parents); + $categories[] = $this; + + $names = array_map(function($category) use ($options, $includelinks) { + if ($includelinks) { + return html_writer::link($category->get_view_link(), $category->get_formatted_name($options)); + } else { + return $category->get_formatted_name($options); + } + + }, $categories); + + return implode($separator, $names); + } + /** * Returns ids of all parents of the category. Last element in the return array is the direct parent * diff --git a/lib/tests/coursecatlib_test.php b/lib/tests/coursecatlib_test.php index 4662fcbd9bc..1dea0e66d63 100644 --- a/lib/tests/coursecatlib_test.php +++ b/lib/tests/coursecatlib_test.php @@ -750,6 +750,22 @@ class core_coursecatlib_testcase extends advanced_testcase { $this->assertEquals(1, count($courses[$c5->id]->get_course_overviewfiles())); } + public function test_get_nested_name() { + $cat1name = 'Cat1'; + $cat2name = 'Cat2'; + $cat3name = 'Cat3'; + $cat4name = 'Cat4'; + $category1 = coursecat::create(array('name' => $cat1name)); + $category2 = coursecat::create(array('name' => $cat2name, 'parent' => $category1->id)); + $category3 = coursecat::create(array('name' => $cat3name, 'parent' => $category2->id)); + $category4 = coursecat::create(array('name' => $cat4name, 'parent' => $category2->id)); + + $this->assertEquals($cat1name, $category1->get_nested_name(false)); + $this->assertEquals("{$cat1name} / {$cat2name}", $category2->get_nested_name(false)); + $this->assertEquals("{$cat1name} / {$cat2name} / {$cat3name}", $category3->get_nested_name(false)); + $this->assertEquals("{$cat1name} / {$cat2name} / {$cat4name}", $category4->get_nested_name(false)); + } + /** * Creates a draft area for current user and fills it with fake files * From 5ba6507e5b7bdb4bcff325b1f7fd67b7fde0d45c Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 19 Sep 2017 11:31:50 +0800 Subject: [PATCH 07/15] MDL-59890 calendar: Exporters support for categories --- .../external/calendar_event_exporter.php | 11 ++ calendar/classes/external/day_exporter.php | 21 ++- .../classes/external/event_exporter_base.php | 20 +++ calendar/classes/external/month_exporter.php | 6 +- .../classes/external/week_day_exporter.php | 10 +- lang/en/calendar.php | 1 + .../external/coursecat_summary_exporter.php | 133 ++++++++++++++++++ 7 files changed, 192 insertions(+), 10 deletions(-) create mode 100644 lib/classes/external/coursecat_summary_exporter.php diff --git a/calendar/classes/external/calendar_event_exporter.php b/calendar/classes/external/calendar_event_exporter.php index f20c7f71990..c27e3647a1a 100644 --- a/calendar/classes/external/calendar_event_exporter.php +++ b/calendar/classes/external/calendar_event_exporter.php @@ -126,6 +126,17 @@ class calendar_event_exporter extends event_exporter_base { } } + // Include category name into the event name, if applicable. + $proxy = $this->event->get_category(); + if ($proxy && $proxy->get('id')) { + $category = $proxy->get_proxied_instance(); + $eventnameparams = (object) [ + 'name' => $values['popupname'], + 'category' => $category->get_formatted_name(), + ]; + $values['popupname'] = get_string('eventnameandcategory', 'calendar', $eventnameparams); + } + // Include course's shortname into the event name, if applicable. $course = $this->event->get_course(); if ($course && $course->get('id') && $course->get('id') !== SITEID) { diff --git a/calendar/classes/external/day_exporter.php b/calendar/classes/external/day_exporter.php index 2965451c086..ebe8d8686c8 100644 --- a/calendar/classes/external/day_exporter.php +++ b/calendar/classes/external/day_exporter.php @@ -59,11 +59,20 @@ class day_exporter extends exporter { */ public function __construct(\calendar_information $calendar, $data, $related) { $this->calendar = $calendar; - $this->url = new moodle_url('/calendar/view.php', [ - 'view' => 'day', - 'time' => $calendar->time, - 'course' => $this->calendar->course->id, - ]); + + $url = new moodle_url('/calendar/view.php', [ + 'view' => 'day', + 'time' => $calendar->time, + ]); + + if ($this->calendar->course && SITEID !== $this->calendar->course->id) { + $url->param('course', $this->calendar->course->id); + } else if ($this->calendar->categoryid) { + $url->param('category', $this->calendar->categoryid); + } + + $this->url = $url; + parent::__construct($data, $related); } @@ -179,9 +188,9 @@ class day_exporter extends exporter { 'navigation' => $this->get_navigation(), 'filter_selector' => $this->get_course_filter_selector($output), 'new_event_button' => $this->get_new_event_button(), + 'viewdaylink' => $this->url->out(false), ]; - $return['viewdaylink'] = $this->url->out(false); $cache = $this->related['cache']; $eventexporters = array_map(function($event) use ($cache, $output) { diff --git a/calendar/classes/external/event_exporter_base.php b/calendar/classes/external/event_exporter_base.php index cb3ba804f89..9af9ce2139b 100644 --- a/calendar/classes/external/event_exporter_base.php +++ b/calendar/classes/external/event_exporter_base.php @@ -34,6 +34,7 @@ use \core_calendar\local\event\container; use \core_calendar\local\event\entities\event_interface; use \core_calendar\local\event\entities\action_event_interface; use \core_course\external\course_summary_exporter; +use \core\external\coursecat_summary_exporter; use \renderer_base; use moodle_url; @@ -64,6 +65,7 @@ class event_exporter_base extends exporter { $endtimestamp = $event->get_times()->get_end_time()->getTimestamp(); $groupid = $event->get_group() ? $event->get_group()->get('id') : null; $userid = $event->get_user() ? $event->get_user()->get('id') : null; + $categoryid = $event->get_category() ? $event->get_category()->get('id') : null; $data = new \stdClass(); $data->id = $event->get_id(); @@ -79,6 +81,7 @@ class event_exporter_base extends exporter { $data->descriptionformat = $event->get_description()->get_format(); $data->groupid = $groupid; $data->userid = $userid; + $data->categoryid = $categoryid; $data->eventtype = $event->get_type(); $data->timestart = $starttimestamp; $data->timeduration = $endtimestamp - $starttimestamp; @@ -120,6 +123,12 @@ class event_exporter_base extends exporter { 'default' => null, 'null' => NULL_ALLOWED ], + 'categoryid' => [ + 'type' => PARAM_INT, + 'optional' => true, + 'default' => null, + 'null' => NULL_ALLOWED + ], 'groupid' => [ 'type' => PARAM_INT, 'optional' => true, @@ -175,6 +184,10 @@ class event_exporter_base extends exporter { 'icon' => [ 'type' => event_icon_exporter::read_properties_definition(), ], + 'category' => [ + 'type' => coursecat_summary_exporter::read_properties_definition(), + 'optional' => true, + ], 'course' => [ 'type' => course_summary_exporter::read_properties_definition(), 'optional' => true, @@ -239,6 +252,13 @@ class event_exporter_base extends exporter { $subscriptionexporter = new event_subscription_exporter($event); $values['subscription'] = $subscriptionexporter->export($output); + $proxy = $this->event->get_category(); + if ($proxy && $proxy->get('id')) { + $category = $proxy->get_proxied_instance(); + $categorysummaryexporter = new coursecat_summary_exporter($category, ['context' => $context]); + $values['category'] = $categorysummaryexporter->export($output); + } + if ($course = $this->related['course']) { $coursesummaryexporter = new course_summary_exporter($course, ['context' => $context]); $values['course'] = $coursesummaryexporter->export($output); diff --git a/calendar/classes/external/month_exporter.php b/calendar/classes/external/month_exporter.php index b71ccc85a3a..3fff7fe22b7 100644 --- a/calendar/classes/external/month_exporter.php +++ b/calendar/classes/external/month_exporter.php @@ -75,8 +75,10 @@ class month_exporter extends exporter { 'time' => $calendar->time, ]); - if ($this->calendar->courseid) { - $this->url->param('course', $this->calendar->courseid); + if ($this->calendar->course && SITEID !== $this->calendar->course->id) { + $this->url->param('course', $this->calendar->course->id); + } else if ($this->calendar->categoryid) { + $this->url->param('category', $this->calendar->categoryid); } $related['type'] = $type; diff --git a/calendar/classes/external/week_day_exporter.php b/calendar/classes/external/week_day_exporter.php index e974154a518..5d87d1870ec 100644 --- a/calendar/classes/external/week_day_exporter.php +++ b/calendar/classes/external/week_day_exporter.php @@ -118,10 +118,16 @@ class week_day_exporter extends day_exporter { $url = new moodle_url('/calendar/view.php', [ 'view' => 'day', 'time' => $timestamp, - 'course' => $this->calendar->course->id, - ]); + ]); + + if ($this->calendar->course && SITEID !== $this->calendar->course->id) { + $url->param('course', $this->calendar->course->id); + } else if ($this->calendar->categoryid) { + $url->param('category', $this->calendar->categoryid); + } $return['viewdaylink'] = $url->out(false); + if ($popovertitle = $this->get_popover_title()) { $return['popovertitle'] = $popovertitle; } diff --git a/lang/en/calendar.php b/lang/en/calendar.php index 28280d02cf5..7401c9d8530 100644 --- a/lang/en/calendar.php +++ b/lang/en/calendar.php @@ -98,6 +98,7 @@ $string['eventendtimewrapped'] = '{$a} (End time)'; $string['eventinstanttime'] = 'Time'; $string['eventkind'] = 'Type of event'; $string['eventname'] = 'Event title'; +$string['eventnameandcategory'] = '{$a->category}: {$a->name}'; $string['eventnameandcourse'] = '{$a->course}: {$a->name}'; $string['eventnone'] = 'No events'; $string['eventrepeat'] = 'Repeats'; diff --git a/lib/classes/external/coursecat_summary_exporter.php b/lib/classes/external/coursecat_summary_exporter.php new file mode 100644 index 00000000000..de3428eeadf --- /dev/null +++ b/lib/classes/external/coursecat_summary_exporter.php @@ -0,0 +1,133 @@ +. + +/** + * Class for exporting summary information for a course category. + * + * @package core + * @copyright 2017 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\external; +defined('MOODLE_INTERNAL') || die(); + +use renderer_base; +use moodle_url; + +/** + * Class for exporting a course summary from an stdClass. + * + * @copyright 2017 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class coursecat_summary_exporter extends \core\external\exporter { + + /** + * @var \coursecat $category + */ + protected $category; + + public function __construct(\coursecat $category, $related) { + $this->category = $category; + + $data = []; + // Specify some defaults. + foreach ($category as $key => $value) { + $data[$key] = $value; + } + + return parent::__construct($data, $related); + } + + protected static function define_related() { + return [ + 'context' => 'context', + ]; + } + + public static function define_other_properties() { + return [ + 'nestedname' => [ + 'type' => PARAM_RAW, + ], + 'url' => [ + 'type' => PARAM_URL, + ], + ]; + } + + protected function get_other_values(renderer_base $output) { + $return = [ + 'nestedname' => $this->category->get_nested_name(), + 'url' => (new moodle_url('/course/index.php', [ + 'categoryid' => $this->category->id, + ]))->out(false), + ]; + + return $return; + } + + public static function define_properties() { + return [ + 'id' => [ + 'type' => PARAM_INT, + ], + 'name' => [ + 'type' => PARAM_TEXT, + 'default' => '', + ], + 'idnumber' => [ + 'type' => PARAM_RAW, + 'null' => NULL_ALLOWED, + ], + 'description' => [ + 'type' => PARAM_RAW, + 'optional' => true, + ], + 'parent' => [ + 'type' => PARAM_INT, + ], + 'coursecount' => [ + 'type' => PARAM_INT, + 'default' => 0, + ], + 'visible' => [ + 'type' => PARAM_INT, + 'default' => 1, + ], + 'timemodified' => [ + 'type' => PARAM_INT, + 'default' => 0, + ], + 'depth' => [ + 'type' => PARAM_INT, + 'default' => 0, + ], + ]; + } + + /** + * Get the formatting parameters for the summary. + * + * @return array + */ + protected function get_format_parameters_for_description() { + return [ + 'component' => 'coursecat', + 'filearea' => 'description', + ]; + } +} From d0e56d847a13880b0cefcff995abe77222a4c8fe Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 19 Sep 2017 11:17:04 +0800 Subject: [PATCH 08/15] MDL-59890 calendar: Display category events on calendars --- .../calendar_month/block_calendar_month.php | 15 ++++++- calendar/amd/build/calendar.min.js | 2 +- calendar/amd/build/calendar_threemonth.min.js | 2 +- calendar/amd/build/repository.min.js | 2 +- calendar/amd/build/selectors.min.js | 2 +- calendar/amd/build/view_manager.min.js | 2 +- calendar/amd/src/calendar.js | 2 +- calendar/amd/src/calendar_threemonth.js | 13 ++++-- calendar/amd/src/repository.js | 6 ++- calendar/amd/src/view_manager.js | 33 ++++++++++----- .../external/calendar_event_exporter.php | 2 + calendar/classes/external/event_exporter.php | 13 ------ .../classes/external/event_exporter_base.php | 6 +++ .../classes/external/event_icon_exporter.php | 17 +++++--- calendar/classes/external/month_exporter.php | 9 ++++ calendar/event.php | 2 +- calendar/export.php | 2 +- calendar/externallib.php | 41 ++++++++++++++----- calendar/lib.php | 2 +- .../templates/event_summary_body.mustache | 3 ++ calendar/templates/month_detailed.mustache | 3 +- calendar/templates/month_mini.mustache | 1 + calendar/view.php | 23 +++++++++-- lang/en/calendar.php | 1 + .../output/icon_system_fontawesome.php | 1 + pix/i/categoryevent.svg | 3 ++ theme/boost/scss/moodle/calendar.scss | 17 ++++++++ theme/bootstrapbase/less/moodle/calendar.less | 15 +++++++ theme/bootstrapbase/style/moodle.css | 14 +++++++ 29 files changed, 194 insertions(+), 60 deletions(-) create mode 100644 pix/i/categoryevent.svg diff --git a/blocks/calendar_month/block_calendar_month.php b/blocks/calendar_month/block_calendar_month.php index 6833188c997..082579809e4 100644 --- a/blocks/calendar_month/block_calendar_month.php +++ b/blocks/calendar_month/block_calendar_month.php @@ -51,11 +51,24 @@ class block_calendar_month extends block_base { $courseid = $this->page->course->id; $issite = ($courseid == SITEID); + $course = null; + $courses = null; + $categories = null; + if ($issite) { // Being displayed at site level. This will cause the filter to fall back to auto-detecting // the list of courses it will be grabbing events from. $course = get_site(); $courses = calendar_get_default_courses(); + + if ($this->page->context->contextlevel === CONTEXT_COURSECAT) { + // Restrict to categories, and their parents, and the courses that the user is enrolled in within those + // categories. + $categories = array_keys($this->page->categories); + $courses = array_filter($courses, function($course) use ($categories) { + return array_search($course->category, $categories) !== false; + }); + } } else { // Forcibly filter events to include only those from the particular course we are in. $course = $this->page->course; @@ -65,7 +78,7 @@ class block_calendar_month extends block_base { $renderer = $this->page->get_renderer('core_calendar'); $calendar = new calendar_information(); - $calendar->prepare_for_view($course, $courses); + $calendar->set_sources($course, $courses, $this->page->category); list($data, $template) = calendar_get_view($calendar, 'mini'); $this->content->text .= $renderer->render_from_template($template, $data); diff --git a/calendar/amd/build/calendar.min.js b/calendar/amd/build/calendar.min.js index a1c8e36ee51..4778e33b726 100644 --- a/calendar/amd/build/calendar.min.js +++ b/calendar/amd/build/calendar.min.js @@ -1 +1 @@ -define(["jquery","core/ajax","core/str","core/templates","core/notification","core/custom_interaction_events","core/modal_events","core/modal_factory","core_calendar/modal_event_form","core_calendar/summary_modal","core_calendar/repository","core_calendar/events","core_calendar/view_manager"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){var n={ROOT:"[data-region='calendar']",DAY:"[data-region='day']",EVENT_ITEM:"[data-region='event-item']",EVENT_LINK:"[data-action='view-event']",NEW_EVENT_BUTTON:"[data-action='new-event-button']",DAY_CONTENT:"[data-region='day-content']",LOADING_ICON:".loading-icon",VIEW_DAY_LINK:"[data-action='view-day-link']",CALENDAR_MONTH_WRAPPER:".calendarwrapper",COURSE_SELECTOR:'select[name="course"]',TODAY:".today"},o=function(a){var b="type"+a;return c.get_string(b,"core_calendar").then(function(a){return a})},p=function(a){k.getEventById(a).then(function(b){if(!b.event)throw new Error("Error encountered while trying to fetch calendar event with ID: "+a);var c=b.event;return o(c.eventtype).then(function(a){return c.eventtype=a,c})}).then(function(a){var b={title:a.name,type:j.TYPE,body:d.render("core_calendar/event_summary_body",a),templateContext:{canedit:a.canedit,candelete:a.candelete,isactionevent:a.isactionevent,url:a.url}};return h.create(b)}).done(function(a){a.getRoot().on(g.hidden,function(){a.destroy()}),a.show()}).fail(e.exception)},q=function(b,c,f,g){var h=null,i=g.attr("data-day-timestamp");f&&(h=f.attr("data-day-timestamp")),f&&h==i||d.render("core/loading",{}).then(function(a,b){g.find(n.DAY_CONTENT).addClass("hidden"),d.appendNodeContents(g,a,b),f&&(f.find(n.DAY_CONTENT).addClass("hidden"),d.appendNodeContents(f,a,b))}).then(function(){return k.updateEventStartDay(c,i)}).then(function(){a("body").trigger(l.eventMoved,[c,f,g])}).always(function(){var a=g.find(n.LOADING_ICON);if(g.find(n.DAY_CONTENT).removeClass("hidden"),d.replaceNode(a,"",""),f){var b=f.find(n.LOADING_ICON);f.find(n.DAY_CONTENT).removeClass("hidden"),d.replaceNode(b,"","")}}).fail(e.exception)},r=function(a){var b=a.find(n.NEW_EVENT_BUTTON),c=b.attr("data-context-id");return h.create({type:i.TYPE,large:!0,templateContext:{contextid:c}})},s=function(b,c){var d=a("body"),f=a(b).find(n.CALENDAR_MONTH_WRAPPER).data("courseid");d.on(l.created,function(){m.reloadCurrentMonth(b)}),d.on(l.deleted,function(){m.reloadCurrentMonth(b)}),d.on(l.updated,function(){m.reloadCurrentMonth(b)}),d.on(l.editActionEvent,function(a,b){window.location.assign(b)}),d.on(l.moveEvent,q),d.on(l.eventMoved,function(){m.reloadCurrentMonth(b)}),c.then(function(a){d.on(l.editEvent,function(b,c){a.setEventId(c),a.show()}),a.setCourseId(f)}).fail(e.exception)},t=function(b){b.on("click",n.EVENT_ITEM,function(b){b.preventDefault(),b.stopPropagation();var c=a(b.target),d=null;d=c.is(n.EVENT_LINK)?c.attr("data-event-id"):c.find(n.EVENT_LINK).attr("data-event-id"),p(d)}),b.on("change",n.COURSE_SELECTOR,function(){var c=a(this),d=c.val();m.reloadCurrentMonth(b,d).then(function(){return b.find(n.COURSE_SELECTOR).val(d)}).fail(e.exception)});var c=r(b);s(b,c),b.on("click",n.NEW_EVENT_BUTTON,function(a){c.then(function(a){var c=b.find(n.TODAY);c.length||a.setStartTime(b.find(n.DAY).attr("data-new-event-timestamp")),a.show()}).fail(e.exception),a.preventDefault()}),b.on("click",n.DAY,function(b){var d=a(b.target);if(!d.is(n.VIEW_DAY_LINK)){var f=a(this).attr("data-new-event-timestamp");c.then(function(a){a.setStartTime(f),a.show()}).fail(e.exception),b.preventDefault()}})};return{init:function(b){b=a(b),m.init(b),t(b)}}}); \ No newline at end of file +define(["jquery","core/ajax","core/str","core/templates","core/notification","core/custom_interaction_events","core/modal_events","core/modal_factory","core_calendar/modal_event_form","core_calendar/summary_modal","core_calendar/repository","core_calendar/events","core_calendar/view_manager"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){var n={ROOT:"[data-region='calendar']",DAY:"[data-region='day']",EVENT_ITEM:"[data-region='event-item']",EVENT_LINK:"[data-action='view-event']",NEW_EVENT_BUTTON:"[data-action='new-event-button']",DAY_CONTENT:"[data-region='day-content']",LOADING_ICON:".loading-icon",VIEW_DAY_LINK:"[data-action='view-day-link']",CALENDAR_MONTH_WRAPPER:".calendarwrapper",COURSE_SELECTOR:'select[name="course"]',TODAY:".today"},o=function(a){var b="type"+a;return c.get_string(b,"core_calendar").then(function(a){return a})},p=function(a){k.getEventById(a).then(function(b){if(!b.event)throw new Error("Error encountered while trying to fetch calendar event with ID: "+a);var c=b.event;return o(c.eventtype).then(function(a){return c.eventtype=a,c})}).then(function(a){var b={title:a.name,type:j.TYPE,body:d.render("core_calendar/event_summary_body",a),templateContext:{canedit:a.canedit,candelete:a.candelete,isactionevent:a.isactionevent,url:a.url}};return h.create(b)}).done(function(a){a.getRoot().on(g.hidden,function(){a.destroy()}),a.show()}).fail(e.exception)},q=function(b,c,f,g){var h=null,i=g.attr("data-day-timestamp");f&&(h=f.attr("data-day-timestamp")),f&&h==i||d.render("core/loading",{}).then(function(a,b){g.find(n.DAY_CONTENT).addClass("hidden"),d.appendNodeContents(g,a,b),f&&(f.find(n.DAY_CONTENT).addClass("hidden"),d.appendNodeContents(f,a,b))}).then(function(){return k.updateEventStartDay(c,i)}).then(function(){a("body").trigger(l.eventMoved,[c,f,g])}).always(function(){var a=g.find(n.LOADING_ICON);if(g.find(n.DAY_CONTENT).removeClass("hidden"),d.replaceNode(a,"",""),f){var b=f.find(n.LOADING_ICON);f.find(n.DAY_CONTENT).removeClass("hidden"),d.replaceNode(b,"","")}}).fail(e.exception)},r=function(a){var b=a.find(n.NEW_EVENT_BUTTON),c=b.attr("data-context-id");return h.create({type:i.TYPE,large:!0,templateContext:{contextid:c}})},s=function(b,c){var d=a("body"),f=a(b).find(n.CALENDAR_MONTH_WRAPPER).data("courseid");d.on(l.created,function(){m.reloadCurrentMonth(b)}),d.on(l.deleted,function(){m.reloadCurrentMonth(b)}),d.on(l.updated,function(){m.reloadCurrentMonth(b)}),d.on(l.editActionEvent,function(a,b){window.location.assign(b)}),d.on(l.moveEvent,q),d.on(l.eventMoved,function(){m.reloadCurrentMonth(b)}),c.then(function(a){d.on(l.editEvent,function(b,c){a.setEventId(c),a.show()}),a.setCourseId(f)}).fail(e.exception)},t=function(b){b.on("click",n.EVENT_ITEM,function(b){b.preventDefault(),b.stopPropagation();var c=a(b.target),d=null;d=c.is(n.EVENT_LINK)?c.attr("data-event-id"):c.find(n.EVENT_LINK).attr("data-event-id"),p(d)}),b.on("change",n.COURSE_SELECTOR,function(){var c=a(this),d=c.val();m.reloadCurrentMonth(b,d,null).then(function(){return b.find(n.COURSE_SELECTOR).val(d)}).fail(e.exception)});var c=r(b);s(b,c),b.on("click",n.NEW_EVENT_BUTTON,function(a){c.then(function(a){var c=b.find(n.TODAY);c.length||a.setStartTime(b.find(n.DAY).attr("data-new-event-timestamp")),a.show()}).fail(e.exception),a.preventDefault()}),b.on("click",n.DAY,function(b){var d=a(b.target);if(!d.is(n.VIEW_DAY_LINK)){var f=a(this).attr("data-new-event-timestamp");c.then(function(a){a.setStartTime(f),a.show()}).fail(e.exception),b.preventDefault()}})};return{init:function(b){b=a(b),m.init(b),t(b)}}}); \ No newline at end of file diff --git a/calendar/amd/build/calendar_threemonth.min.js b/calendar/amd/build/calendar_threemonth.min.js index ac1bb8cce4b..f0300b1d2b1 100644 --- a/calendar/amd/build/calendar_threemonth.min.js +++ b/calendar/amd/build/calendar_threemonth.min.js @@ -1 +1 @@ -define(["jquery","core_calendar/selectors","core_calendar/events","core/templates","core_calendar/view_manager"],function(a,b,c,d,e){var f=function(d){var f=a("body");f.on(c.monthChanged,function(a,b,c,e){d.queue(function(d){return g(a,b,c,e).then(function(){return d()})})});var g=function(c,f,g,h){var i=d.find('[data-year="'+f+'"][data-month="'+g+'"]'),j=i.closest(b.calendarPeriods.month),k=d.find(b.calendarPeriods.month),l=a(k[0]),m=a(k[2]),n=a("");n.attr("data-template","core_calendar/threemonth_month"),n.attr("data-includenavigation",!1);var o=a("
");o.hide(),o.append(n);var p,q,r;return j.is(l)?(o.insertBefore(l),p=l.data("previousYear"),q=l.data("previousMonth"),r=m):j.is(m)&&(o.insertAfter(m),p=m.data("nextYear"),q=m.data("nextMonth"),r=l),e.refreshMonthContent(n,p,q,h,n).then(function(){var b=a.Deferred(),c=a.Deferred();return r.slideUp("fast",function(){a(this).remove(),b.resolve()}),o.slideDown("fast",function(){c.resolve()}),a.when(b,c)})}};return{init:function(b){b=a(b),f(b)}}}); \ No newline at end of file +define(["jquery","core/notification","core_calendar/selectors","core_calendar/events","core/templates","core_calendar/view_manager"],function(a,b,c,d,e,f){var g=function(e){var g=a("body");g.on(d.monthChanged,function(a,c,d,f,g){e.queue(function(e){return h(a,c,d,f,g).then(function(){return e()}).fail(b.exception)})});var h=function(b,d,g,h,i){var j=e.find('[data-year="'+d+'"][data-month="'+g+'"]'),k=j.closest(c.calendarPeriods.month),l=e.find(c.calendarPeriods.month),m=a(l[0]),n=a(l[2]),o=a("");o.attr("data-template","core_calendar/threemonth_month"),o.attr("data-includenavigation",!1);var p=a("
");p.hide(),p.append(o);var q,r,s;return k.is(m)?(p.insertBefore(m),q=m.data("previousYear"),r=m.data("previousMonth"),s=n):k.is(n)&&(p.insertAfter(n),q=n.data("nextYear"),r=n.data("nextMonth"),s=m),f.refreshMonthContent(o,q,r,h,i,o).then(function(){var b=a.Deferred(),c=a.Deferred();return s.slideUp("fast",function(){a(this).remove(),b.resolve()}),p.slideDown("fast",function(){c.resolve()}),a.when(b,c)})}};return{init:function(b){b=a(b),g(b)}}}); \ No newline at end of file diff --git a/calendar/amd/build/repository.min.js b/calendar/amd/build/repository.min.js index c970b3f7a6c..57dbe976851 100644 --- a/calendar/amd/build/repository.min.js +++ b/calendar/amd/build/repository.min.js @@ -1 +1 @@ -define(["jquery","core/ajax"],function(a,b){var c=function(a,c){"undefined"==typeof c&&(c=!1);var d={methodname:"core_calendar_delete_calendar_events",args:{events:[{eventid:a,repeat:c}]}};return b.call([d])[0]},d=function(a){var c={methodname:"core_calendar_get_calendar_event_by_id",args:{eventid:a}};return b.call([c])[0]},e=function(a){var c={methodname:"core_calendar_submit_create_update_form",args:{formdata:a}};return b.call([c])[0]},f=function(a,c,d,e){var f={methodname:"core_calendar_get_calendar_monthly_view",args:{year:a,month:c,courseid:d,includenavigation:e}};return b.call([f])[0]},g=function(a,c){var d={methodname:"core_calendar_update_event_start_day",args:{eventid:a,daytimestamp:c}};return b.call([d])[0]};return{getEventById:d,deleteEvent:c,updateEventStartDay:g,submitCreateUpdateForm:e,getCalendarMonthData:f}}); \ No newline at end of file +define(["jquery","core/ajax"],function(a,b){var c=function(a,c){"undefined"==typeof c&&(c=!1);var d={methodname:"core_calendar_delete_calendar_events",args:{events:[{eventid:a,repeat:c}]}};return b.call([d])[0]},d=function(a){var c={methodname:"core_calendar_get_calendar_event_by_id",args:{eventid:a}};return b.call([c])[0]},e=function(a){var c={methodname:"core_calendar_submit_create_update_form",args:{formdata:a}};return b.call([c])[0]},f=function(a,c,d,e,f){var g={methodname:"core_calendar_get_calendar_monthly_view",args:{year:a,month:c,courseid:d,categoryid:e,includenavigation:f}};return b.call([g])[0]},g=function(a,c){var d={methodname:"core_calendar_update_event_start_day",args:{eventid:a,daytimestamp:c}};return b.call([d])[0]};return{getEventById:d,deleteEvent:c,updateEventStartDay:g,submitCreateUpdateForm:e,getCalendarMonthData:f}}); \ No newline at end of file diff --git a/calendar/amd/build/selectors.min.js b/calendar/amd/build/selectors.min.js index a81f332e0c2..ed31fd7e78c 100644 --- a/calendar/amd/build/selectors.min.js +++ b/calendar/amd/build/selectors.min.js @@ -1 +1 @@ -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]"},calendarPeriods:{month:"[data-period='month']"}}}); \ No newline at end of file +define([],function(){return{eventFilterItem:"[data-action='filter-event-type']",eventType:{site:"[data-eventtype-site]",category:"[data-eventtype-category]",course:"[data-eventtype-course]",group:"[data-eventtype-group]",user:"[data-eventtype-user]"},popoverType:{site:"[data-popover-eventtype-site]",category:"[data-popover-eventtype-category]",course:"[data-popover-eventtype-course]",group:"[data-popover-eventtype-group]",user:"[data-popover-eventtype-user]"},calendarPeriods:{month:"[data-period='month']"}}}); \ No newline at end of file diff --git a/calendar/amd/build/view_manager.min.js b/calendar/amd/build/view_manager.min.js index 61f7942b88c..04dc7f6196b 100644 --- a/calendar/amd/build/view_manager.min.js +++ b/calendar/amd/build/view_manager.min.js @@ -1 +1 @@ -define(["jquery","core/templates","core/notification","core_calendar/repository","core_calendar/events"],function(a,b,c,d,e){var f={ROOT:"[data-region='calendar']",CALENDAR_NAV_LINK:".calendarwrapper .arrow_link",CALENDAR_MONTH_WRAPPER:".calendarwrapper",LOADING_ICON_CONTAINER:'[data-region="overlay-icon-container"]'},g=function(b){b=a(b),b.on("click",f.CALENDAR_NAV_LINK,function(c){var d=a(b).find(f.CALENDAR_MONTH_WRAPPER).data("courseid"),e=a(c.currentTarget);i(b,e.attr("href"),e.data("year"),e.data("month"),d),c.preventDefault()})},h=function(g,h,i,j,m){k(g),m=m||g.find(f.CALENDAR_MONTH_WRAPPER),M.util.js_pending([g.get("id"),h,i,j].join("-"));var n=g.data("includenavigation");return d.getCalendarMonthData(h,i,j,n).then(function(a){return b.render(g.attr("data-template"),a)}).then(function(a,c){return b.replaceNode(m,a,c)}).then(function(){a("body").trigger(e.viewUpdated)}).always(function(){return M.util.js_complete([g.get("id"),h,i,j].join("-")),l(g)}).fail(c.exception)},i=function(b,c,d,f,g){return h(b,d,f,g).then(function(){return c.length&&"#"!==c&&window.history.pushState({},"",c),arguments}).then(function(){return a("body").trigger(e.monthChanged,[d,f,g]),arguments})},j=function(a,b){var c=a.find(f.CALENDAR_MONTH_WRAPPER).data("year"),d=a.find(f.CALENDAR_MONTH_WRAPPER).data("month");return b||(b=a.find(f.CALENDAR_MONTH_WRAPPER).data("courseid")),h(a,c,d,b)},k=function(a){var b=a.find(f.LOADING_ICON_CONTAINER);b.removeClass("hidden")},l=function(a){var b=a.find(f.LOADING_ICON_CONTAINER);b.addClass("hidden")};return{init:function(a){g(a)},reloadCurrentMonth:j,changeMonth:i,refreshMonthContent:h}}); \ No newline at end of file +define(["jquery","core/templates","core/notification","core_calendar/repository","core_calendar/events"],function(a,b,c,d,e){var f={ROOT:"[data-region='calendar']",CALENDAR_NAV_LINK:".calendarwrapper .arrow_link",CALENDAR_MONTH_WRAPPER:".calendarwrapper",LOADING_ICON_CONTAINER:'[data-region="overlay-icon-container"]'},g=function(b){b=a(b),b.on("click",f.CALENDAR_NAV_LINK,function(c){var d=b.find(f.CALENDAR_MONTH_WRAPPER),e=d.data("courseid"),g=d.data("categoryid"),h=a(c.currentTarget);i(b,h.attr("href"),h.data("year"),h.data("month"),e,g),c.preventDefault()})},h=function(g,h,i,j,m,n){k(g),n=n||g.find(f.CALENDAR_MONTH_WRAPPER),M.util.js_pending([g.get("id"),h,i,j].join("-"));var o=g.data("includenavigation");return d.getCalendarMonthData(h,i,j,m,o).then(function(a){return b.render(g.attr("data-template"),a)}).then(function(a,c){return b.replaceNode(n,a,c)}).then(function(){a("body").trigger(e.viewUpdated)}).always(function(){return M.util.js_complete([g.get("id"),h,i,j].join("-")),l(g)}).fail(c.exception)},i=function(b,c,d,f,g,i){return h(b,d,f,g,i).then(function(){return c.length&&"#"!==c&&window.history.pushState({},"",c),arguments}).then(function(){return a("body").trigger(e.monthChanged,[d,f,g,i]),arguments})},j=function(a,b,c){var d=a.find(f.CALENDAR_MONTH_WRAPPER).data("year"),e=a.find(f.CALENDAR_MONTH_WRAPPER).data("month");return"undefined"==typeof b&&(b=a.find(f.CALENDAR_MONTH_WRAPPER).data("courseid")),"undefined"==typeof c&&(c=a.find(f.CALENDAR_MONTH_WRAPPER).data("categoryid")),h(a,d,e,b,c)},k=function(a){var b=a.find(f.LOADING_ICON_CONTAINER);b.removeClass("hidden")},l=function(a){var b=a.find(f.LOADING_ICON_CONTAINER);b.addClass("hidden")};return{init:function(a){g(a)},reloadCurrentMonth:j,changeMonth:i,refreshMonthContent:h}}); \ No newline at end of file diff --git a/calendar/amd/src/calendar.js b/calendar/amd/src/calendar.js index 0c436cc0a6a..90213bc4868 100644 --- a/calendar/amd/src/calendar.js +++ b/calendar/amd/src/calendar.js @@ -287,7 +287,7 @@ define([ root.on('change', SELECTORS.COURSE_SELECTOR, function() { var selectElement = $(this); var courseId = selectElement.val(); - CalendarViewManager.reloadCurrentMonth(root, courseId) + CalendarViewManager.reloadCurrentMonth(root, courseId, null) .then(function() { // We need to get the selector again because the content has changed. return root.find(SELECTORS.COURSE_SELECTOR).val(courseId); diff --git a/calendar/amd/src/calendar_threemonth.js b/calendar/amd/src/calendar_threemonth.js index a043887dca3..3743d4b9e90 100644 --- a/calendar/amd/src/calendar_threemonth.js +++ b/calendar/amd/src/calendar_threemonth.js @@ -24,6 +24,7 @@ */ define([ 'jquery', + 'core/notification', 'core_calendar/selectors', 'core_calendar/events', 'core/templates', @@ -31,6 +32,7 @@ define([ ], function( $, + Notification, CalendarSelectors, CalendarEvents, Templates, @@ -45,18 +47,20 @@ function( */ var registerCalendarEventListeners = function(root) { var body = $('body'); - body.on(CalendarEvents.monthChanged, function(e, year, month, courseId) { + body.on(CalendarEvents.monthChanged, function(e, year, month, courseId, categoryId) { // We have to use a queue here because the calling code is decoupled from these listeners. // It's possible for the event to be called multiple times before one call is fully resolved. root.queue(function(next) { - return processRequest(e, year, month, courseId) + return processRequest(e, year, month, courseId, categoryId) .then(function() { return next(); - }); + }) + .fail(Notification.exception) + ; }); }); - var processRequest = function(e, year, month, courseId) { + var processRequest = function(e, year, month, courseId, categoryId) { var newCurrentMonth = root.find('[data-year="' + year + '"][data-month="' + month + '"]'); var newParent = newCurrentMonth.closest(CalendarSelectors.calendarPeriods.month); var allMonths = root.find(CalendarSelectors.calendarPeriods.month); @@ -95,6 +99,7 @@ function( requestYear, requestMonth, courseId, + categoryId, placeHolder ) .then(function() { diff --git a/calendar/amd/src/repository.js b/calendar/amd/src/repository.js index 5e1321ced36..0b59073c3ae 100644 --- a/calendar/amd/src/repository.js +++ b/calendar/amd/src/repository.js @@ -29,7 +29,7 @@ define(['jquery', 'core/ajax'], function($, Ajax) { * * @method deleteEvent * @param {int} eventId The event id. - * @arapm {bool} deleteSeries Whether to delete all events in the series + * @param {bool} deleteSeries Whether to delete all events in the series * @return {promise} Resolved with requested calendar event */ var deleteEvent = function(eventId, deleteSeries) { @@ -93,16 +93,18 @@ define(['jquery', 'core/ajax'], function($, Ajax) { * @param {Number} year Year * @param {Number} month Month * @param {Number} courseid The course id. + * @param {Number} categoryid The category id. * @param {Bool} includenavigation Whether to include navigation. * @return {promise} Resolved with the month view data. */ - var getCalendarMonthData = function(year, month, courseid, includenavigation) { + var getCalendarMonthData = function(year, month, courseid, categoryid, includenavigation) { var request = { methodname: 'core_calendar_get_calendar_monthly_view', args: { year: year, month: month, courseid: courseid, + categoryid: categoryid, includenavigation: includenavigation, } }; diff --git a/calendar/amd/src/view_manager.js b/calendar/amd/src/view_manager.js index 0934a510161..1ed771aaa89 100644 --- a/calendar/amd/src/view_manager.js +++ b/calendar/amd/src/view_manager.js @@ -40,9 +40,11 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito root = $(root); root.on('click', SELECTORS.CALENDAR_NAV_LINK, function(e) { - var courseId = $(root).find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('courseid'); + var wrapper = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER); + var courseId = wrapper.data('courseid'); + var categoryId = wrapper.data('categoryid'); var link = $(e.currentTarget); - changeMonth(root, link.attr('href'), link.data('year'), link.data('month'), courseId); + changeMonth(root, link.attr('href'), link.data('year'), link.data('month'), courseId, categoryId); e.preventDefault(); }); @@ -55,17 +57,18 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito * @param {Number} year Year * @param {Number} month Month * @param {Number} courseid The id of the course whose events are shown + * @param {Number} categoryid The id of the category whose events are shown * @param {object} target The element being replaced. If not specified, the calendarwrapper is used. * @return {promise} */ - var refreshMonthContent = function(root, year, month, courseid, target) { + var refreshMonthContent = function(root, year, month, courseid, categoryid, target) { startLoading(root); target = target || root.find(SELECTORS.CALENDAR_MONTH_WRAPPER); M.util.js_pending([root.get('id'), year, month, courseid].join('-')); var includenavigation = root.data('includenavigation'); - return CalendarRepository.getCalendarMonthData(year, month, courseid, includenavigation) + return CalendarRepository.getCalendarMonthData(year, month, courseid, categoryid, includenavigation) .then(function(context) { return Templates.render(root.attr('data-template'), context); }) @@ -86,15 +89,16 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito /** * Handle changes to the current calendar view. * - * @param {object} root The root element. + * @param {object} root The container element * @param {String} url The calendar url to be shown * @param {Number} year Year * @param {Number} month Month * @param {Number} courseid The id of the course whose events are shown + * @param {Number} categoryid The id of the category whose events are shown * @return {promise} */ - var changeMonth = function(root, url, year, month, courseid) { - return refreshMonthContent(root, year, month, courseid) + var changeMonth = function(root, url, year, month, courseid, categoryid) { + return refreshMonthContent(root, year, month, courseid, categoryid) .then(function() { if (url.length && url !== '#') { window.history.pushState({}, '', url); @@ -102,7 +106,7 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito return arguments; }) .then(function() { - $('body').trigger(CalendarEvents.monthChanged, [year, month, courseid]); + $('body').trigger(CalendarEvents.monthChanged, [year, month, courseid, categoryid]); return arguments; }); }; @@ -112,16 +116,23 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito * * @param {object} root The container element. * @param {Number} courseId The course id. + * @param {Number} categoryId The id of the category whose events are shown * @return {promise} */ - var reloadCurrentMonth = function(root, courseId) { + var reloadCurrentMonth = function(root, courseId, categoryId) { var year = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('year'); var month = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('month'); - if (!courseId) { + if (typeof courseId === 'undefined') { courseId = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('courseid'); } - return refreshMonthContent(root, year, month, courseId); + + if (typeof categoryId === 'undefined') { + categoryId = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('categoryid'); + } + + return refreshMonthContent(root, year, month, courseId, categoryId); + }; /** diff --git a/calendar/classes/external/calendar_event_exporter.php b/calendar/classes/external/calendar_event_exporter.php index c27e3647a1a..423cd92cc75 100644 --- a/calendar/classes/external/calendar_event_exporter.php +++ b/calendar/classes/external/calendar_event_exporter.php @@ -82,6 +82,8 @@ class calendar_event_exporter extends event_exporter_base { $params = array('update' => $moduleid, 'return' => true, 'sesskey' => sesskey()); $editurl = new \moodle_url('/course/mod.php', $params); $values['editurl'] = $editurl->out(false); + } else if ($event->get_type() == 'category') { + $url = $event->get_category()->get_proxied_instance()->get_view_link(); } else if ($event->get_type() == 'course') { $url = course_get_url($event->get_course()->get('id') ?: SITEID); } else { diff --git a/calendar/classes/external/event_exporter.php b/calendar/classes/external/event_exporter.php index d687eb1b191..5ebfdcef957 100644 --- a/calendar/classes/external/event_exporter.php +++ b/calendar/classes/external/event_exporter.php @@ -48,22 +48,13 @@ class event_exporter extends event_exporter_base { * @return array */ protected static function define_other_properties() { - $values = parent::define_other_properties(); - $values['isactionevent'] = ['type' => PARAM_BOOL]; - $values['iscourseevent'] = ['type' => PARAM_BOOL]; - $values['iscategoryevent'] = ['type' => PARAM_BOOL]; - $values['candelete'] = ['type' => PARAM_BOOL]; $values['url'] = ['type' => PARAM_URL]; $values['action'] = [ 'type' => event_action_exporter::read_properties_definition(), 'optional' => true, ]; - $values['editurl'] = [ - 'type' => PARAM_URL, - 'optional' => true, - ]; return $values; } @@ -82,9 +73,6 @@ class event_exporter extends event_exporter_base { $event = $this->event; $context = $this->related['context']; - $values['isactionevent'] = false; - $values['iscourseevent'] = false; - $values['iscategoryevent'] = false; if ($moduleproxy = $event->get_course_module()) { $modulename = $moduleproxy->get('modname'); $moduleid = $moduleproxy->get('id'); @@ -95,7 +83,6 @@ class event_exporter extends event_exporter_base { $editurl = new \moodle_url('/course/mod.php', $params); $values['editurl'] = $editurl->out(false); } else if ($event->get_type() == 'category') { - $values['iscategoryevent'] = true; $url = $event->get_category()->get_proxied_instance()->get_view_link(); } else if ($event->get_type() == 'course') { $url = \course_get_url($this->related['course'] ?: SITEID); diff --git a/calendar/classes/external/event_exporter_base.php b/calendar/classes/external/event_exporter_base.php index 9af9ce2139b..85457e2d05a 100644 --- a/calendar/classes/external/event_exporter_base.php +++ b/calendar/classes/external/event_exporter_base.php @@ -217,6 +217,9 @@ class event_exporter_base extends exporter { 'iscourseevent' => [ 'type' => PARAM_BOOL ], + 'iscategoryevent' => [ + 'type' => PARAM_BOOL + ], 'groupname' => [ 'type' => PARAM_RAW, 'optional' => true, @@ -239,10 +242,13 @@ class event_exporter_base extends exporter { $context = $this->related['context']; $values['isactionevent'] = false; $values['iscourseevent'] = false; + $values['iscategoryevent'] = false; if ($moduleproxy = $event->get_course_module()) { $values['isactionevent'] = true; } else if ($event->get_type() == 'course') { $values['iscourseevent'] = true; + } else if ($event->get_type() == 'category') { + $values['iscategoryevent'] = true; } $timesort = $event->get_times()->get_sort_time()->getTimestamp(); $iconexporter = new event_icon_exporter($event, ['context' => $context]); diff --git a/calendar/classes/external/event_icon_exporter.php b/calendar/classes/external/event_icon_exporter.php index 5f4bd4dc29e..79d30daab26 100644 --- a/calendar/classes/external/event_icon_exporter.php +++ b/calendar/classes/external/event_icon_exporter.php @@ -46,6 +46,8 @@ class event_icon_exporter extends exporter { */ public function __construct(event_interface $event, $related = []) { $coursemodule = $event->get_course_module(); + $category = $event->get_category(); + $categoryid = $category ? $category->get('id') : null; $course = $event->get_course(); $courseid = $course ? $course->get('id') : null; $group = $event->get_group(); @@ -54,6 +56,7 @@ class event_icon_exporter extends exporter { $userid = $user ? $user->get('id') : null; $isactivityevent = !empty($coursemodule); $isglobalevent = ($course && $courseid == SITEID); + $iscategoryevent = ($category && !empty($categoryid)); $iscourseevent = ($course && !empty($courseid) && $courseid != SITEID && empty($groupid)); $isgroupevent = ($group && !empty($groupid)); $isuserevent = ($user && !empty($userid)); @@ -70,24 +73,28 @@ class event_icon_exporter extends exporter { } else if ($isglobalevent) { $key = 'i/siteevent'; $component = 'core'; - $alttext = get_string('globalevent', 'calendar'); + $alttext = get_string('typesite', 'calendar'); + } else if ($iscategoryevent) { + $key = 'i/categoryevent'; + $component = 'core'; + $alttext = get_string('typecategory', 'calendar'); } else if ($iscourseevent) { $key = 'i/courseevent'; $component = 'core'; - $alttext = get_string('courseevent', 'calendar'); + $alttext = get_string('typecourse', 'calendar'); } else if ($isgroupevent) { $key = 'i/groupevent'; $component = 'core'; - $alttext = get_string('groupevent', 'calendar'); + $alttext = get_string('typegroup', 'calendar'); } else if ($isuserevent) { $key = 'i/userevent'; $component = 'core'; - $alttext = get_string('userevent', 'calendar'); + $alttext = get_string('typeuser', 'calendar'); } else { // Default to site event icon? $key = 'i/siteevent'; $component = 'core'; - $alttext = get_string('globalevent', 'calendar'); + $alttext = get_string('typesite', 'calendar'); } $data = new \stdClass(); diff --git a/calendar/classes/external/month_exporter.php b/calendar/classes/external/month_exporter.php index 3fff7fe22b7..e67c8431d10 100644 --- a/calendar/classes/external/month_exporter.php +++ b/calendar/classes/external/month_exporter.php @@ -108,6 +108,11 @@ class month_exporter extends exporter { 'courseid' => [ 'type' => PARAM_INT, ], + 'categoryid' => [ + 'type' => PARAM_INT, + 'optional' => true, + 'default' => 0, + ], 'filter_selector' => [ 'type' => PARAM_RAW, ], @@ -211,6 +216,10 @@ class month_exporter extends exporter { $return['defaulteventcontext'] = $context->id; } + if ($this->calendar->categoryid) { + $return['categoryid'] = $this->calendar->categoryid; + } + return $return; } diff --git a/calendar/event.php b/calendar/event.php index dad5fd58159..9eb3708bdfc 100644 --- a/calendar/event.php +++ b/calendar/event.php @@ -108,7 +108,7 @@ if ($action === 'delete' && $eventid > 0) { } $calendar = new calendar_information(0, 0, 0, $time); -$calendar->prepare_for_view($course, $courses); +$calendar->set_sources($course, $courses); $formoptions = new stdClass; if ($eventid !== 0) { diff --git a/calendar/export.php b/calendar/export.php index 04e6cf39a1c..89ca1e0b401 100644 --- a/calendar/export.php +++ b/calendar/export.php @@ -100,7 +100,7 @@ if ($course !== NULL) { $PAGE->set_url($url); $calendar = new calendar_information(0, 0, 0, $time); -$calendar->prepare_for_view($course, $courses); +$calendar->set_sources($course, $courses); $pagetitle = get_string('export', 'calendar'); diff --git a/calendar/externallib.php b/calendar/externallib.php index 336c0f0916f..35bc6e738d4 100644 --- a/calendar/externallib.php +++ b/calendar/externallib.php @@ -882,10 +882,11 @@ class core_calendar_external extends external_api { * @param int $year The year to be shown * @param int $month The month to be shown * @param int $courseid The course to be included + * @param int $categoryid The category to be included * @param bool $includenavigation Whether to include navigation * @return array */ - public static function get_calendar_monthly_view($year, $month, $courseid, $includenavigation) { + public static function get_calendar_monthly_view($year, $month, $courseid, $categoryid, $includenavigation) { global $CFG, $DB, $USER, $PAGE; require_once($CFG->dirroot."/calendar/lib.php"); @@ -894,27 +895,44 @@ class core_calendar_external extends external_api { 'year' => $year, 'month' => $month, 'courseid' => $courseid, + 'categoryid' => $categoryid, 'includenavigation' => $includenavigation, ]); - if ($courseid != SITEID && !empty($courseid)) { - // Course ID must be valid and existing. - $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); - $courses = [$course->id => $course]; - } else { - $course = get_site(); - $courses = calendar_get_default_courses(); - } - // TODO: Copy what we do in calendar/view.php. $context = \context_user::instance($USER->id); self::validate_context($context); + if ($courseid != SITEID && !empty($courseid)) { + // Course ID must be valid and existing. + $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); + $courses = [$course->id => $course]; + + $coursecat = \coursecat::get($course->category); + $category = $coursecat->get_db_record(); + } else { + $course = get_site(); + $courses = calendar_get_default_courses(); + $category = null; + + if ($categoryid) { + self::validate_context(context_coursecat::instance($categoryid)); + $ids = [$categoryid]; + $category = \coursecat::get($categoryid); + $ids += $category->get_parents(); + $categories = \coursecat::get_many($ids); + $courses = array_filter($courses, function($course) use ($categories) { + return array_search($course->category, $categories) !== false; + }); + $category = $category->get_db_record(); + } + } + $type = \core_calendar\type_factory::get_calendar_instance(); $time = $type->convert_to_timestamp($year, $month, 1); $calendar = new calendar_information(0, 0, 0, $time); - $calendar->prepare_for_view($course, $courses); + $calendar->set_sources($course, $courses, $category); list($data, $template) = calendar_get_view($calendar, 'month', $params['includenavigation']); @@ -932,6 +950,7 @@ class core_calendar_external extends external_api { 'year' => new external_value(PARAM_INT, 'Month to be viewed', VALUE_REQUIRED), 'month' => new external_value(PARAM_INT, 'Year to be viewed', VALUE_REQUIRED), 'courseid' => new external_value(PARAM_INT, 'Course being viewed', VALUE_DEFAULT, SITEID, NULL_ALLOWED), + 'categoryid' => new external_value(PARAM_INT, 'Category being viewed', VALUE_DEFAULT, null, NULL_ALLOWED), 'includenavigation' => new external_value( PARAM_BOOL, 'Whether to show course navigation', diff --git a/calendar/lib.php b/calendar/lib.php index 4d86ee29143..68bdc5b73c8 100644 --- a/calendar/lib.php +++ b/calendar/lib.php @@ -1059,7 +1059,7 @@ class calendar_information { $this->categoryid = null; $this->categories = null; - if (null !== $category) { + if (null !== $category && $category->id > 0) { // A specific category was requested - set the current category, and include all parents of that category. $category = \coursecat::get($category->id); $this->categoryid = $category->id; diff --git a/calendar/templates/event_summary_body.mustache b/calendar/templates/event_summary_body.mustache index b139b692dd3..e5b646f84ae 100644 --- a/calendar/templates/event_summary_body.mustache +++ b/calendar/templates/event_summary_body.mustache @@ -46,6 +46,9 @@ {{/description}}

{{#str}} eventtype, core_calendar {{/str}}

{{eventtype}} + {{#iscategoryevent}} +
{{{category.nestedname}}}
+ {{/iscategoryevent}} {{#iscourseevent}} {{/iscourseevent}} diff --git a/calendar/templates/month_detailed.mustache b/calendar/templates/month_detailed.mustache index baa901345f1..8d1e49552ed 100644 --- a/calendar/templates/month_detailed.mustache +++ b/calendar/templates/month_detailed.mustache @@ -33,7 +33,8 @@ }} diff --git a/calendar/templates/month_mini.mustache b/calendar/templates/month_mini.mustache index bc180c7802f..6f6773fd215 100644 --- a/calendar/templates/month_mini.mustache +++ b/calendar/templates/month_mini.mustache @@ -35,6 +35,7 @@ }} id="month-mini-{{date.year}}-{{date.month}}-{{uniqid}}"{{! }} class="calendarwrapper"{{! }} data-courseid="{{courseid}}"{{! + }} data-categoryid="{{categoryid}}"{{! }} data-month="{{date.mon}}"{{! }} data-year="{{date.year}}"{{! }}> diff --git a/calendar/view.php b/calendar/view.php index fe6a82eae06..9265b19738b 100644 --- a/calendar/view.php +++ b/calendar/view.php @@ -49,6 +49,7 @@ require_once('../config.php'); require_once($CFG->dirroot.'/course/lib.php'); require_once($CFG->dirroot.'/calendar/lib.php'); +$categoryid = optional_param('category', null, PARAM_INT); $courseid = optional_param('course', SITEID, PARAM_INT); $view = optional_param('view', 'upcoming', PARAM_ALPHA); $time = optional_param('time', 0, PARAM_INT); @@ -63,6 +64,10 @@ if ($courseid != SITEID) { $url->param('course', $courseid); } +if ($categoryid) { + $url->param('categoryid', $categoryid); +} + if ($view !== 'upcoming') { $time = usergetmidnight($time); $url->param('view', $view); @@ -76,18 +81,30 @@ if ($courseid != SITEID && !empty($courseid)) { // Course ID must be valid and existing. $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); $courses = array($course->id => $course); - $issite = false; navigation_node::override_active_url(new moodle_url('/course/view.php', array('id' => $course->id))); } else { $course = get_site(); $courses = calendar_get_default_courses(); - $issite = true; + if ($categoryid) { + $PAGE->set_category_by_id($categoryid); + } else { + $PAGE->set_context(context_system::instance()); + } + if ($PAGE->context->contextlevel === CONTEXT_COURSECAT) { + // Restrict to categories, and their parents, and the courses that the user is enrolled in within those + // categories. + $categories = array_keys($PAGE->categories); + $courses = array_filter($courses, function($course) use ($categories) { + return array_search($course->category, $categories) !== false; + }); + navigation_node::override_active_url(new moodle_url('/course/index.php', array('categoryid' => $categoryid))); + } } require_login($course, false); $calendar = new calendar_information(0, 0, 0, $time); -$calendar->prepare_for_view($course, $courses); +$calendar->set_sources($course, $courses, $PAGE->category); $pagetitle = ''; diff --git a/lang/en/calendar.php b/lang/en/calendar.php index 7401c9d8530..3572a91a5e5 100644 --- a/lang/en/calendar.php +++ b/lang/en/calendar.php @@ -32,6 +32,7 @@ $string['calendarheading'] = '{$a} Calendar'; $string['calendarpreferences'] = 'Calendar preferences'; $string['calendartypes'] = 'Calendar types'; $string['calendarurl'] = 'Calendar URL: {$a}'; +$string['categoryevent'] = 'Category event'; $string['clickhide'] = 'click to hide'; $string['clickshow'] = 'click to show'; $string['colcalendar'] = 'Calendar'; diff --git a/lib/classes/output/icon_system_fontawesome.php b/lib/classes/output/icon_system_fontawesome.php index b1085cac943..b3f326b1a0a 100644 --- a/lib/classes/output/icon_system_fontawesome.php +++ b/lib/classes/output/icon_system_fontawesome.php @@ -203,6 +203,7 @@ class icon_system_fontawesome extends icon_system_font { 'core:i/completion_self' => 'fa-user-o', 'core:i/dashboard' => 'fa-tachometer', 'core:i/lock' => 'fa-lock', + 'core:i/categoryevent' => 'fa-users', 'core:i/courseevent' => 'fa-calendar', 'core:i/db' => 'fa-database', 'core:i/delete' => 'fa-trash', diff --git a/pix/i/categoryevent.svg b/pix/i/categoryevent.svg new file mode 100644 index 00000000000..b511f8ffc3d --- /dev/null +++ b/pix/i/categoryevent.svg @@ -0,0 +1,3 @@ + +]> \ No newline at end of file diff --git a/theme/boost/scss/moodle/calendar.scss b/theme/boost/scss/moodle/calendar.scss index b13af94c9bd..38d1e952775 100644 --- a/theme/boost/scss/moodle/calendar.scss +++ b/theme/boost/scss/moodle/calendar.scss @@ -1,12 +1,16 @@ /* calendar.less */ // Calendar colour variables defined. +$calendarEventCategoryColor: #d8bfd8 !default; // Pale purple. $calendarEventCourseColor: #ffd3bd !default; // Pale red. $calendarEventGlobalColor: #d6f8cd !default; // Pale green. $calendarEventGroupColor: #fee7ae !default; // Pale yellow. $calendarEventUserColor: #dce7ec !default; // Pale blue. // Calendar event background colours defined. +.calendar_event_category { + background-color: $calendarEventCategoryColor; +} .calendar_event_course { background-color: $calendarEventCourseColor; } @@ -129,6 +133,7 @@ $calendarEventUserColor: #dce7ec !default; // Pale blue. margin: 10px auto; } + .calendar_event_category, .calendar_event_course, .calendar_event_site, .calendar_event_group, @@ -141,6 +146,9 @@ $calendarEventUserColor: #dce7ec !default; // Pale blue. } } + .calendar_event_category { + border-color: $calendarEventCategoryColor; + } .calendar_event_course { border-color: $calendarEventCourseColor; } @@ -231,6 +239,15 @@ $calendarEventUserColor: #dce7ec !default; // Pale blue. } } + &.duration_category { + border-top: 1px solid $calendarEventCategoryColor; + border-bottom: 1px solid $calendarEventCategoryColor; + + &.duration_finish { + background-color: $calendarEventCategoryColor; + } + } + &.duration_course { border-top: 1px solid $calendarEventCourseColor; border-bottom: 1px solid $calendarEventCourseColor; diff --git a/theme/bootstrapbase/less/moodle/calendar.less b/theme/bootstrapbase/less/moodle/calendar.less index 72fb4a48402..19fc8a434b1 100644 --- a/theme/bootstrapbase/less/moodle/calendar.less +++ b/theme/bootstrapbase/less/moodle/calendar.less @@ -1,12 +1,16 @@ /* calendar.less */ // Calendar colour variables defined. +@calendarEventCategoryColor: #d8bfd8; // Pale purple. @calendarEventCourseColor: #ffd3bd; // Pale red. @calendarEventGlobalColor: #d6f8cd; // Pale green. @calendarEventGroupColor: #fee7ae; // Pale yellow. @calendarEventUserColor: #dce7ec; // Pale blue. // Calendar event background colours defined. +.calendar_event_category { + background-color: @calendarEventCategoryColor; +} .calendar_event_course { background-color: @calendarEventCourseColor; } @@ -110,6 +114,7 @@ width: 98%; margin: 10px auto; } + .calendar_event_category, .calendar_event_course, .calendar_event_site, .calendar_event_group, @@ -121,6 +126,9 @@ } } } + .calendar_event_category { + border-color: @calendarEventCategoryColor; + } .calendar_event_course { border-color: @calendarEventCourseColor; } @@ -244,6 +252,13 @@ background-color: @calendarEventGlobalColor; } } + &.duration_category { + border-top: 1px solid @calendarEventCategoryColor; + border-bottom: 1px solid @calendarEventCategoryColor; + &.duration_finish { + background-color: @calendarEventCategoryColor; + } + } &.duration_course { border-top: 1px solid @calendarEventCourseColor; border-bottom: 1px solid @calendarEventCourseColor; diff --git a/theme/bootstrapbase/style/moodle.css b/theme/bootstrapbase/style/moodle.css index f8141957a22..e178774d2da 100644 --- a/theme/bootstrapbase/style/moodle.css +++ b/theme/bootstrapbase/style/moodle.css @@ -5563,6 +5563,9 @@ img.iconsmall { background-color: #fcf8e3; } /* calendar.less */ +.calendar_event_category { + background-color: #d8bfd8; +} .calendar_event_course { background-color: #ffd3bd; } @@ -5655,6 +5658,7 @@ img.iconsmall { width: 98%; margin: 10px auto; } +.path-calendar .maincalendar .calendar_event_category:hover a, .path-calendar .maincalendar .calendar_event_course:hover a, .path-calendar .maincalendar .calendar_event_site:hover a, .path-calendar .maincalendar .calendar_event_group:hover a, @@ -5662,6 +5666,9 @@ img.iconsmall { color: #003d5c; text-decoration: underline; } +.path-calendar .maincalendar .calendar_event_category { + border-color: #d8bfd8; +} .path-calendar .maincalendar .calendar_event_course { border-color: #ffd3bd; } @@ -5768,6 +5775,13 @@ img.iconsmall { .block .minicalendar td.duration_global.duration_finish { background-color: #d6f8cd; } +.block .minicalendar td.duration_category { + border-top: 1px solid #d8bfd8; + border-bottom: 1px solid #d8bfd8; +} +.block .minicalendar td.duration_category.duration_finish { + background-color: #d8bfd8; +} .block .minicalendar td.duration_course { border-top: 1px solid #ffd3bd; border-bottom: 1px solid #ffd3bd; From 74475e5bb41b60deb2d46e03473e8f20e0033bfa Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 21 Sep 2017 11:09:50 +0800 Subject: [PATCH 09/15] MDL-59890 calendar: Delete category events upon category removal --- lib/coursecatlib.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/coursecatlib.php b/lib/coursecatlib.php index 916c7d830e9..02123a1b702 100644 --- a/lib/coursecatlib.php +++ b/lib/coursecatlib.php @@ -1756,6 +1756,9 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate { throw new moodle_exception('cannotdeletecategoryquestions', '', '', $this->get_formatted_name()); } + // Delete all events in the category. + $DB->delete_records('event', array('categoryid' => $this->id)); + // Finally delete the category and it's context. $DB->delete_records('course_categories', array('id' => $this->id)); From 936235133a8f0c3fc40ef2e6bd5f1b074b3fd518 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 19 Sep 2017 11:15:13 +0800 Subject: [PATCH 10/15] MDL-59890 calendar: Support for categories in legacy event fetch --- calendar/lib.php | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/calendar/lib.php b/calendar/lib.php index 68bdc5b73c8..fe9aac6aff1 100644 --- a/calendar/lib.php +++ b/calendar/lib.php @@ -1164,15 +1164,17 @@ class calendar_information { * @param boolean $withduration whether only events starting within time range selected * or events in progress/already started selected as well * @param boolean $ignorehidden whether to select only visible events or all events + * @param array|int|boolean $categories array of categories, category id or boolean for all/no course events * @return array $events of selected events or an empty array if there aren't any (or there was an error) */ -function calendar_get_events($tstart, $tend, $users, $groups, $courses, $withduration=true, $ignorehidden=true) { +function calendar_get_events($tstart, $tend, $users, $groups, $courses, + $withduration = true, $ignorehidden = true, $categories = []) { global $DB; $whereclause = ''; $params = array(); // Quick test. - if (empty($users) && empty($groups) && empty($courses)) { + if (empty($users) && empty($groups) && empty($courses) && empty($categories)) { return array(); } @@ -1180,12 +1182,12 @@ function calendar_get_events($tstart, $tend, $users, $groups, $courses, $withdur // Events from a number of users if(!empty($whereclause)) $whereclause .= ' OR'; list($insqlusers, $inparamsusers) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED); - $whereclause .= " (e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0)"; + $whereclause .= " (e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0 AND e.categoryid = 0)"; $params = array_merge($params, $inparamsusers); } else if($users === true) { // Events from ALL users if(!empty($whereclause)) $whereclause .= ' OR'; - $whereclause .= ' (e.userid != 0 AND e.courseid = 0 AND e.groupid = 0)'; + $whereclause .= ' (e.userid != 0 AND e.courseid = 0 AND e.groupid = 0 AND e.categoryid = 0)'; } else if($users === false) { // No user at all, do nothing } @@ -1214,6 +1216,18 @@ function calendar_get_events($tstart, $tend, $users, $groups, $courses, $withdur $whereclause .= ' (e.groupid = 0 AND e.courseid != 0)'; } + if ((is_array($categories) && !empty($categories)) || is_numeric($categories)) { + if(!empty($whereclause)) $whereclause .= ' OR'; + list($insqlcategories, $inparamscategories) = $DB->get_in_or_equal($categories, SQL_PARAMS_NAMED); + $whereclause .= " (e.groupid = 0 AND e.courseid = 0 AND e.categoryid $insqlcategories)"; + $params = array_merge($params, $inparamscategories); + } else if ($categories === true) { + // Events from ALL categories. + if(!empty($whereclause)) $whereclause .= ' OR'; + $whereclause .= ' (e.groupid = 0 AND e.courseid = 0 AND e.categoryid != 0)'; + } + + // Security check: if, by now, we have NOTHING in $whereclause, then it means // that NO event-selecting clauses were defined. Thus, we won't be returning ANY // events no matter what. Allowing the code to proceed might return a completely @@ -3183,7 +3197,12 @@ function calendar_get_view(\calendar_information $calendar, $view, $includenavig if ($proxy = $event->get_course_module()) { $cminfo = $proxy->get_proxied_instance(); return $cminfo->uservisible; + } + if ($proxy = $event->get_category()) { + $category = $proxy->get_proxied_instance(); + + return $category->is_uservisible(); } return true; From d097bfdda3763d02d97f395a42bab8cc157adb1c Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Fri, 22 Sep 2017 09:56:18 +0800 Subject: [PATCH 11/15] MDL-59890 calendar: Select current category when adding event --- calendar/amd/build/calendar.min.js | 2 +- calendar/amd/build/modal_event_form.min.js | 2 +- calendar/amd/src/calendar.js | 24 ++++++++-- calendar/amd/src/modal_event_form.js | 52 +++++++++++++++++++--- calendar/externallib.php | 1 + calendar/lib.php | 13 ++++-- 6 files changed, 80 insertions(+), 14 deletions(-) diff --git a/calendar/amd/build/calendar.min.js b/calendar/amd/build/calendar.min.js index 4778e33b726..f9167a16cfa 100644 --- a/calendar/amd/build/calendar.min.js +++ b/calendar/amd/build/calendar.min.js @@ -1 +1 @@ -define(["jquery","core/ajax","core/str","core/templates","core/notification","core/custom_interaction_events","core/modal_events","core/modal_factory","core_calendar/modal_event_form","core_calendar/summary_modal","core_calendar/repository","core_calendar/events","core_calendar/view_manager"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){var n={ROOT:"[data-region='calendar']",DAY:"[data-region='day']",EVENT_ITEM:"[data-region='event-item']",EVENT_LINK:"[data-action='view-event']",NEW_EVENT_BUTTON:"[data-action='new-event-button']",DAY_CONTENT:"[data-region='day-content']",LOADING_ICON:".loading-icon",VIEW_DAY_LINK:"[data-action='view-day-link']",CALENDAR_MONTH_WRAPPER:".calendarwrapper",COURSE_SELECTOR:'select[name="course"]',TODAY:".today"},o=function(a){var b="type"+a;return c.get_string(b,"core_calendar").then(function(a){return a})},p=function(a){k.getEventById(a).then(function(b){if(!b.event)throw new Error("Error encountered while trying to fetch calendar event with ID: "+a);var c=b.event;return o(c.eventtype).then(function(a){return c.eventtype=a,c})}).then(function(a){var b={title:a.name,type:j.TYPE,body:d.render("core_calendar/event_summary_body",a),templateContext:{canedit:a.canedit,candelete:a.candelete,isactionevent:a.isactionevent,url:a.url}};return h.create(b)}).done(function(a){a.getRoot().on(g.hidden,function(){a.destroy()}),a.show()}).fail(e.exception)},q=function(b,c,f,g){var h=null,i=g.attr("data-day-timestamp");f&&(h=f.attr("data-day-timestamp")),f&&h==i||d.render("core/loading",{}).then(function(a,b){g.find(n.DAY_CONTENT).addClass("hidden"),d.appendNodeContents(g,a,b),f&&(f.find(n.DAY_CONTENT).addClass("hidden"),d.appendNodeContents(f,a,b))}).then(function(){return k.updateEventStartDay(c,i)}).then(function(){a("body").trigger(l.eventMoved,[c,f,g])}).always(function(){var a=g.find(n.LOADING_ICON);if(g.find(n.DAY_CONTENT).removeClass("hidden"),d.replaceNode(a,"",""),f){var b=f.find(n.LOADING_ICON);f.find(n.DAY_CONTENT).removeClass("hidden"),d.replaceNode(b,"","")}}).fail(e.exception)},r=function(a){var b=a.find(n.NEW_EVENT_BUTTON),c=b.attr("data-context-id");return h.create({type:i.TYPE,large:!0,templateContext:{contextid:c}})},s=function(b,c){var d=a("body"),f=a(b).find(n.CALENDAR_MONTH_WRAPPER).data("courseid");d.on(l.created,function(){m.reloadCurrentMonth(b)}),d.on(l.deleted,function(){m.reloadCurrentMonth(b)}),d.on(l.updated,function(){m.reloadCurrentMonth(b)}),d.on(l.editActionEvent,function(a,b){window.location.assign(b)}),d.on(l.moveEvent,q),d.on(l.eventMoved,function(){m.reloadCurrentMonth(b)}),c.then(function(a){d.on(l.editEvent,function(b,c){a.setEventId(c),a.show()}),a.setCourseId(f)}).fail(e.exception)},t=function(b){b.on("click",n.EVENT_ITEM,function(b){b.preventDefault(),b.stopPropagation();var c=a(b.target),d=null;d=c.is(n.EVENT_LINK)?c.attr("data-event-id"):c.find(n.EVENT_LINK).attr("data-event-id"),p(d)}),b.on("change",n.COURSE_SELECTOR,function(){var c=a(this),d=c.val();m.reloadCurrentMonth(b,d,null).then(function(){return b.find(n.COURSE_SELECTOR).val(d)}).fail(e.exception)});var c=r(b);s(b,c),b.on("click",n.NEW_EVENT_BUTTON,function(a){c.then(function(a){var c=b.find(n.TODAY);c.length||a.setStartTime(b.find(n.DAY).attr("data-new-event-timestamp")),a.show()}).fail(e.exception),a.preventDefault()}),b.on("click",n.DAY,function(b){var d=a(b.target);if(!d.is(n.VIEW_DAY_LINK)){var f=a(this).attr("data-new-event-timestamp");c.then(function(a){a.setStartTime(f),a.show()}).fail(e.exception),b.preventDefault()}})};return{init:function(b){b=a(b),m.init(b),t(b)}}}); \ No newline at end of file +define(["jquery","core/ajax","core/str","core/templates","core/notification","core/custom_interaction_events","core/modal_events","core/modal_factory","core_calendar/modal_event_form","core_calendar/summary_modal","core_calendar/repository","core_calendar/events","core_calendar/view_manager"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){var n={ROOT:"[data-region='calendar']",DAY:"[data-region='day']",EVENT_ITEM:"[data-region='event-item']",EVENT_LINK:"[data-action='view-event']",NEW_EVENT_BUTTON:"[data-action='new-event-button']",DAY_CONTENT:"[data-region='day-content']",LOADING_ICON:".loading-icon",VIEW_DAY_LINK:"[data-action='view-day-link']",CALENDAR_MONTH_WRAPPER:".calendarwrapper",COURSE_SELECTOR:'select[name="course"]',TODAY:".today"},o=function(a){var b="type"+a;return c.get_string(b,"core_calendar").then(function(a){return a})},p=function(a){k.getEventById(a).then(function(b){if(!b.event)throw new Error("Error encountered while trying to fetch calendar event with ID: "+a);var c=b.event;return o(c.eventtype).then(function(a){return c.eventtype=a,c})}).then(function(a){var b={title:a.name,type:j.TYPE,body:d.render("core_calendar/event_summary_body",a),templateContext:{canedit:a.canedit,candelete:a.candelete,isactionevent:a.isactionevent,url:a.url}};return h.create(b)}).done(function(a){a.getRoot().on(g.hidden,function(){a.destroy()}),a.show()}).fail(e.exception)},q=function(b,c,f,g){var h=null,i=g.attr("data-day-timestamp");f&&(h=f.attr("data-day-timestamp")),f&&h==i||d.render("core/loading",{}).then(function(a,b){g.find(n.DAY_CONTENT).addClass("hidden"),d.appendNodeContents(g,a,b),f&&(f.find(n.DAY_CONTENT).addClass("hidden"),d.appendNodeContents(f,a,b))}).then(function(){return k.updateEventStartDay(c,i)}).then(function(){a("body").trigger(l.eventMoved,[c,f,g])}).always(function(){var a=g.find(n.LOADING_ICON);if(g.find(n.DAY_CONTENT).removeClass("hidden"),d.replaceNode(a,"",""),f){var b=f.find(n.LOADING_ICON);f.find(n.DAY_CONTENT).removeClass("hidden"),d.replaceNode(b,"","")}}).fail(e.exception)},r=function(a){var b=a.find(n.NEW_EVENT_BUTTON),c=b.attr("data-context-id");return h.create({type:i.TYPE,large:!0,templateContext:{contextid:c}})},s=function(b,c){var d=a("body");d.on(l.created,function(){m.reloadCurrentMonth(b)}),d.on(l.deleted,function(){m.reloadCurrentMonth(b)}),d.on(l.updated,function(){m.reloadCurrentMonth(b)}),d.on(l.editActionEvent,function(a,b){window.location.assign(b)}),d.on(l.moveEvent,q),d.on(l.eventMoved,function(){m.reloadCurrentMonth(b)}),c.then(function(a){d.on(l.editEvent,function(b,c){a.setEventId(c),a.show()})}).fail(e.exception)},t=function(b){b.on("click",n.EVENT_ITEM,function(b){b.preventDefault(),b.stopPropagation();var c=a(b.target),d=null;d=c.is(n.EVENT_LINK)?c.attr("data-event-id"):c.find(n.EVENT_LINK).attr("data-event-id"),p(d)}),b.on("change",n.COURSE_SELECTOR,function(){var c=a(this),d=c.val();m.reloadCurrentMonth(b,d,null).then(function(){return b.find(n.COURSE_SELECTOR).val(d)}).fail(e.exception)});var c=r(b);s(b,c),f.define(b,[f.events.activate]),b.on("click",n.NEW_EVENT_BUTTON,function(a){c.then(function(a){var c=b.find(n.CALENDAR_MONTH_WRAPPER);a.setCourseId(c.data("courseid"));var d=c.data("categoryid");"undefined"!=typeof d&&a.setCategoryId(d);var e=b.find(n.TODAY);e.length||a.setStartTime(b.find(n.DAY).attr("data-new-event-timestamp")),a.show()}).fail(e.exception),a.preventDefault()}),b.on("click",n.DAY,function(b){var d=a(b.target);if(!d.is(n.VIEW_DAY_LINK)){var f=a(this).attr("data-new-event-timestamp");c.then(function(a){var b=d.closest(n.CALENDAR_MONTH_WRAPPER);a.setCourseId(b.data("courseid"));var c=b.data("categoryid");"undefined"!=typeof c&&a.setCategoryId(c),a.setStartTime(f),a.show()}).fail(e.exception),b.preventDefault()}})};return{init:function(b){b=a(b),m.init(b),t(b)}}}); \ No newline at end of file diff --git a/calendar/amd/build/modal_event_form.min.js b/calendar/amd/build/modal_event_form.min.js index 4d5280418c5..816815a7c24 100644 --- a/calendar/amd/build/modal_event_form.min.js +++ b/calendar/amd/build/modal_event_form.min.js @@ -1 +1 @@ -define(["jquery","core/event","core/str","core/notification","core/templates","core/custom_interaction_events","core/modal","core/modal_registry","core/fragment","core_calendar/events","core_calendar/repository"],function(a,b,c,d,e,f,g,h,i,j,k){var l=!1,m={SAVE_BUTTON:'[data-action="save"]',LOADING_ICON_CONTAINER:'[data-region="loading-icon-container"]'},n=function(a){g.call(this,a),this.eventId=null,this.startTime=null,this.courseId=null,this.reloadingBody=!1,this.reloadingTitle=!1,this.saveButton=this.getFooter().find(m.SAVE_BUTTON)};return n.TYPE="core_calendar-modal_event_form",n.prototype=Object.create(g.prototype),n.prototype.constructor=n,n.prototype.setCourseId=function(a){this.courseId=a},n.prototype.getCourseId=function(){return this.courseId},n.prototype.hasCourseId=function(){return null!==this.courseId},n.prototype.setEventId=function(a){this.eventId=a},n.prototype.getEventId=function(){return this.eventId},n.prototype.hasEventId=function(){return null!==this.eventId},n.prototype.setStartTime=function(a){this.startTime=a},n.prototype.getStartTime=function(){return this.startTime},n.prototype.hasStartTime=function(){return null!==this.startTime},n.prototype.getForm=function(){return this.getBody().find("form")},n.prototype.disableButtons=function(){this.saveButton.prop("disabled",!0)},n.prototype.enableButtons=function(){this.saveButton.prop("disabled",!1)},n.prototype.reloadTitleContent=function(){return this.reloadingTitle?this.titlePromise:(this.reloadingTitle=!0,this.hasEventId()?this.titlePromise=c.get_string("editevent","calendar"):this.titlePromise=c.get_string("newevent","calendar"),this.titlePromise.then(function(a){return this.setTitle(a),a}.bind(this)).always(function(){this.reloadingTitle=!1}.bind(this)),this.titlePromise)},n.prototype.reloadBodyContent=function(a){if(this.reloadingBody)return this.bodyPromise;this.reloadingBody=!0,this.disableButtons();var b=this.saveButton.attr("data-context-id"),c={};return this.hasEventId()&&(c.eventid=this.getEventId()),this.hasStartTime()&&(c.starttime=this.getStartTime()),this.hasCourseId()&&(c.courseid=this.getCourseId()),"undefined"!=typeof a&&(c.formdata=a),this.bodyPromise=i.loadFragment("calendar","event_form",b,c),this.setBody(this.bodyPromise),this.bodyPromise.then(function(){this.enableButtons()}.bind(this))["catch"](d.exception).always(function(){this.reloadingBody=!1}.bind(this)),this.bodyPromise},n.prototype.reloadAllContent=function(){return a.when(this.reloadTitleContent(),this.reloadBodyContent())},n.prototype.show=function(){this.reloadAllContent(),g.prototype.show.call(this)},n.prototype.hide=function(){g.prototype.hide.call(this),this.setEventId(null),this.setStartTime(null)},n.prototype.getFormData=function(){return this.getForm().serialize()},n.prototype.save=function(){var b=this.saveButton.find(m.LOADING_ICON_CONTAINER);b.removeClass("hidden"),this.disableButtons();var c=this.getFormData();return k.submitCreateUpdateForm(c).then(function(b){return b.validationerror?this.reloadBodyContent(c):(this.hide(),void(this.hasEventId()?a("body").trigger(j.updated,[b.event]):a("body").trigger(j.created,[b.event])))}.bind(this)).always(function(){b.addClass("hidden"),this.enableButtons()}.bind(this))["catch"](d.exception)},n.prototype.registerEventListeners=function(){g.prototype.registerEventListeners.call(this),this.getModal().on(f.events.activate,m.SAVE_BUTTON,function(a,b){this.getForm().submit(),b.originalEvent.preventDefault(),a.stopPropagation()}.bind(this)),this.getModal().on("submit",function(a){this.save(),a.preventDefault(),a.stopPropagation()}.bind(this))},l||(h.register(n.TYPE,n,"calendar/modal_event_form"),l=!0),n}); \ No newline at end of file +define(["jquery","core/event","core/str","core/notification","core/templates","core/custom_interaction_events","core/modal","core/modal_registry","core/fragment","core_calendar/events","core_calendar/repository"],function(a,b,c,d,e,f,g,h,i,j,k){var l=!1,m={SAVE_BUTTON:'[data-action="save"]',LOADING_ICON_CONTAINER:'[data-region="loading-icon-container"]'},n=function(a){g.call(this,a),this.eventId=null,this.startTime=null,this.courseId=null,this.categoryId=null,this.reloadingBody=!1,this.reloadingTitle=!1,this.saveButton=this.getFooter().find(m.SAVE_BUTTON)};return n.TYPE="core_calendar-modal_event_form",n.prototype=Object.create(g.prototype),n.prototype.constructor=n,n.prototype.setCourseId=function(a){this.courseId=a},n.prototype.getCourseId=function(){return this.courseId},n.prototype.setCategoryId=function(a){this.categoryId=a},n.prototype.getCategoryId=function(){return this.categoryId},n.prototype.hasCourseId=function(){return null!==this.courseId},n.prototype.hasCategoryId=function(){return null!==this.categoryId},n.prototype.setEventId=function(a){this.eventId=a},n.prototype.getEventId=function(){return this.eventId},n.prototype.hasEventId=function(){return null!==this.eventId},n.prototype.setStartTime=function(a){this.startTime=a},n.prototype.getStartTime=function(){return this.startTime},n.prototype.hasStartTime=function(){return null!==this.startTime},n.prototype.getForm=function(){return this.getBody().find("form")},n.prototype.disableButtons=function(){this.saveButton.prop("disabled",!0)},n.prototype.enableButtons=function(){this.saveButton.prop("disabled",!1)},n.prototype.reloadTitleContent=function(){return this.reloadingTitle?this.titlePromise:(this.reloadingTitle=!0,this.hasEventId()?this.titlePromise=c.get_string("editevent","calendar"):this.titlePromise=c.get_string("newevent","calendar"),this.titlePromise.then(function(a){return this.setTitle(a),a}.bind(this)).always(function(){this.reloadingTitle=!1}.bind(this)).fail(d.exception),this.titlePromise)},n.prototype.reloadBodyContent=function(a){if(this.reloadingBody)return this.bodyPromise;this.reloadingBody=!0,this.disableButtons();var b=this.saveButton.attr("data-context-id"),c={};return this.hasEventId()&&(c.eventid=this.getEventId()),this.hasStartTime()&&(c.starttime=this.getStartTime()),this.hasCourseId()&&(c.courseid=this.getCourseId()),this.hasCategoryId()&&(c.categoryid=this.getCategoryId()),"undefined"!=typeof a&&(c.formdata=a),this.bodyPromise=i.loadFragment("calendar","event_form",b,c),this.setBody(this.bodyPromise),this.bodyPromise.then(function(){this.enableButtons()}.bind(this)).fail(d.exception).always(function(){this.reloadingBody=!1}.bind(this)).fail(d.exception),this.bodyPromise},n.prototype.reloadAllContent=function(){return a.when(this.reloadTitleContent(),this.reloadBodyContent())},n.prototype.show=function(){this.reloadAllContent(),g.prototype.show.call(this)},n.prototype.hide=function(){g.prototype.hide.call(this),this.setEventId(null),this.setStartTime(null),this.setCourseId(null),this.setCategoryId(null)},n.prototype.getFormData=function(){return this.getForm().serialize()},n.prototype.save=function(){var b=this.saveButton.find(m.LOADING_ICON_CONTAINER);b.removeClass("hidden"),this.disableButtons();var c=this.getFormData();return k.submitCreateUpdateForm(c).then(function(b){return b.validationerror?void this.reloadBodyContent(c):(this.hide(),void(this.hasEventId()?a("body").trigger(j.updated,[b.event]):a("body").trigger(j.created,[b.event])))}.bind(this)).always(function(){b.addClass("hidden"),this.enableButtons()}.bind(this)).fail(d.exception)},n.prototype.registerEventListeners=function(){g.prototype.registerEventListeners.call(this),this.getModal().on(f.events.activate,m.SAVE_BUTTON,function(a,b){this.getForm().submit(),b.originalEvent.preventDefault(),a.stopPropagation()}.bind(this)),this.getModal().on("submit",function(a){this.save(),a.preventDefault(),a.stopPropagation()}.bind(this))},l||(h.register(n.TYPE,n,"calendar/modal_event_form"),l=!0),n}); \ No newline at end of file diff --git a/calendar/amd/src/calendar.js b/calendar/amd/src/calendar.js index 90213bc4868..ef43e41f4d1 100644 --- a/calendar/amd/src/calendar.js +++ b/calendar/amd/src/calendar.js @@ -223,8 +223,7 @@ define([ * @param {object} eventFormModalPromise A promise reolved with the event form modal */ var registerCalendarEventListeners = function(root, eventFormModalPromise) { - var body = $('body'), - courseId = $(root).find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('courseid'); + var body = $('body'); body.on(CalendarEvents.created, function() { CalendarViewManager.reloadCurrentMonth(root); @@ -246,14 +245,14 @@ define([ CalendarViewManager.reloadCurrentMonth(root); }); - eventFormModalPromise.then(function(modal) { + eventFormModalPromise + .then(function(modal) { // When something within the calendar tells us the user wants // to edit an event then show the event form modal. body.on(CalendarEvents.editEvent, function(e, eventId) { modal.setEventId(eventId); modal.show(); }); - modal.setCourseId(courseId); return; }) .fail(Notification.exception); @@ -299,8 +298,16 @@ define([ registerCalendarEventListeners(root, eventFormPromise); // Bind click event on the new event button. + CustomEvents.define(root, [CustomEvents.events.activate]); root.on('click', SELECTORS.NEW_EVENT_BUTTON, function(e) { eventFormPromise.then(function(modal) { + var wrapper = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER); + modal.setCourseId(wrapper.data('courseid')); + var categoryId = wrapper.data('categoryid'); + if (typeof categoryId !== 'undefined') { + modal.setCategoryId(categoryId); + } + // Attempt to find the cell for today. // If it can't be found, then use the start time of the first day on the calendar. var today = root.find(SELECTORS.TODAY); @@ -323,6 +330,15 @@ define([ if (!target.is(SELECTORS.VIEW_DAY_LINK)) { var startTime = $(this).attr('data-new-event-timestamp'); eventFormPromise.then(function(modal) { + var wrapper = target.closest(SELECTORS.CALENDAR_MONTH_WRAPPER); + + modal.setCourseId(wrapper.data('courseid')); + + var categoryId = wrapper.data('categoryid'); + if (typeof categoryId !== 'undefined') { + modal.setCategoryId(categoryId); + } + modal.setStartTime(startTime); modal.show(); return; diff --git a/calendar/amd/src/modal_event_form.js b/calendar/amd/src/modal_event_form.js index 24f7b8458ee..e057e297920 100644 --- a/calendar/amd/src/modal_event_form.js +++ b/calendar/amd/src/modal_event_form.js @@ -65,6 +65,7 @@ define([ this.eventId = null; this.startTime = null; this.courseId = null; + this.categoryId = null; this.reloadingBody = false; this.reloadingTitle = false; this.saveButton = this.getFooter().find(SELECTORS.SAVE_BUTTON); @@ -94,6 +95,26 @@ define([ return this.courseId; }; + /** + * Set the category id to the given value. + * + * @method setCategoryId + * @param {int} id The event id + */ + ModalEventForm.prototype.setCategoryId = function(id) { + this.categoryId = id; + }; + + /** + * Retrieve the current category id, if any. + * + * @method getCategoryId + * @return {int|null} The event id + */ + ModalEventForm.prototype.getCategoryId = function() { + return this.categoryId; + }; + /** * Check if the modal has an course id. * @@ -104,6 +125,16 @@ define([ return this.courseId !== null; }; + /** + * Check if the modal has an category id. + * + * @method hasCategoryId + * @return {bool} + */ + ModalEventForm.prototype.hasCategoryId = function() { + return this.categoryId !== null; + }; + /** * Set the event id to the given value. * @@ -220,7 +251,8 @@ define([ .always(function() { this.reloadingTitle = false; return; - }.bind(this)); + }.bind(this)) + .fail(Notification.exception); return this.titlePromise; }; @@ -261,6 +293,10 @@ define([ args.courseid = this.getCourseId(); } + if (this.hasCategoryId()) { + args.categoryid = this.getCategoryId(); + } + if (typeof formData !== 'undefined') { args.formdata = formData; } @@ -273,11 +309,12 @@ define([ this.enableButtons(); return; }.bind(this)) - .catch(Notification.exception) + .fail(Notification.exception) .always(function() { this.reloadingBody = false; return; - }.bind(this)); + }.bind(this)) + .fail(Notification.exception); return this.bodyPromise; }; @@ -321,6 +358,8 @@ define([ Modal.prototype.hide.call(this); this.setEventId(null); this.setStartTime(null); + this.setCourseId(null); + this.setCategoryId(null); }; /** @@ -361,7 +400,8 @@ 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); + this.reloadBodyContent(formData); + return; } else { // No problemo! Our work here is done. this.hide(); @@ -382,8 +422,10 @@ define([ // the loading icon and re-enable the buttons. loadingContainer.addClass('hidden'); this.enableButtons(); + + return; }.bind(this)) - .catch(Notification.exception); + .fail(Notification.exception); }; /** diff --git a/calendar/externallib.php b/calendar/externallib.php index 35bc6e738d4..cf82f437ffa 100644 --- a/calendar/externallib.php +++ b/calendar/externallib.php @@ -902,6 +902,7 @@ class core_calendar_external extends external_api { // TODO: Copy what we do in calendar/view.php. $context = \context_user::instance($USER->id); self::validate_context($context); + $PAGE->set_url('/calendar/'); if ($courseid != SITEID && !empty($courseid)) { // Course ID must be valid and existing. diff --git a/calendar/lib.php b/calendar/lib.php index fe9aac6aff1..5bc27586a33 100644 --- a/calendar/lib.php +++ b/calendar/lib.php @@ -1217,17 +1217,20 @@ function calendar_get_events($tstart, $tend, $users, $groups, $courses, } if ((is_array($categories) && !empty($categories)) || is_numeric($categories)) { - if(!empty($whereclause)) $whereclause .= ' OR'; + if (!empty($whereclause)) { + $whereclause .= ' OR'; + } list($insqlcategories, $inparamscategories) = $DB->get_in_or_equal($categories, SQL_PARAMS_NAMED); $whereclause .= " (e.groupid = 0 AND e.courseid = 0 AND e.categoryid $insqlcategories)"; $params = array_merge($params, $inparamscategories); } else if ($categories === true) { // Events from ALL categories. - if(!empty($whereclause)) $whereclause .= ' OR'; + if (!empty($whereclause)) { + $whereclause .= ' OR'; + } $whereclause .= ' (e.groupid = 0 AND e.courseid = 0 AND e.categoryid != 0)'; } - // Security check: if, by now, we have NOTHING in $whereclause, then it means // that NO event-selecting clauses were defined. Thus, we won't be returning ANY // events no matter what. Allowing the code to proceed might return a completely @@ -3244,6 +3247,7 @@ function calendar_output_fragment_event_form($args) { $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; + $categoryid = isset($args['categoryid']) ? clean_param($args['categoryid'], PARAM_INT) : null; $event = null; $hasformdata = isset($args['formdata']) && !empty($args['formdata']); $context = \context_user::instance($USER->id); @@ -3276,6 +3280,9 @@ function calendar_output_fragment_event_form($args) { $data['eventtype'] = 'course'; $data['courseid'] = $courseid; $data['groupcourseid'] = $courseid; + } else if (!empty($categoryid)) { + $data['eventtype'] = 'category'; + $data['categoryid'] = $categoryid; } $mform->set_data($data); } else { From 303d649aca88aa12eb0123dca459f4ec143a2847 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Fri, 22 Sep 2017 12:01:00 +0800 Subject: [PATCH 12/15] MDL-59890 calendar: Correct context checks for edit test --- calendar/lib.php | 24 +++++++++++++++++------- calendar/tests/externallib_test.php | 2 +- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/calendar/lib.php b/calendar/lib.php index 5bc27586a33..1039d23b9bc 100644 --- a/calendar/lib.php +++ b/calendar/lib.php @@ -318,7 +318,9 @@ class calendar_event { global $USER, $DB; $context = null; - if (isset($this->properties->courseid) && $this->properties->courseid > 0) { + if (isset($this->properties->categoryid) && $this->properties->categoryid > 0) { + $context = \context_coursecat::instance($this->properties->categoryid); + } else if (isset($this->properties->courseid) && $this->properties->courseid > 0) { $context = \context_course::instance($this->properties->courseid); } else if (isset($this->properties->course) && $this->properties->course > 0) { $context = \context_course::instance($this->properties->course); @@ -2144,19 +2146,27 @@ function calendar_edit_event_allowed($event, $manualedit = false) { // Allow users to add/edit group events if - // 1) They have manageentries for the course OR // 2) They have managegroupentries AND are in the group. + $eventcontext = \context_course::instance($event->courseid); $group = $DB->get_record('groups', array('id' => $event->groupid)); return $group && ( - has_capability('moodle/calendar:manageentries', $event->context) || - (has_capability('moodle/calendar:managegroupentries', $event->context) + has_capability('moodle/calendar:manageentries', $eventcontext) || + (has_capability('moodle/calendar:managegroupentries', $eventcontext) && groups_is_member($event->groupid))); } else if (!empty($event->courseid)) { - // If groupid is not set, but course is set, it's definiely a course event. - return has_capability('moodle/calendar:manageentries', $event->context); + // If groupid is not set, but course is set, it's definitely a course event. + $eventcontext = \context_course::instance($event->courseid); + return has_capability('moodle/calendar:manageentries', $eventcontext); + } else if (!empty($event->categoryid)) { + // If groupid is not set, but category is set, it's definitely a category event. + $eventcontext = \context_coursecat::instance($event->categoryid); + return has_capability('moodle/calendar:manageentries', $eventcontext); } else if (!empty($event->userid) && $event->userid == $USER->id) { // If course is not set, but userid id set, it's a user event. - return (has_capability('moodle/calendar:manageownentries', $event->context)); + $eventcontext = \context_user::instance($event->userid); + return (has_capability('moodle/calendar:manageownentries', $eventcontext)); } else if (!empty($event->userid)) { - return (has_capability('moodle/calendar:manageentries', $event->context)); + $eventcontext = \context_user::instance($event->userid); + return (has_capability('moodle/calendar:manageentries', $eventcontext)); } return false; diff --git a/calendar/tests/externallib_test.php b/calendar/tests/externallib_test.php index ea3edfd8ef8..f60d57f546b 100644 --- a/calendar/tests/externallib_test.php +++ b/calendar/tests/externallib_test.php @@ -1600,7 +1600,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase { public function test_submit_create_update_form_create_site_event() { $generator = $this->getDataGenerator(); $user = $generator->create_user(); - $context = context_course::instance(SITEID); + $context = context_system::instance(); $roleid = $generator->create_role(); $timestart = new DateTime(); $interval = new DateInterval("P1D"); // One day. From fb3c0fc38bd240064ec92e597c7f151572d22933 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 28 Sep 2017 13:50:26 +0800 Subject: [PATCH 13/15] MDL-59890 calendar: Allow event creation in behat --- lib/testing/generator/data_generator.php | 55 +++++++++++++++++++++++ lib/tests/behat/behat_data_generators.php | 9 ++++ 2 files changed, 64 insertions(+) diff --git a/lib/testing/generator/data_generator.php b/lib/testing/generator/data_generator.php index 5b0bc694482..e3d42a83531 100644 --- a/lib/testing/generator/data_generator.php +++ b/lib/testing/generator/data_generator.php @@ -1103,4 +1103,59 @@ EOD; // Get the tool associated with this instance. return $DB->get_record('enrol_lti_tools', array('enrolid' => $instanceid)); } + + /** + * Helper function used to create an event. + * + * @param array $data + * @return stdClass + */ + public function create_event($data = []) { + global $CFG; + + require_once($CFG->dirroot . '/calendar/lib.php'); + $record = new \stdClass(); + $record->name = 'event name'; + $record->eventtype = 'global'; + $record->repeat = 0; + $record->repeats = 0; + $record->timestart = time(); + $record->timeduration = 0; + $record->timesort = 0; + $record->eventtype = 'user'; + $record->courseid = 0; + + foreach ($data as $key => $value) { + $record->$key = $value; + } + + switch ($record->eventtype) { + case 'user': + unset($record->categoryid); + unset($record->courseid); + unset($record->groupid); + break; + case 'group': + unset($record->categoryid); + break; + case 'course': + unset($record->categoryid); + unset($record->groupid); + break; + case 'category': + unset($record->courseid); + unset($record->groupid); + break; + case 'global': + unset($record->categoryid); + unset($record->courseid); + unset($record->groupid); + break; + } + + $event = new calendar_event($record); + $event->create($record); + + return $event->properties(); + } } diff --git a/lib/tests/behat/behat_data_generators.php b/lib/tests/behat/behat_data_generators.php index b54e3d6ae6a..d0bdb8eee7c 100644 --- a/lib/tests/behat/behat_data_generators.php +++ b/lib/tests/behat/behat_data_generators.php @@ -174,6 +174,15 @@ class behat_data_generators extends behat_base { 'datagenerator' => 'tag', 'required' => array('name') ), + 'events' => array( + 'datagenerator' => 'event', + 'required' => array('name', 'eventtype'), + 'switchids' => array( + 'user' => 'userid', + 'course' => 'courseid', + 'category' => 'categoryid', + ) + ), ); /** From 1957d33996309d6fd448ce34c8568b904f78953c Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 28 Sep 2017 13:50:45 +0800 Subject: [PATCH 14/15] MDL-59890 behat: Check empty not isset for switchids --- lib/tests/behat/behat_data_generators.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tests/behat/behat_data_generators.php b/lib/tests/behat/behat_data_generators.php index d0bdb8eee7c..7f0d2bf2d63 100644 --- a/lib/tests/behat/behat_data_generators.php +++ b/lib/tests/behat/behat_data_generators.php @@ -227,7 +227,7 @@ class behat_data_generators extends behat_base { $methodname = 'get_' . $element . '_id'; // Not all the switch fields are required, default vars will be assigned by data generators. - if (isset($elementdata[$element])) { + if (!empty($elementdata[$element])) { // Temp $id var to avoid problems when $element == $field. $id = $this->{$methodname}($elementdata[$element]); unset($elementdata[$element]); From 02150c9a1ec4455b616b32d0e0fcdc52f2af1d86 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 28 Sep 2017 13:54:02 +0800 Subject: [PATCH 15/15] MDL-59890 calendar: Add behat tests --- calendar/tests/behat/category_events.feature | 96 ++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 calendar/tests/behat/category_events.feature diff --git a/calendar/tests/behat/category_events.feature b/calendar/tests/behat/category_events.feature new file mode 100644 index 00000000000..be4721b630b --- /dev/null +++ b/calendar/tests/behat/category_events.feature @@ -0,0 +1,96 @@ +@core @core_calendar +Feature: Course Category Events + In order to inform multiple courses of shared events + As a manager + I need to create catgory events + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | managera | Manager | A | managera@example.com | + | managera1 | Manager | A1 | managera1@example.com | + | managera2 | Manager | A2 | managera2@example.com | + | teachera1i | Teacher | A1i | teachera1i@example.com | + | managerb | Manager | B | managerb@example.com | + | managerb1 | Manager | B1 | managerb1@example.com | + | managerb2 | Manager | B2 | managerb2@example.com | + | teacherb1i | Teacher | B1i | teacherb1i@example.com | + | student1 | Student | 1 | student1@example.com | + | student2 | Student | 2 | student2@example.com | + And the following "categories" exist: + | name | idnumber | category | + | Year | year | | + | Faculty A | faculty-a | year | + | Faculty B | faculty-b | year | + | Department A1 | department-a1 | faculty-a | + | Department A2 | department-a2 | faculty-a | + | Department B1 | department-b1 | faculty-b | + | Department B2 | department-b2 | faculty-b | + And the following "courses" exist: + | fullname | shortname | idnumber | format | category | + | Course A1i | A1i | A1i | topics | department-a1 | + | Course A2i | A2i | A2i | topics | department-a2 | + | Course B1i | B1i | B1i | topics | department-b1 | + | Course B2i | B2i | B2i | topics | department-b2 | + And the following "role assigns" exist: + | user | role | contextlevel | reference | + | managera | manager | Category | faculty-a | + | managera1 | manager | Category | department-a1 | + | managerb | manager | Category | faculty-b | + | managerb1 | manager | Category | department-b1 | + And the following "course enrolments" exist: + | user | course | role | + | teachera1i | A1i | editingteacher | + | teacherb1i | B1i | editingteacher | + | student1 | A1i | student | + | student1 | A2i | student | + | student2 | B1i | student | + | student2 | B2i | student | + And the following "events" exist: + | name | eventtype | category | course | + | Site event | global | | | + | FA event | category | faculty-a | | + | DA1 event | category | department-a1 | | + | DA2 event | category | department-a1 | | + | FB event | category | faculty-b | | + | DB1 event | category | department-b1 | | + | DB2 event | category | department-b1 | | + | CA1i event | course | | A1i | + | CA2i event | course | | A2i | + | CB1i event | course | | B1i | + | CB2i event | course | | B2i | + + @javascript + Scenario: Manager of a Category can see all child and parent events in their category + Given I log in as "managera" + When I navigate to "Calendar" node in "Site pages" + Then I should see "FA event" + And I should see "DA1 event" + And I should see "DA2 event" + And I should not see "FB event" + And I should not see "DB1 event" + And I should not see "DB2 event" + And I log out + Given I log in as "managerb" + When I navigate to "Calendar" node in "Site pages" + Then I should see "FB event" + And I should see "DB1 event" + And I should see "DB2 event" + And I should not see "FA event" + And I should not see "DA1 event" + And I should not see "DA2 event" + + @javascript + Scenario: Users enrolled in a course can see all child and parent events in their category + Given I log in as "student1" + When I navigate to "Calendar" node in "Site pages" + Then I should see "FA event" + And I should see "DA1 event" + And I should see "DA2 event" + And I should see "CA1i event" + And I should see "CA2i event" + And I should not see "FB event" + And I should not see "DB1 event" + And I should not see "DB2 event" + And I should not see "CB1i event" + And I should not see "CB2i event"