Skip to content

Commit 0beb10e

Browse files
committed
Adjusting interface for external groups (and attributes) retrieving.
1 parent 67df6ca commit 0beb10e

5 files changed

Lines changed: 145 additions & 51 deletions

File tree

app/V1Module/presenters/GroupExternalAttributesPresenter.php

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@
88
use App\Helpers\MetaFormats\Validators\VString;
99
use App\Helpers\MetaFormats\Validators\VUuid;
1010
use App\Exceptions\ForbiddenRequestException;
11-
use App\Exceptions\BadRequestException;
1211
use App\Model\Repository\GroupExternalAttributes;
12+
use App\Model\Repository\GroupMemberships;
1313
use App\Model\Repository\Groups;
1414
use App\Model\Entity\GroupExternalAttribute;
1515
use App\Model\View\GroupViewFactory;
1616
use App\Security\ACL\IGroupPermissions;
17-
use InvalidArgumentException;
1817

1918
/**
2019
* Additional attributes used by 3rd parties to keep relations between groups and entities in external systems.
@@ -28,6 +27,12 @@ class GroupExternalAttributesPresenter extends BasePresenter
2827
*/
2928
public $groupExternalAttributes;
3029

30+
/**
31+
* @var GroupMemberships
32+
* @inject
33+
*/
34+
public $groupMemberships;
35+
3136
/**
3237
* @var Groups
3338
* @inject
@@ -54,42 +59,34 @@ public function checkDefault()
5459
}
5560

5661
/**
57-
* Return all attributes that correspond to given filtering parameters.
62+
* Return special brief groups entities with injected external attributes and given user affiliation.
5863
* @GET
59-
*
60-
* The filter is encoded as array of objects (logically represented as disjunction of clauses)
61-
* -- i.e., [clause1 OR clause2 ...]. Each clause is an object with the following keys:
62-
* "group", "service", "key", "value" that match properties of GroupExternalAttribute entity.
63-
* The values are expected values matched with == in the search. Any of the keys may be omitted or null
64-
* which indicate it should not be matched in the particular clause.
65-
* A clause must contain at least one of the four keys.
66-
*
67-
* The endpoint will return a list of matching attributes and all related group entities.
6864
*/
69-
#[Query("filter", new VString(), "JSON-encoded filter query in DNF as [clause OR clause...]", required: true)]
70-
public function actionDefault(?string $filter)
65+
#[Query("instance", new VUuid(), "ID of the instance, whose groups are returned.", required: true)]
66+
#[Query(
67+
"service",
68+
new VString(),
69+
"ID of the external service, of which the attributes are returned. If missing, all attributes are returned.",
70+
required: false
71+
)]
72+
#[Query(
73+
"user",
74+
new VUuid(),
75+
"Relationship info of this user is included for each returned group.",
76+
required: false
77+
)]
78+
public function actionDefault(string $instance, ?string $service, ?string $user)
7179
{
72-
$filterStruct = json_decode($filter ?? '', true);
73-
if (!$filterStruct || !is_array($filterStruct)) {
74-
throw new BadRequestException("Invalid filter format.");
75-
}
76-
77-
try {
78-
$attributes = $this->groupExternalAttributes->findByFilter($filterStruct);
79-
} catch (InvalidArgumentException $e) {
80-
throw new BadRequestException($e->getMessage(), '', null, $e);
81-
}
82-
83-
$groupIds = [];
84-
foreach ($attributes as $attribute) {
85-
$groupIds[$attribute->getGroup()->getId()] = true; // id is key to make it unique
86-
}
87-
88-
$groups = $this->groups->groupsAncestralClosure(array_keys($groupIds));
89-
$this->sendSuccessResponse([
90-
"attributes" => $attributes,
91-
"groups" => $this->groupViewFactory->getGroups($groups),
92-
]);
80+
$filter = $service ? [['service' => $service]] : [];
81+
$attributes = $this->groupExternalAttributes->findByFilter($filter); // all attributes of selected service
82+
$groups = $this->groups->findFiltered(null, $instance, null, false); // all but archived groups
83+
$memberships = $user ? $this->groupMemberships->findByUser($user) : [];
84+
85+
$this->sendSuccessResponse($this->groupViewFactory->getGroupsForExtension(
86+
$groups,
87+
$attributes,
88+
$memberships,
89+
));
9390
}
9491

9592

app/model/repository/GroupMemberships.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,18 @@ public function __construct(EntityManagerInterface $em)
1414
{
1515
parent::__construct($em, GroupMembership::class);
1616
}
17+
18+
/**
19+
* Find all group memberships for a specific user in non-archived groups.
20+
* @param string $userId
21+
* @return GroupMembership[]
22+
*/
23+
public function findByUser(string $userId): array
24+
{
25+
$qb = $this->createQueryBuilder('gm')->join('gm.group', 'g');
26+
$qb->where('g.archivedAt IS NULL');
27+
$qb->andWhere($qb->expr()->eq('gm.user', ':userId'))
28+
->setParameter('userId', $userId);
29+
return $qb->getQuery()->getResult();
30+
}
1731
}

app/model/view/GroupViewFactory.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use App\Model\Entity\AssignmentSolution;
1010
use App\Model\Entity\Group;
1111
use App\Model\Entity\GroupExamLock;
12+
use App\Model\Entity\GroupExternalAttribute;
13+
use App\Model\Entity\GroupMembership;
1214
use App\Model\Entity\ShadowAssignment;
1315
use App\Model\Entity\ShadowAssignmentPoints;
1416
use App\Model\Entity\User;
@@ -326,4 +328,88 @@ public function getGroupExamLocks(Group $group, array $locks): array
326328
return $res;
327329
}, $locks);
328330
}
331+
332+
/**
333+
* Get a subset of group data relevant (available) for ReCodEx extensions (plugins).
334+
* @param Group $group to be rendered
335+
* @param array $injectAttributes [service][key] => value
336+
* @param string|null $membershipType GroupMembership::TYPE_* value for user who made the search
337+
* @return array
338+
*/
339+
public function getGroupForExtension(
340+
Group $group,
341+
array $injectAttributes = [],
342+
?string $membershipType = null,
343+
array $adminUsers = [],
344+
): array {
345+
$admins = [];
346+
foreach ($group->getPrimaryAdminsIds() as $id) {
347+
$admins[$id] = null;
348+
if (array_key_exists($id, $adminUsers)) {
349+
$admins[$id] = $adminUsers[$id]->getNameParts(); // an array with name and title components
350+
$admins[$id]['email'] = $adminUsers[$id]->getEmail();
351+
}
352+
}
353+
return [
354+
"id" => $group->getId(),
355+
"parentGroupId" => $group->getParentGroup() ? $group->getParentGroup()->getId() : null,
356+
"admins" => $admins,
357+
"localizedTexts" => $group->getLocalizedTexts()->getValues(),
358+
"organizational" => $group->isOrganizational(),
359+
"exam" => $group->isExam(),
360+
"public" => $group->isPublic(),
361+
"detaining" => $group->isDetaining(),
362+
"attributes" => $injectAttributes,
363+
"membership" => $membershipType,
364+
];
365+
}
366+
367+
/**
368+
* Get a subset of groups data relevant (available) for ReCodEx extensions (plugins).
369+
* @param Group[] $groups
370+
* @param GroupExternalAttribute[] $attributes
371+
* @param GroupMembership[] $memberships GroupMembership[] to filter by
372+
*/
373+
public function getGroupsForExtension(array $groups, array $attributes = [], array $memberships = []): array
374+
{
375+
// create a multi-dimensional map [groupId][attr-service][attr-key] => attr-value
376+
$attributesMap = [];
377+
foreach ($attributes as $attribute) {
378+
$gid = $attribute->getGroup()->getId();
379+
$attributesMap[$gid] = $attributesMap[$gid] ?? [];
380+
381+
$service = $attribute->getService();
382+
$attributesMap[$gid][$service] = $attributesMap[$gid][$service] ?? [];
383+
384+
$key = $attribute->getKey();
385+
$attributesMap[][$service][$key] = $attribute->getValue();
386+
}
387+
388+
// create membership mapping group-id => membership-type
389+
$membershipsMap = [];
390+
foreach ($memberships as $membership) {
391+
$gid = $membership->getGroup()->getId();
392+
$membershipsMap[$gid] = $membership->getType();
393+
}
394+
395+
$admins = [];
396+
foreach ($groups as $group) {
397+
foreach ($group->getPrimaryAdmins() as $admin) {
398+
$admins[$admin->getId()] = $admin;
399+
}
400+
}
401+
402+
return array_map(
403+
function (Group $group) use ($attributesMap, $membershipsMap, $admins) {
404+
$id = $group->getId();
405+
return $this->getGroupForExtension(
406+
$group,
407+
$attributesMap[$id] ?? [],
408+
$membershipsMap[$id] ?? null,
409+
$admins
410+
);
411+
},
412+
$groups
413+
);
414+
}
329415
}

tests/Presenters/GroupExternalAttributesPresenter.phpt

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22

33
$container = require_once __DIR__ . "/../bootstrap.php";
44

5-
use App\Model\Entity\Group;
6-
use App\Model\Entity\GroupInvitation;
7-
use App\Model\Repository\Groups;
8-
use App\Model\Repository\GroupInvitations;
95
use App\V1Module\Presenters\GroupExternalAttributesPresenter;
106
use App\Exceptions\BadRequestException;
117
use App\Security\TokenScope;
@@ -80,11 +76,12 @@ class TestGroupExternalAttributesPresenter extends Tester\TestCase
8076
}
8177
}
8278

79+
/*
8380
public function testGetAttributesSemester()
8481
{
8582
PresenterTestHelper::loginDefaultAdmin($this->container, [TokenScope::GROUP_EXTERNAL_ATTRIBUTES]);
8683
87-
$filter = [[ "key" => "semester", "value" => "summer" ]];
84+
$filter = [["key" => "semester", "value" => "summer"]];
8885
$payload = PresenterTestHelper::performPresenterRequest(
8986
$this->presenter,
9087
'V1:GroupExternalAttributes',
@@ -102,7 +99,7 @@ class TestGroupExternalAttributesPresenter extends Tester\TestCase
10299
{
103100
PresenterTestHelper::loginDefaultAdmin($this->container, [TokenScope::GROUP_EXTERNAL_ATTRIBUTES]);
104101
105-
$filter = [[ "key" => "lecture", "value" => "demo" ]];
102+
$filter = [["key" => "lecture", "value" => "demo"]];
106103
$payload = PresenterTestHelper::performPresenterRequest(
107104
$this->presenter,
108105
'V1:GroupExternalAttributes',
@@ -121,8 +118,8 @@ class TestGroupExternalAttributesPresenter extends Tester\TestCase
121118
PresenterTestHelper::loginDefaultAdmin($this->container, [TokenScope::GROUP_EXTERNAL_ATTRIBUTES]);
122119
123120
$filter = [
124-
[ "service" => "test", "key" => "semester", ],
125-
[ "key" => "lecture", "value" => "demo" ],
121+
["service" => "test", "key" => "semester",],
122+
["key" => "lecture", "value" => "demo"],
126123
];
127124
$payload = PresenterTestHelper::performPresenterRequest(
128125
$this->presenter,
@@ -145,7 +142,7 @@ class TestGroupExternalAttributesPresenter extends Tester\TestCase
145142
{
146143
PresenterTestHelper::loginDefaultAdmin($this->container, [TokenScope::GROUP_EXTERNAL_ATTRIBUTES]);
147144
148-
$filter = [[ "key" => "lecture", "value" => "sleeping" ]];
145+
$filter = [["key" => "lecture", "value" => "sleeping"]];
149146
$payload = PresenterTestHelper::performPresenterRequest(
150147
$this->presenter,
151148
'V1:GroupExternalAttributes',
@@ -160,7 +157,7 @@ class TestGroupExternalAttributesPresenter extends Tester\TestCase
160157
{
161158
PresenterTestHelper::loginDefaultAdmin($this->container, [TokenScope::GROUP_EXTERNAL_ATTRIBUTES]);
162159
163-
$filter = [[ "service" => "3rdparty", "key" => "lecture" ]];
160+
$filter = [["service" => "3rdparty", "key" => "lecture"]];
164161
$payload = PresenterTestHelper::performPresenterRequest(
165162
$this->presenter,
166163
'V1:GroupExternalAttributes',
@@ -176,10 +173,10 @@ class TestGroupExternalAttributesPresenter extends Tester\TestCase
176173
PresenterTestHelper::loginDefaultAdmin($this->container, [TokenScope::GROUP_EXTERNAL_ATTRIBUTES]);
177174
178175
$filters = [
179-
[ "key" => "semester", "value" => "summer" ],
176+
["key" => "semester", "value" => "summer"],
180177
"semester: summer",
181-
[[ "semester" => "summer" ]],
182-
[[ "key" => "semester", "value" => 1 ]],
178+
[["semester" => "summer"]],
179+
[["key" => "semester", "value" => 1]],
183180
];
184181
foreach ($filters as $filter) {
185182
Assert::exception(function () use ($filter) {
@@ -192,7 +189,7 @@ class TestGroupExternalAttributesPresenter extends Tester\TestCase
192189
}, BadRequestException::class);
193190
}
194191
}
195-
192+
*/
196193
public function testGetAttributesAdd()
197194
{
198195
PresenterTestHelper::loginDefaultAdmin($this->container, [TokenScope::GROUP_EXTERNAL_ATTRIBUTES]);

tests/Presenters/SubmitPresenter.phpt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class TestSubmitPresenter extends Tester\TestCase
5757
$this->assignments = $container->getByType(Assignments::class);
5858
$this->assignmentSolvers = $container->getByType(AssignmentSolvers::class);
5959

60-
// patch container, since we cannot create actual file storage manarer
60+
// patch container, since we cannot create actual file storage manager
6161
$fsName = current($this->container->findByType(FileStorageManager::class));
6262
$this->container->removeService($fsName);
6363
$this->container->addService($fsName, new FileStorageManager(
@@ -270,7 +270,7 @@ class TestSubmitPresenter extends Tester\TestCase
270270
Assert::equal($tasksCount, $webSocketChannel['expectedTasksCount']);
271271

272272
$author = $this->presenter->users->findOrThrow($solution['authorId']);
273-
$solvers = $this->assignmentSolvers->findBy([ "assignment" => $assignment, "solver" => $author ]);
273+
$solvers = $this->assignmentSolvers->findBy(["assignment" => $assignment, "solver" => $author]);
274274
Assert::count(1, $solvers);
275275
Assert::equal($solvers[0]->getLastAttemptIndex(), $solution['attemptIndex']);
276276
}

0 commit comments

Comments
 (0)