MDL-58087 core_calendar: Move logic from get_events to the local API

The logic from get_events has now been moved in to our local API
and is used by the event vault to retrieve events, instead of
querying the database directly.

This has the advantage that it deals with overrides and only
returns events for the relevant user.

Part of MDL-55611 epic.
This commit is contained in:
Cameron Ball
2017-04-04 11:01:40 +01:00
committed by Dan Poltawski
parent 02854eac9e
commit 258a570563
8 changed files with 955 additions and 333 deletions
+27 -174
View File
@@ -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)];
}, []);
}
/**
+67
View File
@@ -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
@@ -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);
}
}
@@ -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
)
);
}
}
@@ -0,0 +1,254 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Raw event retrieval strategy.
*
* @package core_calendar
* @copyright 2017 Cameron Ball <[email protected]>
* @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 <[email protected]>
* @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;
}
}
@@ -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.
*
@@ -0,0 +1,61 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* core_calendar
*
* @package Raw event strategy retrieval interface.
* @copyright 2017 Cameron Ball <[email protected]>
* @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 <[email protected]>
* @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
);
}
@@ -0,0 +1,281 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Raw event retrieval strategy tests.
*
* @package core_calendar
* @copyright 2017 Cameron Ball <[email protected]>
* @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 <[email protected]>
* @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);
}
}