From 1c0552e0c8e1e33febcec12560570187d9ffc903 Mon Sep 17 00:00:00 2001 From: Michael Vasseur <14887731+vmcj@users.noreply.github.com> Date: Mon, 10 Nov 2025 21:08:45 +0100 Subject: [PATCH 1/7] Allow teams to download all samples/attachments We already allow this over the API so use the same URL. --- .../templates/partials/problem_list.html.twig | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/webapp/templates/partials/problem_list.html.twig b/webapp/templates/partials/problem_list.html.twig index b85f2437df..62929bc5bd 100644 --- a/webapp/templates/partials/problem_list.html.twig +++ b/webapp/templates/partials/problem_list.html.twig @@ -4,16 +4,21 @@

{{ contest.name | default('Contest') }} problems - {% if contest and show_contest_problemset and contest.contestProblemsetType is not empty %} - {% if contest_problemset_add_cid %} - {% set contest_problemset_url = path(contest_problemset_path, {'cid': contest.cid}) %} - {% else %} - {% set contest_problemset_url = path(contest_problemset_path) %} + {% if contest and show_contest_problemset %} + {% if contest.contestProblemsetType is not empty %} + {% if contest_problemset_add_cid %} + {% set contest_problemset_url = path(contest_problemset_path, {'cid': contest.cid}) %} + {% else %} + {% set contest_problemset_url = path(contest_problemset_path) %} + {% endif %} + + + problemset + {% endif %} - - - problemset + + samples {% endif %}

From 8ddd4aa92fdc176bf7009905075165561b4c6758 Mon Sep 17 00:00:00 2001 From: Michael Vasseur <14887731+vmcj@users.noreply.github.com> Date: Tue, 11 Nov 2025 19:26:14 +0100 Subject: [PATCH 2/7] Check if there would be any attachments/samples in the contest samples.zip We always try to exit early and skip the actual adding. As checking and creation are more or less the same code we wrap the check inside the actual creation to prevent duplication. --- webapp/src/Service/DOMJudgeService.php | 53 +++++++++++++++++++++----- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index 5101285bf8..a9ee563268 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -830,7 +830,7 @@ public function getSamplesZipContent(ContestProblem $contestProblem): string return $zipFileContents; } - protected function addSamplesToZip(ZipArchive $zip, ContestProblem $problem, ?string $directory = null): void + protected function addSamplesToZip(?ZipArchive $zip, ContestProblem $problem, ?string $directory = null, bool $fullZip = true): bool { /** @var Testcase[] $testcases */ $testcases = $this->em->createQueryBuilder() @@ -849,6 +849,9 @@ protected function addSamplesToZip(ZipArchive $zip, ContestProblem $problem, ?st ->getResult(); foreach ($testcases as $index => $testcase) { + if (!$fullZip) { + return true; + } foreach (['input', 'output'] as $type) { $extension = Testcase::EXTENSION_MAPPING[$type]; @@ -870,6 +873,7 @@ protected function addSamplesToZip(ZipArchive $zip, ContestProblem $problem, ?st $zip->addFromString($filename, $content); } } + return false; } public function getSamplesZipStreamedResponse(ContestProblem $contestProblem): StreamedResponse @@ -879,7 +883,7 @@ public function getSamplesZipStreamedResponse(ContestProblem $contestProblem): S return Utils::streamAsBinaryFile($zipFileContent, $outputFilename, 'zip'); } - public function getSamplesZipForContest(Contest $contest): StreamedResponse + public function helperSamplesZipForContest(Contest $contest, bool $fullZip): StreamedResponse|bool { // Note, we reload the contest with the problems and attachments, to reduce the number of queries // We do not load the testcases here since addSamplesToZip loads them @@ -896,30 +900,42 @@ public function getSamplesZipForContest(Contest $contest): StreamedResponse ->getQuery() ->getSingleResult(); - $zip = new ZipArchive(); - if (!($tempFilename = tempnam($this->getDomjudgeTmpDir(), "export-"))) { - throw new ServiceUnavailableHttpException(null, 'Could not create temporary file.'); - } + $zip = null; + if ($fullZip) { + $zip = new ZipArchive(); + if (!($tempFilename = tempnam($this->getDomjudgeTmpDir(), "export-"))) { + throw new ServiceUnavailableHttpException(null, 'Could not create temporary file.'); + } - $res = $zip->open($tempFilename, ZipArchive::OVERWRITE); - if ($res !== true) { - throw new ServiceUnavailableHttpException(null, 'Could not create temporary zip file.'); + $res = $zip->open($tempFilename, ZipArchive::OVERWRITE); + if ($res !== true) { + throw new ServiceUnavailableHttpException(null, 'Could not create temporary zip file.'); + } } /** @var ContestProblem $problem */ foreach ($contest->getProblems() as $problem) { // We don't include the samples for interactive problems. if (!$problem->getProblem()->isInteractiveProblem()) { - $this->addSamplesToZip($zip, $problem, $problem->getShortname()); + $samplesFound = $this->addSamplesToZip($zip, $problem, $problem->getShortname(), fullZip: $fullZip);; + if (!$fullZip && $samplesFound) { + return true; + } } if ($problem->getProblem()->getProblemstatementType()) { + if (!$fullZip) { + return true; + } $filename = sprintf('%s/statement.%s', $problem->getShortname(), $problem->getProblem()->getProblemstatementType()); $zip->addFromString($filename, $problem->getProblem()->getProblemstatement()); } /** @var ProblemAttachment $attachment */ foreach ($problem->getProblem()->getAttachments() as $attachment) { + if (!$fullZip) { + return true; + } $filename = sprintf('%s/attachments/%s', $problem->getShortname(), $attachment->getName()); $zip->addFromString($filename, $attachment->getContent()->getContent()); if ($attachment->getContent()->isExecutable()) { @@ -934,10 +950,17 @@ public function getSamplesZipForContest(Contest $contest): StreamedResponse } if ($contest->getContestProblemsetType()) { + if (!$fullZip) { + return true; + } $filename = sprintf('contest.%s', $contest->getContestProblemsetType()); $zip->addFromString($filename, $contest->getContestProblemset()); } + if (!$fullZip) { + return false; + } + $zip->close(); $zipFileContents = file_get_contents($tempFilename); unlink($tempFilename); @@ -945,6 +968,16 @@ public function getSamplesZipForContest(Contest $contest): StreamedResponse return Utils::streamAsBinaryFile($zipFileContents, 'samples.zip', 'zip'); } + public function checkIfSamplesZipForContest(Contest $contest): bool + { + return self::helperSamplesZipForContest($contest, fullZip: false); + } + + public function getSamplesZipForContest(Contest $contest): StreamedResponse + { + return self::helperSamplesZipForContest($contest, fullZip: true); + } + /** * @throws NonUniqueResultException */ From f4b37086db8d2b836b839ac63a8eeda5620211f6 Mon Sep 17 00:00:00 2001 From: Michael Vasseur <14887731+vmcj@users.noreply.github.com> Date: Tue, 11 Nov 2025 19:38:58 +0100 Subject: [PATCH 3/7] Only show contest samples zip when there are attachments --- webapp/src/Service/DOMJudgeService.php | 1 + webapp/templates/partials/problem_list.html.twig | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index a9ee563268..0712c68ee9 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -1130,6 +1130,7 @@ public function getTwigDataForProblemsAction( 'problems' => $problems, 'samples' => $samples, 'showLimits' => $showLimits, + 'showSamples' => $this->checkIfSamplesZipForContest($contest), 'defaultMemoryLimit' => $defaultMemoryLimit, 'timeFactorDiffers' => $timeFactorDiffers, 'clarifications' => $clars, diff --git a/webapp/templates/partials/problem_list.html.twig b/webapp/templates/partials/problem_list.html.twig index 62929bc5bd..7c042652dc 100644 --- a/webapp/templates/partials/problem_list.html.twig +++ b/webapp/templates/partials/problem_list.html.twig @@ -17,9 +17,11 @@ problemset {% endif %} - - samples - + {% if showSamples %} + + samples + + {% endif %} {% endif %} From 8ffa0500fb649ed23b2ebc40e585a4eb89015cb0 Mon Sep 17 00:00:00 2001 From: Michael Vasseur <14887731+vmcj@users.noreply.github.com> Date: Wed, 12 Nov 2025 22:10:16 +0100 Subject: [PATCH 4/7] Revert "Check if there would be any attachments/samples in the contest samples.zip" This reverts commit d83c09eac76fecd52841e6d8d8c3eec74ee8fe95. --- webapp/src/Service/DOMJudgeService.php | 53 +++++--------------------- 1 file changed, 10 insertions(+), 43 deletions(-) diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index 0712c68ee9..ac73db6987 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -830,7 +830,7 @@ public function getSamplesZipContent(ContestProblem $contestProblem): string return $zipFileContents; } - protected function addSamplesToZip(?ZipArchive $zip, ContestProblem $problem, ?string $directory = null, bool $fullZip = true): bool + protected function addSamplesToZip(ZipArchive $zip, ContestProblem $problem, ?string $directory = null): void { /** @var Testcase[] $testcases */ $testcases = $this->em->createQueryBuilder() @@ -849,9 +849,6 @@ protected function addSamplesToZip(?ZipArchive $zip, ContestProblem $problem, ?s ->getResult(); foreach ($testcases as $index => $testcase) { - if (!$fullZip) { - return true; - } foreach (['input', 'output'] as $type) { $extension = Testcase::EXTENSION_MAPPING[$type]; @@ -873,7 +870,6 @@ protected function addSamplesToZip(?ZipArchive $zip, ContestProblem $problem, ?s $zip->addFromString($filename, $content); } } - return false; } public function getSamplesZipStreamedResponse(ContestProblem $contestProblem): StreamedResponse @@ -883,7 +879,7 @@ public function getSamplesZipStreamedResponse(ContestProblem $contestProblem): S return Utils::streamAsBinaryFile($zipFileContent, $outputFilename, 'zip'); } - public function helperSamplesZipForContest(Contest $contest, bool $fullZip): StreamedResponse|bool + public function getSamplesZipForContest(Contest $contest): StreamedResponse { // Note, we reload the contest with the problems and attachments, to reduce the number of queries // We do not load the testcases here since addSamplesToZip loads them @@ -900,42 +896,30 @@ public function helperSamplesZipForContest(Contest $contest, bool $fullZip): Str ->getQuery() ->getSingleResult(); - $zip = null; - if ($fullZip) { - $zip = new ZipArchive(); - if (!($tempFilename = tempnam($this->getDomjudgeTmpDir(), "export-"))) { - throw new ServiceUnavailableHttpException(null, 'Could not create temporary file.'); - } + $zip = new ZipArchive(); + if (!($tempFilename = tempnam($this->getDomjudgeTmpDir(), "export-"))) { + throw new ServiceUnavailableHttpException(null, 'Could not create temporary file.'); + } - $res = $zip->open($tempFilename, ZipArchive::OVERWRITE); - if ($res !== true) { - throw new ServiceUnavailableHttpException(null, 'Could not create temporary zip file.'); - } + $res = $zip->open($tempFilename, ZipArchive::OVERWRITE); + if ($res !== true) { + throw new ServiceUnavailableHttpException(null, 'Could not create temporary zip file.'); } /** @var ContestProblem $problem */ foreach ($contest->getProblems() as $problem) { // We don't include the samples for interactive problems. if (!$problem->getProblem()->isInteractiveProblem()) { - $samplesFound = $this->addSamplesToZip($zip, $problem, $problem->getShortname(), fullZip: $fullZip);; - if (!$fullZip && $samplesFound) { - return true; - } + $this->addSamplesToZip($zip, $problem, $problem->getShortname()); } if ($problem->getProblem()->getProblemstatementType()) { - if (!$fullZip) { - return true; - } $filename = sprintf('%s/statement.%s', $problem->getShortname(), $problem->getProblem()->getProblemstatementType()); $zip->addFromString($filename, $problem->getProblem()->getProblemstatement()); } /** @var ProblemAttachment $attachment */ foreach ($problem->getProblem()->getAttachments() as $attachment) { - if (!$fullZip) { - return true; - } $filename = sprintf('%s/attachments/%s', $problem->getShortname(), $attachment->getName()); $zip->addFromString($filename, $attachment->getContent()->getContent()); if ($attachment->getContent()->isExecutable()) { @@ -950,17 +934,10 @@ public function helperSamplesZipForContest(Contest $contest, bool $fullZip): Str } if ($contest->getContestProblemsetType()) { - if (!$fullZip) { - return true; - } $filename = sprintf('contest.%s', $contest->getContestProblemsetType()); $zip->addFromString($filename, $contest->getContestProblemset()); } - if (!$fullZip) { - return false; - } - $zip->close(); $zipFileContents = file_get_contents($tempFilename); unlink($tempFilename); @@ -968,16 +945,6 @@ public function helperSamplesZipForContest(Contest $contest, bool $fullZip): Str return Utils::streamAsBinaryFile($zipFileContents, 'samples.zip', 'zip'); } - public function checkIfSamplesZipForContest(Contest $contest): bool - { - return self::helperSamplesZipForContest($contest, fullZip: false); - } - - public function getSamplesZipForContest(Contest $contest): StreamedResponse - { - return self::helperSamplesZipForContest($contest, fullZip: true); - } - /** * @throws NonUniqueResultException */ From 77928f78d237bd14a82885bf55e9b2bb80583e83 Mon Sep 17 00:00:00 2001 From: Michael Vasseur <14887731+vmcj@users.noreply.github.com> Date: Wed, 12 Nov 2025 22:39:40 +0100 Subject: [PATCH 5/7] Try to do everything with shorter queries Probably we can even do this with one by using leftJoins and checking if any of the columns we need become NON NULL but that makes it a lot harder to get right in the next iteration. --- webapp/src/Service/DOMJudgeService.php | 57 ++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index ac73db6987..390911e6f5 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -879,8 +879,65 @@ public function getSamplesZipStreamedResponse(ContestProblem $contestProblem): S return Utils::streamAsBinaryFile($zipFileContent, $outputFilename, 'zip'); } + public function checkIfSamplesZipForContest(Contest $contest): bool + { + // Note, we reload the contest with the problems and attachments, to reduce the number of queries + // We do not load the testcases here since addSamplesToZip loads them + $contestQuery = $this->em->createQueryBuilder() + ->from(Contest::class, 'c') + ->innerJoin('c.problems', 'cp') + ->innerJoin('cp.problem', 'p') + ->leftJoin('p.attachments', 'a') + ->leftJoin('p.problemStatementContent', 'content') + ->select('c', 'cp', 'p', 'a', 'content') + ->andWhere('c.cid = :cid') + ->andWhere('cp.allowSubmit = 1') // Do we need this... + ->setParameter('cid', $contest->getCid()); + + /** @var Contest $contest */ + $contest = $contestQuery->getQuery()->getSingleResult(); + + if ($contest->getContestProblemsetType()) { + return true; + } + + /** @var Problem[] $nonInteractiveProblems */ + $nonInteractiveProblems = $contestQuery + ->innerJoin('p.testcases', 'tc') + ->andWhere('tc.sample = 1') + ->andWhere('BIT_AND(p.types, :interactiveType) = 0') + ->setParameter('interactiveType', Problem::TYPE_INTERACTIVE) + ->getQuery() + ->getResult(); + if (!empty($nonInteractiveProblems)) { + return true; + } + + /** @var Problem[] $problemsWithProblemStatement */ + $problemsWithProblemStatement = $contestQuery + ->andWhere('p.problemstatementFile IS NOT NULL') + ->getQuery() + ->getResult(); + if (!empty($problemsWithProblemStatement)) { + return true; + } + + /** @var Problem[] $problemsWithAttachments */ + $problemsWithAttachments = $contestQuery + ->innerJoin('p.attachments', 'a') + ->getQuery() + ->getResult(); + if (!empty($problemsWithAttachments)) { + return true; + } + + return false; + } + public function getSamplesZipForContest(Contest $contest): StreamedResponse { + // When updating this, also update checkIfSamplesZipForContest. + // Note, we reload the contest with the problems and attachments, to reduce the number of queries // We do not load the testcases here since addSamplesToZip loads them /** @var Contest $contest */ From 01b754d4db22ea899f3930f6545e82bffe5836cf Mon Sep 17 00:00:00 2001 From: Michael Vasseur <14887731+vmcj@users.noreply.github.com> Date: Sun, 23 Nov 2025 21:19:23 +0100 Subject: [PATCH 6/7] Handle situation without contest --- webapp/src/Service/DOMJudgeService.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index 390911e6f5..b7e53591f1 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -895,7 +895,10 @@ public function checkIfSamplesZipForContest(Contest $contest): bool ->setParameter('cid', $contest->getCid()); /** @var Contest $contest */ - $contest = $contestQuery->getQuery()->getSingleResult(); + $contest = $contestQuery->getQuery()->getOneOrNullResult(); + if (!$contest) { + return false; + } if ($contest->getContestProblemsetType()) { return true; From 58e26affd3db36a8a041cb2b104850a58841616f Mon Sep 17 00:00:00 2001 From: Michael Vasseur <14887731+vmcj@users.noreply.github.com> Date: Sun, 23 Nov 2025 22:10:04 +0100 Subject: [PATCH 7/7] Don't check if there is no contest --- webapp/src/Service/DOMJudgeService.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index b7e53591f1..fad4c51c49 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -896,10 +896,6 @@ public function checkIfSamplesZipForContest(Contest $contest): bool /** @var Contest $contest */ $contest = $contestQuery->getQuery()->getOneOrNullResult(); - if (!$contest) { - return false; - } - if ($contest->getContestProblemsetType()) { return true; } @@ -1157,7 +1153,7 @@ public function getTwigDataForProblemsAction( 'problems' => $problems, 'samples' => $samples, 'showLimits' => $showLimits, - 'showSamples' => $this->checkIfSamplesZipForContest($contest), + 'showSamples' => $contest && $this->checkIfSamplesZipForContest($contest), 'defaultMemoryLimit' => $defaultMemoryLimit, 'timeFactorDiffers' => $timeFactorDiffers, 'clarifications' => $clars,