Skip to content

Commit 40847af

Browse files
committed
Add option to restrict a problem to a set of languages.
This is especially useful for output-only problems (part of #2521).
1 parent 78e48af commit 40847af

File tree

7 files changed

+148
-4
lines changed

7 files changed

+148
-4
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 Version20250228123717 extends AbstractMigration
14+
{
15+
public function getDescription(): string
16+
{
17+
return 'Add option to restrict a problem to a set of languages.';
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 problemlanguage (probid INT UNSIGNED NOT NULL COMMENT \'Problem ID\', langid VARCHAR(32) NOT NULL COMMENT \'Language ID (string)\', INDEX IDX_46B150BBEF049279 (probid), INDEX IDX_46B150BB2271845 (langid), PRIMARY KEY(probid, langid)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
24+
$this->addSql('ALTER TABLE problemlanguage ADD CONSTRAINT FK_46B150BBEF049279 FOREIGN KEY (probid) REFERENCES problem (probid) ON DELETE CASCADE');
25+
$this->addSql('ALTER TABLE problemlanguage ADD CONSTRAINT FK_46B150BB2271845 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 problemlanguage DROP FOREIGN KEY FK_46B150BBEF049279');
32+
$this->addSql('ALTER TABLE problemlanguage DROP FOREIGN KEY FK_46B150BB2271845');
33+
$this->addSql('DROP TABLE problemlanguage');
34+
}
35+
36+
public function isTransactional(): bool
37+
{
38+
return false;
39+
}
40+
}

webapp/src/Entity/Language.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,13 @@ class Language extends BaseApiEntity implements
156156
#[Serializer\Exclude]
157157
private Collection $contests;
158158

159+
/**
160+
* @var Collection<int, Problem>
161+
*/
162+
#[ORM\ManyToMany(targetEntity: Problem::class, mappedBy: 'languages')]
163+
#[Serializer\Exclude]
164+
private Collection $problems;
165+
159166
/**
160167
* @param Collection<int, Version> $versions
161168
*/
@@ -394,6 +401,7 @@ public function __construct()
394401
$this->submissions = new ArrayCollection();
395402
$this->versions = new ArrayCollection();
396403
$this->contests = new ArrayCollection();
404+
$this->problems = new ArrayCollection();
397405
}
398406

399407
public function addSubmission(Submission $submission): Language
@@ -443,4 +451,23 @@ public function getContests(): Collection
443451
{
444452
return $this->contests;
445453
}
454+
455+
public function addProblem(Problem $problem): Language
456+
{
457+
$this->problems[] = $problem;
458+
$problem->addLanguage($this);
459+
return $this;
460+
}
461+
462+
public function removeProblem(Problem $problem): Language
463+
{
464+
$this->problems->removeElement($problem);
465+
$problem->removeLanguage($this);
466+
return $this;
467+
}
468+
469+
public function getProblems(): Collection
470+
{
471+
return $this->problems;
472+
}
446473
}

webapp/src/Entity/Problem.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,17 @@ class Problem extends BaseApiEntity implements
191191
#[Serializer\Exclude]
192192
private ?FileWithName $statementForApi = null;
193193

194+
195+
/**
196+
* @var Collection<int, Language>
197+
*/
198+
#[ORM\ManyToMany(targetEntity: Language::class, inversedBy: 'problems')]
199+
#[ORM\JoinTable(name: 'problemlanguage')]
200+
#[ORM\JoinColumn(name: 'probid', referencedColumnName: 'probid', onDelete: 'CASCADE')]
201+
#[ORM\InverseJoinColumn(name: 'langid', referencedColumnName: 'langid', onDelete: 'CASCADE')]
202+
#[Serializer\Exclude]
203+
private Collection $languages;
204+
194205
public function setProbid(int $probid): Problem
195206
{
196207
$this->probid = $probid;
@@ -385,6 +396,7 @@ public function __construct()
385396
$this->clarifications = new ArrayCollection();
386397
$this->contest_problems = new ArrayCollection();
387398
$this->attachments = new ArrayCollection();
399+
$this->languages = new ArrayCollection();
388400
$this->problemStatementContent = new ArrayCollection();
389401
}
390402

@@ -541,4 +553,24 @@ public function getStatementForApi(): array
541553
{
542554
return array_filter([$this->statementForApi]);
543555
}
556+
557+
public function addLanguage(Language $language): Problem
558+
{
559+
$this->languages[] = $language;
560+
return $this;
561+
}
562+
563+
/**
564+
* @return Collection<int, Language>
565+
*/
566+
public function getLanguages(): Collection
567+
{
568+
return $this->languages;
569+
}
570+
571+
public function removeLanguage(Language $language): Problem
572+
{
573+
$this->languages->removeElement($language);
574+
return $this;
575+
}
544576
}

webapp/src/Form/Type/LanguageType.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use App\Entity\Contest;
66
use App\Entity\Executable;
77
use App\Entity\Language;
8+
use App\Entity\Problem;
89
use Doctrine\ORM\EntityRepository;
910
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
1011
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
@@ -99,6 +100,16 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
99100
->createQueryBuilder('c')
100101
->orderBy('c.name'),
101102
]);
103+
$builder->add('problems', EntityType::class, [
104+
'class' => Problem::class,
105+
'required' => false,
106+
'choice_label' => 'name',
107+
'multiple' => true,
108+
'by_reference' => false,
109+
'query_builder' => fn(EntityRepository $er) => $er
110+
->createQueryBuilder('p')
111+
->orderBy('p.name'),
112+
]);
102113
$builder->add('save', SubmitType::class);
103114

104115
// Remove ID field when doing an edit.

webapp/src/Form/Type/ProblemType.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace App\Form\Type;
44

55
use App\Entity\Executable;
6+
use App\Entity\Language;
67
use App\Entity\Problem;
78
use Doctrine\ORM\EntityRepository;
89
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
@@ -90,6 +91,13 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
9091
'required' => false,
9192
'help' => 'leave empty for default',
9293
]);
94+
$builder->add('languages', EntityType::class, [
95+
'required' => false,
96+
'class' => Language::class,
97+
'multiple' => true,
98+
'choice_label' => fn(Language $language) => sprintf('%s (%s)', $language->getName(), $language->getExternalid()),
99+
'help' => 'List of languages that can be used for this problem. Leave empty to allow all languages that are enabled for this contest.',
100+
]);
93101
$builder->add('save', SubmitType::class);
94102

95103
// Remove clearProblemstatement field when we do not have a problem text.

webapp/src/Service/SubmissionService.php

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -535,10 +535,22 @@ public function submitSolution(
535535
throw new BadRequestHttpException('Submissions for contest (temporarily) disabled');
536536
}
537537

538-
$allowedLanguages = $this->dj->getAllowedLanguagesForContest($contest);
539-
if (!in_array($language, $allowedLanguages, true)) {
540-
throw new BadRequestHttpException(
541-
sprintf("Language '%s' not allowed for contest [c%d].", $language->getLangid(), $contest->getCid()));
538+
// If there is a set of languages configured for the problem, it overrides the languages configured for the
539+
// contest / globally. This is useful for restricting problems to be solved in specific languages, e.g.
540+
// output-only problems.
541+
$allowedLanguages = $problem->getProblem()->getLanguages();
542+
if ($allowedLanguages->isEmpty()) {
543+
$allowedLanguages = $this->dj->getAllowedLanguagesForContest($contest);
544+
if (!in_array($language, $allowedLanguages, strict: true)) {
545+
throw new BadRequestHttpException(
546+
sprintf("Language '%s' not allowed for contest [c%d].", $language->getLangid(), $contest->getCid()));
547+
}
548+
} else {
549+
$allowedLanguages = $allowedLanguages->toArray();
550+
if (!in_array($language, $allowedLanguages, strict: true)) {
551+
throw new BadRequestHttpException(
552+
sprintf("Language '%s' not allowed for problem [p%d].", $language->getLangid(), $problem->getProbid()));
553+
}
542554
}
543555

544556
if ($language->getRequireEntryPoint() && empty($entryPoint)) {

webapp/templates/jury/problem.html.twig

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,20 @@
116116
<td>{{ type }}</td>
117117
</tr>
118118
{% endif %}
119+
<tr>
120+
<th>Languages</th>
121+
<td>
122+
{% if problem.languages is empty %}
123+
<em>all languages enabled for the corresponding contest</em>
124+
{% else %}
125+
<ul>
126+
{% for language in problem.languages %}
127+
<li><a href="{{ path('jury_language', {'langId': language.langid}) }}">{{ language.name }}</a></li>
128+
{% endfor %}
129+
</ul>
130+
{% endif %}
131+
</td>
132+
</tr>
119133
</table>
120134
</div>
121135
</div>

0 commit comments

Comments
 (0)