Skip to content

Commit 063b687

Browse files
committed
Add option to set a different set of languages allowed to submit per contest.
If empty, falls back to the global definition. This is part of #2521.
1 parent 792bb6c commit 063b687

17 files changed

+177
-42
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
/**
11+
* Auto-generated Migration: Please modify to your needs!
12+
*/
13+
final class Version20241122144232 extends AbstractMigration
14+
{
15+
public function getDescription(): string
16+
{
17+
return 'Add many to many relation between contest and langs.';
18+
}
19+
20+
public function up(Schema $schema): void
21+
{
22+
// this up() migration is auto-generated, please modify it to your needs
23+
$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');
24+
$this->addSql('ALTER TABLE contestlanguage ADD CONSTRAINT FK_ADCB43234B30D9C4 FOREIGN KEY (cid) REFERENCES contest (cid) ON DELETE CASCADE');
25+
$this->addSql('ALTER TABLE contestlanguage ADD CONSTRAINT FK_ADCB43232271845 FOREIGN KEY (langid) REFERENCES language (langid) ON DELETE CASCADE');
26+
}
27+
28+
public function down(Schema $schema): void
29+
{
30+
// this down() migration is auto-generated, please modify it to your needs
31+
$this->addSql('ALTER TABLE contestlanguage DROP FOREIGN KEY FK_ADCB43234B30D9C4');
32+
$this->addSql('ALTER TABLE contestlanguage DROP FOREIGN KEY FK_ADCB43232271845');
33+
$this->addSql('DROP TABLE contestlanguage');
34+
}
35+
36+
public function isTransactional(): bool
37+
{
38+
return false;
39+
}
40+
}

webapp/src/Controller/BaseController.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use App\Entity\Contest;
99
use App\Entity\ContestProblem;
1010
use App\Entity\ExternalIdFromInternalIdInterface;
11+
use App\Entity\Language;
1112
use App\Entity\Problem;
1213
use App\Entity\RankCache;
1314
use App\Entity\ScoreCache;
@@ -16,6 +17,7 @@
1617
use App\Service\DOMJudgeService;
1718
use App\Service\EventLogService;
1819
use App\Utils\Utils;
20+
use Doctrine\Common\Collections\Collection;
1921
use Doctrine\DBAL\Exception as DBALException;
2022
use Doctrine\Inflector\InflectorFactory;
2123
use Doctrine\ORM\EntityManagerInterface;
@@ -522,6 +524,10 @@ protected function contestsForEntity(mixed $entity): array
522524
$contests = $this->dj->getCurrentContests();
523525
}
524526

527+
if ($contests instanceof Collection) {
528+
$contests = $contests->toArray();
529+
}
530+
525531
return $contests;
526532
}
527533

webapp/src/Controller/Jury/ContestController.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,12 +346,15 @@ public function viewAction(Request $request, int $contestId): Response
346346
->getQuery()
347347
->getResult();
348348

349+
$languages = $this->dj->getAllowedLanguagesForContest($contest);
350+
349351
return $this->render('jury/contest.html.twig', [
350352
'contest' => $contest,
351353
'allowRemovedIntervals' => $this->getParameter('removed_intervals'),
352354
'removedIntervalForm' => $form,
353355
'removedIntervals' => $removedIntervals,
354356
'problems' => $problems,
357+
'languages' => $languages,
355358
]);
356359
}
357360

webapp/src/Controller/Team/LanguageController.php

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,9 @@ public function languagesAction(): Response
4040
if (!$languagesEnabled) {
4141
throw new BadRequestHttpException("You are not allowed to view this page.");
4242
}
43+
$currentContest = $this->dj->getCurrentContest();
4344
/** @var Language[] $languages */
44-
$languages = $this->em->createQueryBuilder()
45-
->select('l')
46-
->from(Language::class, 'l')
47-
->andWhere('l.allowSubmit = 1')
48-
->orderBy('l.langid')
49-
->getQuery()->getResult();
45+
$languages = $this->dj->getAllowedLanguagesForContest($currentContest);
5046
return $this->render('team/languages.html.twig', ['languages' => $languages]);
5147
}
5248
}

webapp/src/Controller/Team/MiscController.php

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -195,13 +195,9 @@ public function printAction(Request $request): Response
195195
]);
196196
}
197197

198+
$currentContest = $this->dj->getCurrentContest();
198199
/** @var Language[] $languages */
199-
$languages = $this->em->createQueryBuilder()
200-
->from(Language::class, 'l')
201-
->select('l')
202-
->andWhere('l.allowSubmit = 1')
203-
->getQuery()
204-
->getResult();
200+
$languages = $this->dj->getAllowedLanguagesForContest($currentContest);
205201

206202
return $this->render('team/print.html.twig', [
207203
'form' => $form,

webapp/src/Entity/Contest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,16 @@ class Contest extends BaseApiEntity implements
343343
#[Serializer\Exclude]
344344
private Collection $teams;
345345

346+
/**
347+
* @var Collection<int, Language>
348+
*/
349+
#[ORM\ManyToMany(targetEntity: Language::class, inversedBy: 'contests')]
350+
#[ORM\JoinTable(name: 'contestlanguage')]
351+
#[ORM\JoinColumn(name: 'cid', referencedColumnName: 'cid', onDelete: 'CASCADE')]
352+
#[ORM\InverseJoinColumn(name: 'langid', referencedColumnName: 'langid', onDelete: 'CASCADE')]
353+
#[Serializer\Exclude]
354+
private Collection $languages;
355+
346356
/**
347357
* @var Collection<int, TeamCategory>
348358
*/
@@ -437,6 +447,7 @@ public function __construct()
437447
{
438448
$this->problems = new ArrayCollection();
439449
$this->teams = new ArrayCollection();
450+
$this->languages = new ArrayCollection();
440451
$this->removedIntervals = new ArrayCollection();
441452
$this->clarifications = new ArrayCollection();
442453
$this->submissions = new ArrayCollection();
@@ -896,6 +907,22 @@ public function getTeams(): Collection
896907
return $this->teams;
897908
}
898909

910+
public function addLanguage(Language $language): Contest
911+
{
912+
$this->languages[] = $language;
913+
return $this;
914+
}
915+
916+
public function removeLanguage(Language $language): void
917+
{
918+
$this->languages->removeElement($language);
919+
}
920+
921+
public function getLanguages(): Collection
922+
{
923+
return $this->languages;
924+
}
925+
899926
public function addProblem(ContestProblem $problem): Contest
900927
{
901928
$this->problems[] = $problem;

webapp/src/Entity/Language.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,13 @@ class Language extends BaseApiEntity implements
149149
#[Serializer\Exclude]
150150
private ?string $runnerVersionCommand = null;
151151

152+
/**
153+
* @var Collection<int, Contest>
154+
*/
155+
#[ORM\ManyToMany(targetEntity: Contest::class, mappedBy: 'languages')]
156+
#[Serializer\Exclude]
157+
private Collection $contests;
158+
152159
/**
153160
* @param Collection<int, Version> $versions
154161
*/
@@ -386,6 +393,7 @@ public function __construct()
386393
{
387394
$this->submissions = new ArrayCollection();
388395
$this->versions = new ArrayCollection();
396+
$this->contests = new ArrayCollection();
389397
}
390398

391399
public function addSubmission(Submission $submission): Language
@@ -416,4 +424,23 @@ public function getEditorLanguage(): string
416424
default => $this->getLangid(),
417425
};
418426
}
427+
428+
public function addContest(Contest $contest): Language
429+
{
430+
$this->contests[] = $contest;
431+
$contest->addLanguage($this);
432+
return $this;
433+
}
434+
435+
public function removeContest(Contest $contest): Language
436+
{
437+
$this->contests->removeElement($contest);
438+
$contest->removeLanguage($this);
439+
return $this;
440+
}
441+
442+
public function getContests(): Collection
443+
{
444+
return $this->contests;
445+
}
419446
}

webapp/src/Form/Type/ContestType.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use App\Entity\Contest;
66
use App\Entity\ContestProblem;
7+
use App\Entity\Language;
78
use App\Entity\Team;
89
use App\Entity\TeamCategory;
910
use App\Service\DOMJudgeService;
@@ -200,6 +201,13 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
200201
'allow_delete' => true,
201202
'label' => false,
202203
]);
204+
$builder->add('languages', EntityType::class, [
205+
'required' => false,
206+
'class' => Language::class,
207+
'multiple' => true,
208+
'choice_label' => fn(Language $language) => sprintf('%s (%s)', $language->getName(), $language->getExternalid()),
209+
'help' => 'List of languages that can be used in this contest. Leave empty to allow all languages that are enabled globally.',
210+
]);
203211

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

webapp/src/Form/Type/LanguageType.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Form\Type;
44

5+
use App\Entity\Contest;
56
use App\Entity\Executable;
67
use App\Entity\Language;
78
use Doctrine\ORM\EntityRepository;
@@ -88,6 +89,16 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
8889
'label' => 'Runner version command',
8990
'required' => false,
9091
]);
92+
$builder->add('contests', EntityType::class, [
93+
'class' => Contest::class,
94+
'required' => false,
95+
'choice_label' => 'name',
96+
'multiple' => true,
97+
'by_reference' => false,
98+
'query_builder' => fn(EntityRepository $er) => $er
99+
->createQueryBuilder('c')
100+
->orderBy('c.name'),
101+
]);
91102
$builder->add('save', SubmitType::class);
92103

93104
// Remove ID field when doing an edit.

webapp/src/Form/Type/SubmitProblemType.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,10 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
5454
];
5555
$builder->add('problem', EntityType::class, $problemConfig);
5656

57+
$languages = $this->dj->getAllowedLanguagesForContest($contest);
5758
$builder->add('language', EntityType::class, [
5859
'class' => Language::class,
59-
'query_builder' => fn(EntityRepository $er) => $er
60-
->createQueryBuilder('l')
61-
->andWhere('l.allowSubmit = 1'),
60+
'choices' => $languages,
6261
'choice_label' => 'name',
6362
'placeholder' => 'Select a language',
6463
]);

webapp/src/Service/DOMJudgeService.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1001,11 +1001,13 @@ public function getTwigDataForProblemsAction(
10011001
$defaultMemoryLimit = (int)$this->config->get('memory_limit');
10021002
$timeFactorDiffers = false;
10031003
if ($showLimits) {
1004+
$languages = $this->getAllowedLanguagesForContest($contest);
10041005
$timeFactorDiffers = $this->em->createQueryBuilder()
10051006
->from(Language::class, 'l')
10061007
->select('COUNT(l)')
1007-
->andWhere('l.allowSubmit = true')
10081008
->andWhere('l.timeFactor <> 1')
1009+
->andWhere('l IN (:languages)')
1010+
->setParameter('languages', $languages)
10091011
->getQuery()
10101012
->getSingleScalarResult() > 0;
10111013
}
@@ -1683,4 +1685,20 @@ public function shadowMode(): bool
16831685
{
16841686
return (bool)$this->config->get('shadow_mode');
16851687
}
1688+
1689+
/** @return Language[] */
1690+
public function getAllowedLanguagesForContest(?Contest $contest) : array {
1691+
if ($contest) {
1692+
$languages = $contest->getLanguages();
1693+
if (!$languages->isEmpty()) {
1694+
return $languages->toArray();
1695+
}
1696+
}
1697+
return $this->em->createQueryBuilder(Language::class)
1698+
->select('l')
1699+
->from(Language::class, 'l')
1700+
->where('l.allowSubmit = 1')
1701+
->getQuery()
1702+
->getResult();
1703+
}
16861704
}

webapp/src/Service/ImportProblemService.php

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -653,12 +653,7 @@ public function importZippedProblem(
653653

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

663658
// Read submission details from optional file.
664659
$submission_file_string = $zip->getFromName($submission_file);
@@ -708,9 +703,9 @@ public function importZippedProblem(
708703
continue;
709704
}
710705
$extension = end($parts);
711-
foreach ($allowedLanguages as $key => $language) {
706+
foreach ($allowedLanguages as $language) {
712707
if (in_array($extension, $language->getExtensions())) {
713-
$languageToUse = $key;
708+
$languageToUse = $language->getLangid();
714709
break 2;
715710
}
716711
}

webapp/src/Service/StatisticsService.php

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class StatisticsService
2525
'all' => 'All teams',
2626
];
2727

28-
public function __construct(private readonly EntityManagerInterface $em)
28+
public function __construct(private readonly EntityManagerInterface $em, private readonly DOMJudgeService $dj)
2929
{
3030
}
3131

@@ -544,15 +544,9 @@ public function getGroupedProblemsStats(
544544
public function getLanguagesStats(Contest $contest, string $view): array
545545
{
546546
/** @var Language[] $languages */
547-
$languages = $this->em->getRepository(Language::class)
548-
->createQueryBuilder('l')
549-
->andWhere('l.allowSubmit = 1')
550-
->orderBy('l.name')
551-
->getQuery()
552-
->getResult();
547+
$languages = $this->dj->getAllowedLanguagesForContest($contest);
553548

554549
$languageStats = [];
555-
556550
foreach ($languages as $language) {
557551
$languageStats[$language->getLangid()] = [
558552
'name' => $language->getName(),

webapp/src/Service/SubmissionService.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -453,9 +453,10 @@ public function submitSolution(
453453
throw new BadRequestHttpException('Submissions for contest (temporarily) disabled');
454454
}
455455

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

461462
if ($language->getRequireEntryPoint() && empty($entryPoint)) {
@@ -781,7 +782,7 @@ public function getSubmissionFileResponse(Submission $submission): StreamedRespo
781782
{
782783
/** @var SubmissionFile[] $files */
783784
$files = $submission->getFiles();
784-
785+
785786
if (count($files) !== 1) {
786787
throw new ServiceUnavailableHttpException(null, 'Submission does not contain exactly one file.');
787788
}

0 commit comments

Comments
 (0)