From a10332721a84555e8f73921ef3952e944d2776ab Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Sun, 15 Dec 2024 22:10:46 +0100 Subject: [PATCH] Add pagination to submission lists Fixes #2511 --- webapp/composer.json | 1 + webapp/composer.lock | 258 +++++++++++++++++- webapp/config/bundles.php | 1 + webapp/config/packages/knp_paginator.yaml | 5 + .../src/Controller/API/MetricsController.php | 3 +- .../Controller/Jury/LanguageController.php | 3 +- .../src/Controller/Jury/ProblemController.php | 4 +- .../Controller/Jury/RejudgingController.php | 8 +- .../Jury/ShadowDifferencesController.php | 1 + .../Controller/Jury/SubmissionController.php | 83 ++++-- .../Jury/TeamCategoryController.php | 6 +- webapp/src/Controller/Jury/TeamController.php | 6 +- webapp/src/Controller/Jury/UserController.php | 3 +- webapp/src/Controller/Team/MiscController.php | 3 +- .../SubmissionRestriction.php | 72 +++-- .../src/Form/Type/SubmissionsFilterType.php | 18 +- webapp/src/Service/SubmissionService.php | 91 +++++- webapp/symfony.lock | 16 ++ webapp/templates/jury/language.html.twig | 1 + .../jury/partials/submission_list.html.twig | 11 +- webapp/templates/jury/problem.html.twig | 1 + webapp/templates/jury/rejudging.html.twig | 1 + .../jury/shadow_differences.html.twig | 1 + webapp/templates/jury/submissions.html.twig | 64 +---- webapp/templates/jury/team.html.twig | 1 + webapp/templates/jury/team_category.html.twig | 1 + webapp/templates/jury/user.html.twig | 1 + .../Jury/SubmissionControllerTest.php | 2 +- .../Integration/QueuetaskIntegrationTest.php | 4 +- 29 files changed, 533 insertions(+), 137 deletions(-) create mode 100644 webapp/config/packages/knp_paginator.yaml diff --git a/webapp/composer.json b/webapp/composer.json index be21703756..5d8b901bc8 100644 --- a/webapp/composer.json +++ b/webapp/composer.json @@ -67,6 +67,7 @@ "friendsofsymfony/rest-bundle": "^3.5", "ircmaxell/password-compat": "*", "jms/serializer-bundle": "^5.2", + "knplabs/knp-paginator-bundle": "^6.6", "league/commonmark": "^2.3", "mbostock/d3": "^3.5", "nelmio/api-doc-bundle": "^4.11", diff --git a/webapp/composer.lock b/webapp/composer.lock index 13daeedd38..e24711e652 100644 --- a/webapp/composer.lock +++ b/webapp/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "95d8ad1aada125b0c8078f1dccb08998", + "content-hash": "6401b4bdcb1db7464a2e9e4bcbf894b2", "packages": [ { "name": "apalfrey/select2-bootstrap-5-theme", @@ -2452,6 +2452,167 @@ ], "time": "2023-12-12T15:33:15+00:00" }, + { + "name": "knplabs/knp-components", + "version": "v5.1.0", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/knp-components.git", + "reference": "a1ccc001fd243670ca1e970356027a53d6fca2af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/knp-components/zipball/a1ccc001fd243670ca1e970356027a53d6fca2af", + "reference": "a1ccc001fd243670ca1e970356027a53d6fca2af", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/event-dispatcher-contracts": "^3.0" + }, + "conflict": { + "doctrine/dbal": "<3.8" + }, + "require-dev": { + "doctrine/dbal": "^3.8 || ^4.0", + "doctrine/mongodb-odm": "^2.5.5", + "doctrine/orm": "^2.13 || ^3.0", + "doctrine/phpcr-odm": "^1.8 || ^2.0", + "ext-pdo_sqlite": "*", + "jackalope/jackalope-doctrine-dbal": "^1.12 || ^2.0", + "phpunit/phpunit": "^10.5 || ^11.3", + "propel/propel1": "^1.7", + "ruflin/elastica": "^7.0", + "solarium/solarium": "^6.0", + "symfony/http-foundation": "^5.4.38 || ^6.4.4 || ^7.0", + "symfony/http-kernel": "^5.4.38 || ^6.4.4 || ^7.0", + "symfony/property-access": "^5.4.38 || ^6.4.4 || ^7.0" + }, + "suggest": { + "doctrine/common": "to allow usage pagination with Doctrine ArrayCollection", + "doctrine/mongodb-odm": "to allow usage pagination with Doctrine ODM MongoDB", + "doctrine/orm": "to allow usage pagination with Doctrine ORM", + "doctrine/phpcr-odm": "to allow usage pagination with Doctrine ODM PHPCR", + "propel/propel1": "to allow usage pagination with Propel ORM", + "ruflin/elastica": "to allow usage pagination with ElasticSearch Client", + "solarium/solarium": "to allow usage pagination with Solarium Client", + "symfony/http-foundation": "to retrieve arguments from Request", + "symfony/property-access": "to allow sorting arrays" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Knp\\Component\\": "src/Knp/Component" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KnpLabs Team", + "homepage": "https://knplabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/KnpLabs/knp-components/contributors" + } + ], + "description": "Knplabs component library", + "homepage": "https://github.com/KnpLabs/knp-components", + "keywords": [ + "components", + "knp", + "knplabs", + "pager", + "paginator" + ], + "support": { + "issues": "https://github.com/KnpLabs/knp-components/issues", + "source": "https://github.com/KnpLabs/knp-components/tree/v5.1.0" + }, + "time": "2024-09-20T12:03:01+00:00" + }, + { + "name": "knplabs/knp-paginator-bundle", + "version": "v6.6.1", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/KnpPaginatorBundle.git", + "reference": "1a00f88149d25418bd99d8954eba951f04cc3acf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/KnpPaginatorBundle/zipball/1a00f88149d25418bd99d8954eba951f04cc3acf", + "reference": "1a00f88149d25418bd99d8954eba951f04cc3acf", + "shasum": "" + }, + "require": { + "knplabs/knp-components": "^4.4 || ^5.0", + "php": "^8.1", + "symfony/config": "^6.4 || ^7.0", + "symfony/dependency-injection": "^6.4 || ^7.0", + "symfony/event-dispatcher": "^6.4 || ^7.0", + "symfony/http-foundation": "^6.4 || ^7.0", + "symfony/http-kernel": "^6.4 || ^7.0", + "symfony/routing": "^6.4 || ^7.0", + "symfony/translation": "^6.4 || ^7.0", + "twig/twig": "^3.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^10.5 || ^11.3", + "symfony/expression-language": "^6.4 || ^7.0", + "symfony/templating": "^6.4 || ^7.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Knp\\Bundle\\PaginatorBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KnpLabs Team", + "homepage": "https://knplabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/KnpLabs/KnpPaginatorBundle/contributors" + } + ], + "description": "Paginator bundle for Symfony to automate pagination and simplify sorting and other features", + "homepage": "https://github.com/KnpLabs/KnpPaginatorBundle", + "keywords": [ + "bundle", + "knp", + "knplabs", + "pager", + "pagination", + "paginator", + "symfony" + ], + "support": { + "issues": "https://github.com/KnpLabs/KnpPaginatorBundle/issues", + "source": "https://github.com/KnpLabs/KnpPaginatorBundle/tree/v6.6.1" + }, + "time": "2024-10-07T16:11:50+00:00" + }, { "name": "league/commonmark", "version": "2.4.2", @@ -9007,6 +9168,101 @@ ], "time": "2024-11-13T13:31:12+00:00" }, + { + "name": "symfony/translation", + "version": "v6.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "bee9bfabfa8b4045a66bf82520e492cddbaffa66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/bee9bfabfa8b4045a66bf82520e492cddbaffa66", + "reference": "bee9bfabfa8b4045a66bf82520e492cddbaffa66", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5|^3.0" + }, + "conflict": { + "symfony/config": "<5.4", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<5.4", + "symfony/yaml": "<5.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^4.18|^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v6.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-27T18:14:25+00:00" + }, { "name": "symfony/translation-contracts", "version": "v3.5.0", diff --git a/webapp/config/bundles.php b/webapp/config/bundles.php index 2d4ec46fc9..6c5a81e621 100644 --- a/webapp/config/bundles.php +++ b/webapp/config/bundles.php @@ -18,4 +18,5 @@ Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], Sentry\SentryBundle\SentryBundle::class => ['prod' => true], Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], + Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true], ]; diff --git a/webapp/config/packages/knp_paginator.yaml b/webapp/config/packages/knp_paginator.yaml new file mode 100644 index 0000000000..4ec90586bb --- /dev/null +++ b/webapp/config/packages/knp_paginator.yaml @@ -0,0 +1,5 @@ +knp_paginator: + default_options: + default_limit: 50 + template: + pagination: '@KnpPaginator/Pagination/bootstrap_v5_pagination.html.twig' diff --git a/webapp/src/Controller/API/MetricsController.php b/webapp/src/Controller/API/MetricsController.php index 215dea73bb..0b9e53da0d 100644 --- a/webapp/src/Controller/API/MetricsController.php +++ b/webapp/src/Controller/API/MetricsController.php @@ -102,7 +102,8 @@ public function prometheusAction(): Response /** @var Submission[] $submissions */ [$submissions, $submissionCounts] = $this->submissionService->getSubmissionList( [$contest->getCid() => $contest], - new SubmissionRestriction(visible: true) + new SubmissionRestriction(visible: true), + paginated: false ); foreach ($submissionCounts as $kind => $count) { if (!array_key_exists('submissions_' . $kind, $m)) { diff --git a/webapp/src/Controller/Jury/LanguageController.php b/webapp/src/Controller/Jury/LanguageController.php index 734bfc1f13..f9a2412a48 100644 --- a/webapp/src/Controller/Jury/LanguageController.php +++ b/webapp/src/Controller/Jury/LanguageController.php @@ -195,7 +195,8 @@ public function viewAction(Request $request, SubmissionService $submissionServic /** @var Submission[] $submissions */ [$submissions, $submissionCounts] = $submissionService->getSubmissionList( $this->dj->getCurrentContests(honorCookie: true), - new SubmissionRestriction(languageId: $language->getLangid()) + new SubmissionRestriction(languageId: $language->getLangid()), + page: $request->query->getInt('submissions_page', 1), ); $data = [ diff --git a/webapp/src/Controller/Jury/ProblemController.php b/webapp/src/Controller/Jury/ProblemController.php index d25cf7f3cf..d9256f2a6a 100644 --- a/webapp/src/Controller/Jury/ProblemController.php +++ b/webapp/src/Controller/Jury/ProblemController.php @@ -29,6 +29,7 @@ use Doctrine\ORM\NoResultException; use Doctrine\ORM\Query\Expr\Join; use Exception; +use Knp\Component\Pager\Pagination\PaginationInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -487,10 +488,11 @@ public function viewAction(Request $request, SubmissionService $submissionServic return $this->redirectToRoute('jury_problem', ['probId' => $probId]); } - /** @var Submission[] $submissions */ + /** @var PaginationInterface $submissions */ [$submissions, $submissionCounts] = $submissionService->getSubmissionList( $this->dj->getCurrentContests(honorCookie: true), new SubmissionRestriction(problemId: $problem->getProbid()), + page: $request->query->getInt('page', 1), ); $type = ''; diff --git a/webapp/src/Controller/Jury/RejudgingController.php b/webapp/src/Controller/Jury/RejudgingController.php index e73cbd76fa..96ac5ed604 100644 --- a/webapp/src/Controller/Jury/RejudgingController.php +++ b/webapp/src/Controller/Jury/RejudgingController.php @@ -26,6 +26,7 @@ use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Doctrine\ORM\Query\Expr\Join; +use Knp\Component\Pager\Pagination\PaginationInterface; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; @@ -330,7 +331,7 @@ public function viewAction( $verdictTable[$originalVerdict->getResult()][$newVerdict->getResult()][] = $submitid; } - $viewTypes = [0 => 'newest', 1 => 'unverified', 2 => 'unjudged', 3 => 'diff', 4 => 'all']; + $viewTypes = [0 => 'unverified', 1 => 'unjudged', 2 => 'diff', 3 => 'all']; $defaultView = 'diff'; $onlyAHandfulOfSubmissions = $rejudging->getSubmissions()->count() <= 5; if ($onlyAHandfulOfSubmissions) { @@ -362,10 +363,11 @@ public function viewAction( $restrictions->result = $newverdict; } - /** @var Submission[] $submissions */ + /** @var PaginationInterface $submissions */ [$submissions, $submissionCounts] = $submissionService->getSubmissionList( $this->dj->getCurrentContests(honorCookie: true), - $restrictions + $restrictions, + page: $request->query->getInt('page', 1), ); $repetitions = $this->em->createQueryBuilder() diff --git a/webapp/src/Controller/Jury/ShadowDifferencesController.php b/webapp/src/Controller/Jury/ShadowDifferencesController.php index ec80c8339d..6462a4c679 100644 --- a/webapp/src/Controller/Jury/ShadowDifferencesController.php +++ b/webapp/src/Controller/Jury/ShadowDifferencesController.php @@ -205,6 +205,7 @@ public function indexAction( [$submissions, $submissionCounts] = $this->submissions->getSubmissionList( $this->dj->getCurrentContests(honorCookie: true), $restrictions, + page: $request->query->getInt('page', 1), showShadowUnverified: true ); diff --git a/webapp/src/Controller/Jury/SubmissionController.php b/webapp/src/Controller/Jury/SubmissionController.php index d05db0aa47..02b2462a5a 100644 --- a/webapp/src/Controller/Jury/SubmissionController.php +++ b/webapp/src/Controller/Jury/SubmissionController.php @@ -36,6 +36,7 @@ use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Doctrine\ORM\Query\Expr\Join; +use Knp\Component\Pager\Pagination\PaginationInterface; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; @@ -79,7 +80,7 @@ public function indexAction( #[MapQueryParameter(name: 'view')] ?string $viewFromRequest = null, ): Response { - $viewTypes = [0 => 'newest', 1 => 'unverified', 2 => 'unjudged', 3 => 'judging', 4 => 'all']; + $viewTypes = [0 => 'all', 1 => 'unverified', 2 => 'unjudged', 3 => 'judging']; $view = 0; if (($submissionViewCookie = $this->dj->getCookie('domjudge_submissionview')) && isset($viewTypes[$submissionViewCookie])) { @@ -117,13 +118,68 @@ public function indexAction( $contests = [$contest->getCid() => $contest]; } - $latestCount = 50; + // Load preselected filters + $filtersFromCookie = Utils::jsonDecode((string)$this->dj->getCookie('domjudge_submissionsfilter') ?: '[]'); + + $formAssociationFields = [ + 'problem_id' => [Problem::class, 'probid'], + 'language_id' => [Language::class, 'langid'], + 'team_id' => [Team::class, 'teamid'], + 'category_id' => [TeamCategory::class, 'categoryid'], + 'affiliation_id' => [TeamAffiliation::class, 'affilid'], + ]; + + // Build the filter form. + $filtersForForm = ['result' => $filtersFromCookie['result'] ?? []]; + $hasFilters = !empty($filtersForForm['result']); + foreach ($formAssociationFields as $field => [$entityClass, $idField]) { + $filtersForForm[$field] = $this->em->getRepository($entityClass)->findBy([$idField => $filtersFromCookie[$field] ?? []]); + $hasFilters = $hasFilters || !empty($filtersForForm[$field]); + } + $appliedFilters = $filtersForForm; + $form = $this->createForm(SubmissionsFilterType::class, array_merge($filtersForForm, [ + "contests" => $contests, + ])); + + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $filtersForCookie = ['result' => $form->get('result')->getData()]; + $hasFilters = !empty($filtersForCookie['result']); + foreach ($formAssociationFields as $field => [$entityClass, $idField]) { + $method = 'get' . ucfirst($idField); + $filtersForCookie[$field] = array_map(fn($entity) => $entity->$method(), $form->get($field)->getData()); + $hasFilters = $hasFilters || !empty($filtersForCookie[$field]); + } + $response = $this->dj->setCookie('domjudge_submissionsfilter', Utils::jsonEncode($filtersForCookie), response: $response); + $appliedFilters = $filtersForCookie; + } - $limit = $viewTypes[$view] == 'newest' ? $latestCount : 0; + if (!empty($appliedFilters['result'])) { + $restrictions->results = $appliedFilters['result']; + } + if (!empty($appliedFilters['problem_id'])) { + $restrictions->problemIds = $appliedFilters['problem_id']; + } + if (!empty($appliedFilters['language_id'])) { + $restrictions->languageIds = $appliedFilters['language_id']; + } + if (!empty($appliedFilters['team_id'])) { + $restrictions->teamIds = $appliedFilters['team_id']; + } + if (!empty($appliedFilters['category_id'])) { + $restrictions->categoryIds = $appliedFilters['category_id']; + } + if (!empty($appliedFilters['affiliation_id'])) { + $restrictions->affiliationIds = $appliedFilters['affiliation_id']; + } - /** @var Submission[] $submissions */ + /** @var PaginationInterface $submissions */ [$submissions, $submissionCounts] = - $this->submissionService->getSubmissionList($contests, $restrictions, $limit); + $this->submissionService->getSubmissionList( + $contests, + $restrictions, + page: $request->query->getInt('page', 1), + ); $disabledProblems = []; $disabledLangs = []; foreach ($submissions as $submission) { @@ -135,9 +191,6 @@ public function indexAction( } } - // Load preselected filters - $filters = Utils::jsonDecode((string)$this->dj->getCookie('domjudge_submissionsfilter') ?: '[]'); - $results = array_keys($this->config->getVerdicts(['final', 'in_progress'])); $data = [ @@ -147,10 +200,10 @@ public function indexAction( 'submissions' => $submissions, 'submissionCounts' => $submissionCounts, 'showContest' => count($contests) > 1, - 'hasFilters' => !empty($filters), + 'hasFilters' => $hasFilters, 'results' => $results, 'showExternalResult' => $this->dj->shadowMode(), - 'showTestcases' => count($submissions) <= $latestCount, + 'showTestcases' => true, 'disabledProbs' => $disabledProblems, 'disabledLangs' => $disabledLangs, ]; @@ -160,16 +213,6 @@ public function indexAction( return $this->render('jury/partials/submission_list.html.twig', $data); } - // Build the filter form. - $filtersForForm = $filters; - $filtersForForm['problem-id'] = $this->em->getRepository(Problem::class)->findBy(['probid' => $filtersForForm['problem-id'] ?? []]); - $filtersForForm['language-id'] = $this->em->getRepository(Language::class)->findBy(['langid' => $filtersForForm['language-id'] ?? []]); - $filtersForForm['team-id'] = $this->em->getRepository(Team::class)->findBy(['teamid' => $filtersForForm['team-id'] ?? []]); - $filtersForForm['category-id'] = $this->em->getRepository(TeamCategory::class)->findBy(['categoryid' => $filtersForForm['category-id'] ?? []]); - $filtersForForm['affiliation-id'] = $this->em->getRepository(TeamAffiliation::class)->findBy(['affilid' => $filtersForForm['affiliation-id'] ?? []]); - $form = $this->createForm(SubmissionsFilterType::class, array_merge($filtersForForm, [ - "contests" => $contests, - ])); $data["form"] = $form->createView(); return $this->render('jury/submissions.html.twig', $data, $response); diff --git a/webapp/src/Controller/Jury/TeamCategoryController.php b/webapp/src/Controller/Jury/TeamCategoryController.php index 18f6255f0b..4a8205b5a1 100644 --- a/webapp/src/Controller/Jury/TeamCategoryController.php +++ b/webapp/src/Controller/Jury/TeamCategoryController.php @@ -16,6 +16,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; +use Knp\Component\Pager\Pagination\PaginationInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -125,10 +126,11 @@ public function viewAction(Request $request, SubmissionService $submissionServic throw new NotFoundHttpException(sprintf('Team category with ID %s not found', $categoryId)); } - /** @var Submission[] $submissions */ + /** @var PaginationInterface $submissions */ [$submissions, $submissionCounts] = $submissionService->getSubmissionList( $this->dj->getCurrentContests(honorCookie: true), - new SubmissionRestriction(categoryId: $teamCategory->getCategoryid()) + new SubmissionRestriction(categoryId: $teamCategory->getCategoryid()), + page: $request->query->getInt('page', 1), ); $data = [ diff --git a/webapp/src/Controller/Jury/TeamController.php b/webapp/src/Controller/Jury/TeamController.php index 8bbef797b7..bb1ad3aa31 100644 --- a/webapp/src/Controller/Jury/TeamController.php +++ b/webapp/src/Controller/Jury/TeamController.php @@ -286,7 +286,11 @@ public function viewAction( } $restrictions->teamId = $teamId; [$submissions, $submissionCounts] = - $submissionService->getSubmissionList($this->dj->getCurrentContests(honorCookie: true), $restrictions); + $submissionService->getSubmissionList( + $this->dj->getCurrentContests(honorCookie: true), + $restrictions, + page: $request->query->getInt('page', 1), + ); $data['restrictionText'] = $restrictionText; $data['submissions'] = $submissions; diff --git a/webapp/src/Controller/Jury/UserController.php b/webapp/src/Controller/Jury/UserController.php index de13152405..498373a020 100644 --- a/webapp/src/Controller/Jury/UserController.php +++ b/webapp/src/Controller/Jury/UserController.php @@ -171,7 +171,7 @@ public function indexAction(): Response } #[Route(path: '/{userId<\d+>}', name: 'jury_user')] - public function viewAction(int $userId, SubmissionService $submissionService): Response + public function viewAction(Request $request, int $userId, SubmissionService $submissionService): Response { $user = $this->em->getRepository(User::class)->find($userId); if (!$user) { @@ -182,6 +182,7 @@ public function viewAction(int $userId, SubmissionService $submissionService): R [$submissions, $submissionCounts] = $submissionService->getSubmissionList( $this->dj->getCurrentContests(honorCookie: true), new SubmissionRestriction(userId: $user->getUserid()), + page: $request->query->getInt('page', 1), ); return $this->render('jury/user.html.twig', [ diff --git a/webapp/src/Controller/Team/MiscController.php b/webapp/src/Controller/Team/MiscController.php index 701cf9fdab..9b09ac5ed4 100644 --- a/webapp/src/Controller/Team/MiscController.php +++ b/webapp/src/Controller/Team/MiscController.php @@ -89,7 +89,8 @@ public function homeAction(Request $request): Response $this->em->clear(); $data['submissions'] = $this->submissionService->getSubmissionList( [$contest->getCid() => $contest], - new SubmissionRestriction(teamId: $teamId) + new SubmissionRestriction(teamId: $teamId), + paginated: false )[0]; /** @var Clarification[] $clarifications */ diff --git a/webapp/src/DataTransferObject/SubmissionRestriction.php b/webapp/src/DataTransferObject/SubmissionRestriction.php index b6ff13c7fd..b62057d68a 100644 --- a/webapp/src/DataTransferObject/SubmissionRestriction.php +++ b/webapp/src/DataTransferObject/SubmissionRestriction.php @@ -5,35 +5,42 @@ class SubmissionRestriction { /** - * @param int|null $rejudgingId ID of a rejudging to filter on - * @param bool|null $verified If true, only return verified submissions - * If false, only return unverified or unjudged submissions - * @param bool|null $judged If true, only return judged submissions - * If false, only return unjudged submissions - * @param bool|null $judging If true, only return submissions currently being judged - * If false, only return submssions which are already judged or still - * need to be judged - * @param bool|null $rejudgingDifference If true, only return judgings that differ from their - * original result in final verdict. Vice versa if false - * @param int|null $teamId ID of a team to filter on - * @param int|null $categoryId ID of a category to filter on - * @param int|null $problemId ID of a problem to filter on - * @param string|null $languageId ID of a language to filter on - * @param string|null $judgehost Hostname of a judgehost to filter on - * @param string|null $oldResult Result of old judging to filter on - * @param string|null $result Result of current judging to filter on - * @param int|null $userId Filter on specific user - * @param bool|null $visible If true, only return submissions from visible teams - * @param bool|null $externalDifference If true, only return results with a difference with an - * external system - * If false, only return results without a difference with an - * external system - * @param string|null $externalResult Result in the external system - * @param bool|null $externallyJudged If true, only return externally judged submissions - * If false, only return externally unjudged submissions - * @param bool|null $externallyVerified If true, only return verified submissions - * If false, only return unverified or unjudged submissions - * @param bool|null $withExternalId If true, only return submissions with an external ID. + * @param int|null $rejudgingId ID of a rejudging to filter on + * @param bool|null $verified If true, only return verified submissions + * If false, only return unverified or unjudged submissions + * @param bool|null $judged If true, only return judged submissions + * If false, only return unjudged submissions + * @param bool|null $judging If true, only return submissions currently being judged + * If false, only return submssions which are already judged or still + * need to be judged + * @param bool|null $rejudgingDifference If true, only return judgings that differ from their + * original result in final verdict. Vice versa if false + * @param int|null $teamId ID of a team to filter on + * @param list|null $teamIds ID's of teams to filter on + * @param int|null $categoryId ID of a category to filter on + * @param list|null $categoryIds ID's of categories to filter on + * @param int|null $affiliationId ID of an affiliation to filter on + * @param list|null $affiliationIds ID's of affiliations to filter on + * @param int|null $problemId ID of a problem to filter on + * @param list|null $problemIds ID's of problems to filter on + * @param string|null $languageId ID of a language to filter on + * @param list|null $languageIds ID's of languages to filter on + * @param string|null $judgehost Hostname of a judgehost to filter on + * @param string|null $oldResult Result of old judging to filter on + * @param string|null $result Result of current judging to filter on + * @param list|null $results Results of current judging to filter on + * @param int|null $userId Filter on specific user + * @param bool|null $visible If true, only return submissions from visible teams + * @param bool|null $externalDifference If true, only return results with a difference with an + * external system + * If false, only return results without a difference with an + * external system + * @param string|null $externalResult Result in the external system + * @param bool|null $externallyJudged If true, only return externally judged submissions + * If false, only return externally unjudged submissions + * @param bool|null $externallyVerified If true, only return verified submissions + * If false, only return unverified or unjudged submissions + * @param bool|null $withExternalId If true, only return submissions with an external ID. */ public function __construct( public ?int $rejudgingId = null, @@ -42,12 +49,19 @@ public function __construct( public ?bool $judging = null, public ?bool $rejudgingDifference = null, public ?int $teamId = null, + public ?array $teamIds = [], public ?int $categoryId = null, + public ?array $categoryIds = [], + public ?int $affiliationId = null, + public ?array $affiliationIds = [], public ?int $problemId = null, + public ?array $problemIds = [], public ?string $languageId = null, + public ?array $languageIds = [], public ?string $judgehost = null, public ?string $oldResult = null, public ?string $result = null, + public ?array $results = [], public ?int $userId = null, public ?bool $visible = null, public ?bool $externalDifference = null, diff --git a/webapp/src/Form/Type/SubmissionsFilterType.php b/webapp/src/Form/Type/SubmissionsFilterType.php index 87ce7e003c..2392308985 100644 --- a/webapp/src/Form/Type/SubmissionsFilterType.php +++ b/webapp/src/Form/Type/SubmissionsFilterType.php @@ -15,6 +15,7 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ButtonType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\FormBuilderInterface; class SubmissionsFilterType extends AbstractType @@ -39,7 +40,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->addOrderBy("p.name") ->getQuery() ->getResult(); - $builder->add("problem-id", EntityType::class, [ + $builder->add("problem_id", EntityType::class, [ "multiple" => true, "label" => "Filter on problem(s)", "class" => Problem::class, @@ -48,7 +49,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void "choices" => $problems, "attr" => ["data-filter-field" => "problem-id"], ]); - $builder->add("language-id", EntityType::class, [ + $builder->add("language_id", EntityType::class, [ "multiple" => true, "label" => "Filter on language(s)", "class" => Language::class, @@ -60,7 +61,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->orderBy("l.name"), "attr" => ["data-filter-field" => "language-id"], ]); - $builder->add("category-id", EntityType::class, [ + $builder->add("category_id", EntityType::class, [ "multiple" => true, "label" => "Filter on category(s)", "class" => TeamCategory::class, @@ -71,7 +72,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->orderBy("tc.name"), "attr" => ["data-filter-field" => "category-id"], ]); - $builder->add("affiliation-id", EntityType::class, [ + $builder->add("affiliation_id", EntityType::class, [ "multiple" => true, "label" => "Filter on affiliation(s)", "class" => TeamAffiliation::class, @@ -108,7 +109,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void } $teams = $teamsQueryBuilder->getQuery()->getResult(); - $builder->add("team-id", EntityType::class, [ + $builder->add("team_id", EntityType::class, [ "multiple" => true, "label" => "Filter on team(s)", "class" => Team::class, @@ -127,7 +128,12 @@ public function buildForm(FormBuilderInterface $builder, array $options): void "attr" => ["data-filter-field" => "result"], ]); - $builder->add("clear", ButtonType::class, [ + $builder->add("apply", SubmitType::class, [ + "label" => "Apply filters", + + ]); + + $builder->add("clear", SubmitType::class, [ "label" => "Clear all filters", "attr" => ["class" => "btn-secondary"], ]); diff --git a/webapp/src/Service/SubmissionService.php b/webapp/src/Service/SubmissionService.php index e6f63205b5..52d95befa0 100644 --- a/webapp/src/Service/SubmissionService.php +++ b/webapp/src/Service/SubmissionService.php @@ -21,6 +21,8 @@ use Doctrine\ORM\NoResultException; use Doctrine\ORM\Query\Expr\Join; use InvalidArgumentException; +use Knp\Component\Pager\Pagination\PaginationInterface; +use Knp\Component\Pager\PaginatorInterface; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -49,7 +51,8 @@ public function __construct( protected readonly DOMJudgeService $dj, protected readonly ConfigurationService $config, protected readonly EventLogService $eventLogService, - protected readonly ScoreboardService $scoreboardService + protected readonly ScoreboardService $scoreboardService, + protected readonly PaginatorInterface $paginator, ) {} /** @@ -58,8 +61,8 @@ public function __construct( * * @param Contest[] $contests * - * @return array{Submission[], array} array An array with - * two elements: the first one is the list of submissions + * @return array{Submission[], array}|array{PaginationInterface, array} array An array with + * two elements: the first one is the list of submissions or the paginated results * and the second one is an array with counts. * @throws NoResultException * @throws NonUniqueResultException @@ -67,10 +70,14 @@ public function __construct( public function getSubmissionList( array $contests, SubmissionRestriction $restrictions, - int $limit = 0, - bool $showShadowUnverified = false + bool $paginated = true, + ?int $page = null, + bool $showShadowUnverified = false, ): array { if (empty($contests)) { + if ($paginated) { + return [$this->paginator->paginate([], page: 1), []]; + } return [[], []]; } @@ -84,10 +91,6 @@ public function getSubmissionList( ->orderBy('s.submittime', 'DESC') ->addOrderBy('s.submitid', 'DESC'); - if ($limit > 0) { - $queryBuilder->setMaxResults($limit); - } - if ($restrictions->withExternalId ?? false) { $queryBuilder ->andWhere('s.externalid IS NOT NULL') @@ -196,6 +199,12 @@ public function getSubmissionList( ->setParameter('teamid', $restrictions->teamId); } + if (!empty($restrictions->teamIds)) { + $queryBuilder + ->andWhere('s.team IN (:teamids)') + ->setParameter('teamids', $restrictions->teamIds); + } + if (isset($restrictions->userId)) { $queryBuilder ->andWhere('s.user = :userid') @@ -208,6 +217,24 @@ public function getSubmissionList( ->setParameter('categoryid', $restrictions->categoryId); } + if (!empty($restrictions->categoryIds)) { + $queryBuilder + ->andWhere('t.category IN (:categoryids)') + ->setParameter('categoryids', $restrictions->categoryIds); + } + + if (isset($restrictions->affiliationId)) { + $queryBuilder + ->andWhere('t.affiliation = :affiliationid') + ->setParameter('affiliationid', $restrictions->affiliationId); + } + + if (!empty($restrictions->affiliationIds)) { + $queryBuilder + ->andWhere('t.affiliation IN (:affiliationids)') + ->setParameter('affiliationids', $restrictions->affiliationIds); + } + if (isset($restrictions->visible)) { $queryBuilder ->innerJoin('t.category', 'cat') @@ -220,12 +247,24 @@ public function getSubmissionList( ->setParameter('probid', $restrictions->problemId); } + if (!empty($restrictions->problemIds)) { + $queryBuilder + ->andWhere('s.problem IN (:probids)') + ->setParameter('probids', $restrictions->problemIds); + } + if (isset($restrictions->languageId)) { $queryBuilder ->andWhere('s.language = :langid') ->setParameter('langid', $restrictions->languageId); } + if (!empty($restrictions->languageIds)) { + $queryBuilder + ->andWhere('s.language IN (:langids)') + ->setParameter('langids', $restrictions->languageIds); + } + if (isset($restrictions->judgehost)) { $queryBuilder ->andWhere('s.judgehost = :judgehost') @@ -246,6 +285,26 @@ public function getSubmissionList( } } + if (!empty($restrictions->results)) { + $resultsContainJudging = in_array('judging', $restrictions->results, true); + $resultsContainQueued = in_array('queued', $restrictions->results, true); + $resultsContainImportError = in_array('import-error', $restrictions->results, true); + $resultsQuery = 'j.result IN (:results)'; + if ($resultsContainJudging) { + $resultsQuery .= ' OR (j.result IS NULL AND j.starttime IS NOT NULL AND s.importError IS NULL)'; + } + if ($resultsContainQueued) { + $resultsQuery .= ' OR (j.result IS NULL AND j.starttime IS NULL AND s.importError IS NULL)'; + } + if ($resultsContainImportError) { + $resultsQuery .= ' OR s.importError IS NOT NULL'; + } + + $queryBuilder + ->andWhere($resultsQuery) + ->setParameter('results', $restrictions->results); + } + if ($this->dj->shadowMode()) { // When we are shadow, also load the external results $queryBuilder @@ -253,8 +312,16 @@ public function getSubmissionList( ->addSelect('ej'); } - $submissions = $queryBuilder->getQuery()->getResult(); + if ($paginated) { + $submissions = $this->paginator->paginate($queryBuilder, page: $page ?? 1); + } else { + $submissions = $queryBuilder->getQuery()->getResult(); + } if (isset($restrictions->rejudgingId)) { + $paginatedSubmissions = $submissions; + if ($paginated) { + $submissions = $submissions->getItems(); + } // Doctrine will return an array for each item. At index '0' will // be the submission and at index 'oldresult' will be the old // result. Remap this. @@ -264,6 +331,10 @@ public function getSubmissionList( $submission->setOldResult($submissionData['oldresult']); return $submission; }, $submissions); + if ($paginated) { + $paginatedSubmissions->setItems($submissions); + $submissions = $paginatedSubmissions; + } } $counts = []; diff --git a/webapp/symfony.lock b/webapp/symfony.lock index 12a904d36c..271ce89e2b 100644 --- a/webapp/symfony.lock +++ b/webapp/symfony.lock @@ -158,6 +158,9 @@ "config/packages/jms_serializer.yaml" ] }, + "knplabs/knp-paginator-bundle": { + "version": "v6.6.1" + }, "laminas/laminas-code": { "version": "3.4.1" }, @@ -632,6 +635,19 @@ "symfony/string": { "version": "v5.2.3" }, + "symfony/translation": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.3", + "ref": "e28e27f53663cc34f0be2837aba18e3a1bef8e7b" + }, + "files": [ + "config/packages/translation.yaml", + "translations/.gitignore" + ] + }, "symfony/translation-contracts": { "version": "v1.1.6" }, diff --git a/webapp/templates/jury/language.html.twig b/webapp/templates/jury/language.html.twig index 6991e61b8f..974a996a4e 100644 --- a/webapp/templates/jury/language.html.twig +++ b/webapp/templates/jury/language.html.twig @@ -7,6 +7,7 @@ {{ parent() }} {{ macros.table_extrahead() }} {{ macros.toggle_extrahead() }} + {{ knp_pagination_rel_links(submissions) }} {% endblock %} {% block content %} diff --git a/webapp/templates/jury/partials/submission_list.html.twig b/webapp/templates/jury/partials/submission_list.html.twig index 52e0fb2af3..4493e5867d 100644 --- a/webapp/templates/jury/partials/submission_list.html.twig +++ b/webapp/templates/jury/partials/submission_list.html.twig @@ -99,14 +99,7 @@ {% set affilid = '' %} {% endif %} - + {% if showExternalResult and showExternalTestcases %} @@ -291,4 +284,6 @@ + + {{ knp_pagination_render(submissions) }} {% endif %} diff --git a/webapp/templates/jury/problem.html.twig b/webapp/templates/jury/problem.html.twig index 70de500b0f..a58f7cc4d2 100644 --- a/webapp/templates/jury/problem.html.twig +++ b/webapp/templates/jury/problem.html.twig @@ -7,6 +7,7 @@ {{ parent() }} {{ macros.table_extrahead() }} {{ macros.toggle_extrahead() }} + {{ knp_pagination_rel_links(submissions) }} {% endblock %} {% block content %} diff --git a/webapp/templates/jury/rejudging.html.twig b/webapp/templates/jury/rejudging.html.twig index f8f03e145c..de88fed0bd 100644 --- a/webapp/templates/jury/rejudging.html.twig +++ b/webapp/templates/jury/rejudging.html.twig @@ -7,6 +7,7 @@ {{ parent() }} {{ macros.table_extrahead() }} {{ macros.select2_extrahead() }} + {{ knp_pagination_rel_links(submissions) }} {% endblock %} {% block content %} diff --git a/webapp/templates/jury/shadow_differences.html.twig b/webapp/templates/jury/shadow_differences.html.twig index d0e79cd0fe..5934bcf17b 100644 --- a/webapp/templates/jury/shadow_differences.html.twig +++ b/webapp/templates/jury/shadow_differences.html.twig @@ -7,6 +7,7 @@ {{ parent() }} {{ macros.table_extrahead() }} {{ macros.select2_extrahead() }} + {{ knp_pagination_rel_links(submissions) }} {% endblock %} {% block content %} diff --git a/webapp/templates/jury/submissions.html.twig b/webapp/templates/jury/submissions.html.twig index 41a559a41b..d1515d6fa0 100644 --- a/webapp/templates/jury/submissions.html.twig +++ b/webapp/templates/jury/submissions.html.twig @@ -7,6 +7,7 @@ {{ parent() }} {{ macros.table_extrahead() }} {{ macros.select2_extrahead() }} + {{ knp_pagination_rel_links(submissions) }} {% endblock %} {% block content %} @@ -32,7 +33,18 @@
- {{ form(form) }} + {{ form_start(form) }} + {{ form_row(form.problem_id) }} + {{ form_row(form.language_id) }} + {{ form_row(form.category_id) }} + {{ form_row(form.affiliation_id) }} + {{ form_row(form.team_id) }} + {{ form_row(form.result) }} +
+ {{ form_widget(form.apply) }} + {{ form_widget(form.clear) }} +
+ {{ form_end(form) }}
@@ -58,7 +70,7 @@ {% endif %} -
+
{%- include 'jury/partials/submission_list.html.twig' %}
@@ -82,54 +94,6 @@ $('#submissions_filter_clear').on('click', function () { $('select[data-filter-field]').val([]).trigger('change'); }); - - window.process_submissions_filter = function () { - var $trs = $('table.submissions-table > tbody tr'); - - var filters = []; - - $('select[data-filter-field]').each(function () { - var $filterField = $(this); - if ($filterField.val().length) { - filters.push({ - field: $filterField.data('filter-field'), - values: $filterField.val() - }); - } - }); - - var submissions_filter = {}; - for (var i = 0; i < filters.length; i++) { - submissions_filter[filters[i].field] = filters[i].values; - } - - setCookie('domjudge_submissionsfilter', JSON.stringify(submissions_filter)); - - if (filters.length === 0) { - $trs.show(); - } else { - $trs - .hide() - .filter(function () { - var $tr = $(this); - - for (var i = 0; i < filters.length; i++) { - var value = "" + $tr.data(filters[i].field); - if (filters[i].values.indexOf(value) === -1) { - return false; - } - } - - return true; - }) - .show(); - } - - $('table.submissions-table').find('[data-bs-toggle="tooltip"]').tooltip(); - }; - - $('select[data-filter-field]').on('change', process_submissions_filter); - window.process_submissions_filter(); }); {% endblock %} diff --git a/webapp/templates/jury/team.html.twig b/webapp/templates/jury/team.html.twig index c52a09ec0d..8db6ef1f1b 100644 --- a/webapp/templates/jury/team.html.twig +++ b/webapp/templates/jury/team.html.twig @@ -6,6 +6,7 @@ {% block extrahead %} {{ parent() }} {{ macros.table_extrahead() }} + {{ knp_pagination_rel_links(submissions) }} {% endblock %} {% block content %} diff --git a/webapp/templates/jury/team_category.html.twig b/webapp/templates/jury/team_category.html.twig index e1f902668e..22b2790610 100644 --- a/webapp/templates/jury/team_category.html.twig +++ b/webapp/templates/jury/team_category.html.twig @@ -6,6 +6,7 @@ {% block extrahead %} {{ parent() }} {{ macros.table_extrahead() }} + {{ knp_pagination_rel_links(submissions) }} {% endblock %} {% block content %} diff --git a/webapp/templates/jury/user.html.twig b/webapp/templates/jury/user.html.twig index 2982b85c76..a8a071a8f3 100644 --- a/webapp/templates/jury/user.html.twig +++ b/webapp/templates/jury/user.html.twig @@ -6,6 +6,7 @@ {% block extrahead %} {{ parent() }} {{ macros.table_extrahead() }} + {{ knp_pagination_rel_links(submissions) }} {% endblock %} {% block content %} diff --git a/webapp/tests/Unit/Controller/Jury/SubmissionControllerTest.php b/webapp/tests/Unit/Controller/Jury/SubmissionControllerTest.php index 3ef6a7b9b2..4db442dc9c 100644 --- a/webapp/tests/Unit/Controller/Jury/SubmissionControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/SubmissionControllerTest.php @@ -33,7 +33,7 @@ public function testIndexViewFilter(string $filter, array $fixtures): void public function provideViews(): Generator { foreach ([[], [SampleSubmissionsFixture::class]] as $fixtures) { - foreach (['all', 'unjudged', 'unverified', 'newest'] as $view) { + foreach (['all', 'unjudged', 'unverified'] as $view) { yield [$view, $fixtures]; } } diff --git a/webapp/tests/Unit/Integration/QueuetaskIntegrationTest.php b/webapp/tests/Unit/Integration/QueuetaskIntegrationTest.php index 7285d35b21..63c4d13df4 100644 --- a/webapp/tests/Unit/Integration/QueuetaskIntegrationTest.php +++ b/webapp/tests/Unit/Integration/QueuetaskIntegrationTest.php @@ -19,6 +19,7 @@ use App\Service\SubmissionService; use App\Utils\Utils; use Doctrine\ORM\EntityManagerInterface; +use Knp\Component\Pager\PaginatorInterface; use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -79,7 +80,8 @@ protected function setUp(): void $dj, $this->config, self::getContainer()->get(EventLogService::class), - $this->scoreboardService + $this->scoreboardService, + self::getContainer()->get(PaginatorInterface::class) ); // Create a contest, problems and teams for which to test the