From 41b6821a18ec975cbab6430008ac3f971644d187 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Tue, 7 Oct 2025 20:36:28 +0200 Subject: [PATCH 1/2] Add AjaxTemplate attribute + listener and listener for setting custom response object --- webapp/src/Twig/Attribute/AjaxTemplate.php | 25 ++++++++++++++ .../AjaxTemplateAttributeListener.php | 34 +++++++++++++++++++ .../EventListener/CustomResponseListener.php | 33 ++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 webapp/src/Twig/Attribute/AjaxTemplate.php create mode 100644 webapp/src/Twig/EventListener/AjaxTemplateAttributeListener.php create mode 100644 webapp/src/Twig/EventListener/CustomResponseListener.php diff --git a/webapp/src/Twig/Attribute/AjaxTemplate.php b/webapp/src/Twig/Attribute/AjaxTemplate.php new file mode 100644 index 0000000000..e53cd7401f --- /dev/null +++ b/webapp/src/Twig/Attribute/AjaxTemplate.php @@ -0,0 +1,25 @@ +|null $vars + */ + public function __construct( + string $normalTemplate, + public string $ajaxTemplate, + ?array $vars = null, + bool $stream = false, + ) { + parent::__construct($normalTemplate, $vars, $stream); + } +} diff --git a/webapp/src/Twig/EventListener/AjaxTemplateAttributeListener.php b/webapp/src/Twig/EventListener/AjaxTemplateAttributeListener.php new file mode 100644 index 0000000000..cb40d67005 --- /dev/null +++ b/webapp/src/Twig/EventListener/AjaxTemplateAttributeListener.php @@ -0,0 +1,34 @@ + -128 to come before TemplateAttributeListener + #[AsEventListener] + public function onKernelView(ViewEvent $event): void + { + // Based on Symfony's TemplateAttributeListener + + /** @var AjaxTemplate|null $ajaxTemplateAttribute */ + $ajaxTemplateAttribute = $event->controllerArgumentsEvent?->getAttributes(AjaxTemplate::class)[0] ?? null; + if (!$ajaxTemplateAttribute) { + return; + } + + // Create the template attribute that Symfony can use + $template = new Template( + template: $event->getRequest()->isXmlHttpRequest() ? $ajaxTemplateAttribute->ajaxTemplate : $ajaxTemplateAttribute->template, + vars: $ajaxTemplateAttribute->vars, + stream: $ajaxTemplateAttribute->stream, + ); + $attributes = $event->controllerArgumentsEvent?->getAttributes(); + $attributes[Template::class] = [$template]; + $event->controllerArgumentsEvent?->setController($event->controllerArgumentsEvent->getController(), $attributes); + } +} diff --git a/webapp/src/Twig/EventListener/CustomResponseListener.php b/webapp/src/Twig/EventListener/CustomResponseListener.php new file mode 100644 index 0000000000..4b52233b71 --- /dev/null +++ b/webapp/src/Twig/EventListener/CustomResponseListener.php @@ -0,0 +1,33 @@ +response = $response; + } + + #[AsEventListener] + public function onKernelResponse(ResponseEvent $event): void + { + if ($this->response) { + $this->response->setContent($event->getResponse()->getContent()); + $event->setResponse($this->response); + + // Make sure to clear the response if we have more requests after this one. + $this->response = null; + } + } +} From ec6e7009586781fe2eb4a98774396dce647b6112 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Tue, 7 Oct 2025 20:36:52 +0200 Subject: [PATCH 2/2] Use attribute to render templates, simplifying controller code --- .../Controller/Jury/AnalysisController.php | 50 +++++++---- .../Controller/Jury/AuditLogController.php | 21 +++-- .../src/Controller/Jury/BalloonController.php | 24 ++++-- .../Jury/ClarificationController.php | 43 ++++++++-- .../src/Controller/Jury/ConfigController.php | 59 ++++++++++--- .../src/Controller/Jury/ContestController.php | 68 +++++++++++---- .../Controller/Jury/ExecutableController.php | 71 ++++++++++++---- .../Jury/ExternalContestController.php | 47 +++++++--- .../Jury/ImportExportController.php | 51 +++++++++-- .../Jury/InternalErrorController.php | 46 ++++++++-- .../Controller/Jury/JudgehostController.php | 59 +++++++++---- .../Controller/Jury/JuryMiscController.php | 46 ++++++++-- .../Controller/Jury/LanguageController.php | 68 ++++++++++++--- .../src/Controller/Jury/PrintController.php | 12 ++- .../src/Controller/Jury/ProblemController.php | 74 +++++++++++----- .../Controller/Jury/QueueTaskController.php | 32 +++++-- .../Controller/Jury/RejudgingController.php | 55 ++++++++---- .../Controller/Jury/ScoreboardController.php | 13 ++- .../Jury/ShadowDifferencesController.php | 29 +++++-- .../Controller/Jury/SubmissionController.php | 75 +++++++++++++--- .../Jury/TeamAffiliationController.php | 56 +++++++++--- .../Jury/TeamCategoryController.php | 62 +++++++++++--- webapp/src/Controller/Jury/TeamController.php | 52 +++++++++--- webapp/src/Controller/Jury/UserController.php | 85 ++++++++++++++----- .../src/Controller/Jury/VersionController.php | 17 +++- webapp/src/Controller/PublicController.php | 60 +++++++++---- webapp/src/Controller/SecurityController.php | 35 ++++++-- .../Team/ClarificationController.php | 53 +++++++----- .../Controller/Team/LanguageController.php | 10 ++- webapp/src/Controller/Team/MiscController.php | 19 +++-- .../src/Controller/Team/ProblemController.php | 15 +++- .../Controller/Team/ScoreboardController.php | 37 +++++--- .../Controller/Team/SubmissionController.php | 45 ++++++---- 33 files changed, 1137 insertions(+), 352 deletions(-) diff --git a/webapp/src/Controller/Jury/AnalysisController.php b/webapp/src/Controller/Jury/AnalysisController.php index 26fa10b436..b87ae085f3 100644 --- a/webapp/src/Controller/Jury/AnalysisController.php +++ b/webapp/src/Controller/Jury/AnalysisController.php @@ -11,6 +11,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query\Expr; use Symfony\Bridge\Doctrine\Attribute\MapEntity; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; @@ -27,11 +28,24 @@ public function __construct( private readonly EntityManagerInterface $em ) {} + /** + * @return array{error: string}|array{ + * contest: mixed, + * problems: mixed, + * teams: mixed, + * submissions: mixed, + * delayed_judgings: array{data: mixed, overflow: int, delay: int}, + * misc: mixed, + * filters: array, + * view: string + * }|Response + */ + #[Template(template: 'jury/analysis/contest_overview.html.twig')] #[Route(path: '', name: 'analysis_index')] public function indexAction( #[MapQueryParameter] ?string $view = null - ): Response { + ): array|Response { $em = $this->em; $contest = $this->dj->getCurrentContest(); @@ -66,7 +80,7 @@ public function indexAction( ->orderBy('timediff', 'DESC') ->getQuery()->getResult(); - return $this->render('jury/analysis/contest_overview.html.twig', [ + return [ 'contest' => $contest, 'problems' => $problems, 'teams' => $teams, @@ -79,11 +93,15 @@ public function indexAction( 'misc' => $misc, 'filters' => StatisticsService::FILTERS, 'view' => $view, - ]); + ]; } + /** + * @return array|Response + */ + #[Template(template: 'jury/analysis/team.html.twig')] #[Route(path: '/team/{team}', name: 'analysis_team')] - public function teamAction(Team $team): Response + public function teamAction(Team $team): array|Response { $contest = $this->dj->getCurrentContest(); @@ -93,18 +111,20 @@ public function teamAction(Team $team): Response ]); } - return $this->render('jury/analysis/team.html.twig', - $this->stats->getTeamStats($contest, $team) - ); + return $this->stats->getTeamStats($contest, $team); } + /** + * @return array|Response + */ + #[Template(template: 'jury/analysis/problem.html.twig')] #[Route(path: '/problem/{probid}', name: 'analysis_problem')] public function problemAction( #[MapEntity(id: 'probid')] Problem $problem, #[MapQueryParameter] ?string $view = null - ): Response { + ): array|Response { $contest = $this->dj->getCurrentContest(); if ($contest === null) { @@ -116,16 +136,18 @@ public function problemAction( $filterKeys = array_keys(StatisticsService::FILTERS); $view = $view ?: reset($filterKeys); - return $this->render('jury/analysis/problem.html.twig', - $this->stats->getProblemStats($contest, $problem, $view) - ); + return $this->stats->getProblemStats($contest, $problem, $view); } + /** + * @return array|Response + */ + #[Template(template: 'jury/analysis/languages.html.twig')] #[Route(path: '/languages', name: 'analysis_languages')] public function languagesAction( #[MapQueryParameter] ?string $view = null - ): Response { + ): array|Response { $contest = $this->dj->getCurrentContest(); if ($contest === null) { @@ -137,8 +159,6 @@ public function languagesAction( $filterKeys = array_keys(StatisticsService::FILTERS); $view = $view ?: reset($filterKeys); - return $this->render('jury/analysis/languages.html.twig', - $this->stats->getLanguagesStats($contest, $view) - ); + return $this->stats->getLanguagesStats($contest, $view); } } diff --git a/webapp/src/Controller/Jury/AuditLogController.php b/webapp/src/Controller/Jury/AuditLogController.php index ad000bc413..24cefda582 100644 --- a/webapp/src/Controller/Jury/AuditLogController.php +++ b/webapp/src/Controller/Jury/AuditLogController.php @@ -11,8 +11,8 @@ use App\Utils\Utils; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Tools\Pagination\Paginator; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; @@ -28,13 +28,24 @@ public function __construct( protected readonly EventLogService $eventLogService ) {} + /** + * @return array{ + * auditlog: list, actions: list}>, + * table_fields: array, + * table_options: array{ordering: string, searching: string, full_clickable: bool}, + * maxPages: int, + * thisPage: int, + * showAll: bool + * } + */ #[Route(path: '', name: 'jury_auditlog')] + #[Template(template: 'jury/auditlog.html.twig')] public function indexAction( #[MapQueryParameter] bool $showAll = false, #[MapQueryParameter] int $page = 1, - ): Response { + ): array { $timeFormat = (string)$this->config->get('time_format'); $limit = 1000; @@ -96,17 +107,17 @@ public function indexAction( 'what' => ['title' => 'action', 'sort' => false], ]; - $maxPages = ceil($paginator->count() / $limit); + $maxPages = (int)ceil($paginator->count() / $limit); $thisPage = $page; - return $this->render('jury/auditlog.html.twig', [ + return [ 'auditlog' => $auditlog_table, 'table_fields' => $table_fields, 'table_options' => ['ordering' => 'false', 'searching' => 'false', 'full_clickable' => false], 'maxPages' => $maxPages, 'thisPage' => $thisPage, 'showAll' => $showAll, - ]); + ]; } private function generateDatatypeUrl(string $type, int|string|null $id): ?string diff --git a/webapp/src/Controller/Jury/BalloonController.php b/webapp/src/Controller/Jury/BalloonController.php index 9098772310..c9b4388416 100644 --- a/webapp/src/Controller/Jury/BalloonController.php +++ b/webapp/src/Controller/Jury/BalloonController.php @@ -11,10 +11,10 @@ use App\Service\EventLogService; use App\Utils\Utils; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpFoundation\RedirectResponse; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; @@ -49,12 +49,26 @@ private function areDefault(array $filters, array $defaultCategories): bool { return false; } + /** + * @return array{}|array{ + * refresh: array{after: int, url: string, ajax: bool}, + * isfrozen: bool, + * hasFilters: bool, + * filteredAffiliations: list, + * filteredLocations: list, + * filteredCategories: list, + * availableCategories: list, + * defaultCategories: list, + * balloons: list + * } + */ #[Route(path: '', name: 'jury_balloons')] - public function indexAction(BalloonService $balloonService): Response + #[Template(template: 'jury/balloons.html.twig')] + public function indexAction(BalloonService $balloonService): array { $contest = $this->dj->getCurrentContest(); if (is_null($contest)) { - return $this->render('jury/balloons.html.twig'); + return []; } $balloons_table = $balloonService->collectBalloonTable($contest); @@ -151,7 +165,7 @@ public function indexAction(BalloonService $balloonService): Response ->getArrayResult(); $defaultCategories = array_column($defaultCategories, "categoryid"); - return $this->render('jury/balloons.html.twig', [ + return [ 'refresh' => [ 'after' => 60, 'url' => $this->generateUrl('jury_balloons'), @@ -165,7 +179,7 @@ public function indexAction(BalloonService $balloonService): Response 'availableCategories' => $availableCategories, 'defaultCategories' => $defaultCategories, 'balloons' => $balloons_table - ]); + ]; } #[Route(path: '/{balloonId}/done', name: 'jury_balloons_setdone')] diff --git a/webapp/src/Controller/Jury/ClarificationController.php b/webapp/src/Controller/Jury/ClarificationController.php index 4eda5224e2..f975ad0b17 100644 --- a/webapp/src/Controller/Jury/ClarificationController.php +++ b/webapp/src/Controller/Jury/ClarificationController.php @@ -14,8 +14,11 @@ use App\Utils\Utils; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query\Expr\Join; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; @@ -34,13 +37,25 @@ public function __construct( protected readonly EventLogService $eventLogService ) {} + /** + * @return array{ + * newClarifications: list, + * oldClarifications: list, + * generalClarifications: list, + * queues: array, + * currentQueue: string, + * currentFilter: string|null, + * categories: array + * } + */ #[Route(path: '', name: 'jury_clarifications')] + #[Template(template: 'jury/clarifications.html.twig')] public function indexAction( #[MapQueryParameter(name: 'filter')] ?string $currentFilter = null, #[MapQueryParameter(name: 'queue')] string $currentQueue = 'all', - ): Response { + ): array { $categories = $this->config->get('clar_categories'); if ($contest = $this->dj->getCurrentContest()) { $contestIds = [$contest->getCid()]; @@ -102,7 +117,7 @@ public function indexAction( $queues = $this->config->get('clar_queues'); - return $this->render('jury/clarifications.html.twig', [ + return [ 'newClarifications' => $newClarifications, 'oldClarifications' => $oldClarifications, 'generalClarifications' => $generalClarifications, @@ -110,11 +125,19 @@ public function indexAction( 'currentQueue' => $currentQueue, 'currentFilter' => $currentFilter, 'categories' => $categories, - ]); + ]; } + /** + * @return array{ + * list: list>, + * queues: array, + * answers: list, jurymember: string|null + * }|RedirectResponse + */ #[Route(path: '/{id<\d+>}', name: 'jury_clarification')] - public function viewAction(Request $request, int $id): Response + #[Template(template: 'jury/clarification.html.twig')] + public function viewAction(Request $request, int $id): array|RedirectResponse { $clarification = $this->em->getRepository(Clarification::class)->find($id); if (!$clarification) { @@ -230,15 +253,19 @@ public function viewAction(Request $request, int $id): Response ->getQuery() ->getSingleResult()['jury_member']; - return $this->render('jury/clarification.html.twig', $parameters); + return $parameters; } + /** + * @return array{form: FormView}|RedirectResponse + */ #[Route(path: '/send', name: 'jury_clarification_new')] + #[Template(template: 'jury/clarification_new.html.twig')] public function composeClarificationAction( Request $request, #[MapQueryParameter] ?string $teamto = null, - ): Response { + ): array|RedirectResponse { $formData = ['recipient' => JuryClarificationType::RECIPIENT_MUST_SELECT]; if ($teamto !== null) { @@ -253,7 +280,7 @@ public function composeClarificationAction( return $this->processSubmittedClarification($form); } - return $this->render('jury/clarification_new.html.twig', ['form' => $form->createView()]); + return ['form' => $form->createView()]; } #[Route(path: '/{clarId<\d+>}/claim', name: 'jury_clarification_claim')] @@ -354,7 +381,7 @@ public function changeQueueAction(Request $request, int $clarId): Response protected function processSubmittedClarification( FormInterface $form, ?Clarification $inReplTo = null - ): Response { + ): RedirectResponse { $formData = $form->getData(); $clarification = new Clarification(); $clarification->setInReplyTo($inReplTo); diff --git a/webapp/src/Controller/Jury/ConfigController.php b/webapp/src/Controller/Jury/ConfigController.php index a81fcdc531..00cb41f2f8 100644 --- a/webapp/src/Controller/Jury/ConfigController.php +++ b/webapp/src/Controller/Jury/ConfigController.php @@ -11,11 +11,12 @@ use App\Utils\Utils; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; @@ -31,8 +32,30 @@ public function __construct( protected readonly ConfigurationService $config ) {} + /** + * @return array{ + * options: list|null, + * key_options: array|null, + * value_options: array|null, + * key_placeholder: string, + * value_placeholder: string + * }> + * }>, + * errors: array, + * activeCategory: string, + * diffs: array|null + * }|RedirectResponse + */ #[Route(path: '', name: 'jury_config')] - public function indexAction(EventLogService $eventLogService, Request $request): Response + #[Template(template: 'jury/config.html.twig')] + public function indexAction(EventLogService $eventLogService, Request $request): array|RedirectResponse { $specs = $this->config->getConfigSpecification(); foreach ($specs as &$spec) { @@ -163,21 +186,30 @@ public function indexAction(EventLogService $eventLogService, Request $request): if ($diffs !== null) { $diffs = json_decode($diffs, true); } - return $this->render('jury/config.html.twig', [ + return [ 'options' => $allData, 'errors' => $errors ?? [], 'activeCategory' => $activeCategory ?? 'Scoring', 'diffs' => $diffs, - ]); + ]; } + /** + * @return array{ + * results: array, + * stopwatch: mixed, + * dir: array{project: string, log: string}, + * logFilesWithSize: array + * } + */ #[Route(path: '/check', name: 'jury_config_check')] + #[Template(template: 'jury/config_check.html.twig')] public function checkAction( #[Autowire('%kernel.project_dir%')] string $projectDir, #[Autowire('%kernel.logs_dir%')] string $logsDir - ): Response { + ): array { $results = $this->checkConfigService->runAll(); $stopwatch = $this->checkConfigService->getStopwatch(); $logFiles = glob($logsDir . '/*.log'); @@ -185,7 +217,7 @@ public function checkAction( foreach ($logFiles as $logFile) { $logFilesWithSize[str_replace($logsDir . '/', '', $logFile)] = Utils::printsize(filesize($logFile)); } - return $this->render('jury/config_check.html.twig', [ + return [ 'results' => $results, 'stopwatch' => $stopwatch, 'dir' => [ @@ -193,22 +225,29 @@ public function checkAction( 'log' => $logsDir, ], 'logFilesWithSize' => $logFilesWithSize, - ]); + ]; } + /** + * @return array{ + * logFile: string, + * contents: string + * } + */ #[Route(path: '/tail-log/{logFile<[a-z0-9-]+\.log>}', name: 'jury_tail_log')] + #[Template(template: 'jury/tail_log.html.twig')] public function tailLogAction( string $logFile, #[Autowire('%kernel.logs_dir%')] string $logsDir - ): Response { + ): array { $fullFile = "$logsDir/$logFile"; $command = sprintf('tail -n200 %s', escapeshellarg($fullFile)); exec($command, $lines); - return $this->render('jury/tail_log.html.twig', [ + return [ 'logFile' => $logFile, 'contents' => implode("\n", $lines), - ]); + ]; } #[Route(path: '/download-log/{logFile<[a-z0-9-]+\.log>}', name: 'jury_download_log')] diff --git a/webapp/src/Controller/Jury/ContestController.php b/webapp/src/Controller/Jury/ContestController.php index b8c4c5e419..95eb88466c 100644 --- a/webapp/src/Controller/Jury/ContestController.php +++ b/webapp/src/Controller/Jury/ContestController.php @@ -32,6 +32,8 @@ use Doctrine\ORM\NoResultException; use Doctrine\ORM\PersistentCollection; use Doctrine\ORM\Query\Expr\Join; +use App\Twig\Attribute\AjaxTemplate; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -64,11 +66,17 @@ public function __construct( } /** + * @return array{ + * upcoming_contest: Contest|null, + * contests_table: list>, + * table_fields: array> + * } * @throws NonUniqueResultException * @throws NoResultException */ #[Route(path: '', name: 'jury_contests')] - public function indexAction(Request $request): Response + #[Template(template: 'jury/contests.html.twig')] + public function indexAction(Request $request): array { $em = $this->em; @@ -298,15 +306,26 @@ public function indexAction(Request $request): Response ->getQuery() ->getOneOrNullResult(); - return $this->render('jury/contests.html.twig', [ + return [ 'upcoming_contest' => $upcomingContest, 'contests_table' => $contests_table, 'table_fields' => $table_fields, - ]); + ]; } + /** + * @return array{ + * contest: Contest, + * allowRemovedIntervals: bool, + * removedIntervalForm: FormInterface, + * removedIntervals: list, + * problems: list, + * languages: list + * }|Response + */ #[Route(path: '/{contestId<\d+>}', name: 'jury_contest')] - public function viewAction(Request $request, int $contestId): Response + #[Template(template: 'jury/contest.html.twig')] + public function viewAction(Request $request, int $contestId): array|Response { $contest = $this->em->getRepository(Contest::class)->find($contestId); if (!$contest) { @@ -351,14 +370,14 @@ public function viewAction(Request $request, int $contestId): Response $languages = $this->dj->getAllowedLanguagesForContest($contest); - return $this->render('jury/contest.html.twig', [ + return [ 'contest' => $contest, 'allowRemovedIntervals' => $this->getParameter('removed_intervals'), 'removedIntervalForm' => $form, 'removedIntervals' => $removedIntervals, 'problems' => $problems, 'languages' => $languages, - ]); + ]; } #[Route(path: '/{contestId}/toggle/{type}', name: 'jury_contest_toggle')] @@ -440,9 +459,16 @@ public function removeIntervalAction(int $contestId, int $intervalId): RedirectR return $this->redirectToRoute('jury_contest', ['contestId' => $contest->getCid()]); } + /** + * @return array{ + * contest: Contest, + * form: FormInterface + * }|Response + */ #[IsGranted('ROLE_ADMIN')] #[Route(path: '/{contestId<\d+>}/edit', name: 'jury_contest_edit')] - public function editAction(Request $request, int $contestId): Response + #[Template(template: 'jury/contest_edit.html.twig')] + public function editAction(Request $request, int $contestId): array|Response { $contest = $this->em->getRepository(Contest::class)->find($contestId); if (!$contest) { @@ -571,10 +597,10 @@ public function editAction(Request $request, int $contestId): Response $this->em->refresh($contest); - return $this->render('jury/contest_edit.html.twig', [ + return [ 'contest' => $contest, 'form' => $form, - ]); + ]; } #[IsGranted('ROLE_ADMIN')] @@ -617,9 +643,13 @@ public function deleteProblemAction(Request $request, int $contestId, int $probI return $this->deleteEntities($request, [$contestProblem], $this->generateUrl('jury_contest', ['contestId' => $contestId])); } + /** + * @return array{form: FormInterface}|Response + */ #[IsGranted('ROLE_ADMIN')] #[Route(path: '/add', name: 'jury_contest_add')] - public function addAction(Request $request): Response + #[Template(template: 'jury/contest_add.html.twig')] + public function addAction(Request $request): array|Response { $contest = new Contest(); // Set default activate time @@ -670,9 +700,9 @@ function () use ($form, $contest) { return $response; } - return $this->render('jury/contest_add.html.twig', [ + return [ 'form' => $form, - ]); + ]; } #[Route(path: '/{contestId<\d+>}/prefetch', name: 'jury_contest_prefetch')] @@ -755,9 +785,17 @@ public function prefetchAction(Request $request, int $contestId): Response return $this->redirectToRoute('jury_contest', ['contestId' => $contestId]); } + /** + * @return array{ + * contest: Contest, + * blockers: list, + * form: FormInterface + * }|RedirectResponse + */ #[IsGranted('ROLE_ADMIN')] #[Route(path: '/{contestId<\d+>}/finalize', name: 'jury_contest_finalize')] - public function finalizeAction(Request $request, int $contestId): Response + #[Template(template: 'jury/contest_finalize.html.twig')] + public function finalizeAction(Request $request, int $contestId): array|RedirectResponse { /** @var Contest $contest */ $contest = $this->em->getRepository(Contest::class)->find($contestId); @@ -816,11 +854,11 @@ public function finalizeAction(Request $request, int $contestId): Response } } - return $this->render('jury/contest_finalize.html.twig', [ + return [ 'contest' => $contest, 'blockers' => $blockers, 'form' => $form, - ]); + ]; } #[IsGranted('ROLE_ADMIN')] diff --git a/webapp/src/Controller/Jury/ExecutableController.php b/webapp/src/Controller/Jury/ExecutableController.php index cd4b0c1ab0..157a0bb5eb 100644 --- a/webapp/src/Controller/Jury/ExecutableController.php +++ b/webapp/src/Controller/Jury/ExecutableController.php @@ -11,15 +11,19 @@ use App\Service\ConfigurationService; use App\Service\DOMJudgeService; use App\Service\EventLogService; +use App\Twig\Attribute\AjaxTemplate; use App\Utils\Utils; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException as PHPInvalidArgumentException; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\Form\Exception\InvalidArgumentException; use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; @@ -43,8 +47,27 @@ public function __construct( parent::__construct($em, $eventLogService, $dj, $kernel); } + /** + * @return array{ + * executables_used: list>, + * actions: list>, + * link: string, + * cssclass?: string|null + * }>, + * executables_unused: list>, + * actions: list>, + * link: string, + * cssclass?: string|null + * }>, + * table_fields: array>, + * form: FormInterface + * } + */ #[Route(path: '', name: 'jury_executables')] - public function indexAction(Request $request): Response + #[Template(template: 'jury/executables.html.twig')] + public function indexAction(Request $request): array { $executables_tables_used = []; $executables_tables_unused = []; @@ -188,17 +211,21 @@ public function indexAction(Request $request): Response // This is replaced with the icon. unset($table_fields['type']); - return $this->render('jury/executables.html.twig', [ + return [ 'executables_used' => $executables_tables_used, 'executables_unused' => $executables_tables_unused, 'table_fields' => $table_fields, 'form' => $form, - ]); + ]; } + /** + * @return array{form: FormInterface}|RedirectResponse + */ #[IsGranted('ROLE_ADMIN')] #[Route(path: '/add', name: 'jury_executable_add')] - public function addAction(Request $request): Response + #[Template(template: 'jury/executable_add.html.twig')] + public function addAction(Request $request): array|RedirectResponse { $data = []; $form = $this->createForm(ExecutableUploadType::class, $data); @@ -255,18 +282,22 @@ public function addAction(Request $request): Response } } - return $this->render('jury/executable_add.html.twig', [ + return [ 'form' => $form, - ]); + ]; } + /** + * @return array|RedirectResponse + */ #[Route(path: '/{execId}', name: 'jury_executable')] + #[Template(template: 'jury/executable.html.twig')] public function viewAction( Request $request, string $execId, #[MapQueryParameter] ?int $index = null - ): Response { + ): array|RedirectResponse { $executable = $this->em->getRepository(Executable::class)->find($execId); if (!$executable) { throw new NotFoundHttpException(sprintf('Executable with ID %s not found', $execId)); @@ -363,7 +394,7 @@ public function viewAction( return $this->redirectToRoute('jury_executable', ['execId' => $executable->getExecid()]); } - return $this->render('jury/executable.html.twig', array_merge($editorData, [ + return array_merge($editorData, [ 'form' => $form->createView(), 'uploadForm' => $uploadForm->createView(), 'selected' => $index, @@ -371,7 +402,7 @@ public function viewAction( 'default_compare' => (string)$this->config->get('default_compare'), 'default_run' => (string)$this->config->get('default_run'), 'default full debug' => (string)$this->config->get('default_full_debug'), - ])); + ]); } #[Route(path: '/{execId}/download', name: 'jury_executable_download')] @@ -388,9 +419,22 @@ public function downloadAction(string $execId): Response return Utils::streamAsBinaryFile($zipFileContent, $filename, 'zip'); } + /** + * @return array{ + * type: string, + * primaryKey: string, + * description: string, + * messages: list, + * isError: bool, + * showModalSubmit: bool, + * modalUrl: string, + * redirectUrl: string + * }|RedirectResponse|JsonResponse + */ #[IsGranted('ROLE_ADMIN')] #[Route(path: '/{execId}/delete/{rankToDelete}', name: 'jury_executable_delete_single')] - public function deleteSingleAction(Request $request, string $execId, int $rankToDelete): Response + #[AjaxTemplate(normalTemplate: 'jury/delete.html.twig', ajaxTemplate: 'jury/delete_modal.html.twig')] + public function deleteSingleAction(Request $request, string $execId, int $rankToDelete): array|RedirectResponse|JsonResponse { $executable = $this->em->getRepository(Executable::class)->find($execId); if (!$executable) { @@ -410,7 +454,7 @@ public function deleteSingleAction(Request $request, string $execId, int $rankTo } if ($request->isMethod('GET')) { - $data = [ + return [ 'type' => 'ExecutableFile', 'primaryKey' => $execId, 'description' => $fileToDelete->getFilename(), @@ -420,11 +464,6 @@ public function deleteSingleAction(Request $request, string $execId, int $rankTo 'modalUrl' => $request->getRequestUri(), 'redirectUrl' => $this->generateUrl('jury_executable', ['execId' => $execId]), ]; - if ($request->isXmlHttpRequest()) { - return $this->render('jury/delete_modal.html.twig', $data); - } - - return $this->render('jury/delete.html.twig', $data); } else { // Create a copy of all files except $file $files = []; diff --git a/webapp/src/Controller/Jury/ExternalContestController.php b/webapp/src/Controller/Jury/ExternalContestController.php index 93096f18fd..46b1ee09e3 100644 --- a/webapp/src/Controller/Jury/ExternalContestController.php +++ b/webapp/src/Controller/Jury/ExternalContestController.php @@ -11,8 +11,12 @@ use App\Service\DOMJudgeService; use App\Service\EventLogService; use App\Service\ExternalContestSourceService; +use App\Twig\Attribute\AjaxTemplate; use App\Utils\Utils; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bridge\Twig\Attribute\Template; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\KernelInterface; @@ -35,8 +39,28 @@ public function __construct( parent::__construct($em, $eventLog, $dj, $kernel); } + /** + * @return array{ + * externalContestSource: ExternalContestSource, + * status: string, + * sourceService: ExternalContestSourceService, + * webappDir: string, + * warningTableFields: array, + * warningTable: list>, + * actions: list + * }>, + * form: \Symfony\Component\Form\FormView, + * hasFilters: bool, + * refresh: array{after: int, url: string, ajax: bool} + * }|RedirectResponse + */ #[Route(path: '/', name: 'jury_external_contest')] - public function indexAction(Request $request): Response + #[AjaxTemplate( + normalTemplate: 'jury/external_contest.html.twig', + ajaxTemplate: 'jury/partials/external_contest_warnings.html.twig' + )] + public function indexAction(Request $request): array|RedirectResponse { /** @var ExternalContestSource|null $externalContestSource */ $externalContestSource = $this->em->createQueryBuilder() @@ -107,7 +131,7 @@ public function indexAction(Request $request): Response // Build the filter form. $form = $this->createForm(ExternalSourceWarningsFilterType::class, $filters); - $data = [ + return [ 'externalContestSource' => $externalContestSource, 'status' => $status, 'sourceService' => $this->sourceService, @@ -122,16 +146,17 @@ public function indexAction(Request $request): Response 'ajax' => true, ] ]; - - if ($request->isXmlHttpRequest()) { - return $this->render('jury/partials/external_contest_warnings.html.twig', $data); - } else { - return $this->render('jury/external_contest.html.twig', $data); - } } + /** + * @return array{ + * externalContestSource: ExternalContestSource, + * form: FormInterface + * }|RedirectResponse + */ #[Route(path: '/manage', name: 'jury_external_contest_manage')] - public function manageAction(Request $request): Response + #[Template(template: 'jury/external_contest_manage.html.twig')] + public function manageAction(Request $request): array|RedirectResponse { /** @var ExternalContestSource $externalContestSource */ $externalContestSource = $this->em->createQueryBuilder() @@ -162,9 +187,9 @@ public function manageAction(Request $request): Response return $this->redirectToRoute('jury_external_contest'); } - return $this->render('jury/external_contest_manage.html.twig', [ + return [ 'externalContestSource' => $externalContestSource, 'form' => $form, - ]); + ]; } } diff --git a/webapp/src/Controller/Jury/ImportExportController.php b/webapp/src/Controller/Jury/ImportExportController.php index 6f07dfd71a..296da00af1 100644 --- a/webapp/src/Controller/Jury/ImportExportController.php +++ b/webapp/src/Controller/Jury/ImportExportController.php @@ -28,9 +28,12 @@ use Collator; use Doctrine\ORM\EntityManagerInterface; use Exception; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\SubmitButton; use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -69,15 +72,26 @@ public function __construct( } /** + * @return array{ + * tsv_form: FormInterface, + * json_form: FormInterface, + * icpccms_form: FormInterface, + * problem_form: FormInterface, + * contest_export_form: FormInterface, + * contest_import_form: FormInterface, + * problems_import_form: FormInterface, + * export_results_form: FormInterface + * }|Response * @throws ClientExceptionInterface * @throws DecodingExceptionInterface * @throws RedirectionExceptionInterface * @throws ServerExceptionInterface * @throws TransportExceptionInterface */ + #[Template(template: 'jury/import_export.html.twig')] #[Route(path: '', name: 'jury_import_export')] #[IsGranted('ROLE_ADMIN')] - public function indexAction(Request $request): Response + public function indexAction(Request $request): array|Response { $tsvForm = $this->createForm(TsvImportType::class); @@ -328,7 +342,7 @@ public function indexAction(Request $request): Response return $response; } - return $this->render('jury/import_export.html.twig', [ + return [ 'tsv_form' => $tsvForm, 'json_form' => $jsonForm, 'icpccms_form' => $icpcCmsForm, @@ -337,7 +351,7 @@ public function indexAction(Request $request): Response 'contest_import_form' => $contestImportForm, 'problems_import_form' => $problemsImportForm, 'export_results_form' => $exportResultsForm, - ]); + ]; } #[Route(path: '/export/{type}.tsv', name: 'jury_tsv_export')] @@ -375,8 +389,20 @@ public function exportTsvAction(string $type): Response return $response; } + /** + * @return array{ + * domjudgeVersion: string, + * title: string, + * grouped: array>, + * queues: array, + * categories: array, + * contest: Contest, + * problems: array + * }|RedirectResponse + */ #[Route(path: '/export/clarifications.html', name: 'jury_html_export_clarifications')] - public function exportClarificationsHtmlAction(): Response + #[Template(template: 'jury/export/clarifications.html.twig')] + public function exportClarificationsHtmlAction(): array|RedirectResponse { try { return $this->getClarificationsHtml(); @@ -531,7 +557,18 @@ protected function getResultsHtml( return $this->twig->render('jury/export/results.html.twig', $data); } - protected function getClarificationsHtml(): Response + /** + * @return array{ + * domjudgeVersion: string, + * title: string, + * grouped: array>, + * queues: array, + * categories: array, + * contest: Contest, + * problems: array + * } + */ + protected function getClarificationsHtml(): array { $contest = $this->dj->getCurrentContest(); if ($contest === null) { @@ -592,7 +629,7 @@ protected function getClarificationsHtml(): Response } $contestProblems = $contestProblemsIndexed; - return $this->render('jury/export/clarifications.html.twig', [ + return [ 'domjudgeVersion' => $this->domjudgeVersion, 'title' => sprintf('Clarifications for %s', $contest->getName()), 'grouped' => $grouped, @@ -600,7 +637,7 @@ protected function getClarificationsHtml(): Response 'categories' => $clarificationCategories, 'contest' => $contest, 'problems' => $contestProblems, - ]); + ]; } /** diff --git a/webapp/src/Controller/Jury/InternalErrorController.php b/webapp/src/Controller/Jury/InternalErrorController.php index 04642dab40..f5d12c2113 100644 --- a/webapp/src/Controller/Jury/InternalErrorController.php +++ b/webapp/src/Controller/Jury/InternalErrorController.php @@ -13,9 +13,12 @@ use App\Service\RejudgingService; use App\Utils\Utils; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bridge\Twig\Attribute\Template; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\HttpKernel\Profiler\Profiler; @@ -38,8 +41,21 @@ public function __construct( parent::__construct($em, $eventLogService, $dj, $kernel); } + /** + * @return array{ + * internal_errors: list, + * actions: list, + * link: string, + * cssclass: string + * }>, + * table_fields: array>, + * refresh: array{after: int, url: string} + * } + */ #[Route(path: '', name: 'jury_internal_errors')] - public function indexAction(): Response + #[Template(template: 'jury/internal_errors.html.twig')] + public function indexAction(): array { /** @var InternalError[] $internalErrors */ $internalErrors = $this->em->createQueryBuilder() @@ -80,18 +96,26 @@ public function indexAction(): Response ]; } - return $this->render('jury/internal_errors.html.twig', [ + return [ 'internal_errors' => $internal_errors_table, 'table_fields' => $table_fields, 'refresh' => [ 'after' => 15, 'url' => $this->generateUrl('jury_internal_errors'), ] - ]); + ]; } + /** + * @return array{ + * internalError: InternalError, + * affectedLink: string|null, + * affectedText: string|null + * } + */ #[Route(path: '/{errorId<\d+>}', methods: ['GET'], name: 'jury_internal_error')] - public function viewAction(int $errorId): Response + #[Template(template: 'jury/internal_error.html.twig')] + public function viewAction(int $errorId): array { $internalError = $this->em->getRepository(InternalError::class)->find($errorId); if (!$internalError) { @@ -126,15 +150,19 @@ public function viewAction(int $errorId): Response break; } - return $this->render('jury/internal_error.html.twig', [ + return [ 'internalError' => $internalError, 'affectedLink' => $affectedLink, 'affectedText' => $affectedText, - ]); + ]; } + /** + * @return array{url: string}|RedirectResponse|StreamedResponse + */ #[Route(path: '/{errorId<\d+>}/{action}', name: 'jury_internal_error_handle', methods: ['POST'])] - public function handleAction(Request $request, ?Profiler $profiler, int $errorId, string $action): Response + #[Template(template: 'jury/internal_error_resolve.html.twig')] + public function handleAction(Request $request, ?Profiler $profiler, int $errorId, string $action): array|RedirectResponse|StreamedResponse { /** @var InternalError $internalError */ $internalError = $this->em->createQueryBuilder() @@ -211,8 +239,8 @@ public function handleAction(Request $request, ?Profiler $profiler, int $errorId }); } - return $this->render('jury/internal_error_resolve.html.twig', [ + return [ 'url' => $this->generateUrl('jury_internal_error_handle', ['errorId' => $errorId, 'action' => $action]), - ]); + ]; } } diff --git a/webapp/src/Controller/Jury/JudgehostController.php b/webapp/src/Controller/Jury/JudgehostController.php index 58e9eb103a..151fa4d7f3 100644 --- a/webapp/src/Controller/Jury/JudgehostController.php +++ b/webapp/src/Controller/Jury/JudgehostController.php @@ -12,11 +12,14 @@ use App\Service\ConfigurationService; use App\Service\DOMJudgeService; use App\Service\EventLogService; +use App\Twig\Attribute\AjaxTemplate; use App\Utils\Utils; use Doctrine\DBAL\Exception as DBALException; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; +use Symfony\Bridge\Twig\Attribute\Template; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -43,8 +46,25 @@ public function __construct( parent::__construct($em, $eventLog, $dj, $kernel); } + /** + * @return array{ + * judgehosts: list>, + * actions: list>, + * link: string, + * cssclass: string + * }>, + * table_fields: array, + * all_checked_in_recently: bool, + * refresh: array{after: int, url: string, ajax: bool} + * } + */ #[Route(path: '', name: 'jury_judgehosts')] - public function indexAction(Request $request): Response + #[AjaxTemplate( + normalTemplate: 'jury/judgehosts.html.twig', + ajaxTemplate: 'jury/partials/judgehost_list.html.twig' + )] + public function indexAction(Request $request): array { /** @var Judgehost[] $judgehosts */ $judgehosts = $this->em->createQueryBuilder() @@ -210,7 +230,7 @@ public function indexAction(Request $request): Response return strnatcasecmp($a['data']['hostname']['value'], $b['data']['hostname']['value']); }); - $data = [ + return [ 'judgehosts' => $judgehosts_table, 'table_fields' => $table_fields, 'all_checked_in_recently' => $all_checked_in_recently, @@ -220,18 +240,24 @@ public function indexAction(Request $request): Response 'ajax' => true, ] ]; - if ($request->isXmlHttpRequest()) { - return $this->render('jury/partials/judgehost_list.html.twig', $data); - } else { - return $this->render('jury/judgehosts.html.twig', $data); - } } /** + * @return array{ + * judgehost: Judgehost, + * status: string, + * statusIcon: string, + * judgings: list, + * refresh: array{after: int, url: string, ajax: bool} + * } * @throws NonUniqueResultException */ #[Route(path: '/{judgehostid}', methods: ['GET'], name: 'jury_judgehost')] - public function viewAction(Request $request, int $judgehostid): Response + #[AjaxTemplate( + normalTemplate: 'jury/judgehost.html.twig', + ajaxTemplate: 'jury/partials/judgehost_judgings.html.twig' + )] + public function viewAction(Request $request, int $judgehostid): array { /** @var Judgehost|null $judgehost */ $judgehost = $this->em->createQueryBuilder() @@ -278,7 +304,7 @@ public function viewAction(Request $request, int $judgehostid): Response ->getResult(); } - $data = [ + return [ 'judgehost' => $judgehost, 'status' => $status, 'statusIcon' => $statusIcon, @@ -289,11 +315,6 @@ public function viewAction(Request $request, int $judgehostid): Response 'ajax' => true, ], ]; - if ($request->isXmlHttpRequest()) { - return $this->render('jury/partials/judgehost_judgings.html.twig', $data); - } else { - return $this->render('jury/judgehost.html.twig', $data); - } } /** @@ -375,9 +396,13 @@ public function autohideInactive(): RedirectResponse return $this->redirectToRoute('jury_judgehosts'); } + /** + * @return array{form: FormInterface}|RedirectResponse + */ #[IsGranted('ROLE_ADMIN')] #[Route(path: '/edit/multiple', name: 'jury_judgehost_edit')] - public function editMultipleAction(Request $request): Response + #[Template(template: 'jury/judgehosts_edit_multiple.html.twig')] + public function editMultipleAction(Request $request): array|RedirectResponse { $querybuilder = $this->em->createQueryBuilder() ->from(Judgehost::class, 'j') @@ -398,8 +423,8 @@ public function editMultipleAction(Request $request): Response return $this->redirectToRoute('jury_judgehosts'); } - return $this->render('jury/judgehosts_edit_multiple.html.twig', [ + return [ 'form' => $form, - ]); + ]; } } diff --git a/webapp/src/Controller/Jury/JuryMiscController.php b/webapp/src/Controller/Jury/JuryMiscController.php index 9f64a3994f..585017cc04 100644 --- a/webapp/src/Controller/Jury/JuryMiscController.php +++ b/webapp/src/Controller/Jury/JuryMiscController.php @@ -19,6 +19,7 @@ use App\Utils\Utils; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query\Expr\Join; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpFoundation\JsonResponse; @@ -47,9 +48,13 @@ public function __construct( parent::__construct($em, $eventLogService, $dj, $kernel); } + /** + * @return array{adminer_enabled: bool, CCS_SPEC_API_URL: string} + */ #[IsGranted(new Expression("is_granted('ROLE_JURY') or is_granted('ROLE_BALLOON') or is_granted('ROLE_CLARIFICATION_RW')"))] #[Route(path: '', name: 'jury_index')] - public function indexAction(ConfigurationService $config): Response + #[Template(template: 'jury/index.html.twig')] + public function indexAction(ConfigurationService $config): array { if ($this->isGranted('ROLE_ADMIN')) { $innodbSnapshotIsolation = $this->em->getConnection()->executeQuery('SHOW VARIABLES LIKE "innodb_snapshot_isolation"')->fetchAssociative(); @@ -63,10 +68,10 @@ public function indexAction(ConfigurationService $config): Response $this->addFlash('info', 'New release ' . $newestVersion . ' available at: https://www.domjudge.org/download.'); } - return $this->render('jury/index.html.twig', [ + return [ 'adminer_enabled' => $config->get('adminer_enabled'), 'CCS_SPEC_API_URL' => GI::CCS_SPEC_API_URL, - ]); + ]; } #[IsGranted(new Expression("is_granted('ROLE_JURY') or is_granted('ROLE_BALLOON')"))] @@ -200,9 +205,17 @@ public function ajaxDataAction(Request $request, string $datatype): JsonResponse return $this->json(['results' => $results]); } + /** + * @return array{ + * contests: array, + * contest: Contest|null, + * doRefresh: bool + * }|Response + */ + #[Template(template: 'jury/refresh_cache.html.twig')] #[IsGranted('ROLE_ADMIN')] #[Route(path: '/refresh-cache', name: 'jury_refresh_cache')] - public function refreshCacheAction(Request $request, ScoreboardService $scoreboardService): Response + public function refreshCacheAction(Request $request, ScoreboardService $scoreboardService): array|Response { // Note: we use a XMLHttpRequest here as Symfony does not support // streaming Twig output. @@ -240,16 +253,31 @@ public function refreshCacheAction(Request $request, ScoreboardService $scoreboa }); } - return $this->render('jury/refresh_cache.html.twig', [ + return [ 'contests' => $contests, 'contest' => count($contests) === 1 ? reset($contests) : null, 'doRefresh' => $request->request->has('refresh'), - ]); + ]; } + /** + * @return array{ + * numChecked: int, + * numUnchecked: int, + * unexpected: array>, + * multiple: array>, + * verified: array>, + * nomatch: array>, + * earlier: array>, + * problems: array, + * contestId: int|null, + * verifyMultiple: bool + * } + */ + #[Template(template: 'jury/check_judgings.html.twig')] #[IsGranted('ROLE_JURY')] #[Route(path: '/judging-verifier', name: 'jury_judging_verifier')] - public function judgingVerifierAction(Request $request): Response + public function judgingVerifierAction(Request $request): array { /** @var Submission[] $submissions */ $submissions = []; @@ -331,7 +359,7 @@ public function judgingVerifierAction(Request $request): Response $this->em->flush(); - return $this->render('jury/check_judgings.html.twig', [ + return [ 'numChecked' => $numChecked, 'numUnchecked' => $numUnchecked, 'unexpected' => $unexpected, @@ -342,7 +370,7 @@ public function judgingVerifierAction(Request $request): Response 'problems' => $problems, 'contestId' => $this->dj->getCurrentContest()?->getCid(), 'verifyMultiple' => $verifyMultiple, - ]); + ]; } #[Route(path: '/change-contest/{contestId<-?\d+>}', name: 'jury_change_contest')] diff --git a/webapp/src/Controller/Jury/LanguageController.php b/webapp/src/Controller/Jury/LanguageController.php index f9c3981834..ecd38eb12b 100644 --- a/webapp/src/Controller/Jury/LanguageController.php +++ b/webapp/src/Controller/Jury/LanguageController.php @@ -12,9 +12,12 @@ use App\Service\DOMJudgeService; use App\Service\EventLogService; use App\Service\SubmissionService; +use App\Twig\Attribute\AjaxTemplate; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; +use Symfony\Bridge\Twig\Attribute\Template; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -41,8 +44,26 @@ public function __construct( parent::__construct($em, $eventLogService, $dj, $kernel); } + /** + * @return array{ + * enabled_languages: list>, + * actions: list>, + * link: string, + * cssclass: string + * }>, + * disabled_languages: list>, + * actions: list>, + * link: string, + * cssclass: string + * }>, + * table_fields: array> + * } + */ #[Route(path: '', name: 'jury_languages')] - public function indexAction(): Response + #[Template(template: 'jury/languages.html.twig')] + public function indexAction(): array { $em = $this->em; /** @var Language[] $languages */ @@ -138,18 +159,22 @@ public function indexAction(): Response ]; } } - return $this->render('jury/languages.html.twig', [ + return [ 'enabled_languages' => $enabled_languages, 'disabled_languages' => $disabled_languages, 'table_fields' => $table_fields, - ]); + ]; } // Note that the add action appears before the view action to make sure // /add is not seen as a language. + /** + * @return array{form: FormInterface}|Response + */ #[IsGranted('ROLE_ADMIN')] #[Route(path: '/add', name: 'jury_language_add')] - public function addAction(Request $request): Response + #[Template(template: 'jury/language_add.html.twig')] + public function addAction(Request $request): array|Response { $language = new Language(); @@ -175,17 +200,30 @@ function () use ($language) { return $response; } - return $this->render('jury/language_add.html.twig', [ + return [ 'form' => $form, - ]); + ]; } /** + * @return array{ + * language: Language, + * submissions: list, + * submissionCounts: array, + * showContest: bool, + * showExternalResult: bool, + * showTestcases?: bool, + * refresh: array{after: int, url: string, ajax: bool} + * } * @throws NoResultException * @throws NonUniqueResultException */ #[Route(path: '/{langId}', name: 'jury_language')] - public function viewAction(Request $request, SubmissionService $submissionService, string $langId): Response + #[AjaxTemplate( + normalTemplate: 'jury/language.html.twig', + ajaxTemplate: 'jury/partials/submission_list.html.twig' + )] + public function viewAction(Request $request, SubmissionService $submissionService, string $langId): array { $language = $this->em->getRepository(Language::class)->find($langId); if (!$language) { @@ -215,10 +253,9 @@ public function viewAction(Request $request, SubmissionService $submissionServic // For ajax requests, only return the submission list partial. if ($request->isXmlHttpRequest()) { $data['showTestcases'] = false; - return $this->render('jury/partials/submission_list.html.twig', $data); } - return $this->render('jury/language.html.twig', $data); + return $data; } #[Route(path: '/{langId}/toggle-submit', name: 'jury_language_toggle_submit')] @@ -282,9 +319,16 @@ public function toggleFilterCompilerFlagsAction(Request $request, string $langId return $this->redirectToRoute('jury_language', ['langId' => $langId]); } + /** + * @return array{ + * language: Language, + * form: FormInterface + * }|RedirectResponse + */ #[IsGranted('ROLE_ADMIN')] #[Route(path: '/{langId}/edit', name: 'jury_language_edit')] - public function editAction(Request $request, string $langId): Response + #[Template(template: 'jury/language_edit.html.twig')] + public function editAction(Request $request, string $langId): array|RedirectResponse { $language = $this->em->getRepository(Language::class)->find($langId); if (!$language) { @@ -307,10 +351,10 @@ public function editAction(Request $request, string $langId): Response return $this->redirectToRoute('jury_language', ['langId' => $language->getLangid()]); } - return $this->render('jury/language_edit.html.twig', [ + return [ 'language' => $language, 'form' => $form, - ]); + ]; } #[IsGranted('ROLE_ADMIN')] diff --git a/webapp/src/Controller/Jury/PrintController.php b/webapp/src/Controller/Jury/PrintController.php index e9ea3f550e..51369ede02 100644 --- a/webapp/src/Controller/Jury/PrintController.php +++ b/webapp/src/Controller/Jury/PrintController.php @@ -9,7 +9,9 @@ use App\Service\DOMJudgeService; use App\Service\EventLogService; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -32,8 +34,12 @@ public function __construct( parent::__construct($em, $eventLogService, $dj, $kernel); } + /** + * @return array{form: FormInterface}|array{success: bool, output: string}|Response + */ + #[Template(template: 'jury/print.html.twig')] #[Route(path: '', name: 'jury_print')] - public function showAction(Request $request): Response + public function showAction(Request $request): array|Response { if (!$this->config->get('print_command')) { throw new AccessDeniedHttpException("Printing disabled in config"); @@ -62,8 +68,6 @@ public function showAction(Request $request): Response ]); } - return $this->render('jury/print.html.twig', [ - 'form' => $form, - ]); + return ['form' => $form]; } } diff --git a/webapp/src/Controller/Jury/ProblemController.php b/webapp/src/Controller/Jury/ProblemController.php index c3371b43dc..e8b9b1156f 100644 --- a/webapp/src/Controller/Jury/ProblemController.php +++ b/webapp/src/Controller/Jury/ProblemController.php @@ -29,7 +29,10 @@ use Doctrine\ORM\NoResultException; use Doctrine\ORM\Query\Expr\Join; use Exception; +use App\Twig\Attribute\AjaxTemplate; use Knp\Component\Pager\Pagination\PaginationInterface; +use Symfony\Bridge\Twig\Attribute\Template; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -64,8 +67,16 @@ public function __construct( parent::__construct($em, $eventLogService, $dj, $kernel); } + /** + * @return array{ + * problems_current: list>, + * problems_other: list>, + * table_fields: array> + * } + */ #[Route(path: '', name: 'jury_problems')] - public function indexAction(): Response + #[Template(template: 'jury/problems.html.twig')] + public function indexAction(): array { $problems = $this->em->createQueryBuilder() ->select('p', 'COUNT(tc.testcaseid) AS testdatacount') @@ -226,23 +237,22 @@ public function indexAction(): Response $problems_table_other[] = $data_to_add; } } - $data = [ + return [ 'problems_current' => $problems_table_current, 'problems_other' => $problems_table_other, 'table_fields' => $table_fields, ]; - - return $this->render('jury/problems.html.twig', $data); } /** + * @return array * @throws NonUniqueResultException */ #[Route(path: '/problemset', name: 'jury_problemset')] - public function problemsetAction(StatisticsService $stats): Response + #[Template(template: 'jury/problemset.html.twig')] + public function problemsetAction(StatisticsService $stats): array { - return $this->render('jury/problemset.html.twig', - $this->dj->getTwigDataForProblemsAction($stats, forJury: true)); + return $this->dj->getTwigDataForProblemsAction($stats, forJury: true); } #[Route(path: '/{probId<\d+>}/samples.zip', name: 'jury_problem_sample_zip')] @@ -429,11 +439,16 @@ public function exportAction(int $problemId): StreamedResponse } /** + * @return array|RedirectResponse * @throws NoResultException * @throws NonUniqueResultException */ #[Route(path: '/{probId<\d+>}', name: 'jury_problem')] - public function viewAction(Request $request, SubmissionService $submissionService, int $probId): Response + #[AjaxTemplate( + normalTemplate: 'jury/problem.html.twig', + ajaxTemplate: 'jury/partials/submission_list.html.twig' + )] + public function viewAction(Request $request, SubmissionService $submissionService, int $probId): array|RedirectResponse { $problem = $this->em->getRepository(Problem::class)->find($probId); if (!$problem) { @@ -518,10 +533,9 @@ public function viewAction(Request $request, SubmissionService $submissionServic // For ajax requests, only return the submission list partial. if ($request->isXmlHttpRequest()) { $data['showTestcases'] = false; - return $this->render('jury/partials/submission_list.html.twig', $data); } - return $this->render('jury/problem.html.twig', $data); + return $data; } #[Route(path: '/{probId<\d+>}/statement', name: 'jury_problem_statement')] @@ -535,8 +549,18 @@ public function viewTextAction(int $probId): StreamedResponse return $problem->getProblemStatementStreamedResponse(); } + /** + * @return array{ + * problem: Problem, + * testcases: list, + * testcaseData: array, + * extensionMapping: array, + * allowEdit: bool + * }|RedirectResponse + */ #[Route(path: '/{probId<\d+>}/testcases', name: 'jury_problem_testcases')] - public function testcasesAction(Request $request, int $probId): Response + #[Template(template: 'jury/problem_testcases.html.twig')] + public function testcasesAction(Request $request, int $probId): array|RedirectResponse { $problem = $this->em->getRepository(Problem::class)->find($probId); if (!$problem) { @@ -790,15 +814,13 @@ public function testcasesAction(Request $request, int $probId): Response . join($lockedContests) . ', disallowing editing.'); } - $data = [ + return [ 'problem' => $problem, 'testcases' => $testcases, 'testcaseData' => $testcaseData, 'extensionMapping' => Testcase::EXTENSION_MAPPING, 'allowEdit' => $this->isGranted('ROLE_ADMIN') && empty($lockedContests), ]; - - return $this->render('jury/problem_testcases.html.twig', $data); } #[IsGranted('ROLE_ADMIN')] @@ -928,9 +950,17 @@ public function fetchTestcaseAction(int $probId, int $rank, string $type): Respo return $response; } + /** + * @return array{ + * problem: Problem, + * form: FormInterface, + * uploadForm: FormInterface + * }|RedirectResponse + */ #[IsGranted('ROLE_ADMIN')] #[Route(path: '/{probId<\d+>}/edit', name: 'jury_problem_edit')] - public function editAction(Request $request, int $probId): Response + #[Template(template: 'jury/problem_edit.html.twig')] + public function editAction(Request $request, int $probId): array|RedirectResponse { $problem = $this->em->getRepository(Problem::class)->find($probId); if (!$problem) { @@ -997,11 +1027,11 @@ public function editAction(Request $request, int $probId): Response return $this->redirectToRoute('jury_problem', ['probId' => $problem->getProbid()]); } - return $this->render('jury/problem_edit.html.twig', [ + return [ 'problem' => $problem, 'form' => $form, 'uploadForm' => $uploadForm, - ]); + ]; } #[IsGranted('ROLE_ADMIN')] @@ -1106,9 +1136,13 @@ public function deleteTestcaseAction(Request $request, int $testcaseId): Respons return $this->redirectToRoute('jury_problem_testcases', ['probId' => $problem->getProbid()]); } + /** + * @return array{form: FormInterface}|Response + */ #[IsGranted('ROLE_ADMIN')] #[Route(path: '/add', name: 'jury_problem_add')] - public function addAction(Request $request): Response + #[Template(template: 'jury/problem_add.html.twig')] + public function addAction(Request $request): array|Response { $problem = new Problem(); @@ -1123,9 +1157,9 @@ public function addAction(Request $request): Response return $response; } - return $this->render('jury/problem_add.html.twig', [ + return [ 'form' => $form, - ]); + ]; } /** diff --git a/webapp/src/Controller/Jury/QueueTaskController.php b/webapp/src/Controller/Jury/QueueTaskController.php index b7e505df62..014d5140ab 100644 --- a/webapp/src/Controller/Jury/QueueTaskController.php +++ b/webapp/src/Controller/Jury/QueueTaskController.php @@ -9,6 +9,7 @@ use App\Service\EventLogService; use App\Utils\Utils; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; @@ -42,8 +43,16 @@ public function __construct( parent::__construct($em, $eventLogService, $dj, $kernel); } + /** + * @return array{ + * queueTasksTable: list, actions: list}>, + * tableFields: array, + * numActions: int + * } + */ #[Route(path: '', name: 'jury_queue_tasks')] - public function indexAction(): Response + #[Template(template: 'jury/queue_tasks.html.twig')] + public function indexAction(): array { /** @var QueueTask[] $queueTasks */ $queueTasks = $this->em->createQueryBuilder() @@ -121,11 +130,11 @@ public function indexAction(): Response ]; } - return $this->render('jury/queue_tasks.html.twig', [ + return [ 'queueTasksTable' => $queueTasksTable, 'tableFields' => $tableFields, 'numActions' => 4, - ]); + ]; } #[Route(path: '/{queueTaskId}/change-priority/{priority}', name: 'jury_queue_task_change_priority')] @@ -161,8 +170,19 @@ public function changePriorityAction(int $queueTaskId, int $priority): RedirectR return $this->redirectToRoute('jury_queue_tasks'); } + /** + * @return array{ + * firstJudgeTask: JudgeTask|null, + * judgeTaksPriority: string|null, + * queueTask: QueueTask, + * judgeTasksTable: list, actions: list}>, + * tableFields: array, + * numActions: int + * }|RedirectResponse + */ #[Route(path: '/{queueTaskId}/judgetasks', name: 'jury_queue_task_judge_tasks')] - public function viewJudgeTasksAction(int $queueTaskId): Response + #[Template(template: 'jury/judge_tasks.html.twig')] + public function viewJudgeTasksAction(int $queueTaskId): array|RedirectResponse { $queueTask = $this->em->getRepository(QueueTask::class)->find($queueTaskId); if (!$queueTask) { @@ -227,13 +247,13 @@ public function viewJudgeTasksAction(int $queueTaskId): Response $firstJudgeTask = $judgeTasks[0] ?? null; - return $this->render('jury/judge_tasks.html.twig', [ + return [ 'firstJudgeTask' => $firstJudgeTask, 'judgeTaksPriority' => isset($firstJudgeTask) ? static::PRIORITY_MAP[$firstJudgeTask->getPriority()] : null, 'queueTask' => $queueTask, 'judgeTasksTable' => $judgeTasksTable, 'tableFields' => $tableFields, 'numActions' => 0, - ]); + ]; } } diff --git a/webapp/src/Controller/Jury/RejudgingController.php b/webapp/src/Controller/Jury/RejudgingController.php index da05c10a79..2b3767a641 100644 --- a/webapp/src/Controller/Jury/RejudgingController.php +++ b/webapp/src/Controller/Jury/RejudgingController.php @@ -26,8 +26,12 @@ use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Doctrine\ORM\Query\Expr\Join; +use App\Twig\Attribute\AjaxTemplate; use Knp\Component\Pager\Pagination\PaginationInterface; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; @@ -59,11 +63,17 @@ public function __construct( } /** + * @return array{ + * rejudgings: list>, + * table_fields: array>, + * refresh: array{after: int, url: string} + * } * @throws NoResultException * @throws NonUniqueResultException */ #[Route(path: '', name: 'jury_rejudgings')] - public function indexAction(): Response + #[Template(template: 'jury/rejudgings.html.twig')] + public function indexAction(): array { $curContest = $this->dj->getCurrentContest(); $queryBuilder = $this->em->createQueryBuilder() @@ -178,7 +188,7 @@ public function indexAction(): Response } } - $twigData = [ + return [ 'rejudgings' => $filtered_table, 'table_fields' => $table_fields, 'refresh' => [ @@ -186,15 +196,18 @@ public function indexAction(): Response 'url' => $this->generateUrl('jury_rejudgings'), ], ]; - - return $this->render('jury/rejudgings.html.twig', $twigData); } /** + * @return array * @throws NoResultException * @throws NonUniqueResultException */ #[Route(path: '/{rejudgingId<\d+>}', name: 'jury_rejudging')] + #[AjaxTemplate( + normalTemplate: 'jury/rejudging.html.twig', + ajaxTemplate: 'jury/partials/rejudging_submissions.html.twig' + )] public function viewAction( Request $request, SubmissionService $submissionService, @@ -207,7 +220,7 @@ public function viewAction( string $newverdict = 'all', #[MapQueryParameter(name: 'show_statistics')] ?bool $showStatistics = null, - ): Response { + ): array { // Close the session, as this might take a while and we don't need the session below. $this->requestStack->getSession()->save(); @@ -418,23 +431,23 @@ public function viewAction( ]; if ($request->isXmlHttpRequest()) { $data['ajax'] = true; - return $this->render('jury/partials/rejudging_submissions.html.twig', $data); - } else { - return $this->render('jury/rejudging.html.twig', $data); } + return $data; } /** + * @return array{action: string, rejudging: Rejudging}|Response * @throws NonUniqueResultException */ #[Route(path: '/{rejudgingId<\d+>}/{action}', name: 'jury_rejudging_finish')] + #[Template(template: 'jury/rejudging_finish.html.twig')] public function finishAction( Request $request, RejudgingService $rejudgingService, ?Profiler $profiler, int $rejudgingId, string $action - ): Response { + ): array|Response { // Note: we use a XMLHttpRequest here as Symfony does not support streaming Twig output // Disable the profiler toolbar to avoid OOMs. @@ -470,14 +483,18 @@ public function finishAction( }); } - return $this->render('jury/rejudging_finish.html.twig', [ + return [ 'action' => $action, 'rejudging' => $rejudging, - ]); + ]; } + /** + * @return array|Response + */ #[Route(path: '/add', name: 'jury_rejudging_add')] - public function addAction(Request $request, FormFactoryInterface $formFactory): Response + #[Template(template: 'jury/rejudging_add.html.twig')] + public function addAction(Request $request, FormFactoryInterface $formFactory): array|Response { $isContestUpdateAjax = $request->isXmlHttpRequest() && $request->request->getBoolean('refresh_form'); $isCreateRejudgingAjax = $request->isMethod('POST') && $request->isXmlHttpRequest() && !$isContestUpdateAjax; @@ -532,10 +549,10 @@ public function addAction(Request $request, FormFactoryInterface $formFactory): 'referer' => $request->headers->get('referer'), 'overshoot' => $formData['overshoot'], ]; - return $this->render('jury/rejudging_add.html.twig', [ + return [ 'data' => http_build_query($data), 'url' => $this->generateUrl('jury_rejudging_add'), - ]); + ]; } if ($isCreateRejudgingAjax) { $progressReporter = function (int $progress, string $log, ?string $redirect = null) { @@ -665,8 +682,12 @@ public function addAction(Request $request, FormFactoryInterface $formFactory): ]); } + /** + * @return array{data: string, url: string}|Response + */ #[Route(path: '/create', methods: ['POST'], name: 'jury_create_rejudge')] - public function createAction(Request $request): Response + #[Template(template: 'jury/rejudging_add.html.twig')] + public function createAction(Request $request): array|Response { $table = $request->request->get('table'); $id = $request->request->get('id'); @@ -717,10 +738,10 @@ public function createAction(Request $request): Response if (!$request->isXmlHttpRequest()) { $data = $request->request->all(); $data['referer'] = $request->headers->get('referer'); - return $this->render('jury/rejudging_add.html.twig', [ + return [ 'data' => http_build_query($data), 'url' => $this->generateUrl('jury_create_rejudge'), - ]); + ]; } $progressReporter = function (int $progress, string $log, ?string $redirect = null) { diff --git a/webapp/src/Controller/Jury/ScoreboardController.php b/webapp/src/Controller/Jury/ScoreboardController.php index d866fe4039..6a99816945 100644 --- a/webapp/src/Controller/Jury/ScoreboardController.php +++ b/webapp/src/Controller/Jury/ScoreboardController.php @@ -4,6 +4,8 @@ use App\Service\DOMJudgeService; use App\Service\ScoreboardService; +use App\Twig\Attribute\AjaxTemplate; +use App\Twig\EventListener\CustomResponseListener; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpFoundation\Request; @@ -19,8 +21,12 @@ public function __construct(protected readonly DOMJudgeService $dj, protected re { } + /** + * @return array + */ #[Route(path: '', name: 'jury_scoreboard')] - public function scoreboardAction(Request $request): Response + #[AjaxTemplate(normalTemplate: 'jury/scoreboard.html.twig', ajaxTemplate: 'partials/scoreboard.html.twig')] + public function scoreboardAction(Request $request, CustomResponseListener $customResponseListener): array { $response = new Response(); $refreshUrl = $this->generateUrl('jury_scoreboard'); @@ -29,10 +35,11 @@ public function scoreboardAction(Request $request): Response $request, $response, $refreshUrl, $this->isGranted('ROLE_JURY'), false, false, $contest ); + $customResponseListener->setCustomResponse($response); + if ($request->isXmlHttpRequest()) { $data['current_contest'] = $contest; - return $this->render('partials/scoreboard.html.twig', $data, $response); } - return $this->render('jury/scoreboard.html.twig', $data, $response); + return $data; } } diff --git a/webapp/src/Controller/Jury/ShadowDifferencesController.php b/webapp/src/Controller/Jury/ShadowDifferencesController.php index 6462a4c679..737d430d74 100644 --- a/webapp/src/Controller/Jury/ShadowDifferencesController.php +++ b/webapp/src/Controller/Jury/ShadowDifferencesController.php @@ -12,13 +12,14 @@ use App\Service\DOMJudgeService; use App\Service\EventLogService; use App\Service\SubmissionService; +use App\Twig\Attribute\AjaxTemplate; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Doctrine\ORM\Query\Expr\Join; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\Attribute\Route; @@ -41,10 +42,30 @@ public function __construct( } /** + * @return array{ + * verdicts: array, + * used: array, + * verdictTable: array>>, + * viewTypes: array, + * view: int, + * verificationViewTypes: array, + * verificationView: int, + * submissions: list, + * submissionCounts: array, + * external: string, + * local: string, + * showExternalResult: bool, + * showContest: bool, + * showTestcases: bool, + * showExternalTestcases: bool, + * refresh: array{after: int, url: string, ajax: bool}, + * ajax?: bool + * }|RedirectResponse * @throws NoResultException * @throws NonUniqueResultException */ #[Route(path: '', name: 'jury_shadow_differences')] + #[AjaxTemplate(normalTemplate: 'jury/shadow_differences.html.twig', ajaxTemplate: 'jury/partials/shadow_submissions.html.twig')] public function indexAction( Request $request, #[MapQueryParameter(name: 'view')] @@ -55,7 +76,7 @@ public function indexAction( string $external = 'all', #[MapQueryParameter] string $local = 'all', - ): Response { + ): array|RedirectResponse { if (!$this->dj->shadowMode()) { $this->addFlash('danger', 'Shadow differences only supported when shadow_mode is true'); return $this->redirectToRoute('jury_index'); @@ -233,9 +254,7 @@ public function indexAction( ]; if ($request->isXmlHttpRequest()) { $data['ajax'] = true; - return $this->render('jury/partials/shadow_submissions.html.twig', $data); - } else { - return $this->render('jury/shadow_differences.html.twig', $data); } + return $data; } } diff --git a/webapp/src/Controller/Jury/SubmissionController.php b/webapp/src/Controller/Jury/SubmissionController.php index acca869506..deaea0e9ea 100644 --- a/webapp/src/Controller/Jury/SubmissionController.php +++ b/webapp/src/Controller/Jury/SubmissionController.php @@ -40,9 +40,13 @@ use Knp\Component\Pager\Pagination\PaginationInterface; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use App\Twig\Attribute\AjaxTemplate; +use App\Twig\EventListener\CustomResponseListener; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -75,12 +79,34 @@ public function __construct( parent::__construct($em, $eventLogService, $dj, $kernel); } + /** + * @return array{ + * refresh: array{after: int, url: string, ajax: bool}, + * viewTypes: array, + * view: int, + * submissions: PaginationInterface, + * submissionCounts: array, + * showContest: bool, + * hasFilters: bool, + * results: list, + * showExternalResult: bool, + * showTestcases: bool, + * disabledProbs: array, + * disabledLangs: array, + * form?: mixed + * } + */ #[Route(path: '', name: 'jury_submissions')] + #[AjaxTemplate( + normalTemplate: 'jury/submissions.html.twig', + ajaxTemplate: 'jury/partials/submission_list.html.twig' + )] public function indexAction( Request $request, + CustomResponseListener $customResponseListener, #[MapQueryParameter(name: 'view')] ?string $viewFromRequest = null, - ): Response { + ): array { $viewTypes = [0 => 'all', 1 => 'unverified', 2 => 'unjudged', 3 => 'judging']; $view = 0; if (($submissionViewCookie = $this->dj->getCookie('domjudge_submissionview')) && @@ -213,19 +239,21 @@ public function indexAction( ]; // For ajax requests, only return the submission list partial. - if ($request->isXmlHttpRequest()) { - return $this->render('jury/partials/submission_list.html.twig', $data); + if (!$request->isXmlHttpRequest()) { + $data["form"] = $form->createView(); } - $data["form"] = $form->createView(); + $customResponseListener->setCustomResponse($response); - return $this->render('jury/submissions.html.twig', $data, $response); + return $data; } /** + * @return array|RedirectResponse * @throws NonUniqueResultException */ #[Route(path: '/{submitId<\d+>}', name: 'jury_submission')] + #[Template(template: 'jury/submission.html.twig')] public function viewAction( Request $request, int $submitId, @@ -233,7 +261,7 @@ public function viewAction( ?int $judgingId = null, #[MapQueryParameter(name: 'rejudgingid')] ?int $rejudgingId = null, - ): Response { + ): array|RedirectResponse { if (isset($judgingId, $rejudgingId)) { throw new BadRequestHttpException("You cannot specify jid and rejudgingid at the same time."); } @@ -648,7 +676,7 @@ public function viewAction( } } - return $this->render('jury/submission.html.twig', $twigData); + return $twigData; } #[Route(path: '/request-full-debug/{jid}', name: 'request_full_debug')] @@ -805,14 +833,26 @@ private function allowEdit(): bool { } /** + * @return array{ + * submission: Submission, + * files: list, + * oldSubmission: Submission|null, + * oldFiles: list|null, + * oldFileStats: array, + * originalSubmission: Submission|null, + * originalFiles: list|null, + * originalFileStats: array, + * allowEdit: bool + * }|Response * @throws NonUniqueResultException */ #[Route(path: '/{submission}/source', name: 'jury_submission_source')] + #[Template(template: 'jury/submission_source.html.twig')] public function sourceAction( Submission $submission, #[MapQueryParameter] ?int $fetch = null - ): Response { + ): array|Response { if ($fetch !== null) { /** @var SubmissionFile|null $file */ $file = $this->em->createQueryBuilder() @@ -914,7 +954,7 @@ public function sourceAction( $oldFileStats = $oldFiles !== null ? $this->determineFileChanged($files, $oldFiles) : []; $originalFileStats = $originalFiles !== null ? $this->determineFileChanged($files, $originalFiles) : []; - return $this->render('jury/submission_source.html.twig', [ + return [ 'submission' => $submission, 'files' => $files, 'oldSubmission' => $oldSubmission, @@ -924,11 +964,20 @@ public function sourceAction( 'originalFiles' => $originalFiles, 'originalFileStats' => $originalFileStats, 'allowEdit' => $this->allowEdit(), - ]); + ]; } + /** + * @return array{ + * submission: Submission, + * files: list, + * form: FormInterface, + * selected: int|null + * }|RedirectResponse + */ #[Route(path: '/{submission}/edit-source', name: 'jury_submission_edit_source')] - public function editSourceAction(Request $request, Submission $submission, #[MapQueryParameter] ?int $rank = null): Response + #[Template(template: 'jury/submission_edit_source.html.twig')] + public function editSourceAction(Request $request, Submission $submission, #[MapQueryParameter] ?int $rank = null): array|RedirectResponse { if (!$this->allowEdit()) { $this->addFlash('danger', 'You cannot re-submit code without being a team.'); @@ -1039,12 +1088,12 @@ public function editSourceAction(Request $request, Submission $submission, #[Map return $this->redirectToRoute('jury_submission', ['submitId' => $submittedSubmission->getSubmitid()]); } - return $this->render('jury/submission_edit_source.html.twig', [ + return [ 'submission' => $submission, 'files' => $files, 'form' => $form, 'selected' => $rank, - ]); + ]; } /** diff --git a/webapp/src/Controller/Jury/TeamAffiliationController.php b/webapp/src/Controller/Jury/TeamAffiliationController.php index ef0ab44ee5..5b967e56b6 100644 --- a/webapp/src/Controller/Jury/TeamAffiliationController.php +++ b/webapp/src/Controller/Jury/TeamAffiliationController.php @@ -10,8 +10,12 @@ use App\Service\DOMJudgeService; use App\Service\EventLogService; use App\Service\ScoreboardService; +use App\Twig\Attribute\AjaxTemplate; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; @@ -36,11 +40,22 @@ public function __construct( parent::__construct($em, $eventLogService, $dj, $kernel); } + /** + * @return array{ + * team_affiliations: list>, + * actions: list>, + * link: string + * }>, + * table_fields: array> + * } + */ #[Route(path: '', name: 'jury_team_affiliations')] + #[Template(template: 'jury/team_affiliations.html.twig')] public function indexAction( #[Autowire('%kernel.project_dir%')] string $projectDir - ): Response { + ): array { $em = $this->em; $teamAffiliations = $em->createQueryBuilder() ->select('a', 'COUNT(t.teamid) AS num_teams') @@ -125,14 +140,21 @@ public function indexAction( ]; } - return $this->render('jury/team_affiliations.html.twig', [ + return [ 'team_affiliations' => $team_affiliations_table, 'table_fields' => $table_fields, - ]); + ]; } + /** + * @return array + */ #[Route(path: '/{affilId<\d+>}', name: 'jury_team_affiliation')] - public function viewAction(Request $request, ScoreboardService $scoreboardService, int $affilId): Response + #[AjaxTemplate( + normalTemplate: 'jury/team_affiliation.html.twig', + ajaxTemplate: 'partials/scoreboard_table.html.twig' + )] + public function viewAction(Request $request, ScoreboardService $scoreboardService, int $affilId): array { $teamAffiliation = $this->em->getRepository(TeamAffiliation::class)->find($affilId); if (!$teamAffiliation) { @@ -164,15 +186,21 @@ public function viewAction(Request $request, ScoreboardService $scoreboardServic // For ajax requests, only return the submission list partial. if ($request->isXmlHttpRequest()) { $data['displayRank'] = true; - return $this->render('partials/scoreboard_table.html.twig', $data); } - return $this->render('jury/team_affiliation.html.twig', $data); + return $data; } + /** + * @return array{ + * teamAffiliation: TeamAffiliation, + * form: FormInterface + * }|RedirectResponse + */ #[IsGranted('ROLE_ADMIN')] #[Route(path: '/{affilId<\d+>}/edit', name: 'jury_team_affiliation_edit')] - public function editAction(Request $request, int $affilId): Response + #[Template(template: 'jury/team_affiliation_edit.html.twig')] + public function editAction(Request $request, int $affilId): array|RedirectResponse { $teamAffiliation = $this->em->getRepository(TeamAffiliation::class)->find($affilId); if (!$teamAffiliation) { @@ -189,10 +217,10 @@ public function editAction(Request $request, int $affilId): Response return $this->redirectToRoute('jury_team_affiliation', ['affilId' => $teamAffiliation->getAffilid()]); } - return $this->render('jury/team_affiliation_edit.html.twig', [ + return [ 'teamAffiliation' => $teamAffiliation, 'form' => $form, - ]); + ]; } #[IsGranted('ROLE_ADMIN')] @@ -220,9 +248,13 @@ public function deleteMultipleAction(Request $request): Response ); } + /** + * @return array{form: FormInterface}|Response + */ #[IsGranted('ROLE_ADMIN')] #[Route(path: '/add', name: 'jury_team_affiliation_add')] - public function addAction(Request $request): Response + #[Template(template: 'jury/team_affiliation_add.html.twig')] + public function addAction(Request $request): array|Response { $teamAffiliation = new TeamAffiliation(); @@ -243,8 +275,8 @@ function () use ($teamAffiliation) { return $response; } - return $this->render('jury/team_affiliation_add.html.twig', [ + return [ 'form' => $form, - ]); + ]; } } diff --git a/webapp/src/Controller/Jury/TeamCategoryController.php b/webapp/src/Controller/Jury/TeamCategoryController.php index 19f845c94f..209d5b5f6d 100644 --- a/webapp/src/Controller/Jury/TeamCategoryController.php +++ b/webapp/src/Controller/Jury/TeamCategoryController.php @@ -13,10 +13,13 @@ use App\Service\DOMJudgeService; use App\Service\EventLogService; use App\Service\SubmissionService; +use App\Twig\Attribute\AjaxTemplate; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Knp\Component\Pager\Pagination\PaginationInterface; +use Symfony\Bridge\Twig\Attribute\Template; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -43,8 +46,20 @@ public function __construct( parent::__construct($em, $eventLogService, $dj, $kernel); } + /** + * @return array{ + * team_categories: list>, + * actions: list>, + * link: string, + * style: string + * }>, + * table_fields: array> + * } + */ #[Route(path: '', name: 'jury_team_categories')] - public function indexAction(): Response + #[Template(template: 'jury/team_categories.html.twig')] + public function indexAction(): array { $em = $this->em; $teamCategories = $em->createQueryBuilder() @@ -114,18 +129,31 @@ public function indexAction(): Response 'style' => $teamCategory->getColor() ? sprintf('background-color: %s;', $teamCategory->getColor()) : '', ]; } - return $this->render('jury/team_categories.html.twig', [ + return [ 'team_categories' => $team_categories_table, 'table_fields' => $table_fields, - ]); + ]; } /** + * @return array{ + * teamCategory: TeamCategory, + * submissions: PaginationInterface, + * submissionCounts: array, + * showContest: bool, + * showExternalResult: bool, + * showTestcases?: bool, + * refresh: array{after: int, url: string, ajax: bool} + * } * @throws NoResultException * @throws NonUniqueResultException */ #[Route(path: '/{categoryId<\d+>}', name: 'jury_team_category')] - public function viewAction(Request $request, SubmissionService $submissionService, int $categoryId): Response + #[AjaxTemplate( + normalTemplate: 'jury/team_category.html.twig', + ajaxTemplate: 'jury/partials/submission_list.html.twig' + )] + public function viewAction(Request $request, SubmissionService $submissionService, int $categoryId): array { $teamCategory = $this->em->getRepository(TeamCategory::class)->find($categoryId); if (!$teamCategory) { @@ -155,15 +183,21 @@ public function viewAction(Request $request, SubmissionService $submissionServic // For ajax requests, only return the submission list partial. if ($request->isXmlHttpRequest()) { $data['showTestcases'] = false; - return $this->render('jury/partials/submission_list.html.twig', $data); } - return $this->render('jury/team_category.html.twig', $data); + return $data; } + /** + * @return array{ + * teamCategory: TeamCategory, + * form: FormInterface + * }|RedirectResponse + */ #[IsGranted('ROLE_ADMIN')] #[Route(path: '/{categoryId<\d+>}/edit', name: 'jury_team_category_edit')] - public function editAction(Request $request, int $categoryId): Response + #[Template(template: 'jury/team_category_edit.html.twig')] + public function editAction(Request $request, int $categoryId): array|RedirectResponse { $teamCategory = $this->em->getRepository(TeamCategory::class)->find($categoryId); if (!$teamCategory) { @@ -196,10 +230,10 @@ public function editAction(Request $request, int $categoryId): Response return $this->redirectToRoute('jury_team_category', ['categoryId' => $teamCategory->getCategoryid()]); } - return $this->render('jury/team_category_edit.html.twig', [ + return [ 'teamCategory' => $teamCategory, 'form' => $form, - ]); + ]; } #[IsGranted('ROLE_ADMIN')] @@ -214,9 +248,13 @@ public function deleteAction(Request $request, int $categoryId): Response return $this->deleteEntities($request, [$teamCategory], $this->generateUrl('jury_team_categories')); } + /** + * @return array{form: FormInterface}|Response + */ #[IsGranted('ROLE_ADMIN')] #[Route(path: '/add', name: 'jury_team_category_add')] - public function addAction(Request $request): Response + #[Template(template: 'jury/team_category_add.html.twig')] + public function addAction(Request $request): array|Response { $teamCategory = new TeamCategory(); @@ -231,9 +269,9 @@ public function addAction(Request $request): Response return $response; } - return $this->render('jury/team_category_add.html.twig', [ + return [ 'form' => $form, - ]); + ]; } #[IsGranted('ROLE_ADMIN')] diff --git a/webapp/src/Controller/Jury/TeamController.php b/webapp/src/Controller/Jury/TeamController.php index fbc4f2485c..6bdff2b6d4 100644 --- a/webapp/src/Controller/Jury/TeamController.php +++ b/webapp/src/Controller/Jury/TeamController.php @@ -15,8 +15,12 @@ use App\Service\EventLogService; use App\Service\ScoreboardService; use App\Service\SubmissionService; +use App\Twig\Attribute\AjaxTemplate; use App\Utils\Utils; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bridge\Twig\Attribute\Template; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; @@ -42,8 +46,12 @@ public function __construct( parent::__construct($em, $eventLogService, $dj, $kernel); } + /** + * @return array{teams: list>, table_fields: array>} + */ #[Route(path: '', name: 'jury_teams')] - public function indexAction(): Response + #[Template(template: 'jury/teams.html.twig')] + public function indexAction(): array { /** @var Team[] $teams */ $teams = $this->em->createQueryBuilder() @@ -221,13 +229,20 @@ public function indexAction(): Response ($t->getEnabled() ? '' : ' disabled'), ]; } - return $this->render('jury/teams.html.twig', [ + return [ 'teams' => $teams_table, 'table_fields' => $table_fields, - ]); + ]; } + /** + * @return array + */ #[Route(path: '/{teamId<\d+>}', name: 'jury_team')] + #[AjaxTemplate( + normalTemplate: 'jury/team.html.twig', + ajaxTemplate: 'jury/partials/team_score_and_submissions.html.twig' + )] public function viewAction( Request $request, int $teamId, @@ -235,7 +250,7 @@ public function viewAction( SubmissionService $submissionService, #[MapQueryParameter] ?int $cid = null, - ): Response { + ): array { $team = $this->em->getRepository(Team::class)->find($teamId); if (!$team) { throw new NotFoundHttpException(sprintf('Team with ID %s not found', $teamId)); @@ -307,15 +322,21 @@ public function viewAction( if ($request->isXmlHttpRequest()) { $data['displayRank'] = true; $data['jury'] = true; - return $this->render('jury/partials/team_score_and_submissions.html.twig', $data); } - return $this->render('jury/team.html.twig', $data); + return $data; } + /** + * @return array{ + * team: Team, + * form: FormInterface + * }|RedirectResponse + */ #[IsGranted('ROLE_ADMIN')] #[Route(path: '/{teamId<\d+>}/edit', name: 'jury_team_edit')] - public function editAction(Request $request, int $teamId): Response + #[Template(template: 'jury/team_edit.html.twig')] + public function editAction(Request $request, int $teamId): array|RedirectResponse { $team = $this->em->getRepository(Team::class)->find($teamId); if (!$team) { @@ -333,10 +354,10 @@ public function editAction(Request $request, int $teamId): Response return $this->redirectToRoute('jury_team', ['teamId' => $team->getTeamid()]); } - return $this->render('jury/team_edit.html.twig', [ + return [ 'team' => $team, 'form' => $form, - ]); + ]; } #[IsGranted('ROLE_ADMIN')] @@ -365,9 +386,16 @@ public function deleteMultipleAction(Request $request): Response ); } + /** + * @return array{ + * team: Team, + * form: FormInterface + * }|Response + */ #[IsGranted('ROLE_ADMIN')] #[Route(path: '/add', name: 'jury_team_add')] - public function addAction(Request $request): Response + #[Template(template: 'jury/team_add.html.twig')] + public function addAction(Request $request): array|Response { $team = new Team(); $team->setAddUserForTeam(Team::CREATE_NEW_USER); @@ -389,10 +417,10 @@ function () use ($team) { return $response; } - return $this->render('jury/team_add.html.twig', [ + return [ 'team' => $team, 'form' => $form, - ]); + ]; } /** diff --git a/webapp/src/Controller/Jury/UserController.php b/webapp/src/Controller/Jury/UserController.php index 4a91099e16..9ecdd5ed19 100644 --- a/webapp/src/Controller/Jury/UserController.php +++ b/webapp/src/Controller/Jury/UserController.php @@ -16,8 +16,10 @@ use App\Service\SubmissionService; use App\Utils\Utils; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Form\FormInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; @@ -50,8 +52,15 @@ public function __construct( parent::__construct($em, $eventLogService, $dj, $kernel); } + /** + * @return array{ + * users: list>, + * table_fields: array> + * } + */ + #[Template(template: 'jury/users.html.twig')] #[Route(path: '', name: 'jury_users')] - public function indexAction(): Response + public function indexAction(): array { /** @var User[] $users */ $users = $this->em->createQueryBuilder() @@ -171,14 +180,25 @@ public function indexAction(): Response ]; } - return $this->render('jury/users.html.twig', [ + return [ 'users' => $users_table, 'table_fields' => $table_fields, - ]); + ]; } + /** + * @return array{ + * user: User, + * submissions: list, + * submissionCounts: array, + * showContest: bool, + * showExternalResult: bool, + * refresh: array{after: int, url: string, ajax: bool} + * } + */ + #[Template(template: 'jury/user.html.twig')] #[Route(path: '/{userId<\d+>}', name: 'jury_user')] - public function viewAction(Request $request, int $userId, SubmissionService $submissionService): Response + public function viewAction(Request $request, int $userId, SubmissionService $submissionService): array { $user = $this->em->getRepository(User::class)->find($userId); if (!$user) { @@ -192,7 +212,7 @@ public function viewAction(Request $request, int $userId, SubmissionService $sub page: $request->query->getInt('page', 1), ); - return $this->render('jury/user.html.twig', [ + return [ 'user' => $user, 'submissions' => $submissions, 'submissionCounts' => $submissionCounts, @@ -203,26 +223,38 @@ public function viewAction(Request $request, int $userId, SubmissionService $sub 'url' => $this->generateUrl('jury_user', ['userId' => $user->getUserid()]), 'ajax' => true, ], - ]); + ]; } - public function checkPasswordLength(User $user, FormInterface $form): ?Response + /** + * @return array{ + * user: User, + * form: FormInterface + * }|null + */ + public function checkPasswordLength(User $user, FormInterface $form): ?array { if ($user->getPlainPassword() && strlen($user->getPlainPassword()) < $this->minimumPasswordLength) { $this->addFlash('danger', "Password should be " . $this->minimumPasswordLength . "+ chars."); - return $this->render('jury/user_edit.html.twig', [ + return [ 'user' => $user, 'form' => $form, - 'min_password_length' => $this->minimumPasswordLength, - ]); + ]; } return null; } + /** + * @return array{ + * user: User, + * form: FormInterface + * }|RedirectResponse + */ + #[Template(template: 'jury/user_edit.html.twig')] #[IsGranted('ROLE_ADMIN')] #[Route(path: '/{userId<\d+>}/edit', name: 'jury_user_edit')] - public function editAction(Request $request, int $userId): Response + public function editAction(Request $request, int $userId): array|RedirectResponse { $user = $this->em->getRepository(User::class)->find($userId); if (!$user) { @@ -253,10 +285,10 @@ public function editAction(Request $request, int $userId): Response return $this->redirectToRoute('jury_user', ['userId' => $user->getUserid()]); } - return $this->render('jury/user_edit.html.twig', [ - 'user' => $user, - 'form' => $form, - ]); + return [ + 'user' => $user, + 'form' => $form, + ]; } #[IsGranted('ROLE_ADMIN')] @@ -271,13 +303,20 @@ public function deleteAction(Request $request, int $userId): Response return $this->deleteEntities($request, [$user], $this->generateUrl('jury_users')); } + /** + * @return array{ + * user: User, + * form: FormInterface + * }|Response + */ + #[Template(template: 'jury/user_add.html.twig')] #[IsGranted('ROLE_ADMIN')] #[Route(path: '/add', name: 'jury_user_add')] public function addAction( Request $request, #[MapQueryParameter] ?int $team = null, - ): Response { + ): array|Response { $user = new User(); if ($team) { $user->setTeam($this->em->getRepository(Team::class)->find($team)); @@ -302,15 +341,19 @@ function () use ($user, $form) { return $response; } - return $this->render('jury/user_add.html.twig', [ + return [ 'user' => $user, 'form' => $form, - ]); + ]; } + /** + * @return array{form: FormInterface}|Response + */ + #[Template(template: 'jury/user_generate_passwords.html.twig')] #[IsGranted('ROLE_ADMIN')] #[Route(path: '/generate-passwords', name: 'jury_generate_passwords')] - public function generatePasswordsAction(Request $request): Response + public function generatePasswordsAction(Request $request): array|Response { $form = $this->createForm(GeneratePasswordsType::class); $form->handleRequest($request); @@ -368,9 +411,7 @@ public function generatePasswordsAction(Request $request): Response return $response; } - return $this->render('jury/user_generate_passwords.html.twig', [ - 'form' => $form, - ]); + return ['form' => $form]; } #[IsGranted('ROLE_ADMIN')] diff --git a/webapp/src/Controller/Jury/VersionController.php b/webapp/src/Controller/Jury/VersionController.php index f8c0ab2f8b..d2afacabaf 100644 --- a/webapp/src/Controller/Jury/VersionController.php +++ b/webapp/src/Controller/Jury/VersionController.php @@ -8,6 +8,7 @@ use App\Service\DOMJudgeService; use App\Service\EventLogService; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\KernelInterface; @@ -27,8 +28,20 @@ public function __construct( parent::__construct($em, $eventLogService, $dj, $kernel); } + /** + * @return array{ + * data: list, versionid: int}>, + * canonical_compiler_key: string, + * canonical_runner_key: string, + * runner_outputs: array, versionid: int}> + * }> + * } + */ #[Route(path: '', name: 'jury_versions')] - public function indexAction(): Response + #[Template(template: 'jury/versions.html.twig')] + public function indexAction(): array { /** @var Language[] $languages */ $languages = $this->em->createQueryBuilder() @@ -97,7 +110,7 @@ public function indexAction(): Response ]; } - return $this->render('jury/versions.html.twig', ['data' => $data]); + return ['data' => $data]; } #[IsGranted('ROLE_ADMIN')] diff --git a/webapp/src/Controller/PublicController.php b/webapp/src/Controller/PublicController.php index 27b3308b75..9f7b40010c 100644 --- a/webapp/src/Controller/PublicController.php +++ b/webapp/src/Controller/PublicController.php @@ -14,9 +14,15 @@ use App\Service\ScoreboardService; use App\Service\StatisticsService; use App\Service\SubmissionService; +use App\Twig\Attribute\AjaxTemplate; +use App\Twig\EventListener\CustomResponseListener; use App\Twig\TwigExtension; +use App\Utils\Scoreboard\Filter; +use App\Utils\Scoreboard\Scoreboard; +use DateTime; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -47,15 +53,25 @@ public function __construct( parent::__construct($em, $eventLog, $dj, $kernel); } + /** + * @return array{refresh?: array{after: int, url: string, ajax: bool}, static: bool, contest?: Contest, + * scoreFilter?: Filter, scoreboard: Scoreboard, filterValues: array, + * groupedAffiliations: null|array>>, + * showFlags: int, showAffiliationLogos: bool, showAffiliations: int, showPending: int, + * showTeamSubmissions: int, scoreInSeconds: bool, maxWidth: int, jury?: bool, + * public?: bool, ajax?: bool, hide_menu?: bool, current_contest: Contest|null}|RedirectResponse + */ #[Route(path: '', name: 'public_index')] #[Route(path: '/scoreboard')] + #[AjaxTemplate(normalTemplate: 'public/scoreboard.html.twig', ajaxTemplate: 'partials/scoreboard.html.twig')] public function scoreboardAction( Request $request, + CustomResponseListener $customResponseListener, #[MapQueryParameter(name: 'contest')] ?string $contestId = null, #[MapQueryParameter] ?bool $static = false, - ): Response { + ): array|RedirectResponse { $response = new Response(); $refreshUrl = $this->generateUrl('public_index'); $contest = $this->dj->getCurrentContest(onlyPublic: true); @@ -84,16 +100,15 @@ public function scoreboardAction( $request, $response, $refreshUrl, false, true, $static, $contest ); + $customResponseListener->setCustomResponse($response); + if ($static) { $data['hide_menu'] = true; } $data['current_contest'] = $contest; - if ($request->isXmlHttpRequest()) { - return $this->render('partials/scoreboard.html.twig', $data, $response); - } - return $this->render('public/scoreboard.html.twig', $data, $response); + return $data; } #[Route(path: '/scoreboard.zip', name: 'public_scoreboard_data_zip')] @@ -170,8 +185,12 @@ public function changeContestAction(Request $request, RouterInterface $router, i $response); } + /** + * @return array{team: Team|null, showFlags: bool, showAffiliations: bool} + */ #[Route(path: '/team/{teamId<\d+>}', name: 'public_team')] - public function teamAction(Request $request, int $teamId): Response + #[AjaxTemplate(normalTemplate: 'public/team.html.twig', ajaxTemplate: 'public/team_modal.html.twig')] + public function teamAction(Request $request, int $teamId): array { /** @var Team|null $team */ $team = $this->em->getRepository(Team::class)->find($teamId); @@ -186,21 +205,24 @@ public function teamAction(Request $request, int $teamId): Response 'showAffiliations' => $showAffiliations, ]; - if ($request->isXmlHttpRequest()) { - return $this->render('public/team_modal.html.twig', $data); - } - - return $this->render('public/team.html.twig', $data); + return $data; } /** + * @return array{'problems': ContestProblem[], 'samples': string[], 'showLimits': bool, + * 'defaultMemoryLimit': int, 'timeFactorDiffers': bool, + * 'stats': array{'numBuckets': int, 'maxBucketSizeCorrect': int, + * 'maxBucketSizeCorrect': int, 'maxBucketSizeIncorrect': int, + * 'problems': array, + * 'incorrect': array}>}} + * * @throws NonUniqueResultException */ #[Route(path: '/problems', name: 'public_problems')] - public function problemsAction(): Response + #[Template(template: 'public/problems.html.twig')] + public function problemsAction(): array { - return $this->render('public/problems.html.twig', - $this->dj->getTwigDataForProblemsAction($this->stats)); + return $this->dj->getTwigDataForProblemsAction($this->stats); } #[Route(path: '/problems/{probId<\d+>}/statement', name: 'public_problem_statement')] @@ -275,8 +297,12 @@ protected function getBinaryFile(int $probId, callable $response): StreamedRespo return $response($probId, $contest, $contestProblem); } + /** + * @return array{contest: Contest, problem: ContestProblem, team: Team|null} + */ #[Route(path: '/submissions/team/{teamId}/problem/{problemId}', name: 'public_submissions')] - public function submissionsAction(Request $request, string $teamId, string $problemId): Response + #[Template(template: 'public/team_submissions.html.twig')] + public function submissionsAction(Request $request, string $teamId, string $problemId): array { $contest = $this->dj->getCurrentContest(onlyPublic: true); @@ -310,13 +336,11 @@ public function submissionsAction(Request $request, string $teamId, string $prob throw $this->createNotFoundException('Problem not found'); } - $data = [ + return [ 'contest' => $contest, 'problem' => $problem, 'team' => $team, ]; - - return $this->render('public/team_submissions.html.twig', $data); } #[Route(path: '/submissions-data.json', name: 'public_submissions_data')] diff --git a/webapp/src/Controller/SecurityController.php b/webapp/src/Controller/SecurityController.php index 15bc302dff..d16844e5b4 100644 --- a/webapp/src/Controller/SecurityController.php +++ b/webapp/src/Controller/SecurityController.php @@ -9,16 +9,21 @@ use App\Form\Type\UserRegistrationType; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; +use App\Twig\EventListener\CustomResponseListener; use Doctrine\ORM\EntityManagerInterface; use Ramsey\Uuid\Uuid; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; class SecurityController extends AbstractController @@ -31,12 +36,24 @@ public function __construct( private readonly int $minimumPasswordLength, ) {} + /** + * @return array{ + * last_username: string, + * error: AuthenticationException|null, + * allow_registration: bool, + * allowed_authmethods: string[], + * auth_xheaders_present: string|null, + * auth_ipaddress_users: User[] + * }|RedirectResponse + */ #[Route(path: '/login', name: 'login')] + #[Template(template: 'security/login.html.twig')] public function loginAction( Request $request, AuthorizationCheckerInterface $authorizationChecker, - AuthenticationUtils $authUtils - ): Response { + AuthenticationUtils $authUtils, + CustomResponseListener $customResponseListener, + ): array|RedirectResponse { $allowIPAuth = false; $authmethods = $this->config->get('auth_methods'); @@ -70,24 +87,30 @@ public function loginAction( $response->setStatusCode(401); } + $customResponseListener->setCustomResponse($response); + $selfRegistrationCategoriesCount = $em->getRepository(TeamCategory::class)->count(['allow_self_registration' => 1]); - return $this->render('security/login.html.twig', [ + return [ 'last_username' => $lastUsername, 'error' => $error, 'allow_registration' => $selfRegistrationCategoriesCount !== 0, 'allowed_authmethods' => $authmethods, 'auth_xheaders_present' => $request->headers->get('X-DOMjudge-Login'), 'auth_ipaddress_users' => $auth_ipaddress_users, - ], $response); + ]; } + /** + * @return array{registration_form: FormInterface}|RedirectResponse + */ #[Route(path: '/register', name: 'register')] + #[Template(template: 'security/register.html.twig')] public function registerAction( Request $request, AuthorizationCheckerInterface $authorizationChecker, UserPasswordHasherInterface $passwordHasher - ): Response { + ): array|RedirectResponse { // Redirect if already logged in if ($authorizationChecker->isGranted('IS_AUTHENTICATED_FULLY')) { return $this->redirectToRoute('root'); @@ -165,6 +188,6 @@ public function registerAction( return $this->redirectToRoute('login'); } - return $this->render('security/register.html.twig', ['registration_form' => $registration_form]); + return ['registration_form' => $registration_form]; } } diff --git a/webapp/src/Controller/Team/ClarificationController.php b/webapp/src/Controller/Team/ClarificationController.php index c6c5079ff1..8d0b2f07fa 100644 --- a/webapp/src/Controller/Team/ClarificationController.php +++ b/webapp/src/Controller/Team/ClarificationController.php @@ -11,6 +11,7 @@ use App\Service\ConfigurationService; use App\Service\DOMJudgeService; use App\Service\EventLogService; +use App\Twig\Attribute\AjaxTemplate; use App\Utils\Utils; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; @@ -18,6 +19,7 @@ use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -45,8 +47,16 @@ public function __construct( parent::__construct($em, $eventLogService, $dj, $kernel); } + /** + * @return array{ + * clarifications: Clarification[], + * team: Team, + * problem: Problem + * } + */ #[Route(path: '/clarifications/by-problem/{probId<\d+>}', name: 'team_clarification_by_prob')] - public function viewByProblemAction(Request $request, int $probId): Response + #[AjaxTemplate(normalTemplate: 'team/clarifications_by_problem.html.twig', ajaxTemplate: 'team/clarifications_by_problem_modal.html.twig')] + public function viewByProblemAction(Request $request, int $probId): array { $user = $this->dj->getUser(); $team = $user->getTeam(); @@ -88,23 +98,25 @@ public function viewByProblemAction(Request $request, int $probId): Response ->getQuery() ->getResult(); - $data = [ + return [ 'clarifications' => $clarifications, 'team' => $team, 'problem' => $problem, ]; - if ($request->isXmlHttpRequest()) { - return $this->render('team/clarifications_by_problem_modal.html.twig', $data); - } else { - return $this->render('team/clarifications_by_problem.html.twig', $data); - } } /** + * @return array{ + * clarification: Clarification, + * team: Team, + * categories: array, + * form: FormView + * } * @throws NonUniqueResultException */ #[Route(path: '/clarifications/{clarId<\d+>}', name: 'team_clarification')] - public function viewAction(Request $request, int $clarId): Response + #[AjaxTemplate(normalTemplate: 'team/clarification.html.twig', ajaxTemplate: 'team/clarification_modal.html.twig')] + public function viewAction(Request $request, int $clarId): Response|array { $categories = $this->config->get('clar_categories'); $user = $this->dj->getUser(); @@ -171,22 +183,23 @@ public function viewAction(Request $request, int $clarId): Response } $this->em->flush(); - $data = [ + return [ 'clarification' => $clarification, 'team' => $team, 'categories' => $categories, 'form' => $form->createView(), ]; - - if ($request->isXmlHttpRequest()) { - return $this->render('team/clarification_modal.html.twig', $data); - } else { - return $this->render('team/clarification.html.twig', $data); - } } + /** + * @return array{ + * categories: array, + * form: FormView + * } + */ #[Route(path: '/clarifications/add', name: 'team_clarification_add')] - public function addAction(Request $request): Response + #[AjaxTemplate(normalTemplate: 'team/clarification_add.html.twig', ajaxTemplate: 'team/clarification_add_modal.html.twig')] + public function addAction(Request $request): Response|array { $categories = $this->config->get('clar_categories'); $user = $this->dj->getUser(); @@ -206,16 +219,10 @@ public function addAction(Request $request): Response return $this->redirectToRoute('team_index'); } - $data = [ + return [ 'categories' => $categories, 'form' => $form->createView(), ]; - - if ($request->isXmlHttpRequest()) { - return $this->render('team/clarification_add_modal.html.twig', $data); - } else { - return $this->render('team/clarification_add.html.twig', $data); - } } /** diff --git a/webapp/src/Controller/Team/LanguageController.php b/webapp/src/Controller/Team/LanguageController.php index a03bc4e8fe..63e2542743 100644 --- a/webapp/src/Controller/Team/LanguageController.php +++ b/webapp/src/Controller/Team/LanguageController.php @@ -8,8 +8,8 @@ use App\Service\DOMJudgeService; use App\Service\EventLogService; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\ExpressionLanguage\Expression; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\Attribute\Route; @@ -33,8 +33,12 @@ public function __construct( parent::__construct($em, $eventLogService, $dj, $kernel); } + /** + * @return array{languages: Language[]} + */ #[Route(path: '', name: 'team_languages')] - public function languagesAction(): Response + #[Template(template: 'team/languages.html.twig')] + public function languagesAction(): array { $languagesEnabled = $this->config->get('show_language_versions'); if (!$languagesEnabled) { @@ -43,6 +47,6 @@ public function languagesAction(): Response $currentContest = $this->dj->getCurrentContest(); /** @var Language[] $languages */ $languages = $this->dj->getAllowedLanguagesForContest($currentContest); - return $this->render('team/languages.html.twig', ['languages' => $languages]); + return ['languages' => $languages]; } } diff --git a/webapp/src/Controller/Team/MiscController.php b/webapp/src/Controller/Team/MiscController.php index 1ad3d4e7a1..fa108bbd14 100644 --- a/webapp/src/Controller/Team/MiscController.php +++ b/webapp/src/Controller/Team/MiscController.php @@ -12,10 +12,13 @@ use App\Service\EventLogService; use App\Service\ScoreboardService; use App\Service\SubmissionService; +use App\Twig\Attribute\AjaxTemplate; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -50,11 +53,14 @@ public function __construct( } /** + * @return array + * * @throws NoResultException * @throws NonUniqueResultException */ #[Route(path: '', name: 'team_index')] - public function homeAction(Request $request): Response + #[AjaxTemplate(normalTemplate: 'team/index.html.twig', ajaxTemplate: 'team/partials/index_content.html.twig')] + public function homeAction(Request $request): array { $user = $this->dj->getUser(); $team = $user->getTeam(); @@ -134,10 +140,9 @@ public function homeAction(Request $request): Response if ($request->isXmlHttpRequest()) { $data['ajax'] = true; - return $this->render('team/partials/index_content.html.twig', $data); } - return $this->render('team/index.html.twig', $data); + return $data; } #[Route(path: '/updates', methods: ['GET'], name: 'team_ajax_updates')] @@ -195,10 +200,14 @@ public function printAction(Request $request): Response ]); } + /** + * @return array{} + */ #[Route(path: '/docs', name: 'team_docs')] - public function docsAction(): Response + #[Template(template: 'team/docs.html.twig')] + public function docsAction(): array { - return $this->render('team/docs.html.twig'); + return []; } #[Route(path: '/problemset', name: 'team_contest_problemset')] diff --git a/webapp/src/Controller/Team/ProblemController.php b/webapp/src/Controller/Team/ProblemController.php index 9374ee46b4..9eb6353f80 100644 --- a/webapp/src/Controller/Team/ProblemController.php +++ b/webapp/src/Controller/Team/ProblemController.php @@ -9,10 +9,11 @@ use App\Service\DOMJudgeService; use App\Service\EventLogService; use App\Service\StatisticsService; +use DateTime; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\ExpressionLanguage\Expression; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -40,14 +41,20 @@ public function __construct( } /** + * @return array{'problems': ContestProblem[], 'samples': string[], 'showLimits': bool, + * 'defaultMemoryLimit': int, 'timeFactorDiffers': bool, + * 'stats': array{'numBuckets': int, 'maxBucketSizeCorrect': int, + * 'maxBucketSizeIncorrect': int, + * 'problems': array, + * 'incorrect': array}>}} * @throws NonUniqueResultException */ #[Route(path: '/problems', name: 'team_problems')] - public function problemsAction(): Response + #[Template(template: 'team/problems.html.twig')] + public function problemsAction(): array { $teamId = $this->dj->getUser()->getTeam()->getTeamid(); - return $this->render('team/problems.html.twig', - $this->dj->getTwigDataForProblemsAction($this->stats, $teamId)); + return $this->dj->getTwigDataForProblemsAction($this->stats, $teamId); } diff --git a/webapp/src/Controller/Team/ScoreboardController.php b/webapp/src/Controller/Team/ScoreboardController.php index 069e5db60c..d57ed77595 100644 --- a/webapp/src/Controller/Team/ScoreboardController.php +++ b/webapp/src/Controller/Team/ScoreboardController.php @@ -3,12 +3,18 @@ namespace App\Controller\Team; use App\Controller\BaseController; +use App\Entity\Contest; use App\Entity\Team; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; use App\Service\EventLogService; use App\Service\ScoreboardService; +use App\Twig\Attribute\AjaxTemplate; +use App\Twig\EventListener\CustomResponseListener; +use App\Utils\Scoreboard\Filter; +use App\Utils\Scoreboard\Scoreboard; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -36,8 +42,17 @@ public function __construct( parent::__construct($em, $eventLogService, $dj, $kernel); } + /** + * @return array{refresh?: array{after: int, url: string, ajax: bool}, static: bool, contest?: Contest, + * scoreFilter?: Filter, scoreboard: Scoreboard, filterValues: array, + * groupedAffiliations: null|array>>, + * showFlags: int, showAffiliationLogos: bool, showAffiliations: int, showPending: int, + * showTeamSubmissions: int, scoreInSeconds: bool, maxWidth: int, myTeamId: int, + * current_contest?: Contest|null} + */ #[Route(path: '/scoreboard', name: 'team_scoreboard')] - public function scoreboardAction(Request $request): Response + #[AjaxTemplate(normalTemplate: 'team/scoreboard.html.twig', ajaxTemplate: 'partials/scoreboard.html.twig')] + public function scoreboardAction(Request $request, CustomResponseListener $customResponseListener): array { if (!$this->config->get('enable_ranking')) { throw new BadRequestHttpException('Scoreboard is not available.'); @@ -52,15 +67,20 @@ public function scoreboardAction(Request $request): Response ); $data['myTeamId'] = $user->getTeam()->getTeamid(); + $customResponseListener->setCustomResponse($response); + if ($request->isXmlHttpRequest()) { $data['current_contest'] = $contest; - return $this->render('partials/scoreboard.html.twig', $data, $response); } - return $this->render('team/scoreboard.html.twig', $data, $response); + return $data; } + /** + * @return array{team: Team|null, showFlags: bool, showAffiliations: bool} + */ #[Route(path: '/team/{teamId<\d+>}', name: 'team_team')] - public function teamAction(Request $request, int $teamId): Response + #[AjaxTemplate(normalTemplate: 'team/team.html.twig', ajaxTemplate: 'team/team_modal.html.twig')] + public function teamAction(int $teamId): array { if (!$this->config->get('enable_ranking')) { throw new BadRequestHttpException('Scoreboard is not available.'); @@ -73,16 +93,11 @@ public function teamAction(Request $request, int $teamId): Response } $showFlags = (bool)$this->config->get('show_flags'); $showAffiliations = (bool)$this->config->get('show_affiliations'); - $data = [ + + return [ 'team' => $team, 'showFlags' => $showFlags, 'showAffiliations' => $showAffiliations, ]; - - if ($request->isXmlHttpRequest()) { - return $this->render('team/team_modal.html.twig', $data); - } - - return $this->render('team/team.html.twig', $data); } } diff --git a/webapp/src/Controller/Team/SubmissionController.php b/webapp/src/Controller/Team/SubmissionController.php index 037d679c4d..6e465f1742 100644 --- a/webapp/src/Controller/Team/SubmissionController.php +++ b/webapp/src/Controller/Team/SubmissionController.php @@ -14,11 +14,13 @@ use App\Service\DOMJudgeService; use App\Service\EventLogService; use App\Service\SubmissionService; +use App\Twig\Attribute\AjaxTemplate; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\Query\Expr\Join; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\Form\FormView; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -51,8 +53,16 @@ public function __construct( parent::__construct($em, $eventLogService, $dj, $kernel); } + /** + * @return array{ + * form: FormView, + * problem: Problem|null, + * validFilenameRegex: string + * } + */ #[Route(path: '/submit/{problem}', name: 'team_submit')] - public function createAction(Request $request, ?Problem $problem = null): Response + #[AjaxTemplate(normalTemplate: 'team/submit.html.twig', ajaxTemplate: 'team/submit_modal.html.twig')] + public function createAction(Request $request, ?Problem $problem = null): Response|array { $user = $this->dj->getUser(); $team = $user->getTeam(); @@ -101,21 +111,30 @@ public function createAction(Request $request, ?Problem $problem = null): Respon } } - $data = ['form' => $form->createView(), 'problem' => $problem]; - $data['validFilenameRegex'] = SubmissionService::FILENAME_REGEX; - - if ($request->isXmlHttpRequest()) { - return $this->render('team/submit_modal.html.twig', $data); - } else { - return $this->render('team/submit.html.twig', $data); - } + return [ + 'form' => $form->createView(), + 'problem' => $problem, + 'validFilenameRegex' => SubmissionService::FILENAME_REGEX, + ]; } /** + * @return array{ + * judging: Judging|null, + * verificationRequired: bool, + * showCompile: bool, + * allowDownload: bool, + * showSampleOutput: bool, + * runs: array, + * showTooLateResult: bool, + * thumbnailSize: int, + * size?: string + * } * @throws NonUniqueResultException */ #[Route(path: '/submission/{submitId<\d+>}', name: 'team_submission')] - public function viewAction(Request $request, int $submitId): Response + #[AjaxTemplate(normalTemplate: 'team/submission.html.twig', ajaxTemplate: 'team/submission_modal.html.twig')] + public function viewAction(Request $request, int $submitId): array { $verificationRequired = (bool)$this->config->get('verification_required'); $showCompile = $this->config->get('show_compile'); @@ -207,11 +226,7 @@ public function viewAction(Request $request, int $submitId): Response $data['size'] = 'xl'; } - if ($request->isXmlHttpRequest()) { - return $this->render('team/submission_modal.html.twig', $data); - } else { - return $this->render('team/submission.html.twig', $data); - } + return $data; } /**