Skip to content

Commit

Permalink
Adding new filter (selecting runtime environments) for exercise pagin…
Browse files Browse the repository at this point in the history
…ation endpoint.
  • Loading branch information
Martin Krulis committed Nov 24, 2019
1 parent 948228f commit a8b8c7d
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 21 deletions.
2 changes: 1 addition & 1 deletion app/V1Module/presenters/ExercisesPresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public function checkDefault() {
*/
public function actionDefault(int $offset = 0, int $limit = null, string $orderBy = null, array $filters = null, string $locale = null) {
$pagination = $this->getPagination($offset, $limit, $locale, $orderBy,
($filters === null) ? [] : $filters, ['search', 'instanceId', 'groupsIds', 'authorsIds', 'tags']);
($filters === null) ? [] : $filters, ['search', 'instanceId', 'groupsIds', 'authorsIds', 'tags', 'runtimeEnvironments']);

// Get all matching exercises and filter them by ACLs...
$exercises = $this->exercises->getPreparedForPagination($pagination, $this->groups);
Expand Down
5 changes: 3 additions & 2 deletions app/V1Module/router/RouterFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,10 @@ private static function createExercisesRoutes(string $prefix): RouteList {
$router = new RouteList();
$router[] = new GetRoute("$prefix", "Exercises:");
$router[] = new PostRoute("$prefix", "Exercises:create");
$router[] = new GetRoute("$prefix/authors", "Exercises:authors");
$router[] = new PostRoute("$prefix/list", "Exercises:listByIds");
$router[] = new GetRoute("$prefix/authors", "Exercises:authors");
$router[] = new GetRoute("$prefix/tags", "Exercises:allTags");

$router[] = new GetRoute("$prefix/<id>", "Exercises:detail");
$router[] = new DeleteRoute("$prefix/<id>", "Exercises:remove");
$router[] = new PostRoute("$prefix/<id>", "Exercises:updateDetail");
Expand All @@ -140,7 +142,6 @@ private static function createExercisesRoutes(string $prefix): RouteList {
$router[] = new DeleteRoute("$prefix/<id>/groups/<groupId>", "Exercises:detachGroup");
$router[] = new PostRoute("$prefix/<id>/pipelines/<pipelineId>", "Exercises:attachPipeline");
$router[] = new DeleteRoute("$prefix/<id>/pipelines/<pipelineId>", "Exercises:detachPipeline");
$router[] = new GetRoute("$prefix/tags", "Exercises:allTags");
$router[] = new PostRoute("$prefix/<id>/tags/<name>", "Exercises:addTag");
$router[] = new DeleteRoute("$prefix/<id>/tags/<name>", "Exercises:removeTag");

Expand Down
10 changes: 8 additions & 2 deletions app/helpers/Pagination.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,16 @@ public function getOriginalOrderBy(): ?string {

/**
* True if filter of given name is present.
* @param string $name Identifier of the filter
* @param bool $notEmpty If true, the filter value must also be not empty.
*/
public function hasFilter(string $name)
public function hasFilter(string $name, bool $notEmpty = false)
{
return array_key_exists($name, $this->filters);
if ($notEmpty) {
return !empty($this->filters[$name]);
} else {
return array_key_exists($name, $this->filters);
}
}

/**
Expand Down
67 changes: 52 additions & 15 deletions app/model/repository/Exercises.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Kdyby\Doctrine\EntityManager;
use App\Model\Entity\Exercise;
use App\Model\Entity\LocalizedExercise;
Expand Down Expand Up @@ -62,12 +63,56 @@ public function searchByName(?string $search): array {
});
}

/**
* Augment given query builder and add filter that covers groups of residence of the exercise.
* @param QueryBuilder $qb
* @param mixed $groupsIds Value of the filter
* @param Groups $groups Doctrine groups repository
*/
private function getPreparedForPaginationGroupsFilter(QueryBuilder $qb, $groupsIds, Groups $groups)
{
if (!is_array($groupsIds)) {
$groupsIds = [ $groupsIds ];
}

// Each group has a separate OR clause ...
$orExpr = $qb->expr()->orX();
$counter = 0;
foreach ($groups->groupsIdsAncestralClosure($groupsIds) as $id) {
$var = "group" . ++$counter;
$orExpr->add($qb->expr()->isMemberOf(":$var", "e.groups"));
$qb->setParameter($var, $id);
}
$qb->andWhere($orExpr);
}

/**
* Augment given query builder and add filter that handles runtime environments.
* @param QueryBuilder $qb
* @param mixed $groupsIds Value of the filter
* @param Groups $groups Doctrine groups repository
*/
private function getPreparedForPaginationEnvsFilter(QueryBuilder $qb, $envs)
{
if (!is_array($envs)) {
$envs = [ $envs ];
}

$orExpr = $qb->expr()->orX();
$counter = 0;
foreach ($envs as $env) {
$var = "env" . ++$counter;
$orExpr->add($qb->expr()->isMemberOf(":$var", "e.runtimeEnvironments"));
$qb->setParameter($var, $env);
}
$qb->andWhere($orExpr);
}

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

// Only exercises in explicitly given groups (or their ascendants) ...
if ($pagination->hasFilter("groupsIds")) {
$groupsIds = $pagination->getFilter("groupsIds");
if (!is_array($groupsIds)) {
$groupsIds = [ $groupsIds ];
}

// Each group has a separate OR clause ...
$orExpr = $qb->expr()->orX();
$gcounter = 0;
foreach ($groups->groupsIdsAncestralClosure($groupsIds) as $id) {
$var = "group" . ++$gcounter;
$orExpr->add($qb->expr()->isMemberOf(":$var", "e.groups"));
$qb->setParameter($var, $id);
}
$qb->andWhere($orExpr);
$this->getPreparedForPaginationGroupsFilter($qb, $pagination->getFilter("groupsIds"), $groups);
}

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

// Only exercises of specific RTEs (at least one RTE is present)
if ($pagination->hasFilter("runtimeEnvironments", true)) { // true = not empty
$this->getPreparedForPaginationEnvsFilter($qb, $pagination->getFilter("runtimeEnvironments"));
}

if ($pagination->getOrderBy() === "name") {
// Special patch, we need to load localized names from another entity ...
// Note: This requires custom COALESCE_SUB, which is actually normal COALESCE function that allows subqueries inside in DQL
Expand Down
9 changes: 9 additions & 0 deletions fixtures/demo/20-exerciseConfigs.neon
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ App\Model\Entity\ExerciseEnvironmentConfig:
value: "*.java"
'''
- @demoAdmin
demoMonoEnvironmentConfig:
__construct:
- @MonoRuntime
- '''
- name: "source-files"
type: "file[]"
value: "*.cs"
'''
- @demoAdmin
App\Model\Entity\ExerciseTest:
demoExerciseTest1:
Expand Down
4 changes: 3 additions & 1 deletion fixtures/demo/25-exercises.neon
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,14 @@ App\Model\Entity\Exercise:
__construct:
create:
- @demoAdmin
- @demoGroup
- @demoChildGroup
difficulty: "moderate"
addLocalizedText:
- @privateAdminExerciseText
runtimeEnvironments:
- @CRuntime
- @JavaRuntime
- @MonoRuntime
hardwareGroups:
- @demoHWGroup
- @demoHWGroup2
Expand All @@ -104,6 +105,7 @@ App\Model\Entity\Exercise:
exerciseEnvironmentConfigs:
- @demoEnvironmentConfig
- @demoJavaEnvironmentConfig
- @demoMonoEnvironmentConfig
exerciseTests:
- @demoExerciseTest1
- @demoExerciseTest2
Expand Down
34 changes: 34 additions & 0 deletions tests/Presenters/ExercisesPresenter.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use App\Model\Entity\Exercise;
use App\Model\Entity\ExerciseTag;
use App\Model\Entity\LocalizedExercise;
use App\Model\Entity\Pipeline;
use App\Model\Entity\Group;
use App\Security\AccessManager;
use App\V1Module\Presenters\ExercisesPresenter;
use Tester\Assert;
Expand Down Expand Up @@ -155,6 +156,39 @@ class TestExercisesPresenter extends Tester\TestCase
Assert::count(4, $result['payload']['items']);
}

public function testAdminListFilterGroupsExercises()
{
$token = PresenterTestHelper::login($this->container, $this->adminLogin);

$groups = array_filter($this->presenter->groups->findAll(), function (Group $g) {
$texts = $g->getLocalizedTexts()->getValues();
return reset($texts)->getName() === 'Demo group';
});
Assert::true(count($groups) === 1);
$group = reset($groups);

$request = new Nette\Application\Request('V1:Exercises', 'GET', ['action' => 'default', 'filters' => [ 'groupsIds' => $group->getId() ] ]);
$response = $this->presenter->run($request);
Assert::type(Nette\Application\Responses\JsonResponse::class, $response);

$result = $response->getPayload();
Assert::equal(200, $result['code']);
Assert::count(6, $result['payload']['items']); // total 7 exercises, but one is in the child group (filtered out)
}

public function testAdminListFilterEnvExercises()
{
$token = PresenterTestHelper::login($this->container, $this->adminLogin);

$request = new Nette\Application\Request('V1:Exercises', 'GET', ['action' => 'default', 'filters' => [ 'runtimeEnvironments' => 'mono' ] ]);
$response = $this->presenter->run($request);
Assert::type(Nette\Application\Responses\JsonResponse::class, $response);

$result = $response->getPayload();
Assert::equal(200, $result['code']);
Assert::count(1, $result['payload']['items']);
}

public function testGetAllExercisesAuthors()
{
$instances = $this->instances->findAll();
Expand Down

0 comments on commit a8b8c7d

Please sign in to comment.