Skip to content

Commit

Permalink
Add option to set a different set of languages allowed to submit per …
Browse files Browse the repository at this point in the history
…contest.

If empty, falls back to the global definition.

This is part of DOMjudge#2521.
  • Loading branch information
meisterT committed Nov 23, 2024
1 parent 5533e60 commit 4a79f9d
Show file tree
Hide file tree
Showing 17 changed files with 178 additions and 42 deletions.
40 changes: 40 additions & 0 deletions webapp/migrations/Version20241122144232.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20241122144232 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add many to many relation between contest and langs.';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE contestlanguage (cid INT UNSIGNED NOT NULL COMMENT \'Contest ID\', langid VARCHAR(32) NOT NULL COMMENT \'Language ID (string)\', INDEX IDX_ADCB43234B30D9C4 (cid), INDEX IDX_ADCB43232271845 (langid), PRIMARY KEY(cid, langid)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('ALTER TABLE contestlanguage ADD CONSTRAINT FK_ADCB43234B30D9C4 FOREIGN KEY (cid) REFERENCES contest (cid) ON DELETE CASCADE');
$this->addSql('ALTER TABLE contestlanguage ADD CONSTRAINT FK_ADCB43232271845 FOREIGN KEY (langid) REFERENCES language (langid) ON DELETE CASCADE');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE contestlanguage DROP FOREIGN KEY FK_ADCB43234B30D9C4');
$this->addSql('ALTER TABLE contestlanguage DROP FOREIGN KEY FK_ADCB43232271845');
$this->addSql('DROP TABLE contestlanguage');
}

public function isTransactional(): bool
{
return false;
}
}
6 changes: 6 additions & 0 deletions webapp/src/Controller/BaseController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Entity\Contest;
use App\Entity\ContestProblem;
use App\Entity\ExternalIdFromInternalIdInterface;
use App\Entity\Language;
use App\Entity\Problem;
use App\Entity\RankCache;
use App\Entity\ScoreCache;
Expand All @@ -16,6 +17,7 @@
use App\Service\DOMJudgeService;
use App\Service\EventLogService;
use App\Utils\Utils;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\Inflector\InflectorFactory;
use Doctrine\ORM\EntityManagerInterface;
Expand Down Expand Up @@ -522,6 +524,10 @@ protected function contestsForEntity(mixed $entity): array
$contests = $this->dj->getCurrentContests();
}

if ($contests instanceof Collection) {
$contests = $contests->toArray();
}

return $contests;
}

Expand Down
3 changes: 3 additions & 0 deletions webapp/src/Controller/Jury/ContestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -346,12 +346,15 @@ public function viewAction(Request $request, int $contestId): Response
->getQuery()
->getResult();

$languages = $this->dj->getAllowedLanguagesForContest($contest);

return $this->render('jury/contest.html.twig', [
'contest' => $contest,
'allowRemovedIntervals' => $this->getParameter('removed_intervals'),
'removedIntervalForm' => $form,
'removedIntervals' => $removedIntervals,
'problems' => $problems,
'languages' => $languages,
]);
}

Expand Down
8 changes: 2 additions & 6 deletions webapp/src/Controller/Team/LanguageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,9 @@ public function languagesAction(): Response
if (!$languagesEnabled) {
throw new BadRequestHttpException("You are not allowed to view this page.");
}
$currentContest = $this->dj->getCurrentContest();
/** @var Language[] $languages */
$languages = $this->em->createQueryBuilder()
->select('l')
->from(Language::class, 'l')
->andWhere('l.allowSubmit = 1')
->orderBy('l.langid')
->getQuery()->getResult();
$languages = $this->dj->getAllowedLanguagesForContest($currentContest);
return $this->render('team/languages.html.twig', ['languages' => $languages]);
}
}
8 changes: 2 additions & 6 deletions webapp/src/Controller/Team/MiscController.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,13 +195,9 @@ public function printAction(Request $request): Response
]);
}

$currentContest = $this->dj->getCurrentContest();
/** @var Language[] $languages */
$languages = $this->em->createQueryBuilder()
->from(Language::class, 'l')
->select('l')
->andWhere('l.allowSubmit = 1')
->getQuery()
->getResult();
$languages = $this->dj->getAllowedLanguagesForContest($currentContest);

return $this->render('team/print.html.twig', [
'form' => $form,
Expand Down
28 changes: 28 additions & 0 deletions webapp/src/Entity/Contest.php
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,16 @@ class Contest extends BaseApiEntity implements
#[Serializer\Exclude]
private Collection $teams;

/**
* @var Collection<int, Language>
*/
#[ORM\ManyToMany(targetEntity: Language::class, inversedBy: 'contests')]
#[ORM\JoinTable(name: 'contestlanguage')]
#[ORM\JoinColumn(name: 'cid', referencedColumnName: 'cid', onDelete: 'CASCADE')]
#[ORM\InverseJoinColumn(name: 'langid', referencedColumnName: 'langid', onDelete: 'CASCADE')]
#[Serializer\Exclude]
private Collection $languages;

/**
* @var Collection<int, TeamCategory>
*/
Expand Down Expand Up @@ -437,6 +447,7 @@ public function __construct()
{
$this->problems = new ArrayCollection();
$this->teams = new ArrayCollection();
$this->languages = new ArrayCollection();
$this->removedIntervals = new ArrayCollection();
$this->clarifications = new ArrayCollection();
$this->submissions = new ArrayCollection();
Expand Down Expand Up @@ -896,6 +907,22 @@ public function getTeams(): Collection
return $this->teams;
}

public function addLanguage(Language $language): Contest
{
$this->languages[] = $language;
return $this;
}

public function removeLanguage(Language $language): void
{
$this->languages->removeElement($language);
}

public function getLanguages(): Collection
{
return $this->languages;
}

public function addProblem(ContestProblem $problem): Contest
{
$this->problems[] = $problem;
Expand Down Expand Up @@ -1591,4 +1618,5 @@ public function getProblemsetForApi(): array
{
return array_filter([$this->problemsetForApi]);
}

}
27 changes: 27 additions & 0 deletions webapp/src/Entity/Language.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ class Language extends BaseApiEntity implements
#[Serializer\Exclude]
private ?string $runnerVersionCommand = null;

/**
* @var Collection<int, Contest>
*/
#[ORM\ManyToMany(targetEntity: Contest::class, mappedBy: 'languages')]
#[Serializer\Exclude]
private Collection $contests;

/**
* @param Collection<int, Version> $versions
*/
Expand Down Expand Up @@ -386,6 +393,7 @@ public function __construct()
{
$this->submissions = new ArrayCollection();
$this->versions = new ArrayCollection();
$this->contests = new ArrayCollection();
}

public function addSubmission(Submission $submission): Language
Expand Down Expand Up @@ -419,4 +427,23 @@ public function getAceLanguage(): string
default => $this->getLangid(),
};
}

public function addContest(Contest $contest): Language
{
$this->contests[] = $contest;
$contest->addLanguage($this);
return $this;
}

public function removeContest(Contest $contest): Language
{
$this->contests->removeElement($contest);
$contest->removeLanguage($this);
return $this;
}

public function getContests(): Collection
{
return $this->contests;
}
}
8 changes: 8 additions & 0 deletions webapp/src/Form/Type/ContestType.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Entity\Contest;
use App\Entity\ContestProblem;
use App\Entity\Language;
use App\Entity\Team;
use App\Entity\TeamCategory;
use App\Service\DOMJudgeService;
Expand Down Expand Up @@ -200,6 +201,13 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'allow_delete' => true,
'label' => false,
]);
$builder->add('languages', EntityType::class, [
'required' => false,
'class' => Language::class,
'multiple' => true,
'choice_label' => fn(Language $language) => sprintf('%s (%s)', $language->getName(), $language->getExternalid()),
'help' => 'List of languages that can be used in this contest. Leave empty to allow all languages that are enabled globally.',
]);

$builder->add('save', SubmitType::class);

Expand Down
11 changes: 11 additions & 0 deletions webapp/src/Form/Type/LanguageType.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Form\Type;

use App\Entity\Contest;
use App\Entity\Executable;
use App\Entity\Language;
use Doctrine\ORM\EntityRepository;
Expand Down Expand Up @@ -88,6 +89,16 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'label' => 'Runner version command',
'required' => false,
]);
$builder->add('contests', EntityType::class, [
'class' => Contest::class,
'required' => false,
'choice_label' => 'name',
'multiple' => true,
'by_reference' => false,
'query_builder' => fn(EntityRepository $er) => $er
->createQueryBuilder('c')
->orderBy('c.name'),
]);
$builder->add('save', SubmitType::class);

// Remove ID field when doing an edit.
Expand Down
5 changes: 2 additions & 3 deletions webapp/src/Form/Type/SubmitProblemType.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,10 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
];
$builder->add('problem', EntityType::class, $problemConfig);

$languages = $this->dj->getAllowedLanguagesForContest($contest);
$builder->add('language', EntityType::class, [
'class' => Language::class,
'query_builder' => fn(EntityRepository $er) => $er
->createQueryBuilder('l')
->andWhere('l.allowSubmit = 1'),
'choices' => $languages,
'choice_label' => 'name',
'placeholder' => 'Select a language',
]);
Expand Down
20 changes: 19 additions & 1 deletion webapp/src/Service/DOMJudgeService.php
Original file line number Diff line number Diff line change
Expand Up @@ -1000,11 +1000,13 @@ public function getTwigDataForProblemsAction(
$defaultMemoryLimit = (int)$this->config->get('memory_limit');
$timeFactorDiffers = false;
if ($showLimits) {
$languages = $this->getAllowedLanguagesForContest($contest);
$timeFactorDiffers = $this->em->createQueryBuilder()
->from(Language::class, 'l')
->select('COUNT(l)')
->andWhere('l.allowSubmit = true')
->andWhere('l.timeFactor <> 1')
->andWhere('l IN (:languages)')
->setParameter('languages', $languages)
->getQuery()
->getSingleScalarResult() > 0;
}
Expand Down Expand Up @@ -1682,4 +1684,20 @@ public function shadowMode(): bool
{
return (bool)$this->config->get('shadow_mode');
}

/** @return Language[] */
public function getAllowedLanguagesForContest(?Contest $contest) : array {
if ($contest) {
$languages = $contest->getLanguages();
if (!$languages->isEmpty()) {
return $languages->toArray();
}
}
return $this->em->createQueryBuilder(Language::class)
->select('l')
->from(Language::class, 'l')
->where('l.allowSubmit = 1')
->getQuery()
->getResult();
}
}
11 changes: 3 additions & 8 deletions webapp/src/Service/ImportProblemService.php
Original file line number Diff line number Diff line change
Expand Up @@ -653,12 +653,7 @@ public function importZippedProblem(

// First find all submittable languages:
/** @var Language[] $allowedLanguages */
$allowedLanguages = $this->em->createQueryBuilder()
->from(Language::class, 'l', 'l.langid')
->select('l')
->andWhere('l.allowSubmit = true')
->getQuery()
->getResult();
$allowedLanguages = $this->dj->getAllowedLanguagesForContest($contest);

// Read submission details from optional file.
$submission_file_string = $zip->getFromName($submission_file);
Expand Down Expand Up @@ -708,9 +703,9 @@ public function importZippedProblem(
continue;
}
$extension = end($parts);
foreach ($allowedLanguages as $key => $language) {
foreach ($allowedLanguages as $language) {
if (in_array($extension, $language->getExtensions())) {
$languageToUse = $key;
$languageToUse = $language->getLangid();
break 2;
}
}
Expand Down
10 changes: 2 additions & 8 deletions webapp/src/Service/StatisticsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class StatisticsService
'all' => 'All teams',
];

public function __construct(private readonly EntityManagerInterface $em)
public function __construct(private readonly EntityManagerInterface $em, private readonly DOMJudgeService $dj)
{
}

Expand Down Expand Up @@ -544,15 +544,9 @@ public function getGroupedProblemsStats(
public function getLanguagesStats(Contest $contest, string $view): array
{
/** @var Language[] $languages */
$languages = $this->em->getRepository(Language::class)
->createQueryBuilder('l')
->andWhere('l.allowSubmit = 1')
->orderBy('l.name')
->getQuery()
->getResult();
$languages = $this->dj->getAllowedLanguagesForContest($contest);

$languageStats = [];

foreach ($languages as $language) {
$languageStats[$language->getLangid()] = [
'name' => $language->getName(),
Expand Down
7 changes: 4 additions & 3 deletions webapp/src/Service/SubmissionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -453,9 +453,10 @@ public function submitSolution(
throw new BadRequestHttpException('Submissions for contest (temporarily) disabled');
}

if (!$language->getAllowSubmit()) {
$allowedLanguages = $this->dj->getAllowedLanguagesForContest($contest);
if (!in_array($language, $allowedLanguages, true)) {
throw new BadRequestHttpException(
sprintf("Language '%s' not found in database or not submittable.", $language->getLangid()));
sprintf("Language '%s' not allowed for contest [c%d].", $language->getLangid(), $contest->getCid()));
}

if ($language->getRequireEntryPoint() && empty($entryPoint)) {
Expand Down Expand Up @@ -781,7 +782,7 @@ public function getSubmissionFileResponse(Submission $submission): StreamedRespo
{
/** @var SubmissionFile[] $files */
$files = $submission->getFiles();

if (count($files) !== 1) {
throw new ServiceUnavailableHttpException(null, 'Submission does not contain exactly one file.');
}
Expand Down
Loading

0 comments on commit 4a79f9d

Please sign in to comment.