diff --git a/calendar/classes/local/event/data_access/event_vault.php b/calendar/classes/local/event/data_access/event_vault.php index d2849194caa..68ba6204fd4 100644 --- a/calendar/classes/local/event/data_access/event_vault.php +++ b/calendar/classes/local/event/data_access/event_vault.php @@ -45,6 +45,9 @@ use core_calendar\local\interfaces\event_vault_interface; */ class event_vault implements event_vault_interface { + /** + * @var event_factory_interface $factory Factory for creating events. + */ private $factory; /** @@ -75,6 +78,10 @@ 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) @@ -101,22 +108,42 @@ class event_vault implements event_vault_interface { 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) { - $where[] = 'timesort >= :timesortfrom'; - $params['timesortfrom'] = $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) { - $where[] = 'timesort <= :timesortto'; - $params['timesortto'] = $timesortto; - } - - if (!is_null($afterevent)) { - $where[] = 'id > :id'; - $params['id'] = $afterevent->get_id(); + 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; + } } $sql = sprintf("SELECT * FROM {event} WHERE %s ORDER BY timesort ASC, id ASC", @@ -150,8 +177,12 @@ class event_vault implements event_vault_interface { /** * 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 array $courses The courses to filter by + * @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 @@ -172,6 +203,7 @@ class event_vault implements event_vault_interface { 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, @@ -181,24 +213,42 @@ class event_vault implements event_vault_interface { 'courseid = :courseid', ]; + if (!is_null($afterevent)) { + $lastseentimesort = $afterevent->get_times()->get_sort_time()->getTimestamp(); + } + if ($timesortfrom) { - $where[] = 'timesort >= :timesortfrom'; - $params['timesortfrom'] = $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) { - $where[] = 'timesort <= :timesortto'; - $params['timesortto'] = $timesortto; - } - - if (!is_null($afterevent)) { - $where[] = 'id > :id'; - $params['id'] = $afterevent->get_id(); + 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 diff --git a/calendar/classes/local/interfaces/event_vault_interface.php b/calendar/classes/local/interfaces/event_vault_interface.php index 754a55cf2f0..623af51cdbe 100644 --- a/calendar/classes/local/interfaces/event_vault_interface.php +++ b/calendar/classes/local/interfaces/event_vault_interface.php @@ -42,6 +42,9 @@ interface 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 $timesortfrom Events with timesort from this value (inclusive) * @param int $timesortto Events with timesort until this value (inclusive) @@ -56,4 +59,28 @@ interface event_vault_interface { event_interface $afterevent, $limitnum ); + + /** + * 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_course( + \stdClass $user, + \stdClass $course, + $timesortfrom, + $timesortto, + event_interface $afterevent, + $limitnum + ); } diff --git a/calendar/tests/event_vault_test.php b/calendar/tests/event_vault_test.php index 823fea6e034..cb1d78ee788 100644 --- a/calendar/tests/event_vault_test.php +++ b/calendar/tests/event_vault_test.php @@ -277,6 +277,129 @@ class core_calendar_event_vault_testcase extends advanced_testcase { $this->assertEquals(sprintf('Event %d', $limit + 5), $events[4]->get_name()); } + /** + * Test that get_action_events_by_timesort returns events between the + * provided timesort values and after the last seen event when one is + * provided. This should work even when the event ids aren't ordered the + * same as the timesort order. + */ + public function test_get_action_events_by_timesort_non_consecutive_ids() { + $this->resetAfterTest(true); + $this->setAdminuser(); + + $user = $this->getDataGenerator()->create_user(); + $factory = new action_event_test_factory(); + $vault = new event_vault($factory); + + /** + * The events should be ordered by timesort as follows: + * + * 1 event 1 + * 2 event 1 + * 1 event 2 + * 2 event 2 + * 1 event 3 + * 2 event 3 + * 1 event 4 + * 2 event 4 + * 1 event 5 + * 2 event 5 + * 1 event 6 + * 2 event 6 + * 1 event 7 + * 2 event 7 + * 1 event 8 + * 2 event 8 + * 1 event 9 + * 2 event 9 + * 1 event 10 + * 2 event 10 + */ + $records = []; + for ($i = 1; $i < 11; $i++) { + $records[] = create_event([ + 'name' => sprintf('1 event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION + ]); + } + + for ($i = 1; $i < 11; $i++) { + $records[] = create_event([ + 'name' => sprintf('2 event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION + ]); + } + + /** + * Expected result set: + * + * 2 event 4 + * 1 event 5 + * 2 event 5 + * 1 event 6 + * 2 event 6 + * 1 event 7 + * 2 event 7 + * 1 event 8 + * 2 event 8 + */ + $aftereventid = $records[3]->id; + $afterevent = $vault->get_event_by_id($aftereventid); + // Offset results by event with name "1 event 4" which has the same timesort + // value as the lower boundary of this query (3). Confirm that the given + // $afterevent is used to ignore events with the same timesortfrom values. + $events = $vault->get_action_events_by_timesort($user, 3, 8, $afterevent); + + $this->assertCount(9, $events); + $this->assertEquals('2 event 4', $events[0]->get_name()); + $this->assertEquals('2 event 8', $events[8]->get_name()); + + /** + * Expected result set: + * + * 2 event 4 + * 1 event 5 + */ + $events = $vault->get_action_events_by_timesort($user, 3, 8, $afterevent, 2); + + $this->assertCount(2, $events); + $this->assertEquals('2 event 4', $events[0]->get_name()); + $this->assertEquals('1 event 5', $events[1]->get_name()); + + /** + * Expected result set: + * + * 2 event 8 + */ + $aftereventid = $records[7]->id; + $afterevent = $vault->get_event_by_id($aftereventid); + // Offset results by event with name "1 event 8" which has the same timesort + // value as the upper boundary of this query (8). Confirm that the given + // $afterevent is used to ignore events with the same timesortto values. + $events = $vault->get_action_events_by_timesort($user, 3, 8, $afterevent); + + $this->assertCount(1, $events); + $this->assertEquals('2 event 8', $events[0]->get_name()); + + /** + * Expected empty result set. + */ + $aftereventid = $records[18]->id; + $afterevent = $vault->get_event_by_id($aftereventid); + // Offset results by event with name "2 event 9" which has a timesort + // value larger than the upper boundary of this query (9 > 8). Confirm + // that the given $afterevent is used for filtering events. + $events = $vault->get_action_events_by_timesort($user, 3, 8, $afterevent); + + $this->assertEmpty($events); + } + /** * Test that get_action_events_by_course returns events after the * provided timesort value. @@ -611,4 +734,147 @@ class core_calendar_event_vault_testcase extends advanced_testcase { $this->assertEquals(sprintf('Event %d', $limit + 4), $events[3]->get_name()); $this->assertEquals(sprintf('Event %d', $limit + 5), $events[4]->get_name()); } + + /** + * Test that get_action_events_by_course returns events between the + * provided timesort values and after the last seen event when one is + * provided. This should work even when the event ids aren't ordered the + * same as the timesort order. + */ + public function test_get_action_events_by_course_non_consecutive_ids() { + $this->resetAfterTest(true); + $this->setAdminuser(); + + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $factory = new action_event_test_factory(); + $vault = new event_vault($factory); + + $this->setAdminuser(); + $this->getDataGenerator()->enrol_user($user->id, $course1->id); + $this->getDataGenerator()->enrol_user($user->id, $course2->id); + + /** + * The events should be ordered by timesort as follows: + * + * 1 event 1 + * 2 event 1 + * 1 event 2 + * 2 event 2 + * 1 event 3 + * 2 event 3 + * 1 event 4 + * 2 event 4 + * 1 event 5 + * 2 event 5 + * 1 event 6 + * 2 event 6 + * 1 event 7 + * 2 event 7 + * 1 event 8 + * 2 event 8 + * 1 event 9 + * 2 event 9 + * 1 event 10 + * 2 event 10 + */ + $records = []; + for ($i = 1; $i < 11; $i++) { + $records[] = create_event([ + 'name' => sprintf('1 event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course1->id, + ]); + } + + for ($i = 1; $i < 11; $i++) { + $records[] = create_event([ + 'name' => sprintf('2 event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course1->id, + ]); + } + + // Create events for the other course. + for ($i = 1; $i < 11; $i++) { + $records[] = create_event([ + 'name' => sprintf('3 event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course2->id, + ]); + } + + /** + * Expected result set: + * + * 2 event 4 + * 1 event 5 + * 2 event 5 + * 1 event 6 + * 2 event 6 + * 1 event 7 + * 2 event 7 + * 1 event 8 + * 2 event 8 + */ + $aftereventid = $records[3]->id; + $afterevent = $vault->get_event_by_id($aftereventid); + // Offset results by event with name "1 event 4" which has the same timesort + // value as the lower boundary of this query (3). Confirm that the given + // $afterevent is used to ignore events with the same timesortfrom values. + $events = $vault->get_action_events_by_course($user, $course1, 3, 8, $afterevent); + + $this->assertCount(9, $events); + $this->assertEquals('2 event 4', $events[0]->get_name()); + $this->assertEquals('2 event 8', $events[8]->get_name()); + + /** + * Expected result set: + * + * 2 event 4 + * 1 event 5 + */ + $events = $vault->get_action_events_by_course($user, $course1, 3, 8, $afterevent, 2); + + $this->assertCount(2, $events); + $this->assertEquals('2 event 4', $events[0]->get_name()); + $this->assertEquals('1 event 5', $events[1]->get_name()); + + /** + * Expected result set: + * + * 2 event 8 + */ + $aftereventid = $records[7]->id; + $afterevent = $vault->get_event_by_id($aftereventid); + // Offset results by event with name "1 event 8" which has the same timesort + // value as the upper boundary of this query (8). Confirm that the given + // $afterevent is used to ignore events with the same timesortto values. + $events = $vault->get_action_events_by_course($user, $course1, 3, 8, $afterevent); + + $this->assertCount(1, $events); + $this->assertEquals('2 event 8', $events[0]->get_name()); + + /** + * Expected empty result set. + */ + $aftereventid = $records[18]->id; + $afterevent = $vault->get_event_by_id($aftereventid); + // Offset results by event with name "2 event 9" which has a timesort + // value larger than the upper boundary of this query (9 > 8). Confirm + // that the given $afterevent is used for filtering events. + $events = $vault->get_action_events_by_course($user, $course1, 3, 8, $afterevent); + + $this->assertEmpty($events); + } }