Issue #3310286: Add $timestamp parameter to group.membership service methods

This commit is contained in:
Michael Stenta 2022-09-18 09:27:00 -04:00
parent d154db811c
commit 7c9ef9a8bd
4 changed files with 143 additions and 20 deletions

View File

@ -168,17 +168,21 @@ access controls are respected.
**Methods**:
`hasGroup($asset)` - Check if an asset is a member of a group. Returns a
boolean.
`hasGroup($asset, $timestamp = NULL)` - Check if an asset is a member of a group,
optionally at a given timestamp (defaults to current time). Returns a boolean.
`getGroup($asset)` - Get group assets that an asset is a member of. Returns an
`getGroup($asset, $timestamp = NULL)` - Get group assets that an asset is a member
of, optionally at a given timestamp (defaults to current time). Returns an
array of asset entities.
`getGroupAssignmentLog($asset)` - Find the latest group assignment log that
references an asset. Returns a log entity, or `NULL` if no logs were found.
`getGroupAssignmentLog($asset, $timestamp = NULL)` - Find the latest group
assignment log that references an asset, optionally at a given timestamp
(defaults to current time). Returns a log entity, or `NULL` if no logs were
found.
`getGroupMembers($groups, $recurse)` - Get assets that are members of groups,
optionally recursing into child groups.
`getGroupMembers($groups, $recurse = TRUE, $timestamp = NULL)` - Get assets that
are members of groups, optionally recursing into child groups, and optionally
at a given timestamp (defaults to current time).
**Example usage:**

View File

@ -71,40 +71,53 @@ class GroupMembership implements GroupMembershipInterface {
/**
* {@inheritdoc}
*/
public function hasGroup(AssetInterface $asset): bool {
$log = $this->getGroupAssignmentLog($asset);
public function hasGroup(AssetInterface $asset, $timestamp = NULL): bool {
// Load the group assignment log. Bail if empty.
$log = $this->getGroupAssignmentLog($asset, $timestamp);
if (empty($log)) {
return FALSE;
}
// Return emptiness of the group references.
return !$log->get(static::LOG_FIELD_GROUP)->isEmpty();
}
/**
* {@inheritdoc}
*/
public function getGroup(AssetInterface $asset): array {
$log = $this->getGroupAssignmentLog($asset);
public function getGroup(AssetInterface $asset, $timestamp = NULL): array {
// Load the group assignment log. Bail if empty.
$log = $this->getGroupAssignmentLog($asset, $timestamp);
if (empty($log)) {
return [];
}
// Return referenced entities.
return $log->{static::LOG_FIELD_GROUP}->referencedEntities() ?? [];
}
/**
* {@inheritdoc}
*/
public function getGroupAssignmentLog(AssetInterface $asset): ?LogInterface {
public function getGroupAssignmentLog(AssetInterface $asset, $timestamp = NULL): ?LogInterface {
// If the asset is new, no group assignment logs will reference it.
if ($asset->isNew()) {
return NULL;
}
// If $timestamp is NULL, use the current time.
if (is_null($timestamp)) {
$timestamp = $this->time->getRequestTime();
}
// Query for group assignment logs that reference the asset.
// We do not check access on the logs to ensure that none are filtered out.
$options = [
'asset' => $asset,
'timestamp' => $this->time->getRequestTime(),
'timestamp' => $timestamp,
'status' => 'done',
'limit' => 1,
];
@ -134,7 +147,7 @@ class GroupMembership implements GroupMembershipInterface {
/**
* {@inheritdoc}
*/
public function getGroupMembers(array $groups, bool $recurse = TRUE): array {
public function getGroupMembers(array $groups, bool $recurse = TRUE, $timestamp = NULL): array {
// Get group ids.
$group_ids = array_map(function (AssetInterface $group) {
@ -146,6 +159,11 @@ class GroupMembership implements GroupMembershipInterface {
return [];
}
// If $timestamp is NULL, use the current time.
if (is_null($timestamp)) {
$timestamp = $this->time->getRequestTime();
}
// Build query for group members.
$query = "
-- Select asset IDs from the asset base table.
@ -182,7 +200,7 @@ class GroupMembership implements GroupMembershipInterface {
-- Exclude records with future log entries.
AND lfd2.id IS NULL";
$args = [
':timestamp' => $this->time->getRequestTime(),
':timestamp' => $timestamp,
':group_ids[]' => $group_ids,
];
$result = $this->database->query($query, $args)->fetchAll();
@ -204,7 +222,7 @@ class GroupMembership implements GroupMembershipInterface {
return $asset->bundle() === 'group';
});
// Use array_replace so that numeric keys are preserved.
$assets = array_replace($assets, $this->getGroupMembers($groups));
$assets = array_replace($assets, $this->getGroupMembers($groups, $recurse, $timestamp));
}
return $assets;
}

View File

@ -15,33 +15,42 @@ interface GroupMembershipInterface {
*
* @param \Drupal\asset\Entity\AssetInterface $asset
* The Asset entity.
* @param int|null $timestamp
* Include logs with a timestamp less than or equal to this.
* If this is NULL (default), the current time will be used.
*
* @return bool
* Returns TRUE if the asset is a member of a group, FALSE otherwise.
*/
public function hasGroup(AssetInterface $asset): bool;
public function hasGroup(AssetInterface $asset, $timestamp = NULL): bool;
/**
* Get group assets that an asset is a member of.
*
* @param \Drupal\asset\Entity\AssetInterface $asset
* The Asset entity.
* @param int|null $timestamp
* Include logs with a timestamp less than or equal to this.
* If this is NULL (default), the current time will be used.
*
* @return array
* Returns an array of assets.
*/
public function getGroup(AssetInterface $asset): array;
public function getGroup(AssetInterface $asset, $timestamp = NULL): array;
/**
* Find the latest group assignment log that references an asset.
*
* @param \Drupal\asset\Entity\AssetInterface $asset
* The asset entity.
* @param int|null $timestamp
* Include logs with a timestamp less than or equal to this.
* If this is NULL (default), the current time will be used.
*
* @return \Drupal\log\Entity\LogInterface|null
* A log entity, or NULL if no logs were found.
*/
public function getGroupAssignmentLog(AssetInterface $asset): ?LogInterface;
public function getGroupAssignmentLog(AssetInterface $asset, $timestamp = NULL): ?LogInterface;
/**
* Get assets that are members of groups.
@ -51,10 +60,13 @@ interface GroupMembershipInterface {
* @param bool $recurse
* Boolean: whether or not to recurse and load members of sub-groups.
* Defaults to TRUE.
* @param int|null $timestamp
* Include logs with a timestamp less than or equal to this.
* If this is NULL (default), the current time will be used.
*
* @return \Drupal\asset\Entity\AssetInterface[]
* An array of asset objects indexed by their IDs.
*/
public function getGroupMembers(array $groups, bool $recurse = TRUE): array;
public function getGroupMembers(array $groups, bool $recurse = TRUE, $timestamp = NULL): array;
}

View File

@ -260,6 +260,95 @@ class GroupTest extends KernelTestBase {
$this->assertCorrectAssets([$animal, $second_animal], $group_members, TRUE, 'Group members from multiple groups can be queried together.');
}
/**
* Test past/future group membership.
*/
public function testGroupMembershipTimestamp() {
// Create an asset.
/** @var \Drupal\asset\Entity\AssetInterface $asset */
$asset = Asset::create([
'type' => 'animal',
'name' => $this->randomMachineName(),
'status' => 'active',
]);
$asset->save();
// Create two group assets.
/** @var \Drupal\asset\Entity\AssetInterface[] $groups */
$group = Asset::create([
'type' => 'group',
'name' => $this->randomMachineName(),
'status' => 'active',
]);
$group->save();
$groups[] = $group;
$group = Asset::create([
'type' => 'group',
'name' => $this->randomMachineName(),
'status' => 'active',
]);
$group->save();
$groups[] = $group;
// Create a series of timestamps, each 24 hours apart.
$now = \Drupal::time()->getRequestTime();
$timestamps = [];
for ($i = 0; $i < 3; $i++) {
$timestamps[$i] = $now + (86400 * $i);
}
// Create a series of logs that assign the asset to each of the groups, and
// a third that removes the asset from all groups.
/** @var \Drupal\log\Entity\LogInterface[] $logs */
$logs = [];
$group_assignments = [];
for ($i = 0; $i < 2; $i++) {
$group_assignments[] = ['target_id' => $groups[$i]->id()];
}
$group_assignments[] = [];
for ($i = 0; $i < 3; $i++) {
$log = Log::create([
'type' => 'test',
'timestamp' => $timestamps[$i],
'status' => 'done',
'asset' => ['target_id' => $asset->id()],
'is_group_assignment' => TRUE,
'group' => $group_assignments[$i],
]);
$log->save();
$logs[] = $log;
}
// Confirm that the asset has no group membership before all logs.
$timestamp = $now - 86400;
$this->assertEquals(FALSE, $this->groupMembership->hasGroup($asset, $timestamp));
$this->assertEquals([], $this->groupMembership->getGroup($asset, $timestamp));
$this->assertNull($this->groupMembership->getGroupAssignmentLog($asset, $timestamp));
$this->assertEquals([], $this->groupMembership->getGroupMembers($groups, TRUE, $timestamp));
// Confirm that the asset is where it should be after each log.
for ($i = 0; $i < 2; $i++) {
$this->assertEquals(TRUE, $this->groupMembership->hasGroup($asset, $timestamps[$i]));
$this->assertEquals($groups[$i]->id(), $this->groupMembership->getGroup($asset, $timestamps[$i])[0]->id());
$this->assertEquals($logs[$i]->id(), $this->groupMembership->getGroupAssignmentLog($asset, $timestamps[$i])->id());
$this->assertCorrectAssets([$asset], $this->groupMembership->getGroupMembers([$groups[$i]], TRUE, $timestamps[$i]), TRUE);
}
// Confirm that the asset is still in the same group 1 second later.
// This tests the <= operator.
$this->assertEquals(TRUE, $this->groupMembership->hasGroup($asset, $timestamps[1] + 1));
$this->assertEquals($groups[1]->id(), $this->groupMembership->getGroup($asset, $timestamps[1] + 1)[0]->id());
$this->assertEquals($logs[1]->id(), $this->groupMembership->getGroupAssignmentLog($asset, $timestamps[1] + 1)->id());
$this->assertCorrectAssets([$asset], $this->groupMembership->getGroupMembers([$groups[1]], TRUE, $timestamps[1] + 1), TRUE);
// Confirm that the asset has no group membership after the last log.
$this->assertEquals(FALSE, $this->groupMembership->hasGroup($asset, $timestamps[2]));
$this->assertEquals([], $this->groupMembership->getGroup($asset, $timestamps[2]));
$this->assertEquals($logs[2]->id(), $this->groupMembership->getGroupAssignmentLog($asset, $timestamps[2])->id());
$this->assertEquals([], $this->groupMembership->getGroupMembers($groups, TRUE, $timestamps[2]));
}
/**
* Test recursive asset group membership.
*/