diff --git a/calendar/classes/api.php b/calendar/classes/api.php index 846f8bde670..efe57bd7073 100644 --- a/calendar/classes/api.php +++ b/calendar/classes/api.php @@ -681,185 +681,38 @@ class api { * @return array $events of selected events or an empty array if there aren't any (or there was an error) */ public static function get_events($tstart, $tend, $users, $groups, $courses, $withduration = true, $ignorehidden = true) { - global $DB; - - $params = array(); - // Quick test. - if (empty($users) && empty($groups) && empty($courses)) { - return array(); - } - - // Array of filter conditions. To be concatenated by the OR operator. - $filters = []; - - // User filter. - 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)"; - $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)"; - } - // 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)) { - // Events from a number of groups. - list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED); - $filters[] = "e.groupid $insqlgroups"; - $params = array_merge($params, $inparamsgroups); - } else if ($groups === true) { - // Events from ALL groups. - $filters[] = "e.groupid != 0"; - } - // 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)) { - 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); - } else if ($courses === true) { - // Events from ALL courses. - $filters[] = "(e.groupid = 0 AND e.courseid != 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 - // valid query with only time constraints, thus selecting ALL events in that time frame! - if (empty($filters)) { - return array(); - } - - // Build our clause for the filters. - $filterclause = implode(' OR ', $filters); - - // Array of where conditions for our query. To be concatenated by the AND operator. - $whereconditions = ["($filterclause)"]; - - // Time clause. - if ($withduration) { - $timeclause = "((e.timestart >= :tstart1 OR e.timestart + e.timeduration > :tstart2) AND e.timestart <= :tend)"; - $params['tstart1'] = $tstart; - $params['tstart2'] = $tstart; - $params['tend'] = $tend; - } else { - $timeclause = "(e.timestart >= :tstart AND e.timestart <= :tend)"; - $params['tstart'] = $tstart; - $params['tend'] = $tend; - } - $whereconditions[] = $timeclause; - - // Show visible only. - if ($ignorehidden) { - $whereconditions[] = "(e.visible = 1)"; - } - - // Build the main query's WHERE clause. - $whereclause = implode(' AND ', $whereconditions); - - // 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 = self::get_default_courses(); - - // Set calendar filters. - list($usercourses, $usergroups, $user) = self::set_filters($usercourses, true); - $subqueryparams = []; - - // Flag to indicate whether the query needs to exclude group overrides. - $viewgroupsonly = false; - - if ($user) { - // Set filter condition for the user's events. - $subqueryconditions[] = "(ev.userid = :user AND ev.courseid = 0 AND ev.groupid = 0)"; - $subqueryparams['user'] = $user; - - 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; - } - } + $fixedparams = array_map(function($param) { + if ($param === true) { + return null; } - } - // 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); - } + if (!is_array($param)) { + return [$param]; + } - // 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); - // Make sure we remove duplicate values. - $subquerycourses = array_unique($subquerycourses); - } + return $param; + }, [$users, $groups, $courses]); - // 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)"; - $subqueryparams = array_merge($subqueryparams, $incoursesparams); - } + $mapper = \core_calendar\local\event\core_container::get_event_mapper(); + $events = local_api::get_events( + $tstart, + $tend, + null, + null, + null, + null, + 40, + null, + $fixedparams[0], + $fixedparams[1], + $fixedparams[2], + $withduration, + $ignorehidden + ); - // Build the WHERE condition for the sub-query. - if (!empty($subqueryconditions)) { - $subquerywhere = 'WHERE ' . implode(" OR ", $subqueryconditions); - } - - // Merge subquery parameters to the parameters of the main query. - if (!empty($subqueryparams)) { - $params = array_merge($params, $subqueryparams); - } - - // Sub-query that fetches the list of unique events that were filtered based on priority. - $subquery = "SELECT ev.modulename, - ev.instance, - ev.eventtype, - MAX(ev.priority) as priority - FROM {event} ev - $subquerywhere - GROUP BY ev.modulename, ev.instance, ev.eventtype"; - - // Build the main query. - $sql = "SELECT e.* - FROM {event} e - INNER JOIN ($subquery) fe - ON e.modulename = fe.modulename - AND e.instance = fe.instance - AND e.eventtype = fe.eventtype - AND (e.priority = fe.priority OR (e.priority IS NULL AND fe.priority IS NULL)) - LEFT JOIN {modules} m - ON e.modulename = m.name - WHERE (m.visible = 1 OR m.visible IS NULL) AND $whereclause - ORDER BY e.timestart"; - $events = $DB->get_records_sql($sql, $params); - - if ($events === false) { - $events = array(); - } - return $events; + return array_reduce($events, function($carry, $event) use ($mapper) { + return $carry + [$event->get_id() => $mapper->from_event_to_stdclass($event)]; + }, []); } /** diff --git a/calendar/classes/local/api.php b/calendar/classes/local/api.php index 1a91d450c0f..6c365adb732 100644 --- a/calendar/classes/local/api.php +++ b/calendar/classes/local/api.php @@ -36,6 +36,73 @@ use core_calendar\local\event\exceptions\limit_invalid_parameter_exception; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class api { + /** + * Get all events restricted by various parameters, taking in to account user and group overrides. + * + * @param int|null $timestartfrom Events with timestart from this value (inclusive). + * @param int|null $timestartto Events with timestart until this value (inclusive). + * @param int|null $timesortfrom Events with timesort from this value (inclusive). + * @param int|null $timesortto Events with timesort until this value (inclusive). + * @param int|null $timestartaftereventid Restrict the events in the timestart range to ones + after this ID. + * @param int $timesortaftereventid Restrict the events in the timesort range to ones + after this ID. + * @param int $limitnum Return at most this number of events. + * @param int $type Return only events of this type. + * @param array|null $usersfilter Return only events for these users. + * @param array|null $groupsfilter Return only events for these groups. + * @param array|null $coursesfilter Return only events for these courses. + * @param bool $withduration If true return only events starting within specified + * timestart otherwise return in progress events as well. + * @param bool $ignorehidden If true don't return hidden events. + * @return \core_calendar\local\interfaces\event_interface[] Array of event_interfaces. + */ + public static function get_events( + $timestartfrom = null, + $timestartto = null, + $timesortfrom = null, + $timesortto = null, + $timestartaftereventid = null, + $timesortaftereventid = null, + $limitnum = 20, + $type = null, + array $usersfilter = null, + array $groupsfilter = null, + array $coursesfilter = null, + $withduration = true, + $ignorehidden = true + ) { + global $USER; + + $vault = \core_calendar\local\event\core_container::get_event_vault(); + + $timestartafterevent = null; + $timesortafterevent = null; + + if ($timestartaftereventid && $event = $vault->get_event_by_id($timestartaftereventid)) { + $timestartafterevent = $event; + } + + if ($timesortaftereventid && $event = $vault->get_event_by_id($timesortaftereventid)) { + $timesortafterevent = $event; + } + + return $vault->get_events( + $timestartfrom, + $timestartto, + $timesortfrom, + $timesortto, + $timestartafterevent, + $timesortafterevent, + $limitnum, + $type, + $usersfilter, + $groupsfilter, + $coursesfilter, + $withduration, + $ignorehidden + ); + } /** * Get a list of action events for the logged in user by the given diff --git a/calendar/classes/local/event/core_container.php b/calendar/classes/local/event/core_container.php index 39e62797615..dd8656bad25 100644 --- a/calendar/classes/local/event/core_container.php +++ b/calendar/classes/local/event/core_container.php @@ -35,6 +35,7 @@ use core_calendar\local\event\data_access\event_vault; use core_calendar\local\event\factories\action_event_factory; use core_calendar\local\event\factories\event_factory; use core_calendar\local\event\mappers\event_mapper; +use core_calendar\local\event\strategies\raw_event_retrieval_strategy; use core_calendar\local\interfaces\action_event_interface; use core_calendar\local\interfaces\event_interface; @@ -70,6 +71,11 @@ class core_container { */ protected static $eventvault; + /** + * @var \core_calendar\local\event\strategies\raw_event_retrieval_strategy $eventretrievalstrategy Event retrieval strategy. + */ + protected static $eventretrievalstrategy; + /** * @var array A list of callbacks to use. */ @@ -151,6 +157,7 @@ class core_container { } if (empty(self::$eventvault)) { + self::$eventretrievalstrategy = new raw_event_retrieval_strategy(); self::$eventvault = new event_vault(self::$eventfactory, self::$eventretrievalstrategy); } } diff --git a/calendar/classes/local/event/data_access/event_vault.php b/calendar/classes/local/event/data_access/event_vault.php index 68ba6204fd4..40b53f6c361 100644 --- a/calendar/classes/local/event/data_access/event_vault.php +++ b/calendar/classes/local/event/data_access/event_vault.php @@ -27,9 +27,10 @@ namespace core_calendar\local\event\data_access; use core_calendar\local\event\exceptions\limit_invalid_parameter_exception; use core_calendar\local\event\exceptions\timesort_invalid_parameter_exception; use core_calendar\local\interfaces\action_event_interface; -use core_calendar\local\interfaces\event_interface; use core_calendar\local\interfaces\event_factory_interface; +use core_calendar\local\interfaces\event_interface; use core_calendar\local\interfaces\event_vault_interface; +use core_calendar\local\interfaces\raw_event_retrieval_strategy_interface; /** * This class will handle interacting with the database layer to retrieve @@ -48,23 +49,26 @@ class event_vault implements event_vault_interface { /** * @var event_factory_interface $factory Factory for creating events. */ - private $factory; + protected $factory; + + /** + * @var raw_event_retrieval_strategy_interface $retrievalstrategy Strategy for getting events from the DB. + */ + protected $retrievalstrategy; /** * Create an event vault. * * @param event_factory_interface $factory An event factory */ - public function __construct(event_factory_interface $factory) { + public function __construct( + event_factory_interface $factory, + raw_event_retrieval_strategy_interface $retrievalstrategy + ) { $this->factory = $factory; + $this->retrievalstrategy = $retrievalstrategy; } - /** - * Retrieve an event for the given id. - * - * @param int $id The event id - * @return event_interface - */ public function get_event_by_id($id) { global $DB; @@ -75,89 +79,99 @@ class event_vault implements event_vault_interface { } } - /** - * Retrieve an array of events for the given user and time constraints. - * - * If using this function for pagination then you can provide the last event that you've seen - * ($afterevent) and it will be used to appropriately offset the result set so that you don't - * receive the same events again. - * - * @param \stdClass $user The user for whom the events belong - * @param int|null $timesortfrom Events with timesort from this value (inclusive) - * @param int|null $timesortto Events with timesort until this value (inclusive) - * @param event_interface|null $afterevent Only return events after this one - * @param int $limitnum Return at most this number of events - * @throws timesort_invalid_parameter_exception - * @throws limit_invalid_parameter_exception - * @return action_event_interface - */ - public function get_action_events_by_timesort( - \stdClass $user, + public function get_events( + $timestartfrom = null, + $timestartto = null, $timesortfrom = null, $timesortto = null, - event_interface $afterevent = null, - $limitnum = 20 + event_interface $timestartafterevent = null, + event_interface $timesortafterevent = null, + $limitnum = 20, + $type = null, + array $usersfilter = null, + array $groupsfilter = null, + array $coursesfilter = null, + $withduration = true, + $ignorehidden = true, + callable $filter = null ) { global $DB; - if (is_null($timesortfrom) && is_null($timesortto)) { - throw new timesort_invalid_parameter_exception("Must provide a timesort to and/or from value"); - } - if ($limitnum < 1 || $limitnum > 50) { throw new limit_invalid_parameter_exception("Limit must be between 1 and 50 (inclusive)"); } - $lastseentimesort = null; - $params = ['type' => CALENDAR_EVENT_TYPE_ACTION]; - $where = ['type = :type']; - - if (!is_null($afterevent)) { - $lastseentimesort = $afterevent->get_times()->get_sort_time()->getTimestamp(); - } - - if ($timesortfrom) { - if ($lastseentimesort && $lastseentimesort >= $timesortfrom) { - $where[] = '((timesort = :timesortfrom1 AND id > :timesortfromid) '. - 'OR timesort > :timesortfrom2)'; - $params['timesortfromid'] = $afterevent->get_id(); - $params['timesortfrom1'] = $lastseentimesort; - $params['timesortfrom2'] = $lastseentimesort; - } else { - $where[] = 'timesort >= :timesortfrom'; - $params['timesortfrom'] = $timesortfrom; + $fromquery = function($field, $timefrom, $lastseenmethod, $afterevent) { + if (!$timefrom) { + return false; } - } - if ($timesortto) { - if ($lastseentimesort && $lastseentimesort > $timesortto) { - // The last seen event from this set is after the time sort range which - // means all events in this range have been seen, so we can just return - // early here. - return []; - } else if ($lastseentimesort && $lastseentimesort == $timesortto) { - $where[] = '((timesort = :timesortto1 AND id > :timesorttoid) OR timesort < :timesortto2)'; - $params['timesorttoid'] = $afterevent->get_id(); - $params['timesortto1'] = $timesortto; - $params['timesortto2'] = $timesortto; - } else { - $where[] = 'timesort <= :timesortto'; - $params['timesortto'] = $timesortto; + return $this->timefield_pagination_from( + $field, + $timefrom, + $afterevent ? $afterevent->get_times()->{$lastseenmethod}()->getTimestamp() : null, + $afterevent ? $afterevent->get_id() : null + ); + }; + + $toquery = function($field, $timeto, $lastseenmethod, $afterevent) { + if (!$timeto) { + return false; } + + return $this->timefield_pagination_to( + $field, + $timeto, + $afterevent ? $afterevent->get_times()->{$lastseenmethod}()->getTimestamp() : null, + $afterevent ? $afterevent->get_id() : null + ); + }; + + $timesortfromquery = $fromquery('timesort', $timesortfrom, 'get_sort_time', $timesortafterevent); + $timesorttoquery = $toquery('timesort', $timesortto, 'get_sort_time', $timesortafterevent); + $timestartfromquery = $fromquery('timestart', $timestartfrom, 'get_start_time', $timestartafterevent); + $timestarttoquery = $toquery('timestart', $timestartto, 'get_start_time', $timestartafterevent); + + if (($timesortto && !$timesorttoquery) || ($timestartto && !$timestarttoquery)) { + return []; } - $sql = sprintf("SELECT * FROM {event} WHERE %s ORDER BY timesort ASC, id ASC", - implode(' AND ', $where)); + $params = array_merge( + $type ? ['type' => $type] : [], + $timesortfromquery ? $timesortfromquery['params'] : [], + $timesorttoquery ? $timesorttoquery['params'] : [], + $timestartfromquery ? $timestartfromquery['params'] : [], + $timestarttoquery ? $timestarttoquery['params'] : [] + ); + + $where = array_merge( + $type ? ['type = :type'] : [], + $timesortfromquery ? $timesortfromquery['where'] : [], + $timesorttoquery ? $timesorttoquery['where'] : [], + $timestartfromquery ? $timestartfromquery['where'] : [], + $timestarttoquery ? $timestarttoquery['where'] : [] + ); $offset = 0; $events = []; - // We need to continue to pull records from the database until we reach - // the requested amount of events because not all records in the database - // will be visible for the current user. - while ($records = array_values($DB->get_records_sql($sql, $params, $offset, $limitnum))) { + + while ($records = array_values($this->retrievalstrategy->get_raw_events( + $usersfilter, + $groupsfilter, + $coursesfilter, + $where, + $params, + "timesort ASC, id ASC", + $offset, + $limitnum, + $withduration, + $ignorehidden + ))) { foreach ($records as $record) { if ($event = $this->transform_from_database_record($record)) { - if ($event instanceof action_event_interface) { + $filtertest = $filter ? $filter($event) : true; + + if ($event && $filtertest) { $events[] = $event; } @@ -174,21 +188,31 @@ class event_vault implements event_vault_interface { return $events; } - /** - * Retrieve an array of events for the given user filtered by the course and time constraints. - * - * If using this function for pagination then you can provide the last event that you've seen - * ($afterevent) and it will be used to appropriately offset the result set so that you don't - * receive the same events again. - * - * @param \stdClass $user The user for whom the events belong - * @param \stdClass $course The course to filter by - * @param int|null $timesortfrom Events with timesort from this value (inclusive) - * @param int|null $timesortto Events with timesort until this value (inclusive) - * @param event_interface|null $afterevent Only return events after this one - * @param int $limitnum Return at most this number of events - * @return action_event_interface - */ + public function get_action_events_by_timesort( + \stdClass $user, + $timesortfrom = null, + $timesortto = null, + event_interface $afterevent = null, + $limitnum = 20 + ) { + return $this->get_events( + null, + null, + $timesortfrom, + $timesortto, + null, + $afterevent, + $limitnum, + CALENDAR_EVENT_TYPE_ACTION, + [$user->id], + null, + null, + function ($event) { + return $event instanceof action_event_interface; + } + ); + } + public function get_action_events_by_course( \stdClass $user, \stdClass $course, @@ -197,81 +221,88 @@ class event_vault implements event_vault_interface { event_interface $afterevent = null, $limitnum = 20 ) { - global $DB; - - if ($limitnum < 1 || $limitnum > 50) { - throw new limit_invalid_parameter_exception("Limit must be between 1 and 50 (inclusive)"); - } - - $lastseentimesort = null; - $params = [ - 'type' => CALENDAR_EVENT_TYPE_ACTION, - 'courseid' => $course->id, - ]; - $where = [ - 'type = :type', - 'courseid = :courseid', - ]; - - if (!is_null($afterevent)) { - $lastseentimesort = $afterevent->get_times()->get_sort_time()->getTimestamp(); - } - - if ($timesortfrom) { - if ($lastseentimesort && $lastseentimesort >= $timesortfrom) { - $where[] = '((timesort = :timesortfrom1 AND id > :timesortfromid) '. - 'OR timesort > :timesortfrom2)'; - $params['timesortfromid'] = $afterevent->get_id(); - $params['timesortfrom1'] = $lastseentimesort; - $params['timesortfrom2'] = $lastseentimesort; - } else { - $where[] = 'timesort >= :timesortfrom'; - $params['timesortfrom'] = $timesortfrom; - } - } - - if ($timesortto) { - if ($lastseentimesort && $lastseentimesort > $timesortto) { - // The last seen event from this set is after the time sort range which - // means all events in this range have been seen, so we can just return - // early here. - return []; - } else if ($lastseentimesort && $lastseentimesort == $timesortto) { - $where[] = '((timesort = :timesortto1 AND id > :timesorttoid) OR timesort < :timesortto2)'; - $params['timesorttoid'] = $afterevent->get_id(); - $params['timesortto1'] = $timesortto; - $params['timesortto2'] = $timesortto; - } else { - $where[] = 'timesort <= :timesortto'; - $params['timesortto'] = $timesortto; - } - } - - $wheresql = implode(' AND ', $where); - $sql = sprintf("SELECT * FROM {event} WHERE %s ORDER BY timesort ASC, id ASC", $wheresql); - $offset = 0; - $events = []; - // We need to continue to pull records from the database until we reach - // the requested amount of events because not all records in the database - // will be visible for the current user. - while ($records = array_values($DB->get_records_sql($sql, $params, $offset, $limitnum))) { - foreach ($records as $record) { - if ($event = $this->transform_from_database_record($record)) { - if ($event instanceof action_event_interface) { - $events[] = $event; - } - - if (count($events) == $limitnum) { - // We've got all of the events so break both loops. - break 2; - } + return array_values( + $this->get_events( + null, + null, + $timesortfrom, + $timesortto, + null, + $afterevent, + $limitnum, + CALENDAR_EVENT_TYPE_ACTION, + [$user->id], + null, + [$course->id], + function ($event) use ($course) { + return $event instanceof action_event_interface && $event->get_course()->get_id() == $course->id; } + ) + ); + } + + protected function timefield_pagination_from( + $field, + $timefrom, + $lastseentime = null, + $lastseenid = null, + $withduration = true + ) { + $where = ''; + $params = []; + + if ($lastseentime && $lastseentime >= $timefrom) { + $where = '((timesort = :timefrom1 AND e.id > :timefromid) OR timesort > :timefrom2)'; + if ($field === 'timestart') { + $where = '((timestart = :timefrom1 AND e.id > :timefromid) OR timestart > :timefrom2' . + ($withduration ? ' OR timestart + timeduration > :timefrom3' : '') . ')'; + } + $params['timefromid'] = $lastseenid; + $params['timefrom1'] = $lastseentime; + $params['timefrom2'] = $lastseentime; + $params['timefrom3'] = $lastseentime; + } else { + $where = 'timesort >= :timefrom'; + if ($field === 'timestart') { + $where = '(timestart >= :timefrom' . + ($withduration ? ' OR timestart + timeduration > :timefrom2' : '') . ')'; } - $offset += $limitnum; + $params['timefrom'] = $timefrom; + $params['timefrom2'] = $timefrom; } - return $events; + return ['where' => [$where], 'params' => $params]; + } + + protected function timefield_pagination_to( + $field, + $timeto, + $lastseentime = null, + $lastseenid = null + ) { + $where = []; + $params = []; + + if ($lastseentime && $lastseentime > $timeto) { + // The last seen event from this set is after the time sort range which + // means all events in this range have been seen, so we can just return + // early here. + return false; + } else if ($lastseentime && $lastseentime == $timeto) { + $where[] = '((timesort = :timeto1 AND e.id > :timetoid) OR timesort < :timeto2)'; + if ($field === 'timestart') { + $where[] = '((timestart = :timeto1 AND e.id > :timetoid) OR timestart < :timeto2)'; + } + $params['timetoid'] = $lastseenid; + $params['timeto1'] = $timeto; + $params['timeto2'] = $timeto; + } else { + $where[] = ($field === 'timestart' ? 'timestart' : 'timesort') . ' <= :timeto'; + $params['timeto'] = $timeto; + } + + return ['where' => $where, 'params' => $params]; } /** @@ -280,7 +311,34 @@ class event_vault implements event_vault_interface { * @param \stdClass $record The database record * @return event_interface|false */ - private function transform_from_database_record(\stdClass $record) { + protected function transform_from_database_record(\stdClass $record) { + if ($record->courseid == 0 && $record->instance && $record->modulename) { + list($course, $cm) = get_course_and_cm_from_instance($record->instance, $record->modulename); + $record->courseid = $course->id; + } + return $this->factory->create_instance($record); } + + protected function get_from_db( + $userid, + $whereconditions, + $whereparams, + $ordersql, + $offset, + $limitnum + ) { + return array_values( + $this->retrievalstrategy->get_raw_events( + [$userid], + null, + null, + $whereconditions, + $whereparams, + $ordersql, + $offset, + $limitnum + ) + ); + } } diff --git a/calendar/classes/local/event/strategies/raw_event_retrieval_strategy.php b/calendar/classes/local/event/strategies/raw_event_retrieval_strategy.php new file mode 100644 index 00000000000..5d217e756f9 --- /dev/null +++ b/calendar/classes/local/event/strategies/raw_event_retrieval_strategy.php @@ -0,0 +1,254 @@ +. + +/** + * Raw event retrieval strategy. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\strategies; + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\local\interfaces\raw_event_retrieval_strategy_interface; + +/** + * Raw event retrieval strategy. + * + * This strategy is based on what used to be the calendar API's get_events function. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_interface { + public function get_raw_events( + array $usersfilter = null, + array $groupsfilter = null, + array $coursesfilter = null, + array $whereconditions = null, + array $whereparams = null, + $ordersql = null, + $offset = null, + $limitnum = null, + $ignorehidden = true + ) { + return $this->get_raw_events_legacy_implementation( + !is_null($usersfilter) ? $usersfilter : true, //true means no filter in old implementation + !is_null($groupsfilter) ? $groupsfilter : true, + !is_null($coursesfilter) ? $coursesfilter : true, + $whereconditions, + $whereparams, + $ordersql, + $offset, + $limitnum, + $ignorehidden + ); + } + + /** + * The legacy implementation with minor tweaks. + * + * @param array|int|boolean $users array of users, user id or boolean for all/no user events + * @param array|int|boolean $groups array of groups, group id or boolean for all/no group events + * @param array|int|boolean $courses array of courses, course id or boolean for all/no course events + * @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 + * @return array $events of selected events or an empty array if there aren't any (or there was an error) */ + protected function get_raw_events_legacy_implementation( + $users, + $groups, + $courses, + $whereconditions, + $whereparams, + $ordersql, + $offset, + $limitnum, + $ignorehidden + ) { + global $DB; + + $params = array(); + // Quick test. + if (empty($users) && empty($groups) && empty($courses)) { + return array(); + } + + // Array of filter conditions. To be concatenated by the OR operator. + $filters = []; + + // User filter. + 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)"; + $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)"; + } + // 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)) { + // Events from a number of groups. + list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED); + $filters[] = "e.groupid $insqlgroups"; + $params = array_merge($params, $inparamsgroups); + } else if ($groups === true) { + // Events from ALL groups. + $filters[] = "e.groupid != 0"; + } + // 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)) { + 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); + } else if ($courses === true) { + // Events from ALL courses. + $filters[] = "(e.groupid = 0 AND e.courseid != 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 + // valid query with only time constraints, thus selecting ALL events in that time frame! + if (empty($filters)) { + return array(); + } + + // Build our clause for the filters. + $filterclause = implode(' OR ', $filters); + + // Array of where conditions for our query. To be concatenated by the AND operator. + $whereconditions[] = "($filterclause)"; + + // Show visible only. + if ($ignorehidden) { + $whereconditions[] = "(e.visible = 1)"; + } + + // Build the main query's WHERE clause. + $whereclause = implode(' AND ', $whereconditions); + + // 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 = \core_calendar\api::get_default_courses(); + + // Set calendar filters. + list($usercourses, $usergroups, $user) = \core_calendar\api::set_filters($usercourses, true); + $subqueryparams = []; + + // Flag to indicate whether the query needs to exclude group overrides. + $viewgroupsonly = false; + + if ($user) { + // Set filter condition for the user's events. + $subqueryconditions[] = "(ev.userid = :user AND ev.courseid = 0 AND ev.groupid = 0)"; + $subqueryparams['user'] = $user; + + 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; + } + } + } + } + + // 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); + } + + // 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); + // Make sure we remove duplicate values. + $subquerycourses = array_unique($subquerycourses); + } + + // 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)"; + $subqueryparams = array_merge($subqueryparams, $incoursesparams); + } + + // Build the WHERE condition for the sub-query. + if (!empty($subqueryconditions)) { + $subquerywhere = 'WHERE ' . implode(" OR ", $subqueryconditions); + } + + // Merge subquery parameters to the parameters of the main query. + if (!empty($subqueryparams)) { + $params = array_merge($params, $subqueryparams); + } + + // Sub-query that fetches the list of unique events that were filtered based on priority. + $subquery = "SELECT ev.modulename, + ev.instance, + ev.eventtype, + MAX(ev.priority) as priority + FROM {event} ev + $subquerywhere + GROUP BY ev.modulename, ev.instance, ev.eventtype"; + + // Build the main query. + $sql = "SELECT e.* + FROM {event} e + INNER JOIN ($subquery) fe + ON e.modulename = fe.modulename + AND e.instance = fe.instance + AND e.eventtype = fe.eventtype + AND (e.priority = fe.priority OR (e.priority IS NULL AND fe.priority IS NULL)) + LEFT JOIN {modules} m + ON e.modulename = m.name + WHERE (m.visible = 1 OR m.visible IS NULL) AND $whereclause + ORDER BY " . ($ordersql ? $ordersql : "e.timestart"); + + if (!empty($whereparams)) { + $params = array_merge($params, $whereparams); + } + + $events = $DB->get_records_sql($sql, $params, $offset, $limitnum); + + return $events === false ? [] : $events; + } +} diff --git a/calendar/classes/local/interfaces/event_vault_interface.php b/calendar/classes/local/interfaces/event_vault_interface.php index 623af51cdbe..e171aeec522 100644 --- a/calendar/classes/local/interfaces/event_vault_interface.php +++ b/calendar/classes/local/interfaces/event_vault_interface.php @@ -39,6 +39,47 @@ interface event_vault_interface { */ public function get_event_by_id($id); + /** + * Get all events restricted by various parameters, taking in to account user and group overrides. + * + * @param int|null $timestartfrom Events with timestart from this value (inclusive). + * @param int|null $timestartto Events with timestart until this value (inclusive). + * @param int|null $timesortfrom Events with timesort from this value (inclusive). + * @param int|null $timesortto Events with timesort until this value (inclusive). + * @param event_interface|null $timestartafterevent Restrict the events in the timestart range to ones + after this one. + * @param event_interface|null $timesortafterevent Restrict the events in the timesort range to ones + after this one. + * @param int $limitnum Return at most this number of events. + * @param int $type Return only events of this type. + * @param array|null $usersfilter Return only events for these users. + * @param array|null $groupsfilter Return only events for these groups. + * @param array|null $coursesfilter Return only events for these courses. + * @param bool $withduration If true return only events starting within specified + * timestart otherwise return in progress events as well. + * @param bool $ignorehidden If true don't return hidden events. + * @param callable $filter Additional logic to filter out unwanted events. + * Must return true to keep the event, false to discard + * it. + * @return \core_calendar\local\interfaces\event_interface[] Array of event_interfaces. + */ + public function get_events( + $timestartfrom = null, + $timestartto = null, + $timesortfrom = null, + $timesortto = null, + event_interface $timestartafterevent = null, + event_interface $timesortafterevent = null, + $limitnum = 20, + $type = null, + array $usersfilter = null, + array $groupsfilter = null, + array $coursesfilter = null, + $withduration = true, + $ignorehidden = true, + callable $filter = null + ); + /** * Retrieve an array of events for the given user and time constraints. * diff --git a/calendar/classes/local/interfaces/raw_event_retrieval_strategy_interface.php b/calendar/classes/local/interfaces/raw_event_retrieval_strategy_interface.php new file mode 100644 index 00000000000..f6419fea0c1 --- /dev/null +++ b/calendar/classes/local/interfaces/raw_event_retrieval_strategy_interface.php @@ -0,0 +1,61 @@ +. + +/** + * core_calendar + * + * @package Raw event strategy retrieval interface. + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\interfaces; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Interface for an raw event retrival strategy class. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface raw_event_retrieval_strategy_interface { + /** + * Retrieve raw calendar event records from the DB. + * + * @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 $whereconditions Array of where conditions to restrict results. + * @param array|null $whereparams Array of parameters for $whereconditions. + * @param string $ordersql SQL to order results. + * @param int $offset Amount to offset results by. + * @param int $limitnum Return at most this many results. + * @param bool $ignorehidden True to ignore hidden events. False to include them. + * @return \stdClass[] Array of event records. + */ + public function get_raw_events( + array $usersfilter = null, + array $groupsfilter = null, + array $coursesfilter = null, + array $whereconditions = null, + array $whereparams = null, + $ordersql = null, + $offset = null, + $limitnum = 40, + $ignorehidden = true + ); +} diff --git a/calendar/tests/raw_event_retrieval_strategy_test.php b/calendar/tests/raw_event_retrieval_strategy_test.php new file mode 100644 index 00000000000..820d1fc74db --- /dev/null +++ b/calendar/tests/raw_event_retrieval_strategy_test.php @@ -0,0 +1,281 @@ +. + +/** + * Raw event retrieval strategy tests. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/calendar/tests/helpers.php'); + +use core_calendar\local\event\strategies\raw_event_retrieval_strategy; + +/** + * Raw event retrieval strategy testcase. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testcase { + /** + * Test retrieval strategy when module is disabled. + */ + public function test_get_raw_events_with_disabled_module() { + global $DB; + + $this->resetAfterTest(); + $retrievalstrategy = new raw_event_retrieval_strategy(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $student = $generator->create_user(); + $generator->enrol_user($student->id, $course->id, 'student'); + $this->setUser($student); + $events = [ + [ + 'name' => 'Start of assignment', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'assign', + 'instance' => 1, + 'eventtype' => 'due', + 'timestart' => time(), + 'timeduration' => 86400, + 'visible' => 1 + ], [ + 'name' => 'Start of lesson', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'lesson', + 'instance' => 1, + 'eventtype' => 'end', + 'timestart' => time(), + 'timeduration' => 86400, + 'visible' => 1 + ] + ]; + + foreach ($events as $event) { + \core_calendar\event::create($event, false); + } + + // Get all events. + $events = $retrievalstrategy->get_raw_events(true, 0, true, null, null, null, null); + $this->assertCount(2, $events); + + // Disable the lesson module. + $modulerecord = $DB->get_record('modules', ['name' => 'lesson']); + $modulerecord->visible = 0; + $DB->update_record('modules', $modulerecord); + + // Check that we only return the assign event. + $events = $retrievalstrategy->get_raw_events(true, 0, true, null, null, null, null); + $this->assertCount(1, $events); + $event = reset($events); + $this->assertEquals('assign', $event->modulename); + } + + /** + * Test retrieval strategy when there are overrides. + */ + public function test_get_raw_event_strategy_with_overrides() { + $this->resetAfterTest(); + + $retrievalstrategy = new raw_event_retrieval_strategy(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + + $instance = $plugingenerator->create_instance(['course' => $course->id]); + + // Create users. + $useroverridestudent = $generator->create_user(); + $group1student = $generator->create_user(); + $group2student = $generator->create_user(); + $group12student = $generator->create_user(); + $nogroupstudent = $generator->create_user(); + + // Enrol users. + $generator->enrol_user($useroverridestudent->id, $course->id, 'student'); + $generator->enrol_user($group1student->id, $course->id, 'student'); + $generator->enrol_user($group2student->id, $course->id, 'student'); + $generator->enrol_user($group12student->id, $course->id, 'student'); + + $generator->enrol_user($nogroupstudent->id, $course->id, 'student'); + + // Create groups. + $group1 = $generator->create_group(['courseid' => $course->id, 'name' => 'Group 1']); + $group2 = $generator->create_group(['courseid' => $course->id, 'name' => 'Group 2']); + + // Add members to groups. + $generator->create_group_member(['groupid' => $group1->id, 'userid' => $group1student->id]); + $generator->create_group_member(['groupid' => $group2->id, 'userid' => $group2student->id]); + $generator->create_group_member(['groupid' => $group1->id, 'userid' => $group12student->id]); + $generator->create_group_member(['groupid' => $group2->id, 'userid' => $group12student->id]); + + $now = time(); + + // Events with the same module name, instance and event type. + $events = [ + [ + 'name' => 'Assignment 1 due date', + 'description' => '', + 'format' => 0, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'assign', + 'instance' => $instance->id, + 'eventtype' => 'due', + 'timestart' => $now, + 'timeduration' => 0, + 'visible' => 1 + ], [ + 'name' => 'Assignment 1 due date - User override', + 'description' => '', + 'format' => 1, + 'courseid' => 0, + 'groupid' => 0, + 'userid' => $useroverridestudent->id, + 'modulename' => 'assign', + 'instance' => $instance->id, + 'eventtype' => 'due', + 'timestart' => $now + 86400, + 'timeduration' => 0, + 'visible' => 1, + 'priority' => CALENDAR_EVENT_USER_OVERRIDE_PRIORITY + ], [ + 'name' => 'Assignment 1 due date - Group A override', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => $group1->id, + 'userid' => 2, + 'modulename' => 'assign', + 'instance' => $instance->id, + 'eventtype' => 'due', + 'timestart' => $now + (2 * 86400), + 'timeduration' => 0, + 'visible' => 1, + 'priority' => 1, + ], [ + 'name' => 'Assignment 1 due date - Group B override', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => $group2->id, + 'userid' => 2, + 'modulename' => 'assign', + 'instance' => $instance->id, + 'eventtype' => 'due', + 'timestart' => $now + (3 * 86400), + 'timeduration' => 0, + 'visible' => 1, + 'priority' => 2, + ], + ]; + + foreach ($events as $event) { + \core_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); + + // 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 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 B override', $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); + + // Add repeating events. + $repeatingevents = [ + [ + 'name' => 'Repeating site event', + 'description' => '', + 'format' => 1, + 'courseid' => SITEID, + 'groupid' => 0, + 'userid' => 2, + 'repeatid' => 1, + 'modulename' => '0', + 'instance' => 0, + 'eventtype' => 'site', + 'timestart' => $now + 86400, + 'timeduration' => 0, + 'visible' => 1, + ], + [ + 'name' => 'Repeating site event', + 'description' => '', + 'format' => 1, + 'courseid' => SITEID, + 'groupid' => 0, + 'userid' => 2, + 'repeatid' => 1, + 'modulename' => '0', + 'instance' => 0, + 'eventtype' => 'site', + 'timestart' => $now + (2 * 86400), + 'timeduration' => 0, + 'visible' => 1, + ], + ]; + + foreach ($repeatingevents as $event) { + \core_calendar\event::create($event, false); + } + + // Make sure repeating events are not filtered out. + $events = $retrievalstrategy->get_raw_events(); + $this->assertCount(3, $events); + } +}