Skip to content

Commit a8b8c7d

Browse files
author
Martin Krulis
committed
Adding new filter (selecting runtime environments) for exercise pagination endpoint.
1 parent 948228f commit a8b8c7d

File tree

7 files changed

+110
-21
lines changed

7 files changed

+110
-21
lines changed

app/V1Module/presenters/ExercisesPresenter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ public function checkDefault() {
158158
*/
159159
public function actionDefault(int $offset = 0, int $limit = null, string $orderBy = null, array $filters = null, string $locale = null) {
160160
$pagination = $this->getPagination($offset, $limit, $locale, $orderBy,
161-
($filters === null) ? [] : $filters, ['search', 'instanceId', 'groupsIds', 'authorsIds', 'tags']);
161+
($filters === null) ? [] : $filters, ['search', 'instanceId', 'groupsIds', 'authorsIds', 'tags', 'runtimeEnvironments']);
162162

163163
// Get all matching exercises and filter them by ACLs...
164164
$exercises = $this->exercises->getPreparedForPagination($pagination, $this->groups);

app/V1Module/router/RouterFactory.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,10 @@ private static function createExercisesRoutes(string $prefix): RouteList {
126126
$router = new RouteList();
127127
$router[] = new GetRoute("$prefix", "Exercises:");
128128
$router[] = new PostRoute("$prefix", "Exercises:create");
129-
$router[] = new GetRoute("$prefix/authors", "Exercises:authors");
130129
$router[] = new PostRoute("$prefix/list", "Exercises:listByIds");
130+
$router[] = new GetRoute("$prefix/authors", "Exercises:authors");
131+
$router[] = new GetRoute("$prefix/tags", "Exercises:allTags");
132+
131133
$router[] = new GetRoute("$prefix/<id>", "Exercises:detail");
132134
$router[] = new DeleteRoute("$prefix/<id>", "Exercises:remove");
133135
$router[] = new PostRoute("$prefix/<id>", "Exercises:updateDetail");
@@ -140,7 +142,6 @@ private static function createExercisesRoutes(string $prefix): RouteList {
140142
$router[] = new DeleteRoute("$prefix/<id>/groups/<groupId>", "Exercises:detachGroup");
141143
$router[] = new PostRoute("$prefix/<id>/pipelines/<pipelineId>", "Exercises:attachPipeline");
142144
$router[] = new DeleteRoute("$prefix/<id>/pipelines/<pipelineId>", "Exercises:detachPipeline");
143-
$router[] = new GetRoute("$prefix/tags", "Exercises:allTags");
144145
$router[] = new PostRoute("$prefix/<id>/tags/<name>", "Exercises:addTag");
145146
$router[] = new DeleteRoute("$prefix/<id>/tags/<name>", "Exercises:removeTag");
146147

app/helpers/Pagination.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,16 @@ public function getOriginalOrderBy(): ?string {
114114

115115
/**
116116
* True if filter of given name is present.
117+
* @param string $name Identifier of the filter
118+
* @param bool $notEmpty If true, the filter value must also be not empty.
117119
*/
118-
public function hasFilter(string $name)
120+
public function hasFilter(string $name, bool $notEmpty = false)
119121
{
120-
return array_key_exists($name, $this->filters);
122+
if ($notEmpty) {
123+
return !empty($this->filters[$name]);
124+
} else {
125+
return array_key_exists($name, $this->filters);
126+
}
121127
}
122128

123129
/**

app/model/repository/Exercises.php

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Doctrine\Common\Collections\Collection;
66
use Doctrine\Common\Collections\Criteria;
77
use Doctrine\ORM\Query;
8+
use Doctrine\ORM\QueryBuilder;
89
use Kdyby\Doctrine\EntityManager;
910
use App\Model\Entity\Exercise;
1011
use App\Model\Entity\LocalizedExercise;
@@ -62,12 +63,56 @@ public function searchByName(?string $search): array {
6263
});
6364
}
6465

66+
/**
67+
* Augment given query builder and add filter that covers groups of residence of the exercise.
68+
* @param QueryBuilder $qb
69+
* @param mixed $groupsIds Value of the filter
70+
* @param Groups $groups Doctrine groups repository
71+
*/
72+
private function getPreparedForPaginationGroupsFilter(QueryBuilder $qb, $groupsIds, Groups $groups)
73+
{
74+
if (!is_array($groupsIds)) {
75+
$groupsIds = [ $groupsIds ];
76+
}
77+
78+
// Each group has a separate OR clause ...
79+
$orExpr = $qb->expr()->orX();
80+
$counter = 0;
81+
foreach ($groups->groupsIdsAncestralClosure($groupsIds) as $id) {
82+
$var = "group" . ++$counter;
83+
$orExpr->add($qb->expr()->isMemberOf(":$var", "e.groups"));
84+
$qb->setParameter($var, $id);
85+
}
86+
$qb->andWhere($orExpr);
87+
}
88+
89+
/**
90+
* Augment given query builder and add filter that handles runtime environments.
91+
* @param QueryBuilder $qb
92+
* @param mixed $groupsIds Value of the filter
93+
* @param Groups $groups Doctrine groups repository
94+
*/
95+
private function getPreparedForPaginationEnvsFilter(QueryBuilder $qb, $envs)
96+
{
97+
if (!is_array($envs)) {
98+
$envs = [ $envs ];
99+
}
100+
101+
$orExpr = $qb->expr()->orX();
102+
$counter = 0;
103+
foreach ($envs as $env) {
104+
$var = "env" . ++$counter;
105+
$orExpr->add($qb->expr()->isMemberOf(":$var", "e.runtimeEnvironments"));
106+
$qb->setParameter($var, $env);
107+
}
108+
$qb->andWhere($orExpr);
109+
}
65110

66111
/**
67112
* Get a list of exercises filtered and ordered for pagination.
68113
* The exercises must be paginated manually, since they are tested by ACLs.
69114
* @param Pagination $pagination Pagination configuration object.
70-
* @param Groups Groups entity manager.
115+
* @param Groups $groups Doctrine groups repository
71116
* @return Exercise[]
72117
*/
73118
public function getPreparedForPagination(Pagination $pagination, Groups $groups)
@@ -98,20 +143,7 @@ public function getPreparedForPagination(Pagination $pagination, Groups $groups)
98143

99144
// Only exercises in explicitly given groups (or their ascendants) ...
100145
if ($pagination->hasFilter("groupsIds")) {
101-
$groupsIds = $pagination->getFilter("groupsIds");
102-
if (!is_array($groupsIds)) {
103-
$groupsIds = [ $groupsIds ];
104-
}
105-
106-
// Each group has a separate OR clause ...
107-
$orExpr = $qb->expr()->orX();
108-
$gcounter = 0;
109-
foreach ($groups->groupsIdsAncestralClosure($groupsIds) as $id) {
110-
$var = "group" . ++$gcounter;
111-
$orExpr->add($qb->expr()->isMemberOf(":$var", "e.groups"));
112-
$qb->setParameter($var, $id);
113-
}
114-
$qb->andWhere($orExpr);
146+
$this->getPreparedForPaginationGroupsFilter($qb, $pagination->getFilter("groupsIds"), $groups);
115147
}
116148

117149
// Only exercises with given tags
@@ -125,6 +157,11 @@ public function getPreparedForPagination(Pagination $pagination, Groups $groups)
125157
->andWhere($qb->expr()->in("et.name", $tagNames));
126158
}
127159

160+
// Only exercises of specific RTEs (at least one RTE is present)
161+
if ($pagination->hasFilter("runtimeEnvironments", true)) { // true = not empty
162+
$this->getPreparedForPaginationEnvsFilter($qb, $pagination->getFilter("runtimeEnvironments"));
163+
}
164+
128165
if ($pagination->getOrderBy() === "name") {
129166
// Special patch, we need to load localized names from another entity ...
130167
// Note: This requires custom COALESCE_SUB, which is actually normal COALESCE function that allows subqueries inside in DQL

fixtures/demo/20-exerciseConfigs.neon

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ App\Model\Entity\ExerciseEnvironmentConfig:
1717
value: "*.java"
1818
'''
1919
- @demoAdmin
20+
demoMonoEnvironmentConfig:
21+
__construct:
22+
- @MonoRuntime
23+
- '''
24+
- name: "source-files"
25+
type: "file[]"
26+
value: "*.cs"
27+
'''
28+
- @demoAdmin
2029
2130
App\Model\Entity\ExerciseTest:
2231
demoExerciseTest1:

fixtures/demo/25-exercises.neon

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,14 @@ App\Model\Entity\Exercise:
8989
__construct:
9090
create:
9191
- @demoAdmin
92-
- @demoGroup
92+
- @demoChildGroup
9393
difficulty: "moderate"
9494
addLocalizedText:
9595
- @privateAdminExerciseText
9696
runtimeEnvironments:
9797
- @CRuntime
9898
- @JavaRuntime
99+
- @MonoRuntime
99100
hardwareGroups:
100101
- @demoHWGroup
101102
- @demoHWGroup2
@@ -104,6 +105,7 @@ App\Model\Entity\Exercise:
104105
exerciseEnvironmentConfigs:
105106
- @demoEnvironmentConfig
106107
- @demoJavaEnvironmentConfig
108+
- @demoMonoEnvironmentConfig
107109
exerciseTests:
108110
- @demoExerciseTest1
109111
- @demoExerciseTest2

tests/Presenters/ExercisesPresenter.phpt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use App\Model\Entity\Exercise;
77
use App\Model\Entity\ExerciseTag;
88
use App\Model\Entity\LocalizedExercise;
99
use App\Model\Entity\Pipeline;
10+
use App\Model\Entity\Group;
1011
use App\Security\AccessManager;
1112
use App\V1Module\Presenters\ExercisesPresenter;
1213
use Tester\Assert;
@@ -155,6 +156,39 @@ class TestExercisesPresenter extends Tester\TestCase
155156
Assert::count(4, $result['payload']['items']);
156157
}
157158

159+
public function testAdminListFilterGroupsExercises()
160+
{
161+
$token = PresenterTestHelper::login($this->container, $this->adminLogin);
162+
163+
$groups = array_filter($this->presenter->groups->findAll(), function (Group $g) {
164+
$texts = $g->getLocalizedTexts()->getValues();
165+
return reset($texts)->getName() === 'Demo group';
166+
});
167+
Assert::true(count($groups) === 1);
168+
$group = reset($groups);
169+
170+
$request = new Nette\Application\Request('V1:Exercises', 'GET', ['action' => 'default', 'filters' => [ 'groupsIds' => $group->getId() ] ]);
171+
$response = $this->presenter->run($request);
172+
Assert::type(Nette\Application\Responses\JsonResponse::class, $response);
173+
174+
$result = $response->getPayload();
175+
Assert::equal(200, $result['code']);
176+
Assert::count(6, $result['payload']['items']); // total 7 exercises, but one is in the child group (filtered out)
177+
}
178+
179+
public function testAdminListFilterEnvExercises()
180+
{
181+
$token = PresenterTestHelper::login($this->container, $this->adminLogin);
182+
183+
$request = new Nette\Application\Request('V1:Exercises', 'GET', ['action' => 'default', 'filters' => [ 'runtimeEnvironments' => 'mono' ] ]);
184+
$response = $this->presenter->run($request);
185+
Assert::type(Nette\Application\Responses\JsonResponse::class, $response);
186+
187+
$result = $response->getPayload();
188+
Assert::equal(200, $result['code']);
189+
Assert::count(1, $result['payload']['items']);
190+
}
191+
158192
public function testGetAllExercisesAuthors()
159193
{
160194
$instances = $this->instances->findAll();

0 commit comments

Comments
 (0)