From 8e565ca730b3502981f08c235ff67205360b0d15 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Fri, 28 Feb 2025 16:25:09 +0000 Subject: [PATCH 01/12] Fix displaying too late submissions when submitting exactly at the end time of a contest --- webapp/templates/team/partials/submission_list.html.twig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/webapp/templates/team/partials/submission_list.html.twig b/webapp/templates/team/partials/submission_list.html.twig index 77b96645b1..588afb2457 100644 --- a/webapp/templates/team/partials/submission_list.html.twig +++ b/webapp/templates/team/partials/submission_list.html.twig @@ -51,11 +51,11 @@ - {%- if submission.submittime > submission.contest.endtime %} - {{ 'too-late' | printResult }} + {%- if submission.submittime >= submission.contest.endtime %} + {{ 'too-late' | printResult }} {%- endif %} - {%- if submission.submittime <= submission.contest.endtime or showTooLateResult %} - {%- if submission.submittime > submission.contest.endtime %} + {%- if submission.submittime < submission.contest.endtime or showTooLateResult %} + {%- if submission.submittime >= submission.contest.endtime %} / {% endif %} {%- if submission.judgings.first is empty or submission.judgings.first.result is empty %} From f7d50110349b87b6f37dfe963e1f39138ee2eb9c Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Fri, 28 Feb 2025 17:08:53 +0000 Subject: [PATCH 02/12] Show list of submissions on public and team scoreboards when clicking on a cell Fixes #2427 --- webapp/public/style_domjudge.css | 8 +++ webapp/src/Controller/PublicController.php | 65 +++++++++++++++++++ .../Controller/Team/SubmissionController.php | 6 ++ .../SubmissionRestriction.php | 1 + webapp/src/Service/SubmissionService.php | 6 ++ .../partials/scoreboard_table.html.twig | 20 +++++- .../public/partials/submission_list.html.twig | 55 ++++++++++++++++ .../public/team_submissions.html.twig | 13 ++++ .../public/team_submissions_modal.html.twig | 9 +++ 9 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 webapp/templates/public/partials/submission_list.html.twig create mode 100644 webapp/templates/public/team_submissions.html.twig create mode 100644 webapp/templates/public/team_submissions_modal.html.twig diff --git a/webapp/public/style_domjudge.css b/webapp/public/style_domjudge.css index 48c794c925..18eb31ef24 100644 --- a/webapp/public/style_domjudge.css +++ b/webapp/public/style_domjudge.css @@ -647,6 +647,14 @@ tr.ignore td, td.ignore, span.ignore { min-width: 2em; } +h5 .problem-badge { + font-size: 1rem; +} + +h1 .problem-badge { + font-size: 2rem; +} + .tooltip .tooltip-inner { max-width: 500px; } diff --git a/webapp/src/Controller/PublicController.php b/webapp/src/Controller/PublicController.php index 9d368ad964..73602d5b1a 100644 --- a/webapp/src/Controller/PublicController.php +++ b/webapp/src/Controller/PublicController.php @@ -2,6 +2,7 @@ namespace App\Controller; +use App\DataTransferObject\SubmissionRestriction; use App\Entity\Contest; use App\Entity\ContestProblem; use App\Entity\Team; @@ -11,6 +12,7 @@ use App\Service\EventLogService; use App\Service\ScoreboardService; use App\Service\StatisticsService; +use App\Service\SubmissionService; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -33,6 +35,7 @@ public function __construct( protected readonly ConfigurationService $config, protected readonly ScoreboardService $scoreboardService, protected readonly StatisticsService $stats, + protected readonly SubmissionService $submissionService, EntityManagerInterface $em, EventLogService $eventLog, KernelInterface $kernel, @@ -79,6 +82,18 @@ public function scoreboardAction( if ($static) { $data['hide_menu'] = true; + $submissions = $this->submissionService->getSubmissionList( + [$contest->getCid() => $contest], + new SubmissionRestriction(valid: true), + paginated: false + )[0]; + + $submissionsPerTeamAndProblem = []; + foreach ($submissions as $submission) { + $submissionsPerTeamAndProblem[$submission->getTeam()->getTeamid()][$submission->getProblem()->getProbid()][] = $submission; + } + $data['submissionsPerTeamAndProblem'] = $submissionsPerTeamAndProblem; + $data['verificationRequired'] = $this->config->get('verification_required'); } $data['current_contest'] = $contest; @@ -267,4 +282,54 @@ protected function getBinaryFile(int $probId, callable $response): StreamedRespo return $response($probId, $contest, $contestProblem); } + + #[Route(path: '/submissions/team/{teamId<\d+>}/problem/{problemId<\d+>}', name: 'public_submissions')] + public function submissionsAction(Request $request, int $teamId, int $problemId): Response + { + $contest = $this->dj->getCurrentContest(onlyPublic: true); + + if (!$contest) { + throw $this->createNotFoundException('No active contest found'); + } + + /** @var Team|null $team */ + $team = $this->em->getRepository(Team::class)->find($teamId); + if ($team && $team->getCategory() && !$team->getCategory()->getVisible()) { + $team = null; + } + + if (!$team) { + throw $this->createNotFoundException('Team not found'); + } + + /** @var ContestProblem|null $problem */ + $problem = $this->em->getRepository(ContestProblem::class)->find([ + 'problem' => $problemId, + 'contest' => $contest, + ]); + + if (!$problem) { + throw $this->createNotFoundException('Problem not found'); + } + + $submissions = $this->submissionService->getSubmissionList( + [$contest->getCid() => $contest], + new SubmissionRestriction(teamId: $teamId, problemId: $problemId, valid: true), + paginated: false + )[0]; + + $data = [ + 'contest' => $contest, + 'problem' => $problem, + 'team' => $team, + 'submissions' => $submissions, + 'verificationRequired' => $this->config->get('verification_required'), + ]; + + if ($request->isXmlHttpRequest()) { + return $this->render('public/team_submissions_modal.html.twig', $data); + } + + return $this->render('public/team_submissions.html.twig', $data); + } } diff --git a/webapp/src/Controller/Team/SubmissionController.php b/webapp/src/Controller/Team/SubmissionController.php index 037d679c4d..b2abde8412 100644 --- a/webapp/src/Controller/Team/SubmissionController.php +++ b/webapp/src/Controller/Team/SubmissionController.php @@ -250,4 +250,10 @@ public function downloadAction(int $submitId): Response return $this->submissionService->getSubmissionZipResponse($submission); } + + #[Route(path: '/submissions/team/{teamId<\d+>}/problem/{problemId<\d+>}', name: 'team_submissions')] + public function listAction(int $probId): Response + { + + } } diff --git a/webapp/src/DataTransferObject/SubmissionRestriction.php b/webapp/src/DataTransferObject/SubmissionRestriction.php index b62057d68a..c23f86bf4f 100644 --- a/webapp/src/DataTransferObject/SubmissionRestriction.php +++ b/webapp/src/DataTransferObject/SubmissionRestriction.php @@ -69,5 +69,6 @@ public function __construct( public ?bool $externallyJudged = null, public ?bool $externallyVerified = null, public ?bool $withExternalId = null, + public ?bool $valid = null, ) {} } diff --git a/webapp/src/Service/SubmissionService.php b/webapp/src/Service/SubmissionService.php index 60efff8bdd..792038885d 100644 --- a/webapp/src/Service/SubmissionService.php +++ b/webapp/src/Service/SubmissionService.php @@ -306,6 +306,12 @@ public function getSubmissionList( ->setParameter('results', $restrictions->results); } + if (isset($restrictions->valid)) { + $queryBuilder + ->andWhere('s.valid = :valid') + ->setParameter('valid', $restrictions->valid); + } + if ($this->dj->shadowMode()) { // When we are shadow, also load the external results $queryBuilder diff --git a/webapp/templates/partials/scoreboard_table.html.twig b/webapp/templates/partials/scoreboard_table.html.twig index 6b2da2f314..26bdc4e63f 100644 --- a/webapp/templates/partials/scoreboard_table.html.twig +++ b/webapp/templates/partials/scoreboard_table.html.twig @@ -302,15 +302,21 @@ {% endif %} {% endif %} - {% set link = null %} + {% set extra = null %} {% if jury %} {% set restrict = {problemId: problem.probid} %} {% set link = path('jury_team', {teamId: score.team.teamid, restrict: restrict}) %} + {% elseif static %} + {% set link = '#' %} + {% set extra = 'data-bs-toggle="modal" data-bs-target="#team-submissions-modal-' ~ score.team.teamid ~ '-' ~ problem.probid ~ '"' %} + {% else %} + {% set link = path('public_submissions', {teamId: score.team.teamid, problemId: problem.probid}) %} + {% set extra = 'data-ajax-modal' %} {% endif %} {% if numSubmissions != '0' %} - +
{% if matrixItem.isCorrect %}{{ time }}{% else %} {% endif %} @@ -538,6 +544,16 @@ {% include 'partials/team.html.twig' with {size: 6, team: score.team} %} {% endblock %} {% endembed %} + {% for problem in problems %} + {% embed 'partials/modal.html.twig' with {'modalId': 'team-submissions-modal-' ~ score.team.teamid ~ '-' ~ problem.probid} %} + {% block title %} + Submissions for team {{ score.team.effectiveName }} and problem {{ problem | problemBadge }} {{ problem.problem.name }} + {% endblock %} + {% block content %} + {% include 'public/partials/submission_list.html.twig' with {size: 6, team: score.team, submissions: submissionsPerTeamAndProblem[score.team.teamid][problem.probid] ?? []} %} + {% endblock %} + {% endembed %} + {% endfor %} {% endfor %} {% endif %} diff --git a/webapp/templates/public/partials/submission_list.html.twig b/webapp/templates/public/partials/submission_list.html.twig new file mode 100644 index 0000000000..5cf0f9df6e --- /dev/null +++ b/webapp/templates/public/partials/submission_list.html.twig @@ -0,0 +1,55 @@ +{# Render a list of submissions for the scoreboard #} +{# @var submission \App\Entity\Submission #} + +{% if submissions is empty %} +
No submissions
+{% else %} + + + + + + + {% if contest.getRuntimeAsScoreTiebreaker() %} + + {% endif %} + + + + {% for submission in submissions %} + + + + + {% if contest.getRuntimeAsScoreTiebreaker() %} + + {% endif %} + + {% endfor %} + +
timelanguageresultruntime
+ {{ submission.submittime | printtime(null, submission.contest) }} + + {{ submission.language.langid }} + + {% if submission.submittime >= submission.contest.endtime %} + {{ 'too-late' | printResult }} + {% elseif submission.contest.freezetime and submission.submittime >= submission.contest.freezetime and not contest.freezeData.showFinal %} + {{ '' | printResult }} + {% else %} + {% if submission.judgings.first is empty or submission.judgings.first.result is empty %} + {{ '' | printResult }} + {% elseif verificationRequired and not submission.judgings.first.verified %} + {{ '' | printResult }} + {% else %} + {{ submission.judgings.first.result | printResult }} + {% endif %} + {% endif %} + + {% if link and submission.getResult() == 'correct' %} + {{ "%0.3f s" | format(submission.judgings.first.getMaxRuntime()) }} + {% else %} + - + {% endif %} +
+{% endif %} diff --git a/webapp/templates/public/team_submissions.html.twig b/webapp/templates/public/team_submissions.html.twig new file mode 100644 index 0000000000..e95fb92b0e --- /dev/null +++ b/webapp/templates/public/team_submissions.html.twig @@ -0,0 +1,13 @@ +{% extends "public/base.html.twig" %} + +{% block title %} + {% if team is not empty %}Submissions for team {{ team.effectiveName }} and problem {{ problem.problem.name }} - {% endif %}{{ parent() }} +{% endblock %} + +{% block content %} +

+ Submissions for team {{ team.effectiveName }} and problem {{ problem | problemBadge }} {{ problem.problem.name }} +

+ + {% include 'public/partials/submission_list.html.twig' %} +{% endblock %} diff --git a/webapp/templates/public/team_submissions_modal.html.twig b/webapp/templates/public/team_submissions_modal.html.twig new file mode 100644 index 0000000000..e475b3f0b2 --- /dev/null +++ b/webapp/templates/public/team_submissions_modal.html.twig @@ -0,0 +1,9 @@ +{% extends "partials/modal.html.twig" %} + +{% block title %} + Submissions for team {{ team.effectiveName }} and problem {{ problem | problemBadge }} {{ problem.problem.name }} +{% endblock %} + +{% block content %} + {% include 'public/partials/submission_list.html.twig' %} +{% endblock %} From 90b9b85fe022d8ac1b9a47bc3d8ffe6985361e10 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Fri, 28 Feb 2025 17:28:43 +0000 Subject: [PATCH 03/12] Remove unneeded action --- webapp/src/Controller/Team/SubmissionController.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/webapp/src/Controller/Team/SubmissionController.php b/webapp/src/Controller/Team/SubmissionController.php index b2abde8412..037d679c4d 100644 --- a/webapp/src/Controller/Team/SubmissionController.php +++ b/webapp/src/Controller/Team/SubmissionController.php @@ -250,10 +250,4 @@ public function downloadAction(int $submitId): Response return $this->submissionService->getSubmissionZipResponse($submission); } - - #[Route(path: '/submissions/team/{teamId<\d+>}/problem/{problemId<\d+>}', name: 'team_submissions')] - public function listAction(int $probId): Response - { - - } } From 923faabbbc0509c606d1520ffa20ad03632de372 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Fri, 28 Feb 2025 18:15:13 +0000 Subject: [PATCH 04/12] Update webapp/src/Controller/PublicController.php Co-authored-by: MCJ Vasseur <14887731+vmcj@users.noreply.github.com> --- webapp/src/Controller/PublicController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/Controller/PublicController.php b/webapp/src/Controller/PublicController.php index 73602d5b1a..8a9d9c9a40 100644 --- a/webapp/src/Controller/PublicController.php +++ b/webapp/src/Controller/PublicController.php @@ -293,7 +293,7 @@ public function submissionsAction(Request $request, int $teamId, int $problemId) } /** @var Team|null $team */ - $team = $this->em->getRepository(Team::class)->find($teamId); + $team = $this->em->getRepository(Team::class)->find($teamId); if ($team && $team->getCategory() && !$team->getCategory()->getVisible()) { $team = null; } From 58b30e4b0c8617433e60f8ae976a855074b52747 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Fri, 28 Feb 2025 18:15:20 +0000 Subject: [PATCH 05/12] Update webapp/src/Controller/PublicController.php Co-authored-by: MCJ Vasseur <14887731+vmcj@users.noreply.github.com> --- webapp/src/Controller/PublicController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/Controller/PublicController.php b/webapp/src/Controller/PublicController.php index 8a9d9c9a40..e207dfc5be 100644 --- a/webapp/src/Controller/PublicController.php +++ b/webapp/src/Controller/PublicController.php @@ -299,7 +299,7 @@ public function submissionsAction(Request $request, int $teamId, int $problemId) } if (!$team) { - throw $this->createNotFoundException('Team not found'); + throw $this->createNotFoundException('Team not found.'); } /** @var ContestProblem|null $problem */ From b4a37a7d9e1da90c858c5c11225d89af7a6f4295 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Sat, 1 Mar 2025 10:02:52 +0000 Subject: [PATCH 06/12] Update webapp/templates/partials/scoreboard_table.html.twig Co-authored-by: Jaap Eldering --- webapp/templates/partials/scoreboard_table.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/templates/partials/scoreboard_table.html.twig b/webapp/templates/partials/scoreboard_table.html.twig index 26bdc4e63f..2985b78340 100644 --- a/webapp/templates/partials/scoreboard_table.html.twig +++ b/webapp/templates/partials/scoreboard_table.html.twig @@ -547,7 +547,7 @@ {% for problem in problems %} {% embed 'partials/modal.html.twig' with {'modalId': 'team-submissions-modal-' ~ score.team.teamid ~ '-' ~ problem.probid} %} {% block title %} - Submissions for team {{ score.team.effectiveName }} and problem {{ problem | problemBadge }} {{ problem.problem.name }} + Submissions for team {{ score.team.effectiveName }} on problem {{ problem | problemBadge }} {{ problem.problem.name }} {% endblock %} {% block content %} {% include 'public/partials/submission_list.html.twig' with {size: 6, team: score.team, submissions: submissionsPerTeamAndProblem[score.team.teamid][problem.probid] ?? []} %} From ecb7691be85d07a45e1b8bb84e13ce2120ca55a8 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Sat, 1 Mar 2025 10:08:23 +0000 Subject: [PATCH 07/12] Use accepted and rejected instead of full verdict Also drop time tie breaker column. --- webapp/src/Twig/TwigExtension.php | 13 ++++++++++++- .../public/partials/submission_list.html.twig | 14 +------------- webapp/templates/public/team_submissions.html.twig | 4 ++-- .../public/team_submissions_modal.html.twig | 2 +- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/webapp/src/Twig/TwigExtension.php b/webapp/src/Twig/TwigExtension.php index 0155f53e9f..09f8d45c96 100644 --- a/webapp/src/Twig/TwigExtension.php +++ b/webapp/src/Twig/TwigExtension.php @@ -513,7 +513,12 @@ public function displayTestcaseResults(array $testcases, bool $submissionDone, b return $results; } - public function printResult(?string $result, bool $valid = true, bool $jury = false): string + public function printResult( + ?string $result, + bool $valid = true, + bool $jury = false, + bool $onlyAcceptedAndRejected = false, + ): string { $result = strtolower($result ?? ''); switch ($result) { @@ -536,9 +541,15 @@ public function printResult(?string $result, bool $valid = true, bool $jury = fa break; case 'correct': $style = 'sol_correct'; + if ($onlyAcceptedAndRejected) { + $result = 'accepted'; + } break; default: $style = 'sol_incorrect'; + if ($onlyAcceptedAndRejected) { + $result = 'rejected'; + } } return sprintf('%s', $valid ? $style : 'disabled', $result); diff --git a/webapp/templates/public/partials/submission_list.html.twig b/webapp/templates/public/partials/submission_list.html.twig index 5cf0f9df6e..01492331a9 100644 --- a/webapp/templates/public/partials/submission_list.html.twig +++ b/webapp/templates/public/partials/submission_list.html.twig @@ -10,9 +10,6 @@ time language result - {% if contest.getRuntimeAsScoreTiebreaker() %} - runtime - {% endif %} @@ -35,19 +32,10 @@ {% elseif verificationRequired and not submission.judgings.first.verified %} {{ '' | printResult }} {% else %} - {{ submission.judgings.first.result | printResult }} + {{ submission.judgings.first.result | printResult(true, false, true) }} {% endif %} {% endif %} - {% if contest.getRuntimeAsScoreTiebreaker() %} - - {% if link and submission.getResult() == 'correct' %} - {{ "%0.3f s" | format(submission.judgings.first.getMaxRuntime()) }} - {% else %} - - - {% endif %} - - {% endif %} {% endfor %} diff --git a/webapp/templates/public/team_submissions.html.twig b/webapp/templates/public/team_submissions.html.twig index e95fb92b0e..3728501603 100644 --- a/webapp/templates/public/team_submissions.html.twig +++ b/webapp/templates/public/team_submissions.html.twig @@ -1,12 +1,12 @@ {% extends "public/base.html.twig" %} {% block title %} - {% if team is not empty %}Submissions for team {{ team.effectiveName }} and problem {{ problem.problem.name }} - {% endif %}{{ parent() }} + {% if team is not empty %}Submissions for team {{ team.effectiveName }} on problem {{ problem.problem.name }} - {% endif %}{{ parent() }} {% endblock %} {% block content %}

- Submissions for team {{ team.effectiveName }} and problem {{ problem | problemBadge }} {{ problem.problem.name }} + Submissions for team {{ team.effectiveName }} on problem {{ problem | problemBadge }} {{ problem.problem.name }}

{% include 'public/partials/submission_list.html.twig' %} diff --git a/webapp/templates/public/team_submissions_modal.html.twig b/webapp/templates/public/team_submissions_modal.html.twig index e475b3f0b2..cc3ffc4d85 100644 --- a/webapp/templates/public/team_submissions_modal.html.twig +++ b/webapp/templates/public/team_submissions_modal.html.twig @@ -1,7 +1,7 @@ {% extends "partials/modal.html.twig" %} {% block title %} - Submissions for team {{ team.effectiveName }} and problem {{ problem | problemBadge }} {{ problem.problem.name }} + Submissions for team {{ team.effectiveName }} on problem {{ problem | problemBadge }} {{ problem.problem.name }} {% endblock %} {% block content %} From a924ac7245b569b67f013c3a6ba018a84786f10b Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Sat, 1 Mar 2025 16:19:35 +0000 Subject: [PATCH 08/12] Update webapp/src/Controller/PublicController.php Co-authored-by: MCJ Vasseur <14887731+vmcj@users.noreply.github.com> --- webapp/src/Controller/PublicController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/webapp/src/Controller/PublicController.php b/webapp/src/Controller/PublicController.php index e207dfc5be..2f685e93e2 100644 --- a/webapp/src/Controller/PublicController.php +++ b/webapp/src/Controller/PublicController.php @@ -329,7 +329,6 @@ public function submissionsAction(Request $request, int $teamId, int $problemId) if ($request->isXmlHttpRequest()) { return $this->render('public/team_submissions_modal.html.twig', $data); } - return $this->render('public/team_submissions.html.twig', $data); } } From ac3eabeb34ad8b6ee4afb4b56de7f78193df9baf Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Sat, 1 Mar 2025 16:19:42 +0000 Subject: [PATCH 09/12] Update webapp/src/Twig/TwigExtension.php Co-authored-by: MCJ Vasseur <14887731+vmcj@users.noreply.github.com> --- webapp/src/Twig/TwigExtension.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/webapp/src/Twig/TwigExtension.php b/webapp/src/Twig/TwigExtension.php index 09f8d45c96..17ece09b96 100644 --- a/webapp/src/Twig/TwigExtension.php +++ b/webapp/src/Twig/TwigExtension.php @@ -518,8 +518,7 @@ public function printResult( bool $valid = true, bool $jury = false, bool $onlyAcceptedAndRejected = false, - ): string - { + ): string { $result = strtolower($result ?? ''); switch ($result) { case 'too-late': From c6c2f1341167e0e36861e4fc24047ec52c308030 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Sat, 1 Mar 2025 16:19:54 +0000 Subject: [PATCH 10/12] Update webapp/templates/public/partials/submission_list.html.twig Co-authored-by: MCJ Vasseur <14887731+vmcj@users.noreply.github.com> --- .../public/partials/submission_list.html.twig | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/webapp/templates/public/partials/submission_list.html.twig b/webapp/templates/public/partials/submission_list.html.twig index 01492331a9..2277359a34 100644 --- a/webapp/templates/public/partials/submission_list.html.twig +++ b/webapp/templates/public/partials/submission_list.html.twig @@ -6,11 +6,11 @@ {% else %} - - - - - + + + + + {% for submission in submissions %} From 23f7a371a41b5a2fe44341c9b75430e14d541f5c Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Sun, 2 Mar 2025 10:20:58 +0000 Subject: [PATCH 11/12] Don't use accepted --- webapp/src/Twig/TwigExtension.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/webapp/src/Twig/TwigExtension.php b/webapp/src/Twig/TwigExtension.php index 17ece09b96..b350347135 100644 --- a/webapp/src/Twig/TwigExtension.php +++ b/webapp/src/Twig/TwigExtension.php @@ -517,7 +517,7 @@ public function printResult( ?string $result, bool $valid = true, bool $jury = false, - bool $onlyAcceptedAndRejected = false, + bool $onlyRejectedForIncorrect = false, ): string { $result = strtolower($result ?? ''); switch ($result) { @@ -540,13 +540,10 @@ public function printResult( break; case 'correct': $style = 'sol_correct'; - if ($onlyAcceptedAndRejected) { - $result = 'accepted'; - } break; default: $style = 'sol_incorrect'; - if ($onlyAcceptedAndRejected) { + if ($onlyRejectedForIncorrect) { $result = 'rejected'; } } From 0234c25b5f172624ac346eb4ff719f4da59fbca8 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Sat, 5 Jul 2025 22:24:35 +0200 Subject: [PATCH 12/12] Use separate JSON to load submission modal data --- webapp/public/js/domjudge.js | 57 +++++++++ webapp/src/Controller/PublicController.php | 116 +++++++++++++++--- webapp/src/Service/DOMJudgeService.php | 9 ++ webapp/src/Twig/TwigExtension.php | 32 ++++- .../partials/scoreboard_table.html.twig | 39 +++--- .../public/partials/submission_list.html.twig | 43 ------- .../submission_list_template.html.twig | 25 ++++ webapp/templates/public/scoreboard.html.twig | 7 ++ .../public/team_submissions.html.twig | 19 ++- .../public/team_submissions_modal.html.twig | 10 +- 10 files changed, 272 insertions(+), 85 deletions(-) delete mode 100644 webapp/templates/public/partials/submission_list.html.twig create mode 100644 webapp/templates/public/partials/submission_list_template.html.twig diff --git a/webapp/public/js/domjudge.js b/webapp/public/js/domjudge.js index f25233fb88..e3b9250c3b 100644 --- a/webapp/public/js/domjudge.js +++ b/webapp/public/js/domjudge.js @@ -1235,3 +1235,60 @@ $(function() { }); }); }); + +function loadSubmissions(dataElement, $displayElement) { + const url = dataElement.dataset.submissionsUrl + fetch(url) + .then(data => data.json()) + .then(data => { + const teamId = dataElement.dataset.teamId; + const problemId = dataElement.dataset.problemId; + const teamKey = `team-${teamId}`; + const problemKey = `problem-${problemId}`; + if (!data.submissions || !data.submissions[teamKey] || !data.submissions[teamKey][problemKey]) { + return; + } + + const submissions = data.submissions[teamKey][problemKey]; + if (submissions.length === 0) { + $displayElement.html(document.querySelector('#empty-submission-list').innerHTML); + } else { + let templateData = document.querySelector('#submission-list').innerHTML; + const $table = $(templateData); + const itemTemplateData = document.querySelector('#submission-list-item').innerHTML; + const $itemTemplate = $(itemTemplateData); + const $submissionList = $table.find('[data-submission-list]'); + for (const submission of submissions) { + const $item = $itemTemplate.clone(); + $item.find('[data-time]').html(submission.time); + $item.find('[data-language-id]').html(submission.language); + $item.find('[data-verdict]').html(submission.verdict); + $submissionList.append($item); + } + $displayElement.find('.spinner-border').remove(); + $displayElement.append($table); + } + }); +} + +function initScoreboardSubmissions() { + $('[data-submissions-url]').on('click', function (e) { + const linkEl = e.currentTarget; + e.preventDefault(); + const $modal = $('[data-submissions-modal] .modal').clone(); + const $teamEl = $(`[data-team-external-id="${linkEl.dataset.teamId}"]`); + const $problemEl = $(`[data-problem-external-id="${linkEl.dataset.problemId}"]`); + $modal.find('[data-team]').html($teamEl.data('teamName')); + $modal.find('[data-problem-badge]').html($problemEl.data('problemBadge')); + $modal.find('[data-problem-name]').html($problemEl.data('problemName')); + $modal.modal(); + $modal.modal('show'); + $modal.on('hidden.bs.modal', function (e) { + $(e.currentTarget).remove(); + }); + $modal.on('shown.bs.modal', function (e) { + const $modalBody = $(e.currentTarget).find('.modal-body'); + loadSubmissions(linkEl, $modalBody); + }); + }); +} diff --git a/webapp/src/Controller/PublicController.php b/webapp/src/Controller/PublicController.php index 2f685e93e2..f507a3e5f2 100644 --- a/webapp/src/Controller/PublicController.php +++ b/webapp/src/Controller/PublicController.php @@ -5,6 +5,7 @@ use App\DataTransferObject\SubmissionRestriction; use App\Entity\Contest; use App\Entity\ContestProblem; +use App\Entity\Submission; use App\Entity\Team; use App\Entity\TeamCategory; use App\Service\ConfigurationService; @@ -13,8 +14,10 @@ use App\Service\ScoreboardService; use App\Service\StatisticsService; use App\Service\SubmissionService; +use App\Twig\TwigExtension; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; @@ -36,6 +39,7 @@ public function __construct( protected readonly ScoreboardService $scoreboardService, protected readonly StatisticsService $stats, protected readonly SubmissionService $submissionService, + protected readonly TwigExtension $twigExtension, EntityManagerInterface $em, EventLogService $eventLog, KernelInterface $kernel, @@ -283,8 +287,8 @@ protected function getBinaryFile(int $probId, callable $response): StreamedRespo return $response($probId, $contest, $contestProblem); } - #[Route(path: '/submissions/team/{teamId<\d+>}/problem/{problemId<\d+>}', name: 'public_submissions')] - public function submissionsAction(Request $request, int $teamId, int $problemId): Response + #[Route(path: '/submissions/team/{teamId}/problem/{problemId}', name: 'public_submissions')] + public function submissionsAction(Request $request, string $teamId, string $problemId): Response { $contest = $this->dj->getCurrentContest(onlyPublic: true); @@ -293,7 +297,7 @@ public function submissionsAction(Request $request, int $teamId, int $problemId) } /** @var Team|null $team */ - $team = $this->em->getRepository(Team::class)->find($teamId); + $team = $this->em->getRepository(Team::class)->findOneBy(['externalid' => $teamId]); if ($team && $team->getCategory() && !$team->getCategory()->getVisible()) { $team = null; } @@ -303,32 +307,108 @@ public function submissionsAction(Request $request, int $teamId, int $problemId) } /** @var ContestProblem|null $problem */ - $problem = $this->em->getRepository(ContestProblem::class)->find([ - 'problem' => $problemId, - 'contest' => $contest, - ]); + $problem = $this->em->createQueryBuilder() + ->from(ContestProblem::class, 'cp') + ->select('cp') + ->innerJoin('cp.problem', 'p') + ->andWhere('cp.contest = :contest') + ->andWhere('p.externalid = :problem') + ->setParameter('contest', $contest) + ->setParameter('problem', $problemId) + ->getQuery() + ->getOneOrNullResult(); if (!$problem) { throw $this->createNotFoundException('Problem not found'); } - $submissions = $this->submissionService->getSubmissionList( - [$contest->getCid() => $contest], - new SubmissionRestriction(teamId: $teamId, problemId: $problemId, valid: true), - paginated: false - )[0]; - $data = [ 'contest' => $contest, 'problem' => $problem, 'team' => $team, - 'submissions' => $submissions, - 'verificationRequired' => $this->config->get('verification_required'), ]; - if ($request->isXmlHttpRequest()) { - return $this->render('public/team_submissions_modal.html.twig', $data); - } return $this->render('public/team_submissions.html.twig', $data); } + + #[Route(path: '/submissions-data.json', name: 'public_submissions_data')] + #[Route(path: '/submissions-data/team/{teamId}/problem/{problemId}.json', name: 'public_submissions_data_cell')] + public function submissionsDataAction(Request $request, ?string $teamId, ?string $problemId): JsonResponse + { + $contest = $this->dj->getCurrentContest(onlyPublic: true); + + if (!$contest) { + throw $this->createNotFoundException('No active contest found'); + } + + $scoreboard = $this->scoreboardService->getScoreboard($contest); + + /** @var Submission[] $submissions */ + $submissions = $this->submissionService->getSubmissionList( + [$contest->getCid() => $contest], + restrictions: new SubmissionRestriction(valid: true), + paginated: false + )[0]; + + $submissionData = []; + + // We prepend IDs with team- and problem- to make sure they are not + // consecutive integers + foreach ($scoreboard->getTeamsInDescendingOrder() as $team) { + if ($teamId && $teamId !== $team->getExternalid()) { + continue; + } + $teamKey = 'team-' . $team->getExternalid(); + $submissionData[$teamKey] = []; + foreach ($scoreboard->getProblems() as $problem) { + if ($problemId && $problemId !== $problem->getExternalId()) { + continue; + } + $problemKey = 'problem-' . $problem->getExternalId(); + $submissionData[$teamKey][$problemKey] = []; + } + } + + $verificationRequired = $this->config->get('verification_required'); + + foreach ($submissions as $submission) { + $teamKey = 'team-' . $submission->getTeam()->getExternalid(); + $problemKey = 'problem-' . $submission->getProblem()->getExternalid(); + if ($teamId && $teamId !== $submission->getTeam()->getExternalid()) { + continue; + } + if ($problemId && $problemId !== $submission->getProblem()->getExternalid()) { + continue; + } + $submissionData[$teamKey][$problemKey][] = [ + 'time' => $this->twigExtension->printtime($submission->getSubmittime(), contest: $contest), + 'language' => $submission->getLanguageId(), + 'verdict' => $this->submissionVerdict($submission, $contest, $verificationRequired), + ]; + } + + return new JsonResponse([ + 'submissions' => $submissionData, + ]); + } + + protected function submissionVerdict( + Submission $submission, + Contest $contest, + bool $verificationRequired + ): string { + if ($submission->getSubmittime() >= $contest->getEndtime()) { + return $this->twigExtension->printResult('too-late'); + } + if ($contest->getFreezetime() && $submission->getSubmittime() >= $contest->getFreezetime() && !$contest->getFreezeData()->showFinal()) { + return $this->twigExtension->printResult(''); + } + if (!$submission->getJudgings()->first() || !$submission->getJudgings()->first()->getResult()) { + return $this->twigExtension->printResult(''); + } + if ($verificationRequired && !$submission->getJudgings()->first()->getVerified()) { + return $this->twigExtension->printResult(''); + } + return $this->twigExtension->printResult($submission->getJudgings()->first()->getResult(), onlyRejectedForIncorrect: true); + } } diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index 132beee23c..1e838feb8d 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -46,6 +46,7 @@ use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\InputBag; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; @@ -1507,6 +1508,7 @@ public function getScoreboardZip( $assetRegex = '|/CHANGE_ME/([/a-z0-9_\-\.]*)(\??[/a-z0-9_\-\.=]*)|i'; preg_match_all($assetRegex, $contestPage, $assetMatches); $contestPage = preg_replace($assetRegex, '$1$2', $contestPage); + $contestPage = str_replace('/public/submissions-data.json', 'submissions-data.json', $contestPage); $zip = new ZipArchive(); if (!($tempFilename = tempnam($this->getDomjudgeTmpDir(), "contest-"))) { @@ -1519,6 +1521,13 @@ public function getScoreboardZip( } $zip->addFromString('index.html', $contestPage); + $submissionsDataRequest = Request::create('/public/submissions-data.json', Request::METHOD_GET); + $submissionsDataRequest->setSession($this->requestStack->getSession()); + /** @var JsonResponse $response */ + $response = $this->httpKernel->handle($submissionsDataRequest, HttpKernelInterface::SUB_REQUEST); + $submissionsData = $response->getContent(); + $zip->addFromString('submissions-data.json', $submissionsData); + $publicPath = realpath(sprintf('%s/public/', $this->projectDir)); foreach ($assetMatches[1] as $file) { $filepath = realpath($publicPath . '/' . $file); diff --git a/webapp/src/Twig/TwigExtension.php b/webapp/src/Twig/TwigExtension.php index b350347135..aed9adf2ae 100644 --- a/webapp/src/Twig/TwigExtension.php +++ b/webapp/src/Twig/TwigExtension.php @@ -23,6 +23,7 @@ use App\Service\EventLogService; use App\Service\SubmissionService; use App\Utils\Scoreboard\ScoreboardMatrixItem; +use App\Utils\Scoreboard\TeamScore; use App\Utils\Utils; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\EntityManagerInterface; @@ -30,6 +31,7 @@ use Symfony\Component\Intl\Countries; use Symfony\Component\Intl\Exception\MissingResourceException; use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Twig\Environment; @@ -51,6 +53,7 @@ public function __construct( protected readonly AwardService $awards, protected readonly TokenStorageInterface $tokenStorage, protected readonly AuthorizationCheckerInterface $authorizationChecker, + protected readonly RouterInterface $router, #[Autowire('%kernel.project_dir%')] protected readonly string $projectDir ) {} @@ -1224,8 +1227,12 @@ public function problemBadge(ContestProblem $problem, bool $grayedOut = false): ); } - public function problemBadgeMaybe(ContestProblem $problem, ScoreboardMatrixItem $matrixItem): string - { + public function problemBadgeMaybe( + ContestProblem $problem, + ScoreboardMatrixItem $matrixItem, + TeamScore $score, + bool $static = false, + ): string { $rgb = Utils::convertToHex($problem->getColor() ?? '#ffffff'); if (!$matrixItem->isCorrect || empty($rgb)) { $rgb = Utils::convertToHex('whitesmoke'); @@ -1238,10 +1245,27 @@ public function problemBadgeMaybe(ContestProblem $problem, ScoreboardMatrixItem $border = 'linen'; } - $ret = sprintf( - '%s', + $submissionsUrl = $static + ? $this->router->generate('public_submissions_data') + : $this->router->generate('public_submissions_data_cell', [ + 'teamId' => $score->team->getExternalid(), + 'problemId' => $problem->getExternalId(), + ]); + + $ret = sprintf(<< + %s + + HTML, $rgb, $border, + $submissionsUrl, + $score->team->getExternalid(), + $problem->getExternalId(), $foreground, $problem->getShortname() ); diff --git a/webapp/templates/partials/scoreboard_table.html.twig b/webapp/templates/partials/scoreboard_table.html.twig index 2985b78340..e85fcaa897 100644 --- a/webapp/templates/partials/scoreboard_table.html.twig +++ b/webapp/templates/partials/scoreboard_table.html.twig @@ -98,7 +98,13 @@ {% endif %} {% endif %} {% endif %} - + {% if enable_ranking %} {% if medalsEnabled %} + {% if enable_ranking %} @@ -544,16 +559,6 @@ {% include 'partials/team.html.twig' with {size: 6, team: score.team} %} {% endblock %} {% endembed %} - {% for problem in problems %} - {% embed 'partials/modal.html.twig' with {'modalId': 'team-submissions-modal-' ~ score.team.teamid ~ '-' ~ problem.probid} %} - {% block title %} - Submissions for team {{ score.team.effectiveName }} on problem {{ problem | problemBadge }} {{ problem.problem.name }} - {% endblock %} - {% block content %} - {% include 'public/partials/submission_list.html.twig' with {size: 6, team: score.team, submissions: submissionsPerTeamAndProblem[score.team.teamid][problem.probid] ?? []} %} - {% endblock %} - {% endembed %} - {% endfor %} {% endfor %} {% endif %} diff --git a/webapp/templates/public/partials/submission_list.html.twig b/webapp/templates/public/partials/submission_list.html.twig deleted file mode 100644 index 2277359a34..0000000000 --- a/webapp/templates/public/partials/submission_list.html.twig +++ /dev/null @@ -1,43 +0,0 @@ -{# Render a list of submissions for the scoreboard #} -{# @var submission \App\Entity\Submission #} - -{% if submissions is empty %} -
No submissions
-{% else %} -
timelanguageresult
timelanguageresult
+ {{ problem | problemBadge }} {% if showPoints %} @@ -142,7 +148,7 @@ {% else %} {% set color = score.team.category.color %} {% endif %} -
@@ -308,10 +314,14 @@ {% set link = path('jury_team', {teamId: score.team.teamid, restrict: restrict}) %} {% elseif static %} {% set link = '#' %} - {% set extra = 'data-bs-toggle="modal" data-bs-target="#team-submissions-modal-' ~ score.team.teamid ~ '-' ~ problem.probid ~ '"' %} + {% set extra = 'data-submissions-url="' ~ path('public_submissions_data') ~ '"' %} + {% set extra = extra ~ ' data-team-id="' ~ score.team.externalid ~ '"' %} + {% set extra = extra ~ ' data-problem-id="' ~ problem.externalid ~ '"' %} {% else %} - {% set link = path('public_submissions', {teamId: score.team.teamid, problemId: problem.probid}) %} - {% set extra = 'data-ajax-modal' %} + {% set link = path('public_submissions', {teamId: score.team.externalid, problemId: problem.externalid}) %} + {% set extra = 'data-submissions-url="' ~ path('public_submissions_data_cell', {teamId: score.team.externalid, problemId: problem.externalid}) ~ '"' %} + {% set extra = extra ~ ' data-team-id="' ~ score.team.externalid ~ '"' %} + {% set extra = extra ~ ' data-problem-id="' ~ problem.externalid ~ '"' %} {% endif %} @@ -409,7 +419,12 @@ {% else %} {% set color = score.team.category.color %} {% endif %} -
{% if medalsEnabled and medalColor != '' %} @@ -528,7 +543,7 @@ {% for problem in problems %} {% set matrixItem = scoreboard.matrix[score.team.teamid][problem.probid] %} - {{ problem | problemBadgeMaybe(matrixItem) }} + {{ problem | problemBadgeMaybe(matrixItem, score, static) }} {% endfor %}
- - - - - - - - - {% for submission in submissions %} - - - - - - {% endfor %} - -
timelanguageresult
- {{ submission.submittime | printtime(null, submission.contest) }} - - {{ submission.language.langid }} - - {% if submission.submittime >= submission.contest.endtime %} - {{ 'too-late' | printResult }} - {% elseif submission.contest.freezetime and submission.submittime >= submission.contest.freezetime and not contest.freezeData.showFinal %} - {{ '' | printResult }} - {% else %} - {% if submission.judgings.first is empty or submission.judgings.first.result is empty %} - {{ '' | printResult }} - {% elseif verificationRequired and not submission.judgings.first.verified %} - {{ '' | printResult }} - {% else %} - {{ submission.judgings.first.result | printResult(true, false, true) }} - {% endif %} - {% endif %} -
-{% endif %} diff --git a/webapp/templates/public/partials/submission_list_template.html.twig b/webapp/templates/public/partials/submission_list_template.html.twig new file mode 100644 index 0000000000..b76d3872f5 --- /dev/null +++ b/webapp/templates/public/partials/submission_list_template.html.twig @@ -0,0 +1,25 @@ +{# Render templates that can be used to fill the submission modal or detail page #} + + + diff --git a/webapp/templates/public/scoreboard.html.twig b/webapp/templates/public/scoreboard.html.twig index e1f0dbb559..bfab652019 100644 --- a/webapp/templates/public/scoreboard.html.twig +++ b/webapp/templates/public/scoreboard.html.twig @@ -18,12 +18,18 @@
{% include 'partials/scoreboard.html.twig' with {jury: false, public: true} %}
+ +
+ {% include 'public/team_submissions_modal.html.twig' %} +
+ {% include 'public/partials/submission_list_template.html.twig' %} {% endblock %} {% block extrafooter %} {% endblock %} diff --git a/webapp/templates/public/team_submissions_modal.html.twig b/webapp/templates/public/team_submissions_modal.html.twig index cc3ffc4d85..2aac4ccf32 100644 --- a/webapp/templates/public/team_submissions_modal.html.twig +++ b/webapp/templates/public/team_submissions_modal.html.twig @@ -1,9 +1,15 @@ {% extends "partials/modal.html.twig" %} {% block title %} - Submissions for team {{ team.effectiveName }} on problem {{ problem | problemBadge }} {{ problem.problem.name }} + Submissions for team + + on problem + + {% endblock %} {% block content %} - {% include 'public/partials/submission_list.html.twig' %} +
+ Loading... +
{% endblock %}