3
0
Fork 0
mirror of https://github.com/farmOS/farmOS.git synced 2024-02-23 11:37:38 +01:00

Issue #3217183: Query a list of assets in a group

This commit is contained in:
Michael Stenta 2021-06-04 21:12:22 -04:00
commit 2c0cd65b77
5 changed files with 121 additions and 17 deletions

View file

@ -81,6 +81,9 @@ 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.
`getGroupMembers($group, $recurse)` - Get assets that are members of a group,
optionally recursing into child groups.
**Example usage:**
```php

View file

@ -2,4 +2,4 @@ services:
group.membership:
class: Drupal\farm_group\GroupMembership
arguments:
[ '@farm.log_query', '@entity_type.manager', '@datetime.time' ]
[ '@farm.log_query', '@entity_type.manager', '@datetime.time', '@database' ]

View file

@ -4,6 +4,7 @@ namespace Drupal\farm_group;
use Drupal\asset\Entity\AssetInterface;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\farm_log\LogQueryFactoryInterface;
use Drupal\log\Entity\LogInterface;
@ -42,6 +43,13 @@ class GroupMembership implements GroupMembershipInterface {
*/
protected $time;
/**
* The database service.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Class constructor.
*
@ -51,11 +59,14 @@ class GroupMembership implements GroupMembershipInterface {
* Entity type manager.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param \Drupal\Core\Database\Connection $database
* The database service.
*/
public function __construct(LogQueryFactoryInterface $log_query_factory, EntityTypeManagerInterface $entity_type_manager, TimeInterface $time) {
public function __construct(LogQueryFactoryInterface $log_query_factory, EntityTypeManagerInterface $entity_type_manager, TimeInterface $time, Connection $database) {
$this->logQueryFactory = $log_query_factory;
$this->entityTypeManager = $entity_type_manager;
$this->time = $time;
$this->database = $database;
}
/**
@ -65,7 +76,8 @@ class GroupMembership implements GroupMembershipInterface {
return new static(
$container->get('farm.log_query'),
$container->get('entity_type.manager'),
$container->get('datetime.time')
$container->get('datetime.time'),
$container->get('database')
);
}
@ -130,4 +142,72 @@ class GroupMembership implements GroupMembershipInterface {
return NULL;
}
/**
* {@inheritdoc}
*/
public function getGroupMembers(AssetInterface $group, bool $recurse = TRUE): array {
if (empty($group->id())) {
return [];
}
$query = "
-- Select asset IDs from the asset base table.
SELECT a.id
FROM {asset} a
-- Inner join logs that reference the assets.
INNER JOIN {asset_field_data} afd ON afd.id = a.id
INNER JOIN {log__asset} la ON a.id = la.asset_target_id AND la.deleted = 0
INNER JOIN {log_field_data} lfd ON lfd.id = la.entity_id
-- Inner join group assets referenced by the logs.
INNER JOIN {log__group} lg ON lg.entity_id = lfd.id AND lg.deleted = 0
-- Left join ANY future group assignment logs for the same asset.
-- In the WHERE clause we'll exclude all records that have future logs,
-- leaving only the 'current' log entry.
LEFT JOIN (
{log_field_data} lfd2
INNER JOIN {log__asset} la2 ON la2.entity_id = lfd2.id AND la2.deleted = 0
) ON lfd2.is_group_assignment = 1 AND la2.asset_target_id = a.id
-- Future log entries have either a higher timestamp, or an equal timestamp and higher log ID.
AND (lfd2.timestamp > lfd.timestamp OR (lfd2.timestamp = lfd.timestamp AND lfd2.id > lfd.id))
-- Don't include future logs beyond the given timestamp.
-- These conditions should match the values in the WHERE clause.
AND (lfd2.status = 'done') AND (lfd2.timestamp <= :timestamp)
-- Limit results to completed membership assignment logs to the desired
-- group that took place before the given timestamp.
WHERE (lfd.is_group_assignment = 1) AND (lfd.status = 'done') AND (lfd.timestamp <= :timestamp) AND (lg.group_target_id = :group_id)
-- Exclude records with future log entries.
AND lfd2.id IS NULL";
$args = [
':timestamp' => $this->time->getRequestTime(),
':group_id' => $group->id(),
];
$result = $this->database->query($query, $args)->fetchAll();
$asset_ids = [];
foreach ($result as $row) {
if (!empty($row->id)) {
$asset_ids[] = $row->id;
}
}
if (empty($asset_ids)) {
return [];
}
$asset_ids = array_unique($asset_ids);
/** @var \Drupal\asset\Entity\AssetInterface[] $assets */
$assets = $this->entityTypeManager->getStorage('asset')->loadMultiple($asset_ids);
if ($recurse) {
foreach ($assets as $asset) {
if ($asset->bundle() == 'group') {
$assets = array_merge($assets, $this->getGroupMembers($asset));
}
}
}
return $assets;
}
}

View file

@ -43,4 +43,18 @@ interface GroupMembershipInterface {
*/
public function getGroupAssignmentLog(AssetInterface $asset): ?LogInterface;
/**
* Get assets that are members of a group.
*
* @param \Drupal\asset\Entity\AssetInterface $group
* The Asset entity.
* @param bool $recurse
* Boolean: whether or not to recurse and load members of sub-groups.
* Defaults to TRUE.
*
* @return array
* Returns an array of assets.
*/
public function getGroupMembers(AssetInterface $group, bool $recurse = TRUE): array;
}

View file

@ -81,11 +81,7 @@ class GroupTest extends KernelTestBase {
]);
$animal->save();
// When an asset has no group assignment logs, it has no group membership.
$this->assertFalse($this->groupMembership->hasGroup($animal), 'New assets do not have group membership.');
$this->assertEmpty($this->groupMembership->getGroup($animal), 'New assets do not reference any groups.');
// Create a group asset.
// Create group assets.
/** @var \Drupal\asset\Entity\AssetInterface $first_group */
$first_group = Asset::create([
'type' => 'group',
@ -93,6 +89,19 @@ class GroupTest extends KernelTestBase {
'status' => 'active',
]);
$first_group->save();
/** @var \Drupal\asset\Entity\AssetInterface $second_group */
$second_group = Asset::create([
'type' => 'group',
'name' => $this->randomMachineName(),
'status' => 'active',
]);
$second_group->save();
// When an asset has no group assignment logs, it has no group membership.
$this->assertFalse($this->groupMembership->hasGroup($animal), 'New assets do not have group membership.');
$this->assertEmpty($this->groupMembership->getGroup($animal), 'New assets do not reference any groups.');
$this->assertEmpty($this->groupMembership->getGroupMembers($first_group), 'New groups have no members.');
$this->assertEmpty($this->groupMembership->getGroupMembers($second_group), 'New groups have no members.');
// Create a "done" log that assigns the animal to the group.
/** @var \Drupal\log\Entity\LogInterface $first_log */
@ -108,15 +117,8 @@ class GroupTest extends KernelTestBase {
// When an asset has a done group assignment logs, it has group membership.
$this->assertTrue($this->groupMembership->hasGroup($animal), 'Asset with group assignment has group membership.');
$this->assertEquals($first_group->id(), $this->groupMembership->getGroup($animal)[0]->id(), 'Asset with group assignment is in the assigned group.');
// Create a second group asset.
/** @var \Drupal\asset\Entity\AssetInterface $second_group */
$second_group = Asset::create([
'type' => 'group',
'name' => $this->randomMachineName(),
'status' => 'active',
]);
$second_group->save();
$this->assertEquals(1, count($this->groupMembership->getGroupMembers($first_group)), 'When an asset becomes a group member, the group has one member.');
$this->assertEmpty($this->groupMembership->getGroupMembers($second_group), 'When an asset becomes a group member, other groups are unaffected.');
// Create a "pending" log that assigns the animal to the second group.
/** @var \Drupal\log\Entity\LogInterface $second_log */
@ -132,11 +134,13 @@ class GroupTest extends KernelTestBase {
// When an asset has a pending group assignment logs, it still has the same
// group membership as before.
$this->assertEquals($first_group->id(), $this->groupMembership->getGroup($animal)[0]->id(), 'Pending group assignment logs do not affect membership.');
$this->assertEmpty($this->groupMembership->getGroupMembers($second_group), 'Groups with only pending membership have zero members.');
// When the log is marked as "done", the asset's membership is updated.
$second_log->status = 'done';
$second_log->save();
$this->assertEquals($second_group->id(), $this->groupMembership->getGroup($animal)[0]->id(), 'A second group assignment log updates group membership.');
$this->assertEquals(1, count($this->groupMembership->getGroupMembers($second_group)), 'Completed group assignment logs add group members.');
// Create a third "done" log in the future.
/** @var \Drupal\log\Entity\LogInterface $third_log */
@ -153,6 +157,7 @@ class GroupTest extends KernelTestBase {
// When an asset has a "done" group assignment log in the future, the asset
// group membership remains the same as the previous "done" movement log.
$this->assertEquals($second_group->id(), $this->groupMembership->getGroup($animal)[0]->id(), 'A third group assignment log in the future does not update group membership.');
$this->assertEquals(1, count($this->groupMembership->getGroupMembers($second_group)), 'Future group assignment logs do not affect members.');
// Create a fourth log with no group reference.
/** @var \Drupal\log\Entity\LogInterface $fourth_log */
@ -169,6 +174,8 @@ class GroupTest extends KernelTestBase {
// effectively "unsets" the asset's group membership.
$this->assertFalse($this->groupMembership->hasGroup($animal), 'Asset group membership can be unset.');
$this->assertEmpty($this->groupMembership->getGroup($animal), 'Unset group membership does not reference any groups.');
$this->assertEquals(0, count($this->groupMembership->getGroupMembers($first_group)), 'Unset group membership unsets group members.');
$this->assertEquals(0, count($this->groupMembership->getGroupMembers($second_group)), 'Unset group membership unsets group members.');
}
/**