diff --git a/calendar/classes/local/event/container.php b/calendar/classes/local/event/container.php index 597eb70889f..0afc5940281 100644 --- a/calendar/classes/local/event/container.php +++ b/calendar/classes/local/event/container.php @@ -81,6 +81,11 @@ class container { */ protected static $modulecache = array(); + /** + * @var int The requesting user. All capability checks are done against this user. + */ + protected static $requestinguserid; + /** * Initialises the dependency graph if it hasn't yet been. */ @@ -117,11 +122,13 @@ class container { [self::class, 'apply_component_provide_event_action'], [self::class, 'apply_component_is_event_visible'], function ($dbrow) { + $requestinguserid = self::get_requesting_user(); + 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); + $category = \coursecat::get($dbrow->categoryid, IGNORE_MISSING, true, $requestinguserid); - if (empty($category) || !$category->is_uservisible()) { + if (empty($category) || !$category->is_uservisible($requestinguserid)) { return true; } } @@ -131,7 +138,7 @@ class container { return false; } - $instances = get_fast_modinfo($dbrow->courseid)->instances; + $instances = get_fast_modinfo($dbrow->courseid, $requestinguserid)->instances; // If modinfo doesn't know about the module, we should ignore it. if (!isset($instances[$dbrow->modulename]) || !isset($instances[$dbrow->modulename][$dbrow->instance])) { @@ -156,11 +163,13 @@ class container { } $coursecontext = \context_course::instance($dbrow->courseid); - if (!$cm->get_course()->visible && !has_capability('moodle/course:viewhiddencourses', $coursecontext)) { + if (!$cm->get_course()->visible && + !has_capability('moodle/course:viewhiddencourses', $coursecontext, $requestinguserid)) { return true; } - if (!has_capability('moodle/course:view', $coursecontext) && !is_enrolled($coursecontext)) { + if (!has_capability('moodle/course:view', $coursecontext, $requestinguserid) && + !is_enrolled($coursecontext, $requestinguserid)) { return true; } @@ -191,6 +200,7 @@ class container { * Reset all static caches, called between tests. */ public static function reset_caches() { + self::$requestinguserid = null; self::$eventfactory = null; self::$eventmapper = null; self::$eventvault = null; @@ -230,6 +240,31 @@ class container { return self::$eventvault; } + /** + * Sets the requesting user so that all capability checks are done against this user. + * Setting the requesting user (hence calling this function) is optional and if you do not so, + * $USER will be used as the requesting user. However, if you wish to set the requesting user yourself, + * you should call this function before any other function of the container class is called. + * + * @param int $userid The user id. + * @throws \coding_exception + */ + public static function set_requesting_user($userid) { + self::$requestinguserid = $userid; + } + + /** + * Returns the requesting user id. + * It usually is the current user unless it has been set explicitly using set_requesting_user. + * + * @return int + */ + public static function get_requesting_user() { + global $USER; + + return empty(self::$requestinguserid) ? $USER->id : self::$requestinguserid; + } + /** * Calls callback 'core_calendar_provide_event_action' from the component responsible for the event * @@ -245,14 +280,23 @@ class container { $mapper = self::$eventmapper; $action = null; if ($event->get_course_module()) { + $requestinguserid = self::get_requesting_user(); + $legacyevent = $mapper->from_event_to_legacy_event($event); + // We know for a fact that the the requesting user might be different from the logged in user, + // but the event mapper is not aware of that. + if (empty($event->user) && !empty($legacyevent->userid)) { + $legacyevent->userid = $requestinguserid; + } + // TODO MDL-58866 Only activity modules currently support this callback. // Any other event will not be displayed on the dashboard. $action = component_callback( 'mod_' . $event->get_course_module()->get('modname'), 'core_calendar_provide_event_action', [ - $mapper->from_event_to_legacy_event($event), - self::$actionfactory + $legacyevent, + self::$actionfactory, + $requestinguserid ] ); } @@ -279,12 +323,21 @@ class container { $mapper = self::$eventmapper; $eventvisible = null; if ($event->get_course_module()) { + $requestinguserid = self::get_requesting_user(); + $legacyevent = $mapper->from_event_to_legacy_event($event); + // We know for a fact that the the requesting user might be different from the logged in user, + // but the event mapper is not aware of that. + if (empty($event->user) && !empty($legacyevent->userid)) { + $legacyevent->userid = $requestinguserid; + } + // TODO MDL-58866 Only activity modules currently support this callback. $eventvisible = component_callback( 'mod_' . $event->get_course_module()->get('modname'), 'core_calendar_is_event_visible', [ - $mapper->from_event_to_legacy_event($event) + $legacyevent, + $requestinguserid ] ); } 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 5b863b2ccae..37d1a8038e2 100644 --- a/calendar/classes/local/event/strategies/raw_event_retrieval_strategy.php +++ b/calendar/classes/local/event/strategies/raw_event_retrieval_strategy.php @@ -96,11 +96,24 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter return array(); } + if (is_numeric($users)) { + $users = array($users); + } + if (is_numeric($groups)) { + $groups = array($groups); + } + if (is_numeric($courses)) { + $courses = array($courses); + } + if (is_numeric($categories)) { + $categories = array($categories); + } + // Array of filter conditions. To be concatenated by the OR operator. $filters = []; // User filter. - if ((is_array($users) && !empty($users)) or is_numeric($users)) { + if (is_array($users) && !empty($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 AND e.categoryid = 0)"; @@ -112,7 +125,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter // Boolean false (no users at all): We don't need to do anything. // Group filter. - if ((is_array($groups) && !empty($groups)) or is_numeric($groups)) { + if (is_array($groups) && !empty($groups)) { // Events from a number of groups. list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED); $filters[] = "e.groupid $insqlgroups"; @@ -124,7 +137,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter // Boolean false (no groups at all): We don't need to do anything. // Course filter. - if ((is_array($courses) && !empty($courses)) or is_numeric($courses)) { + if (is_array($courses) && !empty($courses)) { list($insqlcourses, $inparamscourses) = $DB->get_in_or_equal($courses, SQL_PARAMS_NAMED); $filters[] = "(e.groupid = 0 AND e.courseid $insqlcourses)"; $params = array_merge($params, $inparamscourses); @@ -134,7 +147,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter } // Category filter. - if ((is_array($categories) && !empty($categories)) or is_numeric($categories)) { + if (is_array($categories) && !empty($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); @@ -168,54 +181,81 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter // Build SQL subquery and conditions for filtered events based on priorities. $subquerywhere = ''; $subqueryconditions = []; - - // Get the user's courses. Otherwise, get the default courses being shown by the calendar. - $usercourses = calendar_get_default_courses(null, 'id, category, groupmode, groupmodeforce'); - - // Set calendar filters. - list($usercourses, $usergroups, $user) = calendar_set_filters($usercourses, true); $subqueryparams = []; + $allusercourses = []; - // Flag to indicate whether the query needs to exclude group overrides. - $viewgroupsonly = false; + if (is_array($users) && !empty($users)) { + $userrecords = $DB->get_records_sql("SELECT * FROM {user} WHERE id $insqlusers", $inparamsusers); + foreach ($userrecords as $userrecord) { + // Get the user's courses. Otherwise, get the default courses being shown by the calendar. + $usercourses = calendar_get_default_courses(null, 'id, category, groupmode, groupmodeforce', + false, $userrecord->id); - if ($user) { - // Set filter condition for the user's events. - $subqueryconditions[] = "(ev.userid = :user AND ev.courseid = 0 AND ev.groupid = 0 AND ev.categoryid = 0)"; - $subqueryparams['user'] = $user; + // Set calendar filters. + list($usercourses, $usergroups, $user) = calendar_set_filters($usercourses, true, $userrecord); - foreach ($usercourses as $courseid) { - if (has_capability('moodle/site:accessallgroups', \context_course::instance($courseid))) { - $usergroupmembership = groups_get_all_groups($courseid, $user, 0, 'g.id'); - if (count($usergroupmembership) == 0) { - $viewgroupsonly = true; - break; + $allusercourses = array_merge($allusercourses, $usercourses); + + // Flag to indicate whether the query needs to exclude group overrides. + $viewgroupsonly = false; + + if ($user) { + // Set filter condition for the user's events. + // Even though $user is a single scalar, we still use get_in_or_equal() because we are inside a loop. + list($inusers, $inuserparams) = $DB->get_in_or_equal($user, SQL_PARAMS_NAMED); + $subqueryconditions[] = "(ev.userid $inusers AND ev.courseid = 0 AND ev.groupid = 0 AND ev.categoryid = 0)"; + $subqueryparams = array_merge($subqueryparams, $inuserparams); + + foreach ($usercourses as $courseid) { + if (has_capability('moodle/site:accessallgroups', \context_course::instance($courseid), $userrecord)) { + $usergroupmembership = groups_get_all_groups($courseid, $user, 0, 'g.id'); + if (count($usergroupmembership) == 0) { + $viewgroupsonly = true; + break; + } + } } } - } - } - // Set filter condition for the user's group events. - if ($usergroups === true || $viewgroupsonly) { - // Fetch group events, but not group overrides. - $subqueryconditions[] = "(ev.groupid != 0 AND ev.eventtype = 'group')"; - } else if (!empty($usergroups)) { - // Fetch group events and group overrides. - list($inusergroups, $inusergroupparams) = $DB->get_in_or_equal($usergroups, SQL_PARAMS_NAMED); - $subqueryconditions[] = "(ev.groupid $inusergroups)"; - $subqueryparams = array_merge($subqueryparams, $inusergroupparams); + // Set filter condition for the user's group events. + if ($usergroups === true || $viewgroupsonly) { + // Fetch group events, but not group overrides. + $subqueryconditions[] = "(ev.groupid != 0 AND ev.eventtype = 'group')"; + } else if (!empty($usergroups)) { + // Fetch group events and group overrides. + list($inusergroups, $inusergroupparams) = $DB->get_in_or_equal($usergroups, SQL_PARAMS_NAMED); + $subqueryconditions[] = "(ev.groupid $inusergroups)"; + $subqueryparams = array_merge($subqueryparams, $inusergroupparams); + } + } + } else if ($users === true) { + // Events from ALL users. + $subqueryconditions[] = "(ev.userid != 0 AND ev.courseid = 0 AND ev.groupid = 0 AND ev.categoryid = 0)"; + + if (is_array($groups)) { + // Events from a number of groups. + list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED); + $subqueryconditions[] = "ev.groupid $insqlgroups"; + $subqueryparams = array_merge($subqueryparams, $inparamsgroups); + } else if ($groups === true) { + // Events from ALL groups. + $subqueryconditions[] = "ev.groupid != 0"; + } + + if ($courses === true) { + // ALL course events. It's not needed to worry about users' access as $users = true. + $subqueryconditions[] = "(ev.groupid = 0 AND ev.courseid != 0 AND ev.categoryid = 0)"; + } } // Get courses to be used for the subquery. $subquerycourses = []; if (is_array($courses)) { $subquerycourses = $courses; - } else if (is_numeric($courses)) { - $subquerycourses[] = $courses; } // Merge with user courses, if necessary. - if (!empty($usercourses)) { - $subquerycourses = array_merge($subquerycourses, $usercourses); + if (!empty($allusercourses)) { + $subquerycourses = array_merge($subquerycourses, $allusercourses); // Make sure we remove duplicate values. $subquerycourses = array_unique($subquerycourses); } diff --git a/calendar/lib.php b/calendar/lib.php index 15dee2ae977..c3c7aac009a 100644 --- a/calendar/lib.php +++ b/calendar/lib.php @@ -2039,34 +2039,29 @@ function calendar_events_by_day($events, $month, $year, &$eventsbyday, &$duratio * * @param array $courseeventsfrom An array of courses to load calendar events for * @param bool $ignorefilters specify the use of filters, false is set as default + * @param stdClass $user The user object. This defaults to the global $USER object. * @return array An array of courses, groups, and user to load calendar events for based upon filters */ -function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false) { - global $USER, $CFG; +function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false, stdClass $user = null) { + global $CFG, $USER; - // For backwards compatability we have to check whether the courses array contains - // just id's in which case we need to load course objects. - $coursestoload = array(); - foreach ($courseeventsfrom as $id => $something) { - if (!is_object($something)) { - $coursestoload[] = $id; - unset($courseeventsfrom[$id]); - } + if (is_null($user)) { + $user = $USER; } $courses = array(); - $user = false; + $userid = false; $group = false; // Get the capabilities that allow seeing group events from all groups. $allgroupscaps = array('moodle/site:accessallgroups', 'moodle/calendar:manageentries'); - $isloggedin = isloggedin(); + $isvaliduser = !empty($user->id); - if ($ignorefilters || calendar_show_event_type(CALENDAR_EVENT_COURSE)) { + if ($ignorefilters || calendar_show_event_type(CALENDAR_EVENT_COURSE, $user)) { $courses = array_keys($courseeventsfrom); } - if ($ignorefilters || calendar_show_event_type(CALENDAR_EVENT_GLOBAL)) { + if ($ignorefilters || calendar_show_event_type(CALENDAR_EVENT_GLOBAL, $user)) { $courses[] = SITEID; } $courses = array_unique($courses); @@ -2080,11 +2075,11 @@ function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false) { $courses[] = SITEID; } - if ($ignorefilters || ($isloggedin && calendar_show_event_type(CALENDAR_EVENT_USER))) { - $user = $USER->id; + if ($ignorefilters || ($isvaliduser && calendar_show_event_type(CALENDAR_EVENT_USER, $user))) { + $userid = $user->id; } - if (!empty($courseeventsfrom) && (calendar_show_event_type(CALENDAR_EVENT_GROUP) || $ignorefilters)) { + if (!empty($courseeventsfrom) && (calendar_show_event_type(CALENDAR_EVENT_GROUP, $user) || $ignorefilters)) { if (count($courseeventsfrom) == 1) { $course = reset($courseeventsfrom); @@ -2096,16 +2091,16 @@ function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false) { if ($group === false) { if (!empty($CFG->calendar_adminseesall) && has_any_capability($allgroupscaps, \context_system::instance())) { $group = true; - } else if ($isloggedin) { + } else if ($isvaliduser) { $groupids = array(); foreach ($courseeventsfrom as $courseid => $course) { // If the user is an editing teacher in there. - if (!empty($USER->groupmember[$course->id])) { + if (!empty($user->groupmember[$course->id])) { // We've already cached the users groups for this course so we can just use that. - $groupids = array_merge($groupids, $USER->groupmember[$course->id]); + $groupids = array_merge($groupids, $user->groupmember[$course->id]); } else if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) { // If this course has groups, show events from all of those related to the current user. - $coursegroups = groups_get_user_groups($course->id, $USER->id); + $coursegroups = groups_get_user_groups($course->id, $user->id); $groupids = array_merge($groupids, $coursegroups['0']); } } @@ -2119,7 +2114,7 @@ function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false) { $courses = false; } - return array($courses, $group, $user); + return array($courses, $group, $userid); } /** @@ -2317,20 +2312,25 @@ function calendar_delete_event_allowed($event) { * * @param int $courseid (optional) If passed, an additional course can be returned for admins (the current course). * @param string $fields Comma separated list of course fields to return. - * @param bool $canmanage If true, this will return the list of courses the current user can create events in, rather + * @param bool $canmanage If true, this will return the list of courses the user can create events in, rather * than the list of courses they see events from (an admin can always add events in a course * calendar, even if they are not enrolled in the course). + * @param int $userid (optional) The user which this function returns the default courses for. + * By default the current user. * @return array $courses Array of courses to display */ -function calendar_get_default_courses($courseid = null, $fields = '*', $canmanage=false) { - global $CFG, $DB; +function calendar_get_default_courses($courseid = null, $fields = '*', $canmanage = false, int $userid = null) { + global $CFG, $USER; - if (!isloggedin()) { - return array(); + if (!$userid) { + if (!isloggedin()) { + return array(); + } + $userid = $USER->id; } - if (has_capability('moodle/calendar:manageentries', context_system::instance()) && - (!empty($CFG->calendar_adminseesall) || $canmanage)) { + if ((!empty($CFG->calendar_adminseesall) || $canmanage) && + has_capability('moodle/calendar:manageentries', context_system::instance(), $userid)) { // Add a c. prefix to every field as expected by get_courses function. $fieldlist = explode(',', $fields); @@ -2340,11 +2340,11 @@ function calendar_get_default_courses($courseid = null, $fields = '*', $canmanag }, $fieldlist); $courses = get_courses('all', 'c.shortname', implode(',', $prefixedfields)); } else { - $courses = enrol_get_my_courses($fields); + $courses = enrol_get_users_courses($userid, true, $fields); } if ($courseid && $courseid != SITEID) { - if (empty($courses[$courseid]) && has_capability('moodle/calendar:manageentries', context_system::instance())) { + if (empty($courses[$courseid]) && has_capability('moodle/calendar:manageentries', context_system::instance(), $userid)) { // Allow a site admin to see calendars from courses he is not enrolled in. // This will come from $COURSE. $courses[$courseid] = get_course($courseid); @@ -3258,6 +3258,11 @@ function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses, return $param; }, [$users, $groups, $courses, $categories]); + // If a single user is provided, we can use that for capability checks. + // Otherwise current logged in user is used - See MDL-58768. + if (is_array($userparam) && count($userparam) == 1) { + \core_calendar\local\event\container::set_requesting_user($userparam[0]); + } $mapper = \core_calendar\local\event\container::get_event_mapper(); $events = \core_calendar\local\api::get_events( $tstart, diff --git a/calendar/tests/lib_test.php b/calendar/tests/lib_test.php index e50e58c2a5e..35809ec18b7 100644 --- a/calendar/tests/lib_test.php +++ b/calendar/tests/lib_test.php @@ -472,6 +472,26 @@ class core_calendar_lib_testcase extends advanced_testcase { // Enrolled course only (ignore current). $this->assertCount(1, $courses); + // Now, log out and test again. + $this->setUser(); + + $CFG->calendar_adminseesall = false; + + $courses = calendar_get_default_courses(null, '*', false, $teacher->id); + // Only enrolled in one course. + $this->assertCount(1, $courses); + $courses = calendar_get_default_courses($course2->id, '*', false, $teacher->id); + // Enrolled course only (ignore current). + $this->assertCount(1, $courses); + // This setting should not affect teachers. + $CFG->calendar_adminseesall = true; + $courses = calendar_get_default_courses(null, '*', false, $teacher->id); + // Only enrolled in one course. + $this->assertCount(1, $courses); + $courses = calendar_get_default_courses($course2->id, '*', false, $teacher->id); + // Enrolled course only (ignore current). + $this->assertCount(1, $courses); + } /** @@ -627,4 +647,129 @@ class core_calendar_lib_testcase extends advanced_testcase { $types = calendar_get_allowed_event_types($course->id); $this->assertTrue($types['group']); } + + /** + * This is a setup helper function that create some users, courses, groups and group memberships. + * This is useful to prepare the environment for testing the calendar_set_filters function. + * + * @return array An array of ($users, $courses, $coursegroups) + */ + protected function setup_test_calendar_set_filters() { + $generator = $this->getDataGenerator(); + + // Create some users. + $users = []; + $users[] = $generator->create_user(); + $users[] = $generator->create_user(); + $users[] = $generator->create_user(); + + // Create some courses. + $courses = []; + $courses[] = $generator->create_course(); + $courses[] = $generator->create_course(); + $courses[] = $generator->create_course(); + $courses[] = $generator->create_course(); + + // Create some groups. + $coursegroups = []; + $coursegroups[$courses[0]->id] = []; + $coursegroups[$courses[0]->id][] = $generator->create_group(['courseid' => $courses[0]->id]); + $coursegroups[$courses[0]->id][] = $generator->create_group(['courseid' => $courses[0]->id]); + $coursegroups[$courses[2]->id] = []; + $coursegroups[$courses[2]->id][] = $generator->create_group(['courseid' => $courses[2]->id]); + $coursegroups[$courses[2]->id][] = $generator->create_group(['courseid' => $courses[2]->id]); + $coursegroups[$courses[3]->id] = []; + $coursegroups[$courses[3]->id][] = $generator->create_group(['courseid' => $courses[3]->id]); + $coursegroups[$courses[3]->id][] = $generator->create_group(['courseid' => $courses[3]->id]); + + // Create some enrolments and group memberships. + $generator->enrol_user($users[0]->id, $courses[0]->id, 'student'); + $generator->create_group_member(['groupid' => $coursegroups[$courses[0]->id][0]->id, 'userid' => $users[0]->id]); + $generator->enrol_user($users[1]->id, $courses[0]->id, 'student'); + $generator->create_group_member(['groupid' => $coursegroups[$courses[0]->id][1]->id, 'userid' => $users[1]->id]); + $generator->enrol_user($users[0]->id, $courses[1]->id, 'student'); + $generator->enrol_user($users[0]->id, $courses[2]->id, 'student'); + + return array($users, $courses, $coursegroups); + } + + /** + * This function tests calendar_set_filters for the case when user is not logged in. + */ + public function test_calendar_set_filters_not_logged_in() { + $this->resetAfterTest(); + + list($users, $courses, $coursegroups) = $this->setup_test_calendar_set_filters(); + + $defaultcourses = calendar_get_default_courses(null, '*', false, $users[0]->id); + list($courseids, $groupids, $userid) = calendar_set_filters($defaultcourses); + + $this->assertEquals( + [$courses[0]->id, $courses[1]->id, $courses[2]->id, SITEID], + array_values($courseids), + '', 0.0, 10, true); + $this->assertFalse($groupids); + $this->assertFalse($userid); + } + + /** + * This function tests calendar_set_filters for the case when no one is logged in, but a user id is provided. + */ + public function test_calendar_set_filters_not_logged_in_with_user() { + $this->resetAfterTest(); + + list($users, $courses, $coursegroups) = $this->setup_test_calendar_set_filters(); + + $defaultcourses = calendar_get_default_courses(null, '*', false, $users[1]->id); + list($courseids, $groupids, $userid) = calendar_set_filters($defaultcourses, false, $users[1]); + + $this->assertEquals(array($courses[0]->id, SITEID), array_values($courseids)); + $this->assertEquals(array($coursegroups[$courses[0]->id][1]->id), $groupids); + $this->assertEquals($users[1]->id, $userid); + + $defaultcourses = calendar_get_default_courses(null, '*', false, $users[0]->id); + list($courseids, $groupids, $userid) = calendar_set_filters($defaultcourses, false, $users[0]); + + $this->assertEquals( + [$courses[0]->id, $courses[1]->id, $courses[2]->id, SITEID], + array_values($courseids), + '', 0.0, 10, true); + $this->assertEquals(array($coursegroups[$courses[0]->id][0]->id), $groupids); + $this->assertEquals($users[0]->id, $userid); + + } + + /** + * This function tests calendar_set_filters for the case when user is logged in, but no user id is provided. + */ + public function test_calendar_set_filters_logged_in_no_user() { + $this->resetAfterTest(); + + list($users, $courses, $coursegroups) = $this->setup_test_calendar_set_filters(); + + $this->setUser($users[0]); + $defaultcourses = calendar_get_default_courses(null, '*', false, $users[0]->id); + list($courseids, $groupids, $userid) = calendar_set_filters($defaultcourses, false); + $this->assertEquals([$courses[0]->id, $courses[1]->id, $courses[2]->id, SITEID], array_values($courseids), '', 0.0, 10, + true); + $this->assertEquals(array($coursegroups[$courses[0]->id][0]->id), $groupids); + $this->assertEquals($users[0]->id, $userid); + } + + /** + * This function tests calendar_set_filters for the case when a user is logged in, but another user id is provided. + */ + public function test_calendar_set_filters_logged_in_another_user() { + $this->resetAfterTest(); + + list($users, $courses, $coursegroups) = $this->setup_test_calendar_set_filters(); + + $this->setUser($users[0]); + $defaultcourses = calendar_get_default_courses(null, '*', false, $users[1]->id); + list($courseids, $groupids, $userid) = calendar_set_filters($defaultcourses, false, $users[1]); + + $this->assertEquals(array($courses[0]->id, SITEID), array_values($courseids)); + $this->assertEquals(array($coursegroups[$courses[0]->id][1]->id), $groupids); + $this->assertEquals($users[1]->id, $userid); + } } diff --git a/calendar/tests/raw_event_retrieval_strategy_test.php b/calendar/tests/raw_event_retrieval_strategy_test.php index 1a927c2a169..82cd7a8aeeb 100644 --- a/calendar/tests/raw_event_retrieval_strategy_test.php +++ b/calendar/tests/raw_event_retrieval_strategy_test.php @@ -90,15 +90,29 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc $this->assertCount(2, $events); // Disable the lesson module. - $modulerecord = $DB->get_record('modules', ['name' => 'lesson']); - $modulerecord->visible = 0; - $DB->update_record('modules', $modulerecord); + $DB->set_field('modules', 'visible', 0, ['name' => 'lesson']); // Check that we only return the assign event. $events = $retrievalstrategy->get_raw_events(null, [0], null); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('assign', $event->modulename); + + // Now, log out and repeat the above test in the reverse order. + $this->setUser(); + + // Check that we only return the assign event (given that the lesson module is still disabled). + $events = $retrievalstrategy->get_raw_events([$student->id], [0], null); + $this->assertCount(1, $events); + $event = reset($events); + $this->assertEquals('assign', $event->modulename); + + // Enable the lesson module. + $DB->set_field('modules', 'visible', 1, ['name' => 'lesson']); + + // Get all events. + $events = $retrievalstrategy->get_raw_events(null, [0], null); + $this->assertCount(2, $events); } /** @@ -209,37 +223,37 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc calendar_event::create($event, false); } - $timestart = $now - 100; - $timeend = $now + (3 * 86400); $groups = [$group1->id, $group2->id]; - // Get user override events. - $this->setUser($useroverridestudent); - $events = $retrievalstrategy->get_raw_events([$useroverridestudent->id], $groups, [$course->id]); - $this->assertCount(1, $events); - $event = reset($events); - $this->assertEquals('Assignment 1 due date - User override', $event->name); + // Do the following tests multiple times when logged in with different users. Also run the whole set when logged out. + // In any cases, the tests should not depend on the logged-in user. + foreach ([$useroverridestudent, $nogroupstudent, $group12student, $group1student, null] as $login) { + $this->setUser($login); - // Get events for user that does not belong to any group and has no user override events. - $this->setUser($nogroupstudent); - $events = $retrievalstrategy->get_raw_events([$nogroupstudent->id], $groups, [$course->id]); - $this->assertCount(1, $events); - $event = reset($events); - $this->assertEquals('Assignment 1 due date', $event->name); + // Get user override events. + $events = $retrievalstrategy->get_raw_events([$useroverridestudent->id], $groups, [$course->id]); + $this->assertCount(1, $events); + $event = reset($events); + $this->assertEquals('Assignment 1 due date - User override', $event->name); - // Get events for user that belongs to groups A and B and has no user override events. - $this->setUser($group12student); - $events = $retrievalstrategy->get_raw_events([$group12student->id], $groups, [$course->id]); - $this->assertCount(1, $events); - $event = reset($events); - $this->assertEquals('Assignment 1 due date - Group A override', $event->name); + // Get events for user that does not belong to any group and has no user override events. + $events = $retrievalstrategy->get_raw_events([$nogroupstudent->id], $groups, [$course->id]); + $this->assertCount(1, $events); + $event = reset($events); + $this->assertEquals('Assignment 1 due date', $event->name); - // Get events for user that belongs to group A and has no user override events. - $this->setUser($group1student); - $events = $retrievalstrategy->get_raw_events([$group1student->id], $groups, [$course->id]); - $this->assertCount(1, $events); - $event = reset($events); - $this->assertEquals('Assignment 1 due date - Group A override', $event->name); + // Get events for user that belongs to groups A and B and has no user override events. + $events = $retrievalstrategy->get_raw_events([$group12student->id], $groups, [$course->id]); + $this->assertCount(1, $events); + $event = reset($events); + $this->assertEquals('Assignment 1 due date - Group A override', $event->name); + + // Get events for user that belongs to group A and has no user override events. + $events = $retrievalstrategy->get_raw_events([$group1student->id], $groups, [$course->id]); + $this->assertCount(1, $events); + $event = reset($events); + $this->assertEquals('Assignment 1 due date - Group A override', $event->name); + } // Add repeating events. $repeatingevents = [ @@ -290,8 +304,6 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc * 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(); @@ -351,4 +363,88 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc $events = $retrievalstrategy->get_raw_events(null, null, null, [$category1->id, $category2->id]); $this->assertCount(2, $events); } + + public function test_get_raw_events_for_multiple_users() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + + // Create users. + $user1 = $generator->create_user(); + $user2 = $generator->create_user(); + $user3 = $generator->create_user(); + + // Create user events. + $events = [ + [ + 'name' => 'User1 Event', + 'eventtype' => 'user', + 'userid' => $user1->id, + 'timestart' => time(), + ], [ + 'name' => 'User2 Event', + 'eventtype' => 'user', + 'userid' => $user2->id, + 'timestart' => time(), + ], [ + 'name' => 'User3 Event', + 'eventtype' => 'user', + 'userid' => $user3->id, + 'timestart' => time(), + ] + ]; + foreach ($events as $event) { + calendar_event::create($event, false); + } + + $retrievalstrategy = new raw_event_retrieval_strategy(); + + // Get all events. + $events = $retrievalstrategy->get_raw_events([$user1->id, $user2->id]); + $this->assertCount(2, $events); + $this->assertEquals( + ['User1 Event', 'User2 Event'], + array_column($events, 'name'), + '', 0.0, 10, true); + } + + public function test_get_raw_events_for_groups_with_no_members() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + + $course = $generator->create_course(); + + // Create groups. + $group1 = $generator->create_group(['courseid' => $course->id, 'name' => 'Group 1']); + $group2 = $generator->create_group(['courseid' => $course->id, 'name' => 'Group 2']); + + // Create group events. + $events = [ + [ + 'name' => 'Group 1 Event', + 'eventtype' => 'group', + 'groupid' => $group1->id, + 'timestart' => time(), + ], [ + 'name' => 'Group 2 Event', + 'eventtype' => 'group', + 'groupid' => $group2->id, + 'timestart' => time(), + ] + ]; + foreach ($events as $event) { + calendar_event::create($event, false); + } + + $retrievalstrategy = new raw_event_retrieval_strategy; + + // Get group eventsl. + $events = $retrievalstrategy->get_raw_events(null, [$group1->id, $group2->id]); + $this->assertCount(2, $events); + $this->assertEquals( + ['Group 1 Event', 'Group 2 Event'], + array_column($events, 'name'), + '', 0.0, 10, true); + } } diff --git a/calendar/upgrade.txt b/calendar/upgrade.txt index 359ee3e796b..6d3489e3b58 100644 --- a/calendar/upgrade.txt +++ b/calendar/upgrade.txt @@ -1,6 +1,12 @@ This files describes API changes in /calendar/* , information provided here is intended especially for developers. +=== 3.6 === +* calendar_get_default_courses() function now has optional $userid parameter. +* calendar_set_filters() function now has optional $user parameter. +* The core_calendar\local\event\container class now provides two new helper methods for getting and setting the requesting user: + set_requesting_user() and get_requesting_user(). + === 3.5 === * core_calendar_external::get_calendar_events now returns the categoryid for category events. diff --git a/lib/coursecatlib.php b/lib/coursecatlib.php index 5ad20b73ac5..8833387190a 100644 --- a/lib/coursecatlib.php +++ b/lib/coursecatlib.php @@ -212,7 +212,7 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate { /** * Returns coursecat object for requested category * - * If category is not visible to user it is treated as non existing + * If category is not visible to the given user, it is treated as non existing * unless $alwaysreturnhidden is set to true * * If id is 0, the pseudo object for root category is returned (convenient @@ -226,10 +226,11 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate { * returned even if this category is not visible to the current user * (category is hidden and user does not have * 'moodle/category:viewhiddencategories' capability). Use with care! + * @param int|stdClass $user The user id or object. By default (null) checks the visibility to the current user. * @return null|coursecat * @throws moodle_exception */ - public static function get($id, $strictness = MUST_EXIST, $alwaysreturnhidden = false) { + public static function get($id, $strictness = MUST_EXIST, $alwaysreturnhidden = false, $user = null) { if (!$id) { if (!isset(self::$coursecat0)) { $record = new stdClass(); @@ -251,7 +252,7 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate { $coursecatrecordcache->set($id, $coursecat); } } - if ($coursecat && ($alwaysreturnhidden || $coursecat->is_uservisible())) { + if ($coursecat && ($alwaysreturnhidden || $coursecat->is_uservisible($user))) { return $coursecat; } else { if ($strictness == MUST_EXIST) { @@ -580,17 +581,18 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate { } /** - * Checks if this course category is visible to current user + * Checks if this course category is visible to a user. * * Please note that methods coursecat::get (without 3rd argumet), * coursecat::get_children(), etc. return only visible categories so it is * usually not needed to call this function outside of this class * + * @param int|stdClass $user The user id or object. By default (null) checks the visibility to the current user. * @return bool */ - public function is_uservisible() { + public function is_uservisible($user = null) { return !$this->id || $this->visible || - has_capability('moodle/category:viewhiddencategories', $this->get_context()); + has_capability('moodle/category:viewhiddencategories', $this->get_context(), $user); } /** diff --git a/lib/tests/coursecatlib_test.php b/lib/tests/coursecatlib_test.php index 54b10995477..1b7e280cbff 100644 --- a/lib/tests/coursecatlib_test.php +++ b/lib/tests/coursecatlib_test.php @@ -797,6 +797,83 @@ class core_coursecatlib_testcase extends advanced_testcase { $this->assertEquals("{$cat1name} / {$cat2name} / {$cat4name}", $category4->get_nested_name(false)); } + public function test_coursecat_is_uservisible() { + global $USER; + + // Create category 1 as visible. + $category1 = coursecat::create(array('name' => 'Cat1', 'visible' => 1)); + // Create category 2 as hidden. + $category2 = coursecat::create(array('name' => 'Cat2', 'visible' => 0)); + + $this->assertTrue($category1->is_uservisible()); + $this->assertFalse($category2->is_uservisible()); + + $this->assign_capability('moodle/category:viewhiddencategories'); + + $this->assertTrue($category1->is_uservisible()); + $this->assertTrue($category2->is_uservisible()); + + // First, store current user's id, then login as another user. + $userid = $USER->id; + $this->setUser($this->getDataGenerator()->create_user()); + + // User $user should still have the moodle/category:viewhiddencategories capability. + $this->assertTrue($category1->is_uservisible($userid)); + $this->assertTrue($category2->is_uservisible($userid)); + + $this->assign_capability('moodle/category:viewhiddencategories', CAP_INHERIT); + + $this->assertTrue($category1->is_uservisible()); + $this->assertFalse($category2->is_uservisible()); + } + + public function test_current_user_coursecat_get() { + $this->assign_capability('moodle/category:viewhiddencategories'); + + // Create category 1 as visible. + $category1 = coursecat::create(array('name' => 'Cat1', 'visible' => 1)); + // Create category 2 as hidden. + $category2 = coursecat::create(array('name' => 'Cat2', 'visible' => 0)); + + $this->assertEquals($category1->id, coursecat::get($category1->id)->id); + $this->assertEquals($category2->id, coursecat::get($category2->id)->id); + + // Login as another user to test coursecat::get. + $this->setUser($this->getDataGenerator()->create_user()); + $this->assertEquals($category1->id, coursecat::get($category1->id)->id); + + // Expecting to get an exception as this new user does not have the moodle/category:viewhiddencategories capability. + $this->expectException('moodle_exception'); + $this->expectExceptionMessage('unknowncategory'); + coursecat::get($category2->id); + } + + public function test_another_user_coursecat_get() { + global $USER; + + $this->assign_capability('moodle/category:viewhiddencategories'); + + // Create category 1 as visible. + $category1 = coursecat::create(array('name' => 'Cat1', 'visible' => 1)); + // Create category 2 as hidden. + $category2 = coursecat::create(array('name' => 'Cat2', 'visible' => 0)); + + // First, store current user's object, then login as another user. + $user1 = $USER; + $user2 = $this->getDataGenerator()->create_user(); + $this->setUser($user2); + + $this->assertEquals($category1->id, coursecat::get($category1->id, MUST_EXIST, false, $user1)->id); + $this->assertEquals($category2->id, coursecat::get($category2->id, MUST_EXIST, false, $user1)->id); + + $this->setUser($user1); + + $this->assertEquals($category1->id, coursecat::get($category1->id, MUST_EXIST, false, $user2)->id); + $this->expectException('moodle_exception'); + $this->expectExceptionMessage('unknowncategory'); + coursecat::get($category2->id, MUST_EXIST, false, $user2); + } + /** * Creates a draft area for current user and fills it with fake files * diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 1e80db1184d..cc918f411de 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -43,6 +43,9 @@ information provided here is intended especially for developers. - events_dequeue() - events_get_handlers() +* coursecat::get() now has optional $user parameter. +* coursecat::is_uservisible() now has optional $user parameter. + === 3.5 === * There is a new privacy API that every subsystem and plugin has to implement so that the site can become GDPR diff --git a/mod/assign/lib.php b/mod/assign/lib.php index 5eeccf327e4..002404dd379 100644 --- a/mod/assign/lib.php +++ b/mod/assign/lib.php @@ -1826,20 +1826,25 @@ function assign_check_updates_since(cm_info $cm, $from, $filter = array()) { * the ASSIGN_EVENT_TYPE_GRADINGDUE event will not be shown to students on their calendar. * * @param calendar_event $event + * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default). * @return bool Returns true if the event is visible to the current user, false otherwise. */ -function mod_assign_core_calendar_is_event_visible(calendar_event $event) { +function mod_assign_core_calendar_is_event_visible(calendar_event $event, $userid = 0) { global $CFG, $USER; require_once($CFG->dirroot . '/mod/assign/locallib.php'); - $cm = get_fast_modinfo($event->courseid)->instances['assign'][$event->instance]; + if (empty($userid)) { + $userid = $USER->id; + } + + $cm = get_fast_modinfo($event->courseid, $userid)->instances['assign'][$event->instance]; $context = context_module::instance($cm->id); $assign = new assign($context, $cm, null); if ($event->eventtype == ASSIGN_EVENT_TYPE_GRADINGDUE) { - return $assign->can_grade(); + return $assign->can_grade($userid); } else { return true; } @@ -1853,22 +1858,28 @@ function mod_assign_core_calendar_is_event_visible(calendar_event $event) { * * @param calendar_event $event * @param \core_calendar\action_factory $factory + * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default). * @return \core_calendar\local\event\entities\action_interface|null */ function mod_assign_core_calendar_provide_event_action(calendar_event $event, - \core_calendar\action_factory $factory) { + \core_calendar\action_factory $factory, + $userid = 0) { global $CFG, $USER; require_once($CFG->dirroot . '/mod/assign/locallib.php'); - $cm = get_fast_modinfo($event->courseid)->instances['assign'][$event->instance]; + if (empty($userid)) { + $userid = $USER->id; + } + + $cm = get_fast_modinfo($event->courseid, $userid)->instances['assign'][$event->instance]; $context = context_module::instance($cm->id); $assign = new assign($context, $cm, null); // Apply overrides. - $assign->update_effective_access($USER->id); + $assign->update_effective_access($userid); if ($event->eventtype == ASSIGN_EVENT_TYPE_GRADINGDUE) { $name = get_string('grade'); @@ -1877,16 +1888,16 @@ function mod_assign_core_calendar_provide_event_action(calendar_event $event, 'action' => 'grader' ]); $itemcount = $assign->count_submissions_need_grading(); - $actionable = $assign->can_grade() && (time() >= $assign->get_instance()->allowsubmissionsfromdate); + $actionable = $assign->can_grade($userid) && (time() >= $assign->get_instance()->allowsubmissionsfromdate); } else { - $usersubmission = $assign->get_user_submission($USER->id, false); + $usersubmission = $assign->get_user_submission($userid, false); if ($usersubmission && $usersubmission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) { // The user has already submitted. // We do not want to change the text to edit the submission, we want to remove the event from the Dashboard entirely. return null; } - $participant = $assign->get_participant($USER->id); + $participant = $assign->get_participant($userid); if (!$participant) { // If the user is not a participant in the assignment then they have @@ -1901,7 +1912,7 @@ function mod_assign_core_calendar_provide_event_action(calendar_event $event, 'action' => 'editsubmission' ]); $itemcount = 1; - $actionable = $assign->is_any_submission_plugin_enabled() && $assign->can_edit_submission($USER->id); + $actionable = $assign->is_any_submission_plugin_enabled() && $assign->can_edit_submission($userid, $userid); } return $factory->create_instance( diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php index 566c386aa1d..e4bdac84794 100644 --- a/mod/assign/locallib.php +++ b/mod/assign/locallib.php @@ -3315,11 +3315,12 @@ class assign { /** * Does this user have grade permission for this assignment? * + * @param int|stdClass $user The object or id of the user who will do the editing (default to current user). * @return bool */ - public function can_grade() { + public function can_grade($user = null) { // Permissions check. - if (!has_capability('mod/assign:grade', $this->context)) { + if (!has_capability('mod/assign:grade', $this->context, $user)) { return false; } diff --git a/mod/assign/tests/lib_test.php b/mod/assign/tests/lib_test.php index 9c758c1086a..4d47f855a94 100644 --- a/mod/assign/tests/lib_test.php +++ b/mod/assign/tests/lib_test.php @@ -426,6 +426,24 @@ class mod_assign_lib_testcase extends advanced_testcase { $this->assertTrue(mod_assign_core_calendar_is_event_visible($event)); } + public function test_assign_core_calendar_is_event_visible_duedate_event_for_teacher() { + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); + $assign = $this->create_instance($course); + + $this->setAdminUser(); + + // Create a calendar event. + $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE); + + // Now, log out. + $this->setUser(); + + // The teacher should see the due date event. + $this->assertTrue(mod_assign_core_calendar_is_event_visible($event, $teacher->id)); + } + public function test_assign_core_calendar_is_event_visible_duedate_event_as_student() { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); @@ -443,6 +461,25 @@ class mod_assign_lib_testcase extends advanced_testcase { $this->assertTrue(mod_assign_core_calendar_is_event_visible($event)); } + public function test_assign_core_calendar_is_event_visible_duedate_event_for_student() { + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]); + + $this->setAdminUser(); + + // Create a calendar event. + $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE); + + // Now, log out. + $this->setUser(); + + // The student should care about the due date event. + $this->assertTrue(mod_assign_core_calendar_is_event_visible($event, $student->id)); + } + public function test_assign_core_calendar_is_event_visible_gradingduedate_event_as_teacher() { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); @@ -458,6 +495,24 @@ class mod_assign_lib_testcase extends advanced_testcase { $this->assertTrue(mod_assign_core_calendar_is_event_visible($event)); } + + public function test_assign_core_calendar_is_event_visible_gradingduedate_event_for_teacher() { + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); + $assign = $this->create_instance($course); + + // Create a calendar event. + $this->setAdminUser(); + $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_GRADINGDUE); + + // Now, log out. + $this->setUser(); + + // The teacher should see the due date event. + $this->assertTrue(mod_assign_core_calendar_is_event_visible($event, $teacher->id)); + } + public function test_assign_core_calendar_is_event_visible_gradingduedate_event_as_student() { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); @@ -473,6 +528,24 @@ class mod_assign_lib_testcase extends advanced_testcase { $this->assertFalse(mod_assign_core_calendar_is_event_visible($event)); } + + public function test_assign_core_calendar_is_event_visible_gradingduedate_event_for_student() { + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $assign = $this->create_instance($course); + + // Create a calendar event. + $this->setAdminUser(); + $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_GRADINGDUE); + + // Now, log out. + $this->setUser(); + + // The student should not see the due date event. + $this->assertFalse(mod_assign_core_calendar_is_event_visible($event, $student->id)); + } + public function test_assign_core_calendar_provide_event_action_duedate_as_teacher() { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); @@ -492,6 +565,27 @@ class mod_assign_lib_testcase extends advanced_testcase { $this->assertNull($actionevent); } + public function test_assign_core_calendar_provide_event_action_duedate_for_teacher() { + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); + $assign = $this->create_instance($course); + + // Create a calendar event. + $this->setAdminUser(); + $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE); + + // Now, log out. + $this->setUser(); + + // Decorate action event for a teacher. + $factory = new \core_calendar\action_factory(); + $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory, $teacher->id); + + // The teacher should not have an action for a due date event. + $this->assertNull($actionevent); + } + public function test_assign_core_calendar_provide_event_action_duedate_as_student() { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); @@ -515,6 +609,31 @@ class mod_assign_lib_testcase extends advanced_testcase { $this->assertTrue($actionevent->is_actionable()); } + public function test_assign_core_calendar_provide_event_action_duedate_for_student() { + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]); + + // Create a calendar event. + $this->setAdminUser(); + $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE); + + // Now, log out. + $this->setUser(); + + // Decorate action event for a student. + $factory = new \core_calendar\action_factory(); + $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory, $student->id); + + // Confirm the event was decorated. + $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); + $this->assertEquals(get_string('addsubmission', 'assign'), $actionevent->get_name()); + $this->assertInstanceOf('moodle_url', $actionevent->get_url()); + $this->assertEquals(1, $actionevent->get_item_count()); + $this->assertTrue($actionevent->is_actionable()); + } + public function test_assign_core_calendar_provide_event_action_gradingduedate_as_teacher() { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); @@ -537,6 +656,31 @@ class mod_assign_lib_testcase extends advanced_testcase { $this->assertTrue($actionevent->is_actionable()); } + public function test_assign_core_calendar_provide_event_action_gradingduedate_for_teacher() { + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); + $assign = $this->create_instance($course); + + // Create a calendar event. + $this->setAdminUser(); + $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_GRADINGDUE); + + // Now, log out. + $this->setUser(); + + // Decorate action event for a teacher. + $factory = new \core_calendar\action_factory(); + $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory, $teacher->id); + + // Confirm the event was decorated. + $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); + $this->assertEquals(get_string('grade'), $actionevent->get_name()); + $this->assertInstanceOf('moodle_url', $actionevent->get_url()); + $this->assertEquals(0, $actionevent->get_item_count()); + $this->assertTrue($actionevent->is_actionable()); + } + public function test_assign_core_calendar_provide_event_action_gradingduedate_as_student() { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); @@ -559,6 +703,31 @@ class mod_assign_lib_testcase extends advanced_testcase { $this->assertFalse($actionevent->is_actionable()); } + public function test_assign_core_calendar_provide_event_action_gradingduedate_for_student() { + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $assign = $this->create_instance($course); + + // Create a calendar event. + $this->setAdminUser(); + $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_GRADINGDUE); + + // Now, log out. + $this->setUser(); + + // Decorate action event for a student. + $factory = new \core_calendar\action_factory(); + $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory, $student->id); + + // Confirm the event was decorated. + $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); + $this->assertEquals(get_string('grade'), $actionevent->get_name()); + $this->assertInstanceOf('moodle_url', $actionevent->get_url()); + $this->assertEquals(0, $actionevent->get_item_count()); + $this->assertFalse($actionevent->is_actionable()); + } + public function test_assign_core_calendar_provide_event_action_duedate_as_student_submitted() { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); @@ -584,6 +753,34 @@ class mod_assign_lib_testcase extends advanced_testcase { $this->assertNull($actionevent); } + public function test_assign_core_calendar_provide_event_action_duedate_for_student_submitted() { + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]); + + $this->setAdminUser(); + + // Create a calendar event. + $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE); + + // Create an action factory. + $factory = new \core_calendar\action_factory(); + + // Submit as the student. + $this->add_submission($student, $assign); + $this->submit_for_grading($student, $assign); + + // Now, log out. + $this->setUser(); + + // Confirm there was no event to action. + $factory = new \core_calendar\action_factory(); + $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory, $student->id); + $this->assertNull($actionevent); + } + /** * Creates an action event. * diff --git a/mod/assign/tests/locallib_test.php b/mod/assign/tests/locallib_test.php index 80ec0cd2a04..d4b17e9a7b8 100644 --- a/mod/assign/tests/locallib_test.php +++ b/mod/assign/tests/locallib_test.php @@ -1720,6 +1720,11 @@ class mod_assign_locallib_testcase extends advanced_testcase { $this->setUser($teacher); $this->assertEquals(true, $assign->can_grade()); + // Test the viewgrades capability for other users. + $this->setUser(); + $this->assertTrue($assign->can_grade($teacher->id)); + $this->assertFalse($assign->can_grade($student->id)); + // Test the viewgrades capability - without mod/assign:grade. $this->setUser($student); diff --git a/mod/assign/upgrade.txt b/mod/assign/upgrade.txt index d32481c41b9..dbcd9fd04f8 100644 --- a/mod/assign/upgrade.txt +++ b/mod/assign/upgrade.txt @@ -4,6 +4,7 @@ This files describes API changes in the assign code. * The mod_assign_base_testcase unit test base class has been deprecated. It encouraged poor unit test design and led to significant performance issues with unit tests. See MDL-55609 for further information. +* The function can_grade() now has optional $user parameter. === 3.5 === * Functions assign:get_assign_grading_summary_renderable, assign:can_view_submission, assign:count_submissions_with_status, diff --git a/mod/upgrade.txt b/mod/upgrade.txt index 268866271bd..7a0e02922c7 100644 --- a/mod/upgrade.txt +++ b/mod/upgrade.txt @@ -6,6 +6,8 @@ information provided here is intended especially for developers. * The final deprecation of xxx_get_types() callback means that this function will no longer be called. Please use get_shortcuts() instead. * lti_get_shortcuts has been deprecated. Please use get_shortcuts() instead to add items to the activity chooser. +* Now, when mod__core_calendar_is_event_visible or mod__core_calendar_provide_event_action callback functions + are called, the userid of the requesting user is also passed to them. === 3.5 === @@ -44,7 +46,7 @@ information provided here is intended especially for developers. MDL-55611 can be found at https://docs.moodle.org/dev/Calendar_API. The 3 new callbacks are: - mod__core_calendar_is_event_visible - mod__core_calendar_provide_event_action - - mod__core_calendar_event_action_show_items_acount + - mod__core_calendar_event_action_shows_item_count * Changes to the moodleform_mod class and its usage (MDL-58138): - the get_data() method has been overriden. The implementation calls parent::get_data() and a new data_postprocessing() method - new data_postprocessing() method added. Mods can override this in their mod_form subclass to modify the submit data. Previously