Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 44 additions & 10 deletions webapp/src/Service/DOMJudgeService.php
Original file line number Diff line number Diff line change
Expand Up @@ -830,7 +830,7 @@
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()
Expand All @@ -849,6 +849,9 @@
->getResult();

foreach ($testcases as $index => $testcase) {
if (!$fullZip) {
return true;
}
foreach (['input', 'output'] as $type) {
$extension = Testcase::EXTENSION_MAPPING[$type];

Expand All @@ -870,6 +873,7 @@
$zip->addFromString($filename, $content);
}
}
return false;
}

public function getSamplesZipStreamedResponse(ContestProblem $contestProblem): StreamedResponse
Expand All @@ -879,7 +883,7 @@
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
Expand All @@ -896,30 +900,42 @@
->getQuery()
->getSingleResult();

$zip = new ZipArchive();
if (!($tempFilename = tempnam($this->getDomjudgeTmpDir(), "export-"))) {
throw new ServiceUnavailableHttpException(null, 'Could not create temporary file.');
}
$zip = null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get what you're doing, but I think just doing a simple query to count the number of sample testcases across all problems in the contest in checkIfSamplesZipForContest is cleaner than this (and more performant)

Copy link
Member Author

@vmcj vmcj Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but in case you have no samples but do have attachments you would not create the zip. But I wonder if this will ever return false, why would there be a contest with neither of:

  • samples,
  • problem statements
  • attachments

All of those are possible, but having neither of those files really feels strange.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The zip with all samples contains only samples or also attachment? In the latter case the button has a weird name maybe, not sure.

But still I would not mis-use a method like this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of those are possible, but having neither of those files really feels strange.

When everything is provided on the team machine and/or as hardcopy problem statements. I don't think that's a far-fetched case.

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);;

Check failure on line 920 in webapp/src/Service/DOMJudgeService.php

View workflow job for this annotation

GitHub Actions / phpcs

Each PHP statement must be on a line by itself
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()) {
Expand All @@ -934,17 +950,34 @@
}

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);

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
*/
Expand Down Expand Up @@ -1097,6 +1130,7 @@
'problems' => $problems,
'samples' => $samples,
'showLimits' => $showLimits,
'showSamples' => $this->checkIfSamplesZipForContest($contest),
'defaultMemoryLimit' => $defaultMemoryLimit,
'timeFactorDiffers' => $timeFactorDiffers,
'clarifications' => $clars,
Expand Down
27 changes: 17 additions & 10 deletions webapp/templates/partials/problem_list.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,24 @@

<h1 class="mt-4 text-center">
{{ 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 %}
<a class="btn btn-secondary" role="button"
href="{{ contest_problemset_url }}">
<i class="fas fa-file-{{ contest.contestProblemsetType }}"></i>
problemset
</a>
{% endif %}
{% if showSamples %}
<a class="btn btn-secondary" href="{{ path('current_samples_data_zip', {cid: contest.cid}) }}" title="Contains samples, attachments and statement for all problems.">
<i class="fas fa-download"></i> samples
</a>
{% endif %}
<a class="btn btn-secondary" role="button"
href="{{ contest_problemset_url }}">
<i class="fas fa-file-{{ contest.contestProblemsetType }}"></i>
problemset
</a>
{% endif %}
</h1>

Expand Down
Loading