From 821b1b8e555af600309961d61c74b367e38332c1 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Fri, 24 Oct 2025 16:02:37 +0200 Subject: [PATCH 01/32] Change language.langid to auto increment integer Part of #3024 --- webapp/migrations/Version20251024122021.php | 141 ++++++++++++++++++ .../src/Controller/API/LanguageController.php | 2 +- .../Controller/Jury/JuryMiscController.php | 8 +- .../Controller/Jury/LanguageController.php | 4 +- .../DefaultData/LanguageFixture.php | 87 ++++++----- .../Test/BalloonCorrectSubmissionFixture.php | 2 +- .../Test/EnableJavaEntrypointFixture.php | 2 +- .../DataFixtures/Test/EnableKotlinFixture.php | 2 +- .../Test/RejudgingFirstToSolveFixture.php | 2 +- .../Test/RejudgingStatesFixture.php | 2 +- .../Test/SampleSubmissionsFixture.php | 2 +- .../SampleSubmissionsInBucketsFixture.php | 2 +- .../SampleSubmissionsMultipleTriesFixture.php | 10 +- ...pleSubmissionsThreeTriesCorrectFixture.php | 2 +- ...nsThreeTriesCorrectSameLanguageFixture.php | 2 +- .../SubmissionRestriction.php | 6 +- webapp/src/Entity/Language.php | 27 ++-- webapp/src/Form/Type/LanguageType.php | 3 - webapp/src/Repository/LanguageRepository.php | 17 +++ webapp/src/Service/CheckConfigService.php | 2 +- webapp/src/Service/DOMJudgeService.php | 13 +- webapp/src/Service/ImportProblemService.php | 2 +- webapp/src/Service/SubmissionService.php | 2 +- .../team/partials/submission_list.html.twig | 2 +- .../API/SubmissionControllerTest.php | 4 +- .../Jury/JuryMiscControllerTest.php | 18 +-- .../Jury/LanguagesControllerTest.php | 67 +++------ .../Controller/Jury/PrintControllerTest.php | 2 +- .../Controller/Team/MiscControllerTest.php | 4 +- .../Integration/ScoreboardIntegrationTest.php | 8 +- .../Unit/Service/ImportExportServiceTest.php | 2 +- 31 files changed, 288 insertions(+), 161 deletions(-) create mode 100644 webapp/migrations/Version20251024122021.php create mode 100644 webapp/src/Repository/LanguageRepository.php diff --git a/webapp/migrations/Version20251024122021.php b/webapp/migrations/Version20251024122021.php new file mode 100644 index 0000000000..368a129ba0 --- /dev/null +++ b/webapp/migrations/Version20251024122021.php @@ -0,0 +1,141 @@ +addSql('ALTER TABLE submission DROP FOREIGN KEY submission_ibfk_4'); + $this->addSql('ALTER TABLE problemlanguage DROP FOREIGN KEY FK_46B150BB2271845'); + $this->addSql('ALTER TABLE version DROP FOREIGN KEY FK_BF1CD3C32271845'); + $this->addSql('ALTER TABLE contestlanguage DROP FOREIGN KEY FK_ADCB43232271845'); + + $this->addSql('ALTER TABLE language ADD langid_int INT UNSIGNED AUTO_INCREMENT UNIQUE AFTER langid'); + $this->addSql('ALTER TABLE contestlanguage ADD langid_int INT UNSIGNED NOT NULL AFTER langid'); + $this->addSql('ALTER TABLE problemlanguage ADD langid_int INT UNSIGNED NOT NULL AFTER langid'); + $this->addSql('ALTER TABLE submission ADD langid_int INT UNSIGNED DEFAULT NULL AFTER langid'); + $this->addSql('ALTER TABLE version ADD langid_int INT UNSIGNED DEFAULT NULL AFTER langid'); + + $this->addSql('UPDATE contestlanguage c JOIN language l ON c.langid = l.langid SET c.langid_int = l.langid_int'); + $this->addSql('UPDATE problemlanguage p JOIN language l ON p.langid = l.langid SET p.langid_int = l.langid_int'); + $this->addSql('UPDATE submission s JOIN language l ON s.langid = l.langid SET s.langid_int = l.langid_int WHERE s.langid IS NOT NULL'); + $this->addSql('UPDATE version v JOIN language l ON v.langid = l.langid SET v.langid_int = l.langid_int WHERE v.langid IS NOT NULL'); + $this->addSql('UPDATE auditlog a JOIN language l ON a.dataid = l.langid AND a.datatype = "language" SET a.dataid = l.langid_int WHERE a.dataid IS NOT NULL'); + + $this->addSql('ALTER TABLE language DROP PRIMARY KEY'); + $this->addSql('ALTER TABLE language DROP COLUMN langid'); + $this->addSql('ALTER TABLE contestlanguage DROP PRIMARY KEY'); + $this->addSql('ALTER TABLE contestlanguage DROP COLUMN langid'); + $this->addSql('ALTER TABLE problemlanguage DROP PRIMARY KEY'); + $this->addSql('ALTER TABLE problemlanguage DROP COLUMN langid'); + $this->addSql('ALTER TABLE submission DROP COLUMN langid'); + $this->addSql('ALTER TABLE version DROP COLUMN langid'); + + $this->addSql('ALTER TABLE language CHANGE langid_int langid INT UNSIGNED AUTO_INCREMENT NOT NULL COMMENT \'Language ID\''); + $this->addSql('ALTER TABLE contestlanguage CHANGE langid_int langid INT UNSIGNED NOT NULL COMMENT \'Language ID\''); + $this->addSql('ALTER TABLE problemlanguage CHANGE langid_int langid INT UNSIGNED NOT NULL COMMENT \'Language ID\''); + $this->addSql('ALTER TABLE submission CHANGE langid_int langid INT UNSIGNED DEFAULT NULL COMMENT \'Language ID\''); + $this->addSql('ALTER TABLE version CHANGE langid_int langid INT UNSIGNED DEFAULT NULL COMMENT \'Language ID\''); + + $this->addSql('ALTER TABLE language ADD PRIMARY KEY (langid), DROP INDEX langid_int'); + $this->addSql('ALTER TABLE contestlanguage ADD PRIMARY KEY (cid, langid)'); + $this->addSql('ALTER TABLE problemlanguage ADD PRIMARY KEY (probid, langid)'); + + $this->addSql('ALTER TABLE submission ADD INDEX langid (langid), ADD CONSTRAINT submission_ibfk_4 FOREIGN KEY (langid) REFERENCES language (langid) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE problemlanguage ADD INDEX IDX_46B150BB2271845 (langid), ADD CONSTRAINT FK_46B150BB2271845 FOREIGN KEY (langid) REFERENCES language (langid) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE version ADD INDEX IDX_BF1CD3C32271845 (langid), ADD CONSTRAINT FK_BF1CD3C32271845 FOREIGN KEY (langid) REFERENCES language (langid) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE contestlanguage ADD INDEX IDX_ADCB43232271845 (langid), ADD CONSTRAINT FK_ADCB43232271845 FOREIGN KEY (langid) REFERENCES language (langid) ON DELETE CASCADE'); + } + + public function down(Schema $schema): void + { + // When migrating back, we do the logic in reverse but we have custom logic for setting + // the string langid. We set it to the external ID except for a set of 10 known languages + // where the external ID differs from our previously used langid. + + $this->addSql('ALTER TABLE submission DROP FOREIGN KEY submission_ibfk_4'); + $this->addSql('ALTER TABLE problemlanguage DROP FOREIGN KEY FK_46B150BB2271845'); + $this->addSql('ALTER TABLE version DROP FOREIGN KEY FK_BF1CD3C32271845'); + $this->addSql('ALTER TABLE contestlanguage DROP FOREIGN KEY FK_ADCB43232271845'); + + $this->addSql('ALTER TABLE language ADD langid_str VARCHAR(32) NOT NULL AFTER langid'); + $this->addSql('ALTER TABLE contestlanguage ADD langid_str VARCHAR(32) NOT NULL AFTER langid'); + $this->addSql('ALTER TABLE problemlanguage ADD langid_str VARCHAR(32) NOT NULL AFTER langid'); + $this->addSql('ALTER TABLE submission ADD langid_str VARCHAR(32) DEFAULT NULL AFTER langid'); + $this->addSql('ALTER TABLE version ADD langid_str VARCHAR(32) DEFAULT NULL AFTER langid'); + + $this->addSql("UPDATE language SET langid_str = CASE externalid + WHEN 'ada' THEN 'adb' + WHEN 'haskell' THEN 'hs' + WHEN 'javascript' THEN 'js' + WHEN 'kotlin' THEN 'kt' + WHEN 'pascal' THEN 'pas' + WHEN 'prolog' THEN 'plg' + WHEN 'python3' THEN 'py3' + WHEN 'python2' THEN 'py2' + WHEN 'ruby' THEN 'rb' + WHEN 'rust' THEN 'rs' + ELSE externalid + END"); + + $this->addSql('UPDATE contestlanguage c JOIN language l ON c.langid = l.langid SET c.langid_str = l.langid_str'); + $this->addSql('UPDATE problemlanguage p JOIN language l ON p.langid = l.langid SET p.langid_str = l.langid_str'); + $this->addSql('UPDATE submission s JOIN language l ON s.langid = l.langid SET s.langid_str = l.langid_str WHERE s.langid IS NOT NULL'); + $this->addSql('UPDATE version v JOIN language l ON v.langid = l.langid SET v.langid_str = l.langid_str WHERE v.langid IS NOT NULL'); + $this->addSql('UPDATE auditlog a JOIN language l ON a.dataid = l.langid AND a.datatype = "language" SET a.dataid = l.langid_str WHERE a.dataid IS NOT NULL'); + + $this->addSql('ALTER TABLE language DROP PRIMARY KEY'); + $this->addSql('ALTER TABLE language DROP COLUMN langid'); + $this->addSql('ALTER TABLE contestlanguage DROP PRIMARY KEY'); + $this->addSql('ALTER TABLE contestlanguage DROP COLUMN langid'); + $this->addSql('ALTER TABLE problemlanguage DROP PRIMARY KEY'); + $this->addSql('ALTER TABLE problemlanguage DROP COLUMN langid'); + $this->addSql('ALTER TABLE submission DROP COLUMN langid'); + $this->addSql('ALTER TABLE version DROP COLUMN langid'); + + $this->addSql('ALTER TABLE language CHANGE langid_str langid VARCHAR(32) NOT NULL COMMENT \'Language ID\''); + $this->addSql('ALTER TABLE contestlanguage CHANGE langid_str langid VARCHAR(32) NOT NULL COMMENT \'Language ID\''); + $this->addSql('ALTER TABLE problemlanguage CHANGE langid_str langid VARCHAR(32) NOT NULL COMMENT \'Language ID\''); + $this->addSql('ALTER TABLE submission CHANGE langid_str langid VARCHAR(32) DEFAULT NULL COMMENT \'Language ID\''); + $this->addSql('ALTER TABLE version CHANGE langid_str langid VARCHAR(32) DEFAULT NULL COMMENT \'Language ID\''); + + $this->addSql('ALTER TABLE language ADD PRIMARY KEY (langid)'); + $this->addSql('ALTER TABLE contestlanguage ADD PRIMARY KEY (cid, langid)'); + $this->addSql('ALTER TABLE problemlanguage ADD PRIMARY KEY (probid, langid)'); + + $this->addSql('ALTER TABLE submission ADD INDEX langid (langid), ADD CONSTRAINT submission_ibfk_4 FOREIGN KEY (langid) REFERENCES language (langid) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE problemlanguage ADD INDEX IDX_46B150BB2271845 (langid), ADD CONSTRAINT FK_46B150BB2271845 FOREIGN KEY (langid) REFERENCES language (langid) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE version ADD INDEX IDX_BF1CD3C32271845 (langid), ADD CONSTRAINT FK_BF1CD3C32271845 FOREIGN KEY (langid) REFERENCES language (langid) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE contestlanguage ADD INDEX IDX_ADCB43232271845 (langid), ADD CONSTRAINT FK_ADCB43232271845 FOREIGN KEY (langid) REFERENCES language (langid) ON DELETE CASCADE'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/src/Controller/API/LanguageController.php b/webapp/src/Controller/API/LanguageController.php index 7073e7bace..5a861e2eac 100644 --- a/webapp/src/Controller/API/LanguageController.php +++ b/webapp/src/Controller/API/LanguageController.php @@ -82,7 +82,7 @@ public function singleAction(Request $request, string $id): Response #[OA\Parameter(ref: '#/components/parameters/id')] public function updateExecutableActions(Request $request, string $id): void { - $language = $this->em->getRepository(Language::class)->find($id); + $language = $this->em->getRepository(Language::class)->findByExternalId($id); if (!$language) { throw new BadRequestHttpException("Unknown language '$id'."); } diff --git a/webapp/src/Controller/Jury/JuryMiscController.php b/webapp/src/Controller/Jury/JuryMiscController.php index 9f64a3994f..c762379206 100644 --- a/webapp/src/Controller/Jury/JuryMiscController.php +++ b/webapp/src/Controller/Jury/JuryMiscController.php @@ -152,18 +152,18 @@ public function ajaxDataAction(Request $request, string $datatype): JsonResponse }, $teams); } elseif ($datatype === 'languages') { $languages = $qb->from(Language::class, 'l') - ->select('l.langid', 'l.name') + ->select('l.externalid', 'l.name') ->where($qb->expr()->like('l.name', '?1')) - ->orWhere($qb->expr()->eq('l.langid', '?2')) + ->orWhere($qb->expr()->eq('l.externalid', '?2')) ->orderBy('l.name', 'ASC') ->getQuery()->setParameter(1, '%' . $q . '%') ->setParameter(2, $q) ->getResult(); $results = array_map(function (array $language) { - $displayname = $language['name'] . " (" . $language['langid'] . ")"; + $displayname = $language['name'] . " (" . $language['externalid'] . ")"; return [ - 'id' => $language['langid'], + 'id' => $language['externalid'], 'text' => $displayname, ]; }, $languages); diff --git a/webapp/src/Controller/Jury/LanguageController.php b/webapp/src/Controller/Jury/LanguageController.php index f9c3981834..a4e577d133 100644 --- a/webapp/src/Controller/Jury/LanguageController.php +++ b/webapp/src/Controller/Jury/LanguageController.php @@ -253,7 +253,7 @@ public function toggleJudgeAction( $this->em->flush(); if ($enabled) { - $this->dj->unblockJudgeTasksForLanguage($langId); + $this->dj->unblockJudgeTasksForLanguage($language->getLangid()); } $this->dj->auditlog('language', $langId, 'set allow judge', @@ -302,7 +302,7 @@ public function editAction(Request $request, string $langId): Response } $this->saveEntity($language, $language->getLangid(), false); if ($language->getAllowJudge()) { - $this->dj->unblockJudgeTasksForLanguage($langId); + $this->dj->unblockJudgeTasksForLanguage($language->getLangid()); } return $this->redirectToRoute('jury_language', ['langId' => $language->getLangid()]); } diff --git a/webapp/src/DataFixtures/DefaultData/LanguageFixture.php b/webapp/src/DataFixtures/DefaultData/LanguageFixture.php index 6c82704e40..302d9153a4 100644 --- a/webapp/src/DataFixtures/DefaultData/LanguageFixture.php +++ b/webapp/src/DataFixtures/DefaultData/LanguageFixture.php @@ -23,66 +23,65 @@ public function __construct( public function load(ObjectManager $manager): void { $data = [ - // ID external ID name extensions require entry point allow allow time compile compiler version runner version - // entry point description submit judge factor script command command - ['adb', 'ada', 'Ada', ['adb', 'ads'], false, null, false, true, 1, 'adb', 'gnatmake --version', ''], - ['awk', 'awk', 'AWK', ['awk'], false, null, false, true, 1, 'awk', 'awk --version', 'awk --version'], - ['bash', 'bash', 'Bash shell', ['bash'], false, 'Main file', false, true, 1, 'bash', 'bash --version', 'bash --version'], - ['c', 'c', 'C', ['c'], false, null, true, true, 1, 'c', 'gcc --version', ''], - ['cpp', 'cpp', 'C++', ['cpp', 'cc', 'cxx', 'c++'], false, null, true, true, 1, 'cpp', 'g++ --version', ''], - ['csharp', 'csharp', 'C#', ['csharp', 'cs'], false, null, false, true, 1, 'csharp', 'mcs --version', 'mono --version'], - ['f95', 'f95', 'Fortran', ['f95', 'f90'], false, null, false, true, 1, 'f95', 'gfortran --version', ''], - ['hs', 'haskell', 'Haskell', ['hs', 'lhs'], false, null, false, true, 1, 'hs', 'ghc --version', ''], - ['java', 'java', 'Java', ['java'], false, 'Main class', true, true, 1, 'java_javac_detect', 'javac -version', 'java -version'], - ['js', 'javascript', 'JavaScript', ['js', 'mjs'], false, 'Main file', false, true, 1, 'js', 'nodejs --version', 'nodejs --version'], - ['lua', 'lua', 'Lua', ['lua'], false, null, false, true, 1, 'lua', 'luac -v', 'lua -v'], - ['kt', 'kotlin', 'Kotlin', ['kt'], true, 'Main class', false, true, 1, 'kt', 'kotlinc -version', 'kotlin -version'], - ['pas', 'pascal', 'Pascal', ['pas', 'p'], false, 'Main file', false, true, 1, 'pas', 'fpc -iW', ''], - ['pl', 'pl', 'Perl', ['pl'], false, 'Main file', false, true, 1, 'pl', 'perl -v', 'perl -v'], - ['plg', 'prolog', 'Prolog', ['plg'], false, 'Main file', false, true, 1, 'plg', 'swipl --version', ''], - ['py3', 'python3', 'Python 3', ['py'], false, 'Main file', true, true, 1, 'py3', 'pypy3 --version', 'pypy3 --version'], - ['ocaml', 'ocaml', 'OCaml', ['ml'], false, null, false, true, 1, 'ocaml', 'ocamlopt --version', ''], - ['r', 'r', 'R', ['R'], false, 'Main file', false, true, 1, 'r', 'Rscript --version', 'Rscript --version'], - ['rb', 'ruby', 'Ruby', ['rb'], false, 'Main file', false, true, 1, 'rb', 'ruby --version', 'ruby --version'], - ['rs', 'rust', 'Rust', ['rs'], false, null, false, true, 1, 'rs', 'rustc --version', ''], - ['scala', 'scala', 'Scala', ['scala'], false, null, false, true, 1, 'scala', 'scalac -version', 'scala -version'], - ['sh', 'sh', 'POSIX shell', ['sh'], false, 'Main file', false, true, 1, 'sh', 'md5sum /bin/sh', 'md5sum /bin/sh'], - ['swift', 'swift', 'Swift', ['swift'], false, 'Main file', false, true, 1, 'swift', 'swiftc --version', ''], + // external ID name extensions require entry point allow allow time compile compiler version runner version + // entry point description submit judge factor script command command + ['ada', 'Ada', ['adb', 'ads'], false, null, false, true, 1, 'adb', 'gnatmake --version', ''], + ['awk', 'AWK', ['awk'], false, null, false, true, 1, 'awk', 'awk --version', 'awk --version'], + ['bash', 'Bash shell', ['bash'], false, 'Main file', false, true, 1, 'bash', 'bash --version', 'bash --version'], + ['c', 'C', ['c'], false, null, true, true, 1, 'c', 'gcc --version', ''], + ['cpp', 'C++', ['cpp', 'cc', 'cxx', 'c++'], false, null, true, true, 1, 'cpp', 'g++ --version', ''], + ['csharp', 'C#', ['csharp', 'cs'], false, null, false, true, 1, 'csharp', 'mcs --version', 'mono --version'], + ['f95', 'Fortran', ['f95', 'f90'], false, null, false, true, 1, 'f95', 'gfortran --version', ''], + ['haskell', 'Haskell', ['hs', 'lhs'], false, null, false, true, 1, 'hs', 'ghc --version', ''], + ['java', 'Java', ['java'], false, 'Main class', true, true, 1, 'java_javac_detect', 'javac -version', 'java -version'], + ['javascript', 'JavaScript', ['js', 'mjs'], false, 'Main file', false, true, 1, 'js', 'nodejs --version', 'nodejs --version'], + ['lua', 'Lua', ['lua'], false, null, false, true, 1, 'lua', 'luac -v', 'lua -v'], + ['kotlin', 'Kotlin', ['kt'], true, 'Main class', false, true, 1, 'kt', 'kotlinc -version', 'kotlin -version'], + ['pascal', 'Pascal', ['pas', 'p'], false, 'Main file', false, true, 1, 'pas', 'fpc -iW', ''], + ['pl', 'Perl', ['pl'], false, 'Main file', false, true, 1, 'pl', 'perl -v', 'perl -v'], + ['prolog', 'Prolog', ['plg'], false, 'Main file', false, true, 1, 'plg', 'swipl --version', ''], + ['python3', 'Python 3', ['py'], false, 'Main file', true, true, 1, 'py3', 'pypy3 --version', 'pypy3 --version'], + ['ocaml', 'OCaml', ['ml'], false, null, false, true, 1, 'ocaml', 'ocamlopt --version', ''], + ['r', 'R', ['R'], false, 'Main file', false, true, 1, 'r', 'Rscript --version', 'Rscript --version'], + ['ruby', 'Ruby', ['rb'], false, 'Main file', false, true, 1, 'rb', 'ruby --version', 'ruby --version'], + ['rust', 'Rust', ['rs'], false, null, false, true, 1, 'rs', 'rustc --version', ''], + ['scala', 'Scala', ['scala'], false, null, false, true, 1, 'scala', 'scalac -version', 'scala -version'], + ['sh', 'POSIX shell', ['sh'], false, 'Main file', false, true, 1, 'sh', 'md5sum /bin/sh', 'md5sum /bin/sh'], + ['swift', 'Swift', ['swift'], false, 'Main file', false, true, 1, 'swift', 'swiftc --version', ''], ]; foreach ($data as $item) { // Note: we only create the language if it doesn't exist yet. // If it does, we will not update the data - if (!$manager->getRepository(Language::class)->find($item[0])) { + if (!$manager->getRepository(Language::class)->findOneBy(['externalid' => $item[0]])) { $file = sprintf('%s/files/defaultdata/%s.zip', - $this->sqlDir, $item[9] + $this->sqlDir, $item[8] ); - if (!($executable = $manager->getRepository(Executable::class)->find($item[9]))) { + if (!($executable = $manager->getRepository(Executable::class)->find($item[8]))) { $executable = (new Executable()) - ->setExecid($item[9]) - ->setDescription($item[9]) + ->setExecid($item[8]) + ->setDescription($item[8]) ->setType('compile') ->setImmutableExecutable($this->createImmutableExecutable($file)); $manager->persist($executable); } else { - $this->logger->info('Executable %s already exists, not created', [ $item[9] ]); + $this->logger->info('Executable %s already exists, not created', [ $item[8] ]); } $language = (new Language()) - ->setLangid($item[0]) - ->setExternalid($item[1]) - ->setName($item[2]) - ->setExtensions($item[3]) - ->setRequireEntryPoint($item[4]) - ->setEntryPointDescription($item[5]) - ->setAllowSubmit($item[6]) - ->setAllowJudge($item[7]) - ->setTimeFactor($item[8]) + ->setExternalid($item[0]) + ->setName($item[1]) + ->setExtensions($item[2]) + ->setRequireEntryPoint($item[3]) + ->setEntryPointDescription($item[4]) + ->setAllowSubmit($item[5]) + ->setAllowJudge($item[6]) + ->setTimeFactor($item[7]) ->setCompileExecutable($executable); - if (!empty($item[10])) { - $language->setCompilerVersionCommand($item[10]); + if (!empty($item[9])) { + $language->setCompilerVersionCommand($item[9]); } - if (!empty($item[11])) { - $language->setRunnerVersionCommand($item[11]); + if (!empty($item[10])) { + $language->setRunnerVersionCommand($item[10]); } $manager->persist($language); } else { diff --git a/webapp/src/DataFixtures/Test/BalloonCorrectSubmissionFixture.php b/webapp/src/DataFixtures/Test/BalloonCorrectSubmissionFixture.php index beadaa8f99..29ab0b98da 100644 --- a/webapp/src/DataFixtures/Test/BalloonCorrectSubmissionFixture.php +++ b/webapp/src/DataFixtures/Test/BalloonCorrectSubmissionFixture.php @@ -45,7 +45,7 @@ public function load(ObjectManager $manager): void ->setContest($contest) ->setTeam($team) ->setContestProblem($cp) - ->setLanguage($manager->getRepository(Language::class)->find($submissionItem[1])) + ->setLanguage($manager->getRepository(Language::class)->findByExternalId($submissionItem[1])) ->setSubmittime(Utils::now()-2) ->setEntryPoint($submissionItem[3]); $judging = (new Judging()) diff --git a/webapp/src/DataFixtures/Test/EnableJavaEntrypointFixture.php b/webapp/src/DataFixtures/Test/EnableJavaEntrypointFixture.php index 43875e1a1e..285390ad3f 100644 --- a/webapp/src/DataFixtures/Test/EnableJavaEntrypointFixture.php +++ b/webapp/src/DataFixtures/Test/EnableJavaEntrypointFixture.php @@ -9,7 +9,7 @@ class EnableJavaEntrypointFixture extends AbstractTestDataFixture { public function load(ObjectManager $manager): void { - $java = $manager->getRepository(Language::class)->find('java'); + $java = $manager->getRepository(Language::class)->findByExternalId('java'); $java->setRequireEntryPoint(true); $manager->flush(); } diff --git a/webapp/src/DataFixtures/Test/EnableKotlinFixture.php b/webapp/src/DataFixtures/Test/EnableKotlinFixture.php index 02b0b4dcfd..6d05423696 100644 --- a/webapp/src/DataFixtures/Test/EnableKotlinFixture.php +++ b/webapp/src/DataFixtures/Test/EnableKotlinFixture.php @@ -9,7 +9,7 @@ class EnableKotlinFixture extends AbstractTestDataFixture { public function load(ObjectManager $manager): void { - $kotlin = $manager->getRepository(Language::class)->find('kt'); + $kotlin = $manager->getRepository(Language::class)->findByExternalId('kotlin'); $kotlin->setAllowSubmit(true); $manager->flush(); } diff --git a/webapp/src/DataFixtures/Test/RejudgingFirstToSolveFixture.php b/webapp/src/DataFixtures/Test/RejudgingFirstToSolveFixture.php index a5fb6c9f06..9d6a276891 100644 --- a/webapp/src/DataFixtures/Test/RejudgingFirstToSolveFixture.php +++ b/webapp/src/DataFixtures/Test/RejudgingFirstToSolveFixture.php @@ -32,7 +32,7 @@ public function load(ObjectManager $manager): void [$team1, $contest->getStarttime() + 400, 'correct'], ]; - $language = $manager->getRepository(Language::class)->find('cpp'); + $language = $manager->getRepository(Language::class)->findByExternalId('cpp'); $problem = $contest->getProblems()->filter(fn(ContestProblem $problem) => $problem->getShortname() === 'A')->first(); foreach ($submissionData as $submissionItem) { diff --git a/webapp/src/DataFixtures/Test/RejudgingStatesFixture.php b/webapp/src/DataFixtures/Test/RejudgingStatesFixture.php index fe7b893955..941653a555 100644 --- a/webapp/src/DataFixtures/Test/RejudgingStatesFixture.php +++ b/webapp/src/DataFixtures/Test/RejudgingStatesFixture.php @@ -50,7 +50,7 @@ public function load(ObjectManager $manager): void /** @var Team $team */ $team = $manager->getRepository(Team::class)->findOneBy(['name' => 'Example teamname']); /** @var Language $language */ - $language = $manager->getRepository(Language::class)->find('java'); + $language = $manager->getRepository(Language::class)->findByExternalId('java'); // A rejudging has both judgings todo and finished for ($b = 0; $b<=1; $b++) { for ($x = 0; $x < $rejudgingStage[2+$b]; $x++) { diff --git a/webapp/src/DataFixtures/Test/SampleSubmissionsFixture.php b/webapp/src/DataFixtures/Test/SampleSubmissionsFixture.php index 82b53c23b3..0cf902632e 100644 --- a/webapp/src/DataFixtures/Test/SampleSubmissionsFixture.php +++ b/webapp/src/DataFixtures/Test/SampleSubmissionsFixture.php @@ -31,7 +31,7 @@ public function load(ObjectManager $manager): void ->setContest($contest) ->setTeam($manager->getRepository(Team::class)->findOneBy(['name' => $submissionItem[0]])) ->setContestProblem($problem) - ->setLanguage($manager->getRepository(Language::class)->find($submissionItem[2])) + ->setLanguage($manager->getRepository(Language::class)->findByExternalId($submissionItem[2])) ->setSubmittime(Utils::toEpochFloat($submissionItem[3])) ->setEntryPoint($submissionItem[4]); $judging = (new Judging()) diff --git a/webapp/src/DataFixtures/Test/SampleSubmissionsInBucketsFixture.php b/webapp/src/DataFixtures/Test/SampleSubmissionsInBucketsFixture.php index 54314f8dad..0c7a354f5f 100644 --- a/webapp/src/DataFixtures/Test/SampleSubmissionsInBucketsFixture.php +++ b/webapp/src/DataFixtures/Test/SampleSubmissionsInBucketsFixture.php @@ -43,7 +43,7 @@ public function load(ObjectManager $manager): void ->setContest($demoContest) ->setTeam($manager->getRepository(Team::class)->findOneBy(['name' => 'Example teamname'])) ->setContestProblem($problem) - ->setLanguage($manager->getRepository(Language::class)->find('cpp')) + ->setLanguage($manager->getRepository(Language::class)->findByExternalId('cpp')) ->setSubmittime(Utils::toEpochFloat($time)); $judging = (new Judging()) ->setContest($demoContest) diff --git a/webapp/src/DataFixtures/Test/SampleSubmissionsMultipleTriesFixture.php b/webapp/src/DataFixtures/Test/SampleSubmissionsMultipleTriesFixture.php index 987cbe70e5..162aec616d 100644 --- a/webapp/src/DataFixtures/Test/SampleSubmissionsMultipleTriesFixture.php +++ b/webapp/src/DataFixtures/Test/SampleSubmissionsMultipleTriesFixture.php @@ -17,10 +17,10 @@ public function load(ObjectManager $manager): void { $submissionData = [ // team name, problem shortname, language, submittime, entry point, result - ['Example teamname', 'C', 'cpp', '2021-02-01 01:00:56', null, 'timelimit'], - ['Example teamname', 'C', 'c', '2021-02-01 03:15:56', null, 'run-error'], - ['Example teamname', 'C', 'java', '2021-02-01 18:00:00', 'Main', 'wrong-answer'], - ['Example teamname', 'C', 'py3', '2021-02-01 18:00:34', 'main', 'compiler-error'], + ['Example teamname', 'C', 'cpp', '2021-02-01 01:00:56', null, 'timelimit'], + ['Example teamname', 'C', 'c', '2021-02-01 03:15:56', null, 'run-error'], + ['Example teamname', 'C', 'java', '2021-02-01 18:00:00', 'Main', 'wrong-answer'], + ['Example teamname', 'C', 'python3', '2021-02-01 18:00:34', 'main', 'compiler-error'], ]; /** @var Contest $contest */ @@ -33,7 +33,7 @@ public function load(ObjectManager $manager): void ->setContest($contest) ->setTeam($manager->getRepository(Team::class)->findOneBy(['name' => $submissionItem[0]])) ->setContestProblem($problem) - ->setLanguage($manager->getRepository(Language::class)->find($submissionItem[2])) + ->setLanguage($manager->getRepository(Language::class)->findByExternalId($submissionItem[2])) ->setSubmittime(Utils::toEpochFloat($submissionItem[3])) ->setEntryPoint($submissionItem[4]); $judging = (new Judging()) diff --git a/webapp/src/DataFixtures/Test/SampleSubmissionsThreeTriesCorrectFixture.php b/webapp/src/DataFixtures/Test/SampleSubmissionsThreeTriesCorrectFixture.php index 326006a327..26046c489e 100644 --- a/webapp/src/DataFixtures/Test/SampleSubmissionsThreeTriesCorrectFixture.php +++ b/webapp/src/DataFixtures/Test/SampleSubmissionsThreeTriesCorrectFixture.php @@ -32,7 +32,7 @@ public function load(ObjectManager $manager): void ->setContest($contest) ->setTeam($manager->getRepository(Team::class)->findOneBy(['name' => $submissionItem[0]])) ->setContestProblem($problem) - ->setLanguage($manager->getRepository(Language::class)->find($submissionItem[2])) + ->setLanguage($manager->getRepository(Language::class)->findByExternalId($submissionItem[2])) ->setSubmittime(Utils::toEpochFloat($submissionItem[3])) ->setEntryPoint($submissionItem[4]); $judging = (new Judging()) diff --git a/webapp/src/DataFixtures/Test/SampleSubmissionsThreeTriesCorrectSameLanguageFixture.php b/webapp/src/DataFixtures/Test/SampleSubmissionsThreeTriesCorrectSameLanguageFixture.php index 4615861c42..5bc7c0dc5e 100644 --- a/webapp/src/DataFixtures/Test/SampleSubmissionsThreeTriesCorrectSameLanguageFixture.php +++ b/webapp/src/DataFixtures/Test/SampleSubmissionsThreeTriesCorrectSameLanguageFixture.php @@ -32,7 +32,7 @@ public function load(ObjectManager $manager): void ->setContest($contest) ->setTeam($manager->getRepository(Team::class)->findOneBy(['name' => $submissionItem[0]])) ->setContestProblem($problem) - ->setLanguage($manager->getRepository(Language::class)->find($submissionItem[2])) + ->setLanguage($manager->getRepository(Language::class)->findByExternalId($submissionItem[2])) ->setSubmittime(Utils::toEpochFloat($submissionItem[3])) ->setEntryPoint($submissionItem[4]); $judging = (new Judging()) diff --git a/webapp/src/DataTransferObject/SubmissionRestriction.php b/webapp/src/DataTransferObject/SubmissionRestriction.php index c23f86bf4f..1d66065c2a 100644 --- a/webapp/src/DataTransferObject/SubmissionRestriction.php +++ b/webapp/src/DataTransferObject/SubmissionRestriction.php @@ -23,8 +23,8 @@ class SubmissionRestriction * @param list|null $affiliationIds ID's of affiliations to filter on * @param int|null $problemId ID of a problem to filter on * @param list|null $problemIds ID's of problems to filter on - * @param string|null $languageId ID of a language to filter on - * @param list|null $languageIds ID's of languages to filter on + * @param int|null $languageId ID of a language to filter on + * @param list|null $languageIds ID's of languages to filter on * @param string|null $judgehost Hostname of a judgehost to filter on * @param string|null $oldResult Result of old judging to filter on * @param string|null $result Result of current judging to filter on @@ -56,7 +56,7 @@ public function __construct( public ?array $affiliationIds = [], public ?int $problemId = null, public ?array $problemIds = [], - public ?string $languageId = null, + public ?int $languageId = null, public ?array $languageIds = [], public ?string $judgehost = null, public ?string $oldResult = null, diff --git a/webapp/src/Entity/Language.php b/webapp/src/Entity/Language.php index ecaed5e89a..38e0bf1d30 100644 --- a/webapp/src/Entity/Language.php +++ b/webapp/src/Entity/Language.php @@ -3,7 +3,7 @@ use App\Controller\API\AbstractRestController as ARC; use App\DataTransferObject\Command; -use App\Validator\Constraints\Identifier; +use App\Repository\LanguageRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -15,7 +15,7 @@ /** * Programming languages in which teams can submit solutions. */ -#[ORM\Entity] +#[ORM\Entity(repositoryClass: LanguageRepository::class)] #[ORM\Table(options: [ 'collation' => 'utf8mb4_unicode_ci', 'charset' => 'utf8mb4', @@ -30,12 +30,10 @@ class Language extends BaseApiEntity implements ExternalIdFromInternalIdInterface { #[ORM\Id] - #[ORM\Column(length: 32, options: ['comment' => 'Language ID (string)'])] - #[Assert\NotBlank] - #[Assert\NotEqualTo('add')] - #[Identifier] + #[ORM\GeneratedValue] + #[ORM\Column(options: ['comment' => 'Language ID', 'unsigned' => true])] #[Serializer\Exclude] - protected ?string $langid = null; + protected ?int $langid = null; #[ORM\Column(nullable: true, options: ['comment' => 'Language ID to expose in the REST API'])] #[Serializer\SerializedName('id')] @@ -264,13 +262,13 @@ public function getRunnerData(): Command return $ret; } - public function setLangid(string $langid): Language + public function setLangid(int $langid): Language { $this->langid = $langid; return $this; } - public function getLangid(): ?string + public function getLangid(): ?int { return $this->langid; } @@ -420,16 +418,11 @@ public function getSubmissions(): Collection public function getEditorLanguage(): string { - return match ($this->getLangid()) { + return match ($this->getExternalid()) { 'bash' => 'shell', - 'cxx' => 'cpp', - 'kt' => 'kotlin', - 'pas' => 'pascal', 'pl' => 'perl', - 'py2', 'py3' => 'python', - 'rb' => 'ruby', - 'rs' => 'rust', - default => $this->getLangid(), + 'python2', 'python3' => 'python', + default => $this->getExternalid(), }; } diff --git a/webapp/src/Form/Type/LanguageType.php b/webapp/src/Form/Type/LanguageType.php index 2f0c70864f..55b733dd65 100644 --- a/webapp/src/Form/Type/LanguageType.php +++ b/webapp/src/Form/Type/LanguageType.php @@ -22,9 +22,6 @@ class LanguageType extends AbstractExternalIdEntityType public function buildForm(FormBuilderInterface $builder, array $options): void { $this->addExternalIdField($builder, Language::class); - $builder->add('langid', TextType::class, [ - 'label' => 'Language ID', - ]); $builder->add('name', TextType::class, [ 'empty_data' => '' ]); diff --git a/webapp/src/Repository/LanguageRepository.php b/webapp/src/Repository/LanguageRepository.php new file mode 100644 index 0000000000..17b639d9a5 --- /dev/null +++ b/webapp/src/Repository/LanguageRepository.php @@ -0,0 +1,17 @@ + + */ +class LanguageRepository extends EntityRepository +{ + public function findByExternalId(string $externalId): ?Language + { + return $this->findOneBy(['externalid' => $externalId]); + } +} diff --git a/webapp/src/Service/CheckConfigService.php b/webapp/src/Service/CheckConfigService.php index 5f1c76f4b3..3c0dacf18d 100644 --- a/webapp/src/Service/CheckConfigService.php +++ b/webapp/src/Service/CheckConfigService.php @@ -698,7 +698,7 @@ public function checkLanguagesValidate(): ConfigCheckItem $languageerrors = $morelanguageerrors = []; $result = 'O'; foreach ($languages as $language) { - $langid = $language->getLangid(); + $langid = $language->getExternalid(); $errors = $this->validator->validate($language); if (count($errors)) { $result = 'E'; diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index ab5a9ce060..c761103869 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -732,23 +732,24 @@ public function openZipFile(string $filename): ZipArchive * * @param string $filename The on-disk file to be printed out * @param string $origname The original filename as submitted by the team - * @param string|null $language Langid of the programming language this file is in + * @param int|null $language Langid of the programming language this file is in * @param bool $asTeam Print the file as the team associated with the user * @return array{0: bool, 1: string} */ public function printUserFile( string $filename, string $origname, - ?string $language, + ?int $language, ?bool $asTeam = false, ): array { $user = $this->getUser(); $team = $user->getTeam(); + $language = $language ? $this->em->getRepository(Language::class)->find($language) : null; if ($asTeam && $team !== null) { $teamid = $team->getLabel() ?? $team->getExternalid(); - return $this->printFile($filename, $origname, $language, $user->getUserIdentifier(), $team->getEffectiveName(), $teamid, $team->getLocation()); + return $this->printFile($filename, $origname, $language?->getExternalid(), $user->getUserIdentifier(), $team->getEffectiveName(), $teamid, $team->getLocation()); } - return $this->printFile($filename, $origname, $language, $user->getUserIdentifier()); + return $this->printFile($filename, $origname, $language?->getExternalid(), $user->getUserIdentifier()); } /** @@ -759,7 +760,7 @@ public function printUserFile( * * @param string $filename The on-disk file to be printed out * @param string $origname The original filename as submitted by the team - * @param string|null $language Langid of the programming language this file is in + * @param string|null $language External ID of the programming language this file is in * @param string $username Username of the print job submitter * @param string|null $teamname Teamname of the team this user belongs to, if any * @param string|null $teamid Teamid of the team this user belongs to, if any @@ -1166,7 +1167,7 @@ public function helperUnblockJudgeTasks(): QueryBuilder ->where('jt.jobid IS NULL'); } - public function unblockJudgeTasksForLanguage(string $langId): void + public function unblockJudgeTasksForLanguage(int $langId): void { // These are all the judgings that don't have associated judgetasks yet. Check whether we unblocked them. $judgings = $this->helperUnblockJudgeTasks() diff --git a/webapp/src/Service/ImportProblemService.php b/webapp/src/Service/ImportProblemService.php index b69a087500..929285b210 100644 --- a/webapp/src/Service/ImportProblemService.php +++ b/webapp/src/Service/ImportProblemService.php @@ -684,7 +684,7 @@ public function importZippedProblem( $extension = end($parts); foreach ($allowedLanguages as $language) { if (in_array($extension, $language->getExtensions())) { - $languageToUse = $language->getLangid(); + $languageToUse = $language->getExternalid(); break 2; } } diff --git a/webapp/src/Service/SubmissionService.php b/webapp/src/Service/SubmissionService.php index 010fd8e59d..48e34cc0d5 100644 --- a/webapp/src/Service/SubmissionService.php +++ b/webapp/src/Service/SubmissionService.php @@ -470,7 +470,7 @@ public function submitSolution( ]); } if (!$language instanceof Language) { - $language = $this->em->getRepository(Language::class)->find($language); + $language = $this->em->getRepository(Language::class)->findByExternalId($language); } if ($originalSubmission !== null && !$originalSubmission instanceof Submission) { $originalSubmission = $this->em->getRepository(Submission::class)->find($originalSubmission); diff --git a/webapp/templates/team/partials/submission_list.html.twig b/webapp/templates/team/partials/submission_list.html.twig index 80c4bd2123..192413f3f4 100644 --- a/webapp/templates/team/partials/submission_list.html.twig +++ b/webapp/templates/team/partials/submission_list.html.twig @@ -43,7 +43,7 @@ - {{ submission.language.langid }} + {{ submission.language.name }} diff --git a/webapp/tests/Unit/Controller/API/SubmissionControllerTest.php b/webapp/tests/Unit/Controller/API/SubmissionControllerTest.php index 1bfe4fba36..b8bdeafaa1 100644 --- a/webapp/tests/Unit/Controller/API/SubmissionControllerTest.php +++ b/webapp/tests/Unit/Controller/API/SubmissionControllerTest.php @@ -305,7 +305,7 @@ public function testAddSuccess( static::assertEquals($expectedProblemId, $submission->getProblem()->getProbid(), 'Wrong problem ID'); static::assertEquals($expectedTeamId, $submission->getTeam()->getTeamid(), 'Wrong team ID'); static::assertEquals($expectedUsername, $submission->getUser()->getUserIdentifier(), 'Wrong user'); - static::assertEquals($expectedLanguageId, $submission->getLanguage()->getLangid(), 'Wrong language ID'); + static::assertEquals($expectedLanguageId, $submission->getLanguage()->getExternalid(), 'Wrong language ID'); if ($expectedSubmissionExternalId) { static::assertEquals($expectedSubmissionExternalId, $submission->getExternalid(), 'Wrong external submission ID'); } @@ -390,7 +390,7 @@ public function provideAddSuccess(): Generator 1, 2, 'demo', - 'kt', + 'kotlin', null, null, ['somefile.kt' => file_get_contents(__FILE__)], diff --git a/webapp/tests/Unit/Controller/Jury/JuryMiscControllerTest.php b/webapp/tests/Unit/Controller/Jury/JuryMiscControllerTest.php index bda8ae8583..bbcbdcd223 100644 --- a/webapp/tests/Unit/Controller/Jury/JuryMiscControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/JuryMiscControllerTest.php @@ -189,27 +189,27 @@ public function provideJuryAjax(): Generator 2 => ['id' => 1, 'text' => 'Hello World (p1)']]]]; yield ['teams', $status, [$role], ['results' => [0 => ['id' => 1, 'text' => 'DOMjudge (t1)'], 1 => ['id' => 2, 'text' => 'Example teamname (t2)']]]]; - yield ['languages', $status, [$role], ['results' => [0 => ['id' => 'adb', 'text' => 'Ada (adb)'], + yield ['languages', $status, [$role], ['results' => [0 => ['id' => 'ada', 'text' => 'Ada (ada)'], 1 => ['id' => 'awk', 'text' => 'AWK (awk)'], 2 => ['id' => 'bash', 'text' => 'Bash shell (bash)'], 3 => ['id' => 'c', 'text' => 'C (c)'], 4 => ['id' => 'csharp', 'text' => 'C# (csharp)'], 5 => ['id' => 'cpp', 'text' => 'C++ (cpp)'], 6 => ['id' => 'f95', 'text' => 'Fortran (f95)'], - 7 => ['id' => 'hs', 'text' => 'Haskell (hs)'], + 7 => ['id' => 'haskell', 'text' => 'Haskell (haskell)'], 8 => ['id' => 'java', 'text' => 'Java (java)'], - 9 => ['id' => 'js', 'text' => 'JavaScript (js)'], - 10 => ['id' => 'kt', 'text' => 'Kotlin (kt)'], + 9 => ['id' => 'javascript', 'text' => 'JavaScript (javascript)'], + 10 => ['id' => 'kotlin', 'text' => 'Kotlin (kotlin)'], 11 => ['id' => 'lua', 'text' => 'Lua (lua)'], 12 => ['id' => 'ocaml', 'text' => 'OCaml (ocaml)'], - 13 => ['id' => 'pas', 'text' => 'Pascal (pas)'], + 13 => ['id' => 'pascal', 'text' => 'Pascal (pascal)'], 14 => ['id' => 'pl', 'text' => 'Perl (pl)'], 15 => ['id' => 'sh', 'text' => 'POSIX shell (sh)'], - 16 => ['id' => 'plg', 'text' => 'Prolog (plg)'], - 17 => ['id' => 'py3', 'text' => 'Python 3 (py3)'], + 16 => ['id' => 'prolog', 'text' => 'Prolog (prolog)'], + 17 => ['id' => 'python3', 'text' => 'Python 3 (python3)'], 18 => ['id' => 'r', 'text' => 'R (r)'], - 19 => ['id' => 'rb', 'text' => 'Ruby (rb)'], - 20 => ['id' => 'rs', 'text' => 'Rust (rs)'], + 19 => ['id' => 'ruby', 'text' => 'Ruby (ruby)'], + 20 => ['id' => 'rust', 'text' => 'Rust (rust)'], 21 => ['id' => 'scala', 'text' => 'Scala (scala)'], 22 => ['id' => 'swift', 'text' => 'Swift (swift)']]]]; yield ['contests', $status, [$role], ['results' => [0 => ['id' => 1, 'text' => 'Demo contest (demo - c1)'] diff --git a/webapp/tests/Unit/Controller/Jury/LanguagesControllerTest.php b/webapp/tests/Unit/Controller/Jury/LanguagesControllerTest.php index 467a90f44f..2e9d40e7f6 100644 --- a/webapp/tests/Unit/Controller/Jury/LanguagesControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/LanguagesControllerTest.php @@ -22,9 +22,8 @@ class LanguagesControllerTest extends JuryControllerTestCase protected static array $DOM_elements = ['h1' => ['Enabled languages', 'Disabled languages']]; protected static ?string $addPlus = 'extensions'; protected static string $addForm = 'language['; - protected static array $addEntitiesShown = ['langid', 'externalid', 'name', 'timefactor']; - protected static array $addEntities = [['langid' => 'simple', - 'name' => 'Simple', + protected static array $addEntitiesShown = ['externalid', 'name', 'timefactor']; + protected static array $addEntities = [['name' => 'Simple', 'externalid' => 'extSimple', 'requireEntryPoint' => '0', 'entryPointDescription' => '', @@ -34,53 +33,33 @@ class LanguagesControllerTest extends JuryControllerTestCase 'compileExecutable' => 'java_javac', 'extensions' => ['1' => 'extension'], 'filterCompilerFiles' => '1'], - ['langid' => 'ext', - 'externalid' => 'diffext', + ['externalid' => 'diffext', 'name' => 'External'], - ['langid' => 'lang123_.-', - 'name' => 'langid_expected_chars'], - ['langid' => 'external_expected_chars', - 'externalid' => 'ext123_.-'], - ['langid' => 'name_special_chars', - 'name' => '🕑ড|{}()*'], - ['langid' => 'entry', - 'requireEntryPoint' => '1', + ['name' => 'langid_expected_chars'], + ['externalid' => 'ext123_.-'], + ['name' => '🕑ড|{}()*'], + ['requireEntryPoint' => '1', 'entryPointDescription' => 'shell'], - ['langid' => 'entry_nodesc', - 'requireEntryPoint' => '1', + ['requireEntryPoint' => '1', 'entryPointDescription' => ''], - ['langid' => 'nosub', - 'allowSubmit' => '0'], - ['langid' => 'nojud', - 'allowJudge' => '0'], - ['langid' => 'timef1', - 'timeFactor' => '3'], - ['langid' => 'timef2', - 'timeFactor' => '1.3'], - ['langid' => 'timef3', - 'timeFactor' => '0.5'], - ['langid' => 'comp', - 'compileExecutable' => 'swift'], - ['langid' => 'multex', - 'extensions' => ['0' => 'a', '1' => 'b', + ['allowSubmit' => '0'], + ['allowJudge' => '0'], + ['timeFactor' => '3'], + ['timeFactor' => '1.3'], + ['timeFactor' => '0.5'], + ['compileExecutable' => 'swift'], + ['extensions' => ['0' => 'a', '1' => 'b', '2' => '1', '3' => ',']], - ['langid' => 'extunicode', - 'extensions' => ['0' => '🕑']], - ['langid' => 'nofilt', - 'filterCompilerFiles' => '0'], - ['langid' => 'compVers', - 'compilerVersionCommand' => 'unit -V'], - ['langid' => 'runVers', - 'runnerVersionCommand' => 'run -x |yes|tr "\n" "\`true\`"']]; - protected static array $addEntitiesFailure = ['Only alphanumeric characters and ._- are allowed' => [['langid' => '§$#`"'], - ['langid' => '()*&']], - 'Only letters, numbers, dashes, underscores and dots are allowed.' => [['externalid' => '§$#'], + ['extensions' => ['0' => '🕑']], + ['filterCompilerFiles' => '0'], + ['compilerVersionCommand' => 'unit -V'], + ['runnerVersionCommand' => 'run -x |yes|tr "\n" "\`true\`"']]; + protected static array $addEntitiesFailure = ['Only letters, numbers, dashes, underscores and dots are allowed.' => [['externalid' => '§$#'], ['externalid' => '@#()|']], 'This value should be positive.' => [['timeFactor' => '0'], ['timeFactor' => '-1'], ['timeFactor' => '-.1']], - 'This value should not be blank.' => [['langid' => ''], - ['name' => '']]]; + 'This value should not be blank.' => [['name' => '']]]; public function helperProvideTranslateAddEntity(array $entity, array $expected): array { @@ -115,7 +94,7 @@ public function testUnlockJudgeTasksFormEdit(): void self::assertEquals(4, $judgeTaskQuery->getSingleScalarResult()); // Now, disable the language. - $url = "/jury/languages/c/edit"; + $url = "/jury/languages/4/edit"; $this->verifyPageResponse('GET', $url, 200); $crawler = $this->getCurrentCrawler(); @@ -159,7 +138,7 @@ public function testUnlockJudgeTasksToggle(): void self::assertEquals(4, $judgeTaskQuery->getSingleScalarResult()); // Now, disable the language. - $url = "/jury/languages/c/toggle-judge"; + $url = "/jury/languages/4/toggle-judge"; $this->client->request(Request::METHOD_POST, $url, ['value' => false]); // Submit again. diff --git a/webapp/tests/Unit/Controller/Jury/PrintControllerTest.php b/webapp/tests/Unit/Controller/Jury/PrintControllerTest.php index 18778aa90c..aaccb0ec6d 100644 --- a/webapp/tests/Unit/Controller/Jury/PrintControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/PrintControllerTest.php @@ -55,7 +55,7 @@ function () { $crawler = $this->client->submitForm('Print code', [ 'print[code]' => $code, - 'print[langid]' => 'csharp', + 'print[langid]' => '6', ]); static::assertSelectorTextContains('div.alert.alert-success', diff --git a/webapp/tests/Unit/Controller/Team/MiscControllerTest.php b/webapp/tests/Unit/Controller/Team/MiscControllerTest.php index e76b3c6204..c55d7aa684 100644 --- a/webapp/tests/Unit/Controller/Team/MiscControllerTest.php +++ b/webapp/tests/Unit/Controller/Team/MiscControllerTest.php @@ -103,14 +103,14 @@ function () { $crawler = $this->client->submitForm('Print code', [ 'print[code]' => $code, - 'print[langid]' => 'kt', + 'print[langid]' => '12', ]); static::assertSelectorTextContains('div.alert.alert-success', 'File has been printed'); $text = trim($crawler->filter('pre')->text(null, false)); - static::assertStringStartsWith('kt', $text); + static::assertStringStartsWith('kotlin', $text); static::assertStringEndsWith( trim(file_get_contents($testFile)), $text); }); diff --git a/webapp/tests/Unit/Integration/ScoreboardIntegrationTest.php b/webapp/tests/Unit/Integration/ScoreboardIntegrationTest.php index 8ff63433d4..0f5478d3fc 100644 --- a/webapp/tests/Unit/Integration/ScoreboardIntegrationTest.php +++ b/webapp/tests/Unit/Integration/ScoreboardIntegrationTest.php @@ -296,7 +296,7 @@ public function testTeamScoreboardFreezeFTS(): void public function testOneSingleFTS(): void { - $lang = $this->em->getRepository(Language::class)->find('c'); + $lang = $this->em->getRepository(Language::class)->findByExternalId('c'); $team = $this->teams[0]; $this->createSubmission($lang, $this->problems[0], $team, 53*60+15.053, 'correct', true) @@ -329,7 +329,7 @@ public function testOneSingleFTS(): void public function testFTSwithVerificationRequired(): void { - $lang = $this->em->getRepository(Language::class)->find('c'); + $lang = $this->em->getRepository(Language::class)->findByExternalId('c'); $team = $this->teams[0]; $this->createSubmission($lang, $this->problems[0], $team, 53*60+15.053, 'correct', true) @@ -365,7 +365,7 @@ public function testFTSwithVerificationRequired(): void public function testFTSwithQueuedRejudging(): void { - $lang = $this->em->getRepository(Language::class)->find('c'); + $lang = $this->em->getRepository(Language::class)->findByExternalId('c'); $team = $this->teams[0]; $this->createSubmission($lang, $this->problems[0], $team, 53 * 60 + 15.053, 'wrong-answer') @@ -445,7 +445,7 @@ protected function recalcScoreCaches(): void protected function createDefaultSubmissions(): void { - $lang = $this->em->getRepository(Language::class)->find('cpp'); + $lang = $this->em->getRepository(Language::class)->findByExternalId('cpp'); $team = $this->teams[0]; $this->createSubmission($lang, $this->problems[0], $team, 53*60+15.053, 'no-output'); diff --git a/webapp/tests/Unit/Service/ImportExportServiceTest.php b/webapp/tests/Unit/Service/ImportExportServiceTest.php index 01453533af..dd10585158 100644 --- a/webapp/tests/Unit/Service/ImportExportServiceTest.php +++ b/webapp/tests/Unit/Service/ImportExportServiceTest.php @@ -1302,7 +1302,7 @@ public function testGetResultsData(bool $full, bool $honors, string $dataSet, st $contestProblemsById[$contestProblem->getExternalid()] = $contestProblem; } - $cpp = $em->getRepository(Language::class)->find('cpp'); + $cpp = $em->getRepository(Language::class)->findByExternalId('cpp'); // We use direct queries here to speed this up $submissionInsertQuery = $em->getConnection()->prepare('INSERT INTO submission (teamid, cid, probid, langid, submittime) VALUES (:teamid, :cid, :probid, :langid, :submittime)'); From 64de02553f0b0a9ddd9fb9a1e1122246c9c96de2 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Sat, 25 Oct 2025 17:29:49 +0200 Subject: [PATCH 02/32] Change public pages to use external IDs instead of internal IDs Part of #3024 --- .../Controller/API/ScoreboardController.php | 8 ++-- .../Jury/ImportExportController.php | 2 +- .../Controller/Jury/JuryMiscController.php | 6 +-- webapp/src/Controller/Jury/TeamController.php | 2 +- webapp/src/Controller/PublicController.php | 45 +++++++++---------- webapp/src/Controller/Team/MiscController.php | 8 ++-- webapp/src/Entity/ContestProblem.php | 3 +- webapp/src/Entity/Problem.php | 3 +- webapp/src/Entity/Team.php | 3 +- .../Repository/ContestProblemRepository.php | 33 ++++++++++++++ .../src/Repository/FindByExternalidTrait.php | 19 ++++++++ webapp/src/Repository/LanguageRepository.php | 8 ++-- webapp/src/Repository/ProblemRepository.php | 17 +++++++ webapp/src/Repository/TeamRepository.php | 18 ++++++++ webapp/src/Service/DOMJudgeService.php | 2 +- webapp/src/Service/ImportExportService.php | 2 +- webapp/src/Service/ScoreboardService.php | 14 +++--- webapp/src/Service/SubmissionService.php | 3 +- webapp/src/Utils/Scoreboard/Filter.php | 6 +-- .../partials/menu_change_contest.html.twig | 2 +- .../templates/partials/problem_list.html.twig | 6 +-- .../partials/scoreboard_table.html.twig | 4 +- .../Integration/ScoreboardIntegrationTest.php | 2 +- 23 files changed, 152 insertions(+), 64 deletions(-) create mode 100644 webapp/src/Repository/ContestProblemRepository.php create mode 100644 webapp/src/Repository/FindByExternalidTrait.php create mode 100644 webapp/src/Repository/ProblemRepository.php create mode 100644 webapp/src/Repository/TeamRepository.php diff --git a/webapp/src/Controller/API/ScoreboardController.php b/webapp/src/Controller/API/ScoreboardController.php index 51f0c23a3c..b0f1c6b672 100644 --- a/webapp/src/Controller/API/ScoreboardController.php +++ b/webapp/src/Controller/API/ScoreboardController.php @@ -64,7 +64,7 @@ public function __construct( name: 'category', description: 'Get the scoreboard for only this category', in: 'query', - schema: new OA\Schema(type: 'integer') + schema: new OA\Schema(type: 'string') )] #[OA\Parameter( name: 'country', @@ -76,7 +76,7 @@ public function __construct( name: 'affiliation', description: 'Get the scoreboard for only this affiliation', in: 'query', - schema: new OA\Schema(type: 'integer') + schema: new OA\Schema(type: 'string') )] #[OA\Parameter( name: 'public', @@ -93,11 +93,11 @@ public function __construct( public function getScoreboardAction( Request $request, #[MapQueryParameter] - ?int $category = null, + ?string $category = null, #[MapQueryParameter] ?string $country = null, #[MapQueryParameter] - ?int $affiliation = null, + ?string $affiliation = null, #[MapQueryParameter(name: 'allteams')] bool $allTeams = false, #[MapQueryParameter(name: 'public')] diff --git a/webapp/src/Controller/Jury/ImportExportController.php b/webapp/src/Controller/Jury/ImportExportController.php index 6f07dfd71a..a7c127c345 100644 --- a/webapp/src/Controller/Jury/ImportExportController.php +++ b/webapp/src/Controller/Jury/ImportExportController.php @@ -400,7 +400,7 @@ protected function getResultsHtml( ->getResult(); $categoryIds = []; foreach ($categories as $category) { - $categoryIds[] = $category->getCategoryid(); + $categoryIds[] = $category->getExternalid(); } $contest = $this->dj->getCurrentContest(); diff --git a/webapp/src/Controller/Jury/JuryMiscController.php b/webapp/src/Controller/Jury/JuryMiscController.php index c762379206..9f077427e6 100644 --- a/webapp/src/Controller/Jury/JuryMiscController.php +++ b/webapp/src/Controller/Jury/JuryMiscController.php @@ -345,15 +345,15 @@ public function judgingVerifierAction(Request $request): Response ]); } - #[Route(path: '/change-contest/{contestId<-?\d+>}', name: 'jury_change_contest')] - public function changeContestAction(Request $request, RouterInterface $router, int $contestId): Response + #[Route(path: '/change-contest/{contestId}', name: 'jury_change_contest')] + public function changeContestAction(Request $request, RouterInterface $router, string $contestId): Response { if ($this->isLocalReferer($router, $request)) { $response = new RedirectResponse($request->headers->get('referer')); } else { $response = $this->redirectToRoute('jury_index'); } - return $this->dj->setCookie('domjudge_cid', (string)$contestId, 0, null, '', false, false, + return $this->dj->setCookie('domjudge_cid', $contestId, 0, null, '', false, false, $response); } diff --git a/webapp/src/Controller/Jury/TeamController.php b/webapp/src/Controller/Jury/TeamController.php index 54522fcf26..9798bab14b 100644 --- a/webapp/src/Controller/Jury/TeamController.php +++ b/webapp/src/Controller/Jury/TeamController.php @@ -266,7 +266,7 @@ public function viewAction( if ($currentContest) { $scoreboard = $scoreboardService - ->getTeamScoreboard($currentContest, $teamId, true); + ->getTeamScoreboard($currentContest, $team->getExternalid(), true); $data = array_merge( $data, $scoreboardService->getScoreboardTwigData( diff --git a/webapp/src/Controller/PublicController.php b/webapp/src/Controller/PublicController.php index 6a22a2d6e1..96380e09b3 100644 --- a/webapp/src/Controller/PublicController.php +++ b/webapp/src/Controller/PublicController.php @@ -74,7 +74,7 @@ public function scoreboardAction( if ($requestedContest = $this->getContestFromRequest($contestId)) { $contest = $requestedContest; - $refreshParams['contest'] = $contest->getCid(); + $refreshParams['contest'] = $contest->getExternalid(); } $refreshUrl = sprintf('?%s', http_build_query($refreshParams)); @@ -143,7 +143,7 @@ protected function getContestFromRequest(?string $contestId = null): ?Contest } else { // Find the contest with the given ID. foreach ($this->dj->getCurrentContests(onlyPublic: true) as $possibleContest) { - if ($possibleContest->getCid() == $contestId || $possibleContest->getExternalid() == $contestId) { + if ($possibleContest->getExternalid() == $contestId) { $contest = $possibleContest; break; } @@ -158,23 +158,23 @@ protected function getContestFromRequest(?string $contestId = null): ?Contest return $contest; } - #[Route(path: '/change-contest/{contestId<-?\d+>}', name: 'public_change_contest')] - public function changeContestAction(Request $request, RouterInterface $router, int $contestId): Response + #[Route(path: '/change-contest/{contestId}', name: 'public_change_contest')] + public function changeContestAction(Request $request, RouterInterface $router, string $contestId): Response { if ($this->isLocalReferer($router, $request)) { $response = new RedirectResponse($request->headers->get('referer')); } else { $response = $this->redirectToRoute('public_index'); } - return $this->dj->setCookie('domjudge_cid', (string)$contestId, 0, null, '', false, false, + return $this->dj->setCookie('domjudge_cid', $contestId, 0, null, '', false, false, $response); } - #[Route(path: '/team/{teamId<\d+>}', name: 'public_team')] - public function teamAction(Request $request, int $teamId): Response + #[Route(path: '/team/{teamId}', name: 'public_team')] + public function teamAction(Request $request, string $teamId): Response { /** @var Team|null $team */ - $team = $this->em->getRepository(Team::class)->find($teamId); + $team = $this->em->getRepository(Team::class)->findByExternalId($teamId); if ($team && $team->getCategory() && !$team->getCategory()->getVisible()) { $team = null; } @@ -203,11 +203,11 @@ public function problemsAction(): Response $this->dj->getTwigDataForProblemsAction($this->stats)); } - #[Route(path: '/problems/{probId<\d+>}/statement', name: 'public_problem_statement')] - public function problemStatementAction(int $probId): StreamedResponse + #[Route(path: '/problems/{probId}/statement', name: 'public_problem_statement')] + public function problemStatementAction(string $probId): StreamedResponse { return $this->getBinaryFile($probId, function ( - int $probId, + string $probId, Contest $contest, ContestProblem $contestProblem ) { @@ -235,20 +235,20 @@ public function contestProblemsetAction(): StreamedResponse /** * @throws NonUniqueResultException */ - #[Route(path: '/{probId<\d+>}/attachment/{attachmentId<\d+>}', name: 'public_problem_attachment')] - public function attachmentAction(int $probId, int $attachmentId): StreamedResponse + #[Route(path: '/{probId}/attachment/{attachmentId<\d+>}', name: 'public_problem_attachment')] + public function attachmentAction(string $probId, int $attachmentId): StreamedResponse { return $this->getBinaryFile($probId, fn( - int $probId, + string $probId, Contest $contest, ContestProblem $contestProblem ) => $this->dj->getAttachmentStreamedResponse($contestProblem, $attachmentId)); } - #[Route(path: '/{probId<\d+>}/samples.zip', name: 'public_problem_sample_zip')] - public function sampleZipAction(int $probId): StreamedResponse + #[Route(path: '/{probId}/samples.zip', name: 'public_problem_sample_zip')] + public function sampleZipAction(string $probId): StreamedResponse { - return $this->getBinaryFile($probId, function (int $probId, Contest $contest, ContestProblem $contestProblem) { + return $this->getBinaryFile($probId, function (string $probId, Contest $contest, ContestProblem $contestProblem) { return $this->dj->getSamplesZipStreamedResponse($contestProblem); }); } @@ -258,16 +258,13 @@ public function sampleZipAction(int $probId): StreamedResponse * * Shared code between testcases, problem text and attachments. */ - protected function getBinaryFile(int $probId, callable $response): StreamedResponse + protected function getBinaryFile(string $probId, callable $response): StreamedResponse { $contest = $this->dj->getCurrentContest(onlyPublic: true); if (!$contest || !$contest->getFreezeData()->started()) { throw new NotFoundHttpException(sprintf('Problem p%d not found or not available', $probId)); } - $contestProblem = $this->em->getRepository(ContestProblem::class)->find([ - 'problem' => $probId, - 'contest' => $contest, - ]); + $contestProblem = $this->em->getRepository(ContestProblem::class)->findByProblemAndContest($contest, $probId); if (!$contestProblem) { throw new NotFoundHttpException(sprintf('Problem p%d not found or not available', $probId)); } @@ -285,7 +282,7 @@ public function submissionsAction(Request $request, string $teamId, string $prob } /** @var Team|null $team */ - $team = $this->em->getRepository(Team::class)->findOneBy(['externalid' => $teamId]); + $team = $this->em->getRepository(Team::class)->findByExternalId($teamId); if ($team && $team->getCategory() && !$team->getCategory()->getVisible()) { $team = null; } @@ -379,7 +376,7 @@ public function submissionsDataAction(Request $request, ?string $teamId, ?string } $submissionData[$teamKey][$problemKey][] = [ 'time' => $this->twigExtension->printtime($submission->getSubmittime(), contest: $contest), - 'language' => $submission->getLanguageId(), + 'language' => $submission->getLanguage()->getName(), 'verdict' => $this->submissionVerdict($submission, $contest, $verificationRequired), ]; } diff --git a/webapp/src/Controller/Team/MiscController.php b/webapp/src/Controller/Team/MiscController.php index 5b894163d4..437c6831f0 100644 --- a/webapp/src/Controller/Team/MiscController.php +++ b/webapp/src/Controller/Team/MiscController.php @@ -73,7 +73,7 @@ public function homeAction(Request $request): Response ]; if ($contest) { $scoreboard = $this->scoreboardService - ->getTeamScoreboard($contest, $teamId, false); + ->getTeamScoreboard($contest, $team->getExternalid(), false); $data = array_merge( $data, $this->scoreboardService->getScoreboardTwigData( @@ -151,15 +151,15 @@ public function updatesAction(): JsonResponse return $this->json(['unread_clarifications' => $this->dj->getUnreadClarifications()]); } - #[Route(path: '/change-contest/{contestId<-?\d+>}', name: 'team_change_contest')] - public function changeContestAction(Request $request, RouterInterface $router, int $contestId): Response + #[Route(path: '/change-contest/{contestId}', name: 'team_change_contest')] + public function changeContestAction(Request $request, RouterInterface $router, string $contestId): Response { if ($this->isLocalReferer($router, $request)) { $response = new RedirectResponse($request->headers->get('referer')); } else { $response = $this->redirectToRoute('team_index'); } - return $this->dj->setCookie('domjudge_cid', (string)$contestId, 0, null, '', false, false, + return $this->dj->setCookie('domjudge_cid', $contestId, 0, null, '', false, false, $response); } diff --git a/webapp/src/Entity/ContestProblem.php b/webapp/src/Entity/ContestProblem.php index 21d02e6bfe..4bd7286240 100644 --- a/webapp/src/Entity/ContestProblem.php +++ b/webapp/src/Entity/ContestProblem.php @@ -2,6 +2,7 @@ namespace App\Entity; use App\Controller\API\AbstractRestController as ARC; +use App\Repository\ContestProblemRepository; use App\Service\DOMJudgeService as DJS; use App\Utils\Utils; use Doctrine\Common\Collections\ArrayCollection; @@ -14,7 +15,7 @@ /** * Many-to-Many mapping of contests and problems. */ -#[ORM\Entity] +#[ORM\Entity(repositoryClass: ContestProblemRepository::class)] #[ORM\Table( name: 'contestproblem', options: [ diff --git a/webapp/src/Entity/Problem.php b/webapp/src/Entity/Problem.php index a715cd9fdc..1dd8889607 100644 --- a/webapp/src/Entity/Problem.php +++ b/webapp/src/Entity/Problem.php @@ -3,6 +3,7 @@ namespace App\Entity; use App\DataTransferObject\FileWithName; +use App\Repository\ProblemRepository; use App\Utils\Utils; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -18,7 +19,7 @@ /** * Stores testcases per problem. */ -#[ORM\Entity] +#[ORM\Entity(repositoryClass: ProblemRepository::class)] #[ORM\Table(options: [ 'collation' => 'utf8mb4_unicode_ci', 'charset' => 'utf8mb4', diff --git a/webapp/src/Entity/Team.php b/webapp/src/Entity/Team.php index fd9d4e4ef2..9778315cdd 100644 --- a/webapp/src/Entity/Team.php +++ b/webapp/src/Entity/Team.php @@ -5,6 +5,7 @@ use App\Controller\API\AbstractRestController as ARC; use App\DataTransferObject\ImageFile; use App\DataTransferObject\TeamLocation; +use App\Repository\TeamRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -18,7 +19,7 @@ /** * All teams participating in the contest. */ -#[ORM\Entity] +#[ORM\Entity(repositoryClass: TeamRepository::class)] #[ORM\Table(options: ['collation' => 'utf8mb4_unicode_ci', 'charset' => 'utf8mb4'])] #[ORM\Index(columns: ['affilid'], name: 'affilid')] #[ORM\Index(columns: ['categoryid'], name: 'categoryid')] diff --git a/webapp/src/Repository/ContestProblemRepository.php b/webapp/src/Repository/ContestProblemRepository.php new file mode 100644 index 0000000000..72611eee1e --- /dev/null +++ b/webapp/src/Repository/ContestProblemRepository.php @@ -0,0 +1,33 @@ + + */ +class ContestProblemRepository extends EntityRepository +{ + public function findByProblemAndContest(Contest|string $contest, Problem|string $problem): ?ContestProblem + { + if ($contest instanceof Contest) { + $contest = $contest->getExternalid(); + } + if ($problem instanceof Problem) { + $problem = $problem->getExternalid(); + } + return $this->createQueryBuilder('cp') + ->innerJoin('cp.contest', 'c') + ->innerJoin('cp.problem', 'p') + ->andWhere('c.externalid = :contestId') + ->andWhere('p.externalid = :problemId') + ->setParameter('contestId', $contest) + ->setParameter('problemId', $problem) + ->getQuery() + ->getOneOrNullResult(); + } +} diff --git a/webapp/src/Repository/FindByExternalidTrait.php b/webapp/src/Repository/FindByExternalidTrait.php new file mode 100644 index 0000000000..ba4082ede0 --- /dev/null +++ b/webapp/src/Repository/FindByExternalidTrait.php @@ -0,0 +1,19 @@ +findOneBy(['externalid' => $externalId]); + } +} diff --git a/webapp/src/Repository/LanguageRepository.php b/webapp/src/Repository/LanguageRepository.php index 17b639d9a5..144a34601c 100644 --- a/webapp/src/Repository/LanguageRepository.php +++ b/webapp/src/Repository/LanguageRepository.php @@ -10,8 +10,8 @@ */ class LanguageRepository extends EntityRepository { - public function findByExternalId(string $externalId): ?Language - { - return $this->findOneBy(['externalid' => $externalId]); - } + /** + * @use FindByExternalidTrait + */ + use FindByExternalidTrait; } diff --git a/webapp/src/Repository/ProblemRepository.php b/webapp/src/Repository/ProblemRepository.php new file mode 100644 index 0000000000..0a1d5131d0 --- /dev/null +++ b/webapp/src/Repository/ProblemRepository.php @@ -0,0 +1,17 @@ + + */ +class ProblemRepository extends EntityRepository +{ + /** + * @use FindByExternalidTrait + */ + use FindByExternalidTrait; +} diff --git a/webapp/src/Repository/TeamRepository.php b/webapp/src/Repository/TeamRepository.php new file mode 100644 index 0000000000..53f94b61e2 --- /dev/null +++ b/webapp/src/Repository/TeamRepository.php @@ -0,0 +1,18 @@ + + */ +class TeamRepository extends EntityRepository +{ + /** + * @use FindByExternalidTrait + */ + use FindByExternalidTrait; +} diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index c761103869..db52bc4f3c 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -196,7 +196,7 @@ public function getCurrentContest(?int $onlyOfTeam = null, bool $onlyPublic = fa } foreach ($contests as $contest) { - if ($contest->getCid() == $selected_cid) { + if ($contest->getExternalid() == $selected_cid) { return $contest; } } diff --git a/webapp/src/Service/ImportExportService.php b/webapp/src/Service/ImportExportService.php index b6e4c21812..c4110ab2ea 100644 --- a/webapp/src/Service/ImportExportService.php +++ b/webapp/src/Service/ImportExportService.php @@ -551,7 +551,7 @@ public function getResultsData( ->getResult(); $categoryIds = []; foreach ($categories as $category) { - $categoryIds[] = $category->getCategoryid(); + $categoryIds[] = $category->getExternalid(); } $scoreIsInSeconds = (bool)$this->config->get('score_in_seconds'); diff --git a/webapp/src/Service/ScoreboardService.php b/webapp/src/Service/ScoreboardService.php index db9e99ecd7..eec5a835ce 100644 --- a/webapp/src/Service/ScoreboardService.php +++ b/webapp/src/Service/ScoreboardService.php @@ -88,11 +88,11 @@ public function getScoreboard( * scorecache table. * * @param Contest $contest The contest to get the scoreboard for. - * @param int $teamId The ID of the team to get the scoreboard for. + * @param string $teamId The ID of the team to get the scoreboard for. * @param bool $showFtsInFreeze If false, the scoreboard will hide first * to solve for submissions after contest freeze. */ - public function getTeamScoreboard(Contest $contest, int $teamId, bool $showFtsInFreeze = true): ?Scoreboard + public function getTeamScoreboard(Contest $contest, string $teamId, bool $showFtsInFreeze = true): ?Scoreboard { $freezeData = new FreezeData($contest); @@ -820,7 +820,7 @@ public function getFilterValues(Contest $contest, bool $jury): array /** @var TeamCategory[] $categories */ $categories = $queryBuilder->getQuery()->getResult(); foreach ($categories as $category) { - $filters['categories'][$category->getCategoryid()] = $category->getName(); + $filters['categories'][$category->getExternalid()] = $category->getName(); } // Show only affiliations / countries with visible teams. @@ -845,7 +845,7 @@ public function getFilterValues(Contest $contest, bool $jury): array /** @var TeamAffiliation[] $affiliations */ $affiliations = $queryBuilder->getQuery()->getResult(); foreach ($affiliations as $affiliation) { - $filters['affiliations'][$affiliation->getAffilid()] = $affiliation->getName(); + $filters['affiliations'][$affiliation->getExternalid()] = $affiliation->getName(); if ($showFlags && $affiliation->getCountry() !== null) { $filters['countries'][] = $affiliation->getCountry(); } @@ -983,13 +983,13 @@ protected function getTeamsInOrder(Contest $contest, bool $jury = false, ?Filter if ($filter) { if ($filter->affiliations) { $queryBuilder - ->andWhere('t.affiliation IN (:affiliations)') + ->andWhere('ta.externalid IN (:affiliations)') ->setParameter('affiliations', $filter->affiliations); } if ($filter->categories) { $queryBuilder - ->andWhere('t.category IN (:categories)') + ->andWhere('tc.externalid IN (:categories)') ->setParameter('categories', $filter->categories); } @@ -1001,7 +1001,7 @@ protected function getTeamsInOrder(Contest $contest, bool $jury = false, ?Filter if ($filter->teams) { $queryBuilder - ->andWhere('t.teamid IN (:teams)') + ->andWhere('t.externalid IN (:teams)') ->setParameter('teams', $filter->teams); } } diff --git a/webapp/src/Service/SubmissionService.php b/webapp/src/Service/SubmissionService.php index 48e34cc0d5..b303005066 100644 --- a/webapp/src/Service/SubmissionService.php +++ b/webapp/src/Service/SubmissionService.php @@ -84,9 +84,10 @@ public function getSubmissionList( $queryBuilder = $this->em->createQueryBuilder() ->from(Submission::class, 's') - ->select('s', 'j', 'cp') + ->select('s', 'j', 'cp', 'l') ->join('s.team', 't') ->join('s.contest_problem', 'cp') + ->join('s.language', 'l') ->andWhere('s.contest IN (:contests)') ->setParameter('contests', array_keys($contests)) ->orderBy('s.submittime', 'DESC') diff --git a/webapp/src/Utils/Scoreboard/Filter.php b/webapp/src/Utils/Scoreboard/Filter.php index 1dee9340b1..0ca2f12332 100644 --- a/webapp/src/Utils/Scoreboard/Filter.php +++ b/webapp/src/Utils/Scoreboard/Filter.php @@ -5,10 +5,10 @@ class Filter { /** - * @param int[] $affiliations + * @param string[] $affiliations * @param string[] $countries - * @param int[] $categories - * @param int[] $teams + * @param string[] $categories + * @param string[] $teams */ public function __construct( public array $affiliations = [], diff --git a/webapp/templates/partials/menu_change_contest.html.twig b/webapp/templates/partials/menu_change_contest.html.twig index 5f6e637201..9ec1f7ec2a 100644 --- a/webapp/templates/partials/menu_change_contest.html.twig +++ b/webapp/templates/partials/menu_change_contest.html.twig @@ -11,7 +11,7 @@ {% endif %} {% for c in contests | filter(c => c != contest) %} {{ c.shortname }} + href="{{ path(change_path, {'contestId': c.externalid}) }}">{{ c.shortname }} {% endfor %} diff --git a/webapp/templates/partials/problem_list.html.twig b/webapp/templates/partials/problem_list.html.twig index e98ba39797..6fc7dca838 100644 --- a/webapp/templates/partials/problem_list.html.twig +++ b/webapp/templates/partials/problem_list.html.twig @@ -163,7 +163,7 @@ {% if problem.problem.problemstatementType is not empty %} + href="{{ path(problem_statement_path, {'probId': problem.externalId}) }}"> statement @@ -171,7 +171,7 @@ {% if numsamples > 0 %} + href="{{ path(problem_sample_zip_path, {'probId': problem.externalId}) }}"> samples {% endif %} @@ -183,7 +183,7 @@ {% for attachment in problem.problem.attachments %}
  • + href="{{ path(problem_attachment_path, {'probId': problem.externalId, 'attachmentId': attachment.attachmentid}) }}"> {{ attachment.name }}
  • diff --git a/webapp/templates/partials/scoreboard_table.html.twig b/webapp/templates/partials/scoreboard_table.html.twig index 71b074da1d..84793d1d08 100644 --- a/webapp/templates/partials/scoreboard_table.html.twig +++ b/webapp/templates/partials/scoreboard_table.html.twig @@ -97,7 +97,7 @@ {% set link = path('jury_problem', {'probId': problem.probid}) %} {% elseif problem.problem.problemstatementType is not empty %} {% if public %} - {% set link = path('public_problem_statement', {probId: problem.probid}) %} + {% set link = path('public_problem_statement', {probId: problem.externalid}) %} {% set target = '_blank' %} {% else %} {% set link = path('team_problem_statement', {probId: problem.probid}) %} @@ -230,7 +230,7 @@ {% if jury %} {% set link = path('jury_team', {teamId: score.team.teamid}) %} {% elseif public %} - {% set link = path('public_team', {teamId: score.team.teamid}) %} + {% set link = path('public_team', {teamId: score.team.externalid}) %} {% set extra = 'data-ajax-modal' %} {% else %} {% set link = path('team_team', {teamId: score.team.teamid}) %} diff --git a/webapp/tests/Unit/Integration/ScoreboardIntegrationTest.php b/webapp/tests/Unit/Integration/ScoreboardIntegrationTest.php index 0f5478d3fc..e76341dda9 100644 --- a/webapp/tests/Unit/Integration/ScoreboardIntegrationTest.php +++ b/webapp/tests/Unit/Integration/ScoreboardIntegrationTest.php @@ -288,7 +288,7 @@ public function testTeamScoreboardFreezeFTS(): void $team = $this->teams[1]; - $scoreboard = $this->ss->getTeamScoreboard($this->contest, $team->getTeamid(), false); + $scoreboard = $this->ss->getTeamScoreboard($this->contest, $team->getExternalid(), false); static::assertInstanceOf(SingleTeamScoreboard::class, $scoreboard); static::assertFTSMatch($expected_fts, $scoreboard); From 31b5d067cd08df0c223d73c9900e3fb4b5f4111a Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Sun, 26 Oct 2025 09:54:46 +0100 Subject: [PATCH 03/32] Change team pages to use external IDs instead of internal IDs Part of #3024 --- webapp/public/js/domjudge.js | 4 +-- webapp/src/Controller/API/PrintController.php | 3 +- .../Team/ClarificationController.php | 34 +++++++++---------- .../src/Controller/Team/ProblemController.php | 25 ++++++-------- .../Controller/Team/ScoreboardController.php | 7 ++-- webapp/src/Form/Type/PrintType.php | 2 +- webapp/src/Form/Type/SubmitProblemType.php | 2 ++ .../src/Form/Type/TeamClarificationType.php | 4 +-- webapp/src/Service/DOMJudgeService.php | 13 ++++--- .../javascript_language_detect.html.twig | 4 +-- .../templates/partials/problem_list.html.twig | 2 +- .../partials/scoreboard_table.html.twig | 13 ++++--- .../team/partials/clarification.html.twig | 4 +-- .../partials/clarification_list.html.twig | 2 +- .../team/partials/submission.html.twig | 2 +- .../team/partials/submission_list.html.twig | 4 +-- .../team/partials/submit_scripts.html.twig | 6 ++-- 17 files changed, 64 insertions(+), 67 deletions(-) diff --git a/webapp/public/js/domjudge.js b/webapp/public/js/domjudge.js index 68d8a7373b..790d7043c3 100644 --- a/webapp/public/js/domjudge.js +++ b/webapp/public/js/domjudge.js @@ -1302,8 +1302,8 @@ function initScoreboardSubmissions() { const linkEl = e.currentTarget; e.preventDefault(); const $modal = $('[data-submissions-modal] .modal').clone(); - const $teamEl = $(`[data-team-external-id="${linkEl.dataset.teamId}"]`); - const $problemEl = $(`[data-problem-external-id="${linkEl.dataset.problemId}"]`); + const $teamEl = $(`tr[data-team-id="${linkEl.dataset.teamId}"]`); + const $problemEl = $(`th[data-problem-id="${linkEl.dataset.problemId}"]`); $modal.find('[data-team]').html($teamEl.data('teamName')); $modal.find('[data-problem-badge]').html($problemEl.data('problemBadge')); $modal.find('[data-problem-name]').html($problemEl.data('problemName')); diff --git a/webapp/src/Controller/API/PrintController.php b/webapp/src/Controller/API/PrintController.php index 221e3a1864..e979fecbd7 100644 --- a/webapp/src/Controller/API/PrintController.php +++ b/webapp/src/Controller/API/PrintController.php @@ -80,10 +80,11 @@ public function printAsTeam( ): JsonResponse { $langid = null; if ($print->language !== null) { + /** @var string|null $langid */ $langid = $this->em ->createQueryBuilder() ->from(Language::class, "l") - ->select("l.langid") + ->select("l.externalid") ->andWhere("l.name = :name") ->setParameter("name", $print->language) ->getQuery() diff --git a/webapp/src/Controller/Team/ClarificationController.php b/webapp/src/Controller/Team/ClarificationController.php index c6c5079ff1..b02dfb1576 100644 --- a/webapp/src/Controller/Team/ClarificationController.php +++ b/webapp/src/Controller/Team/ClarificationController.php @@ -45,17 +45,17 @@ public function __construct( parent::__construct($em, $eventLogService, $dj, $kernel); } - #[Route(path: '/clarifications/by-problem/{probId<\d+>}', name: 'team_clarification_by_prob')] - public function viewByProblemAction(Request $request, int $probId): Response + #[Route(path: '/clarifications/by-problem/{probId}', name: 'team_clarification_by_prob')] + public function viewByProblemAction(Request $request, string $probId): Response { $user = $this->dj->getUser(); $team = $user->getTeam(); $teamId = $team->getTeamid(); $contest = $this->dj->getCurrentContest($teamId); - $problem = $this->em->getRepository(Problem::class)->find($probId); + $problem = $this->em->getRepository(Problem::class)->findByExternalId($probId); if ($problem === null) { - throw new NotFoundHttpException(sprintf('Problem %d not found', $probId)); + throw new NotFoundHttpException(sprintf('Problem %s not found', $probId)); } $contestProblem = $problem->getContestProblems(); $foundProblemInContest = false; @@ -66,7 +66,7 @@ public function viewByProblemAction(Request $request, int $probId): Response } } if (!$foundProblemInContest) { - throw new NotFoundHttpException(sprintf('Problem %d not in current contest', $probId)); + throw new NotFoundHttpException(sprintf('Problem %s not in current contest', $probId)); } /** @var Clarification[] $clarifications */ @@ -103,8 +103,8 @@ public function viewByProblemAction(Request $request, int $probId): Response /** * @throws NonUniqueResultException */ - #[Route(path: '/clarifications/{clarId<\d+>}', name: 'team_clarification')] - public function viewAction(Request $request, int $clarId): Response + #[Route(path: '/clarifications/{clarId}', name: 'team_clarification')] + public function viewAction(Request $request, string $clarId): Response { $categories = $this->config->get('clar_categories'); $user = $this->dj->getUser(); @@ -118,7 +118,7 @@ public function viewAction(Request $request, int $clarId): Response ->leftJoin('p.contest_problems', 'cp', Join::WITH, 'cp.contest = :contest') ->select('c, p, co') ->andWhere('c.contest = :contest') - ->andWhere('c.clarid = :clarId') + ->andWhere('c.externalid = :clarId') ->setParameter('contest', $contest) ->setParameter('clarId', $clarId) ->getQuery() @@ -127,9 +127,9 @@ public function viewAction(Request $request, int $clarId): Response $formData = []; if ($clarification) { if ($clarification->getProblem()) { - $formData['subject'] = sprintf('%d-%d', $clarification->getContest()->getCid(), $clarification->getProblem()->getProbid()); + $formData['subject'] = sprintf('%s|%s', $clarification->getContest()->getExternalid(), $clarification->getProblem()->getExternalid()); } else { - $formData['subject'] = sprintf('%d-%s', $clarification->getContest()->getCid(), $clarification->getCategory()); + $formData['subject'] = sprintf('%s#%s', $clarification->getContest()->getExternalid(), $clarification->getCategory()); } $formData['message'] = "> " . str_replace("\n", "\n> ", Utils::wrapUnquoted($clarification->getBody())) . "\n\n"; @@ -149,7 +149,7 @@ public function viewAction(Request $request, int $clarId): Response } if ($clarification === null) { - throw new NotFoundHttpException(sprintf('Clarification %d not found', $clarId)); + throw new NotFoundHttpException(sprintf('Clarification %s not found', $clarId)); } if (!$team->canViewClarification($clarification)) { @@ -185,7 +185,7 @@ public function viewAction(Request $request, int $clarId): Response } } - #[Route(path: '/clarifications/add', name: 'team_clarification_add')] + #[Route(path: '/clarifications/add', name: 'team_clarification_add', priority: 1)] public function addAction(Request $request): Response { $categories = $this->config->get('clar_categories'); @@ -227,15 +227,15 @@ private function newClarificationHelper( Team $team ): void { $formData = $form->getData(); - // First part will always be the contest ID, as Symfony will validate this. - [, $problemId] = explode('-', $formData['subject']); + // First part will always be the contest external ID, as Symfony will validate this. $problem = null; $category = null; $queue = null; - if (!ctype_digit($problemId)) { - $category = $problemId; + if (str_contains($formData['subject'], '#')) { + [, $category] = explode('#', $formData['subject']); } else { - $problem = $this->em->getRepository(Problem::class)->find($problemId); + [, $problemId] = explode('|', $formData['subject']); + $problem = $this->em->getRepository(Problem::class)->findByExternalId($problemId); $queue = $this->config->get('clar_default_problem_queue'); if ($queue === "") { $queue = null; diff --git a/webapp/src/Controller/Team/ProblemController.php b/webapp/src/Controller/Team/ProblemController.php index 9374ee46b4..9a86d50826 100644 --- a/webapp/src/Controller/Team/ProblemController.php +++ b/webapp/src/Controller/Team/ProblemController.php @@ -51,11 +51,11 @@ public function problemsAction(): Response } - #[Route(path: '/problems/{probId<\d+>}/statement', name: 'team_problem_statement')] - public function problemStatementAction(int $probId): StreamedResponse + #[Route(path: '/problems/{probId}/statement', name: 'team_problem_statement')] + public function problemStatementAction(string $probId): StreamedResponse { return $this->getBinaryFile($probId, function ( - int $probId, + string $probId, Contest $contest, ContestProblem $contestProblem ) { @@ -73,20 +73,20 @@ public function problemStatementAction(int $probId): StreamedResponse /** * @throws NonUniqueResultException */ - #[Route(path: '/{probId<\d+>}/attachment/{attachmentId<\d+>}', name: 'team_problem_attachment')] - public function attachmentAction(int $probId, int $attachmentId): StreamedResponse + #[Route(path: '/{probId}/attachment/{attachmentId<\d+>}', name: 'team_problem_attachment')] + public function attachmentAction(string $probId, int $attachmentId): StreamedResponse { return $this->getBinaryFile($probId, fn( - int $probId, + string $probId, Contest $contest, ContestProblem $contestProblem ) => $this->dj->getAttachmentStreamedResponse($contestProblem, $attachmentId)); } - #[Route(path: '/{probId<\d+>}/samples.zip', name: 'team_problem_sample_zip')] - public function sampleZipAction(int $probId): StreamedResponse + #[Route(path: '/{probId}/samples.zip', name: 'team_problem_sample_zip')] + public function sampleZipAction(string $probId): StreamedResponse { - return $this->getBinaryFile($probId, function (int $probId, Contest $contest, ContestProblem $contestProblem) { + return $this->getBinaryFile($probId, function (string $probId, Contest $contest, ContestProblem $contestProblem) { return $this->dj->getSamplesZipStreamedResponse($contestProblem); }); } @@ -96,17 +96,14 @@ public function sampleZipAction(int $probId): StreamedResponse * * Shared code between testcases, problem text and attachments. */ - protected function getBinaryFile(int $probId, callable $response): StreamedResponse + protected function getBinaryFile(string $probId, callable $response): StreamedResponse { $user = $this->dj->getUser(); $contest = $this->dj->getCurrentContest($user->getTeam()->getTeamid()); if (!$contest || !$contest->getFreezeData()->started()) { throw new NotFoundHttpException(sprintf('Problem p%d not found or not available', $probId)); } - $contestProblem = $this->em->getRepository(ContestProblem::class)->find([ - 'problem' => $probId, - 'contest' => $contest, - ]); + $contestProblem = $this->em->getRepository(ContestProblem::class)->findByProblemAndContest($contest, $probId); if (!$contestProblem) { throw new NotFoundHttpException(sprintf('Problem p%d not found or not available', $probId)); } diff --git a/webapp/src/Controller/Team/ScoreboardController.php b/webapp/src/Controller/Team/ScoreboardController.php index 069e5db60c..c22325471a 100644 --- a/webapp/src/Controller/Team/ScoreboardController.php +++ b/webapp/src/Controller/Team/ScoreboardController.php @@ -59,15 +59,14 @@ public function scoreboardAction(Request $request): Response return $this->render('team/scoreboard.html.twig', $data, $response); } - #[Route(path: '/team/{teamId<\d+>}', name: 'team_team')] - public function teamAction(Request $request, int $teamId): Response + #[Route(path: '/team/{teamId}', name: 'team_team')] + public function teamAction(Request $request, string $teamId): Response { if (!$this->config->get('enable_ranking')) { throw new BadRequestHttpException('Scoreboard is not available.'); } - /** @var Team|null $team */ - $team = $this->em->getRepository(Team::class)->find($teamId); + $team = $this->em->getRepository(Team::class)->findByExternalId($teamId); if ($team && $team->getCategory() && !$team->getCategory()->getVisible() && $teamId !== $this->dj->getUser()->getTeamId()) { $team = null; } diff --git a/webapp/src/Form/Type/PrintType.php b/webapp/src/Form/Type/PrintType.php index a9ea10b54b..d8b852f8e2 100644 --- a/webapp/src/Form/Type/PrintType.php +++ b/webapp/src/Form/Type/PrintType.php @@ -21,7 +21,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $languages = $this->em->getRepository(Language::class)->findAll(); $languageChoices = []; foreach ($languages as $language) { - $languageChoices[$language->getName()] = $language->getLangid(); + $languageChoices[$language->getName()] = $language->getExternalid(); } ksort($languageChoices, SORT_NATURAL | SORT_FLAG_CASE); $languageChoices = ['plain text' => ''] + $languageChoices; diff --git a/webapp/src/Form/Type/SubmitProblemType.php b/webapp/src/Form/Type/SubmitProblemType.php index 997bb21555..9532866b10 100644 --- a/webapp/src/Form/Type/SubmitProblemType.php +++ b/webapp/src/Form/Type/SubmitProblemType.php @@ -50,6 +50,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'choice_label' => fn(Problem $problem) => sprintf( '%s - %s', $problem->getContestProblems()->first()->getShortName(), $problem->getName() ), + 'choice_value' => 'externalid', 'placeholder' => 'Select a problem', ]; $builder->add('problem', EntityType::class, $problemConfig); @@ -59,6 +60,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'class' => Language::class, 'choices' => $languages, 'choice_label' => 'name', + 'choice_value' => 'externalid', 'placeholder' => 'Select a language', ]); diff --git a/webapp/src/Form/Type/TeamClarificationType.php b/webapp/src/Form/Type/TeamClarificationType.php index 32dfcfe5af..babb3617ef 100644 --- a/webapp/src/Form/Type/TeamClarificationType.php +++ b/webapp/src/Form/Type/TeamClarificationType.php @@ -32,7 +32,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $contest = $this->dj->getCurrentContest($user->getTeam()->getTeamid()); if ($contest) { foreach ($categories as $categoryId => $categoryName) { - $subjects[$categoryName] = sprintf('%d-%s', $contest->getCid(), $categoryId); + $subjects[$categoryName] = sprintf('%s#%s', $contest->getExternalid(), $categoryId); } if ($contest->getFreezeData()->started()) { /** @var ContestProblem $problem */ @@ -40,7 +40,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void if ($problem->getAllowSubmit()) { $problemName = sprintf('%s: %s', $problem->getShortname(), $problem->getProblem()->getName()); - $subjects[$problemName] = sprintf('%d-%d', $contest->getCid(), $problem->getProbid()); + $subjects[$problemName] = sprintf('%s|%s', $contest->getExternalid(), $problem->getExternalId()); } } } diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index db52bc4f3c..134a991603 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -311,7 +311,7 @@ public function clearCookie( } /** - * @return array + * @return list */ public function getUnreadClarifications(): array { @@ -326,7 +326,7 @@ public function getUnreadClarifications(): array foreach ($clarifications as $clar) { if ($clar->getContest()->getCid() === $contest->getCid()) { $unreadClarifications[] = [ - 'clarid' => $clar->getClarid(), + 'clarid' => $clar->getExternalid(), 'body' => $clar->getBody(), ]; } @@ -732,24 +732,23 @@ public function openZipFile(string $filename): ZipArchive * * @param string $filename The on-disk file to be printed out * @param string $origname The original filename as submitted by the team - * @param int|null $language Langid of the programming language this file is in + * @param string|null $language Langid of the programming language this file is in * @param bool $asTeam Print the file as the team associated with the user * @return array{0: bool, 1: string} */ public function printUserFile( string $filename, string $origname, - ?int $language, + ?string $language, ?bool $asTeam = false, ): array { $user = $this->getUser(); $team = $user->getTeam(); - $language = $language ? $this->em->getRepository(Language::class)->find($language) : null; if ($asTeam && $team !== null) { $teamid = $team->getLabel() ?? $team->getExternalid(); - return $this->printFile($filename, $origname, $language?->getExternalid(), $user->getUserIdentifier(), $team->getEffectiveName(), $teamid, $team->getLocation()); + return $this->printFile($filename, $origname, $language, $user->getUserIdentifier(), $team->getEffectiveName(), $teamid, $team->getLocation()); } - return $this->printFile($filename, $origname, $language?->getExternalid(), $user->getUserIdentifier()); + return $this->printFile($filename, $origname, $language, $user->getUserIdentifier()); } /** diff --git a/webapp/templates/partials/javascript_language_detect.html.twig b/webapp/templates/partials/javascript_language_detect.html.twig index 78d87c159d..ce36146c2f 100644 --- a/webapp/templates/partials/javascript_language_detect.html.twig +++ b/webapp/templates/partials/javascript_language_detect.html.twig @@ -5,7 +5,7 @@ {% for language in submission_languages %} {% for extension in language.extensions %} case '{{ extension|lower }}': - return '{{ language.langid }}'; + return '{{ language.externalid }}'; {% endfor %} {% endfor %} default: @@ -17,7 +17,7 @@ switch (mainext) { {% for language in submission_languages %} {% if language.requireEntryPoint %} - case '{{ language.langid }}': + case '{{ language.externalid }}': return '{{ language.entryPointDescription | default('Entry point') }}'; {% endif %} {% endfor %} diff --git a/webapp/templates/partials/problem_list.html.twig b/webapp/templates/partials/problem_list.html.twig index 6fc7dca838..a28812f7ab 100644 --- a/webapp/templates/partials/problem_list.html.twig +++ b/webapp/templates/partials/problem_list.html.twig @@ -145,7 +145,7 @@ {% endif %} {% if clarificationsCount > 0 %} + href="{{ path('team_clarification_by_prob', {'probId': problem.externalId}) }}"> {% if clarificationsCount > 0 %} {% set badgeClass = 'text-bg-info' %} diff --git a/webapp/templates/partials/scoreboard_table.html.twig b/webapp/templates/partials/scoreboard_table.html.twig index 84793d1d08..15ca1b287a 100644 --- a/webapp/templates/partials/scoreboard_table.html.twig +++ b/webapp/templates/partials/scoreboard_table.html.twig @@ -100,7 +100,7 @@ {% set link = path('public_problem_statement', {probId: problem.externalid}) %} {% set target = '_blank' %} {% else %} - {% set link = path('team_problem_statement', {probId: problem.probid}) %} + {% set link = path('team_problem_statement', {probId: problem.externalid}) %} {% set target = '_blank' %} {% endif %} {% endif %} @@ -108,7 +108,7 @@ @@ -156,7 +156,7 @@ {% else %} {% set color = score.team.category.color %} {% endif %} - + {% if enable_ranking %} {% if medalsEnabled %} @@ -233,7 +233,7 @@ {% set link = path('public_team', {teamId: score.team.externalid}) %} {% set extra = 'data-ajax-modal' %} {% else %} - {% set link = path('team_team', {teamId: score.team.teamid}) %} + {% set link = path('team_team', {teamId: score.team.externalid}) %} {% set extra = 'data-ajax-modal' %} {% endif %} {% endif %} @@ -441,8 +441,7 @@ {% endif %} {% if enable_ranking %} @@ -508,7 +507,7 @@ {% set link = path('public_team', {teamId: score.team.teamid}) %} {% set extra = 'data-ajax-modal' %} {% else %} - {% set link = path('team_team', {teamId: score.team.teamid}) %} + {% set link = path('team_team', {teamId: score.team.externalid}) %} {% set extra = 'data-ajax-modal' %} {% endif %} {% endif %} diff --git a/webapp/templates/team/partials/clarification.html.twig b/webapp/templates/team/partials/clarification.html.twig index 5004e4c0d4..2d0fba6c9f 100644 --- a/webapp/templates/team/partials/clarification.html.twig +++ b/webapp/templates/team/partials/clarification.html.twig @@ -20,14 +20,14 @@
    From: {% if clarification.sender is not empty %} - {{ clarification.sender.effectiveName }} (t{{ clarification.sender.teamid }}) + {{ clarification.sender.effectiveName }} ({{ clarification.sender.externalid }}) {% else %} Jury {% endif %}
    To: {% if clarification.recipient is not empty %} - {{ clarification.recipient.effectiveName }} (t{{ clarification.recipient.teamid }}) + {{ clarification.recipient.effectiveName }} ({{ clarification.recipient.externalid }}) {% elseif clarification.sender is not null %} Jury {% else %} diff --git a/webapp/templates/team/partials/clarification_list.html.twig b/webapp/templates/team/partials/clarification_list.html.twig index a8afe67896..15e731dc2c 100644 --- a/webapp/templates/team/partials/clarification_list.html.twig +++ b/webapp/templates/team/partials/clarification_list.html.twig @@ -17,7 +17,7 @@ {%- for clarification in clarifications %} - {%- set link = path('team_clarification', {clarId: clarification.clarid}) %} + {%- set link = path('team_clarification', {clarId: clarification.externalid}) %} diff --git a/webapp/templates/team/partials/submission.html.twig b/webapp/templates/team/partials/submission.html.twig index c5c3d9445b..6240c7c905 100644 --- a/webapp/templates/team/partials/submission.html.twig +++ b/webapp/templates/team/partials/submission.html.twig @@ -53,7 +53,7 @@ {% if allowDownload %}
    diff --git a/webapp/templates/team/partials/submission_list.html.twig b/webapp/templates/team/partials/submission_list.html.twig index 192413f3f4..75eaaf45dd 100644 --- a/webapp/templates/team/partials/submission_list.html.twig +++ b/webapp/templates/team/partials/submission_list.html.twig @@ -23,7 +23,7 @@ {%- for submission in submissions %} {% set link = null %} {% if (submission.submittime < current_team_contest.endtime or showTooLateResult) and submission.result and submission.judgings.first is not empty and submission.judgings.first.result is not empty and (not verificationRequired or submission.judgings.first.verified) %} - {% set link = path('team_submission', {submitId: submission.submitid}) %} + {% set link = path('team_submission', {submitId: submission.externalid}) %} {% endif %} {% set classes = '' %} @@ -78,7 +78,7 @@ {% endif %} {% if allowDownload %} - + diff --git a/webapp/templates/team/partials/submit_scripts.html.twig b/webapp/templates/team/partials/submit_scripts.html.twig index bf0bdb1af6..08182ecdbe 100644 --- a/webapp/templates/team/partials/submit_scripts.html.twig +++ b/webapp/templates/team/partials/submit_scripts.html.twig @@ -4,7 +4,7 @@ return filename.replace(/\.[^\.]*$/, ''); } - function entryPointDetectKt(filename) { + function entryPointDetectKotlin(filename) { var filebase = filename.replace(/\.[^\.]*$/, ''); if (filebase === '') return '_Kt'; @@ -44,8 +44,8 @@ case 'java': $entryPointInput.val(entryPointDetectJava(filename)); break; - case 'kt': - $entryPointInput.val(entryPointDetectKt(filename)); + case 'kotlin': + $entryPointInput.val(entryPointDetectKotlin(filename)); break; default: $entryPointInput.val(filename); From f09e4883860493c0d3b12fd262891a3a6601ed9e Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Sun, 26 Oct 2025 10:11:38 +0100 Subject: [PATCH 04/32] Change jury analysis pages to use external IDs instead of internal IDs Part of #3024 --- .../Controller/Jury/AnalysisController.php | 11 +++++--- webapp/src/Service/StatisticsService.php | 28 +++++++++---------- .../jury/analysis/contest_overview.html.twig | 10 +++---- .../jury/analysis/languages.html.twig | 4 +-- .../templates/jury/analysis/problem.html.twig | 4 +-- webapp/templates/jury/analysis/team.html.twig | 2 +- 6 files changed, 31 insertions(+), 28 deletions(-) diff --git a/webapp/src/Controller/Jury/AnalysisController.php b/webapp/src/Controller/Jury/AnalysisController.php index 26fa10b436..4887f55b88 100644 --- a/webapp/src/Controller/Jury/AnalysisController.php +++ b/webapp/src/Controller/Jury/AnalysisController.php @@ -55,12 +55,12 @@ public function indexAction( $delayedJudgings = $em->createQueryBuilder() ->from(Submission::class, 's') ->innerJoin(Judging::class, 'j', Expr\Join::WITH, 's.submitid = j.submission') - ->select('s.submitid, MIN(j.judgingid) AS judgingid, s.submittime, MIN(j.starttime) - s.submittime AS timediff, COUNT(j.judgingid) AS num_judgings') + ->select('s.externalid as submitexternalid, MIN(j.judgingid) AS judgingid, s.submittime, MIN(j.starttime) - s.submittime AS timediff, COUNT(j.judgingid) AS num_judgings') ->andWhere('s.contest = :contest') ->setParameter('contest', $contest) ->andWhere('s.team IN (:teams)') ->setParameter('teams', $teams) - ->groupBy('s.submitid') + ->groupBy('s.externalid') ->andHaving('timediff > :timediff') ->setParameter('timediff', $delayedTimeDiff) ->orderBy('timediff', 'DESC') @@ -83,7 +83,10 @@ public function indexAction( } #[Route(path: '/team/{team}', name: 'analysis_team')] - public function teamAction(Team $team): Response + public function teamAction( + #[MapEntity(mapping: ['team' => 'externalid'])] + Team $team, + ): Response { $contest = $this->dj->getCurrentContest(); @@ -100,7 +103,7 @@ public function teamAction(Team $team): Response #[Route(path: '/problem/{probid}', name: 'analysis_problem')] public function problemAction( - #[MapEntity(id: 'probid')] + #[MapEntity(mapping: ['probid' => 'externalid'])] Problem $problem, #[MapQueryParameter] ?string $view = null diff --git a/webapp/src/Service/StatisticsService.php b/webapp/src/Service/StatisticsService.php index 7dcf9f6181..0612a389b8 100644 --- a/webapp/src/Service/StatisticsService.php +++ b/webapp/src/Service/StatisticsService.php @@ -96,12 +96,12 @@ public function getTeams(Contest $contest, string $filter): array * problem_num_testcases: int[], team_num_submissions: int[], * team_attempted_n_problems: int[], teams_solved_n_problems: int[], * problem_attempts: int[], problem_solutions: int[], - * problem_stats: array{teams_attempted: array, teams_solved: array}, + * problem_stats: array{teams_attempted: array>, teams_solved: array>}, * submissions: Submission[], misery_index: float, * team_stats: array, * language_stats: array{total_submissions: array, total_solutions: array, - * teams_attempted: array, teams_solved: array}} + * teams_attempted: array>, teams_solved: array>}} */ public function getMiscContestStatistics( Contest $contest, @@ -177,14 +177,14 @@ public function getMiscContestStatistics( $misc['total_submissions']++; $teamStats['total_submitted']++; static::setOrIncrement($misc['problem_attempts'], - $s->getProblem()->getProbId()); + $s->getProblem()->getExternalid()); static::setOrIncrement($teamStats['problems_submitted'], - $s->getProblem()->getProbId()); - $misc['problem_stats']['teams_attempted'][$s->getProblem()->getProbId()][$team->getTeamId()] = $team->getTeamId(); + $s->getProblem()->getExternalid()); + $misc['problem_stats']['teams_attempted'][$s->getProblem()->getExternalid()][$team->getExternalid()] = $team->getExternalid(); static::setOrIncrement($misc['language_stats']['total_submissions'], $s->getLanguage()->getName()); - $misc['language_stats']['teams_attempted'][$s->getLanguage()->getName()][$team->getTeamId()] = $team->getTeamId(); + $misc['language_stats']['teams_attempted'][$s->getLanguage()->getName()][$team->getExternalid()] = $team->getExternalid(); if ($s->getResult() != 'correct') { continue; @@ -192,12 +192,12 @@ public function getMiscContestStatistics( $misc['total_accepted']++; $teamStats['total_accepted']++; static::setOrIncrement($teamStats['problems_accepted'], - $s->getProblem()->getProbId()); + $s->getProblem()->getExternalid()); static::setOrIncrement($misc['problem_solutions'], - $s->getProblem()->getProbId()); - $misc['problem_stats']['teams_solved'][$s->getProblem()->getProbId()][$team->getTeamId()] = $team->getTeamId(); + $s->getProblem()->getExternalid()); + $misc['problem_stats']['teams_solved'][$s->getProblem()->getExternalid()][$team->getExternalid()] = $team->getExternalid(); - $misc['language_stats']['teams_solved'][$s->getLanguage()->getName()][$team->getTeamId()] = $team->getTeamId(); + $misc['language_stats']['teams_solved'][$s->getLanguage()->getName()][$team->getExternalid()] = $team->getExternalid(); static::setOrIncrement($misc['language_stats']['total_solutions'], $s->getLanguage()->getName()); @@ -205,7 +205,7 @@ public function getMiscContestStatistics( $lastSubmission = $s; } } - $misc['team_stats'][$team->getTeamId()] = $teamStats; + $misc['team_stats'][$team->getExternalid()] = $teamStats; static::setOrIncrement($misc['team_attempted_n_problems'], count($teamStats['problems_submitted'])); static::setOrIncrement($misc['teams_solved_n_problems'], @@ -222,7 +222,7 @@ public function getMiscContestStatistics( } $miseryMinutes = ($miserySeconds / 60) * 3; - $misc['team_stats'][$team->getTeamId()]['misery_index'] = $miseryMinutes; + $misc['team_stats'][$team->getExternalid()]['misery_index'] = $miseryMinutes; $totalMiseryMinutes += $miseryMinutes; } $misc['misery_index'] = count($teams) > 0 ? $totalMiseryMinutes / count($teams) : 0; @@ -656,7 +656,7 @@ protected function getNumTestcases(Contest $contest): array // Need to query directly the count, otherwise symfony memory explodes // I think because it tries to load the testdata if you do this the naive way. $results = $this->em->createQueryBuilder() - ->select('p.probid, count(tc.testcaseid) as num_testcases') + ->select('p.externalid, count(tc.testcaseid) as num_testcases') ->from(Contest::class, 'c') ->join('c.problems', 'cp') ->join('cp.problem', 'p') @@ -667,7 +667,7 @@ protected function getNumTestcases(Contest $contest): array ->getQuery()->getResult(); $numTestcases = []; foreach ($results as $r) { - $numTestcases[$r['probid']] = $r['num_testcases']; + $numTestcases[$r['externalid']] = $r['num_testcases']; } return $numTestcases; diff --git a/webapp/templates/jury/analysis/contest_overview.html.twig b/webapp/templates/jury/analysis/contest_overview.html.twig index 8b5f628b3d..ddcf887523 100644 --- a/webapp/templates/jury/analysis/contest_overview.html.twig +++ b/webapp/templates/jury/analysis/contest_overview.html.twig @@ -113,7 +113,7 @@ $(function() { {% for p in problems %} - {% set id=p.probid %} + {% set id=p.externalId %} {% set link = path('analysis_problem', {'probid': id, 'view': view}) %} {{ id }} @@ -149,7 +149,7 @@ $(function() { {% for t in teams %} - {% set id=t.teamid %} + {% set id=t.externalid %} {% set link = path('analysis_team', {'team':id}) %} {{ t | entityIdBadge('t') }} @@ -186,7 +186,7 @@ $(function() { {% for j in delayed_judgings.data %} - {% set id=j.submitid %} + {% set id=j.submitexternalid %} {% set link = path('jury_submission', {'submitId': id}) %} {{ id }} @@ -289,7 +289,7 @@ var problem_stats = [ {% for prob in problems %} { "label" : "{{ prob.shortname }}", - "value" : {{ misc.problem_stats.teams_attempted[prob.probid] | default([]) | length }}, + "value" : {{ misc.problem_stats.teams_attempted[prob.externalId] | default([]) | length }}, }, {% endfor %} ] @@ -301,7 +301,7 @@ var problem_stats = [ {% for prob in problems %} { "label" : "{{ prob.shortname }}", - "value" : {{ misc.problem_stats.teams_solved[prob.probid] | default([]) | length }}, + "value" : {{ misc.problem_stats.teams_solved[prob.externalid] | default([]) | length }}, }, {% endfor %} ] diff --git a/webapp/templates/jury/analysis/languages.html.twig b/webapp/templates/jury/analysis/languages.html.twig index 7cb29236af..2128589c1a 100644 --- a/webapp/templates/jury/analysis/languages.html.twig +++ b/webapp/templates/jury/analysis/languages.html.twig @@ -40,12 +40,12 @@ {% for team in language.teams %} - + {{ team.team | entityIdBadge('t') }} - + {{ team.team.effectiveName }} diff --git a/webapp/templates/jury/analysis/problem.html.twig b/webapp/templates/jury/analysis/problem.html.twig index 52288a12e6..97843ccab8 100644 --- a/webapp/templates/jury/analysis/problem.html.twig +++ b/webapp/templates/jury/analysis/problem.html.twig @@ -1,6 +1,6 @@ {% extends "jury/base.html.twig" %} -{% block title %}Analysis - Problem {{ problem.probid }} {{ current_contest.shortname | default('') }} - {{ parent() }}{% endblock %} +{% block title %}Analysis - Problem {{ problem.externalid }} {{ current_contest.shortname | default('') }} - {{ parent() }}{% endblock %} {% block extrahead %} {{ parent() }} @@ -20,7 +20,7 @@ {% endblock %} {% block content %} -

    Problem {{ problem.probid }}: {{ contest_problem | problemBadge }} {{ problem.name }}

    +

    Problem {{ problem.externalid }}: {{ contest_problem | problemBadge }} {{ problem.name }}

    {% include 'jury/partials/analysis_filter.html.twig' %}
    diff --git a/webapp/templates/jury/analysis/team.html.twig b/webapp/templates/jury/analysis/team.html.twig index 55b9e97fce..e7a1f569e9 100644 --- a/webapp/templates/jury/analysis/team.html.twig +++ b/webapp/templates/jury/analysis/team.html.twig @@ -25,7 +25,7 @@ {% endblock %} {% block content %} -

    Team {{ team.teamid }}: {{ team.effectiveName }}

    +

    Team {{ team.externalid }}: {{ team.effectiveName }}

    From 6371dc7cca83b1892fdbb76ba8f4ec9064a1c06c Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Sun, 26 Oct 2025 11:08:30 +0100 Subject: [PATCH 05/32] Change jury audit log pages and logic to use external IDs instead of internal IDs Part of #3024 --- webapp/migrations/Version20251026092516.php | 66 +++++++++++++++++++ .../API/ClarificationController.php | 2 +- .../Controller/API/JudgehostController.php | 11 ++-- .../src/Controller/API/LanguageController.php | 2 +- .../src/Controller/API/ProblemController.php | 4 +- webapp/src/Controller/API/UserController.php | 2 +- webapp/src/Controller/BaseController.php | 15 ++++- .../Controller/Jury/AuditLogController.php | 7 +- .../Jury/ClarificationController.php | 2 +- .../src/Controller/Jury/ContestController.php | 14 ++-- .../Jury/ImportExportController.php | 2 +- .../Jury/InternalErrorController.php | 4 +- .../Controller/Jury/JudgehostController.php | 4 +- .../Controller/Jury/LanguageController.php | 6 +- .../src/Controller/Jury/ProblemController.php | 16 ++--- .../Controller/Jury/SubmissionController.php | 8 +-- webapp/src/Controller/Jury/TeamController.php | 12 ++-- webapp/src/Controller/Jury/UserController.php | 8 +-- .../Team/ClarificationController.php | 4 +- .../Controller/Team/ScoreboardController.php | 2 +- webapp/src/Entity/AuditLog.php | 10 +-- webapp/src/Entity/Contest.php | 3 +- webapp/src/Entity/User.php | 3 +- webapp/src/Repository/ContestRepository.php | 17 +++++ webapp/src/Repository/TeamRepository.php | 1 - webapp/src/Repository/UserRepository.php | 17 +++++ webapp/src/Security/UserStateUpdater.php | 2 +- webapp/src/Service/DOMJudgeService.php | 10 +-- webapp/src/Service/ImportExportService.php | 17 ++--- webapp/src/Service/ImportProblemService.php | 2 +- webapp/src/Service/RejudgingService.php | 4 +- webapp/src/Service/ScoreboardService.php | 2 +- webapp/src/Service/SubmissionService.php | 4 +- 33 files changed, 194 insertions(+), 89 deletions(-) create mode 100644 webapp/migrations/Version20251026092516.php create mode 100644 webapp/src/Repository/ContestRepository.php create mode 100644 webapp/src/Repository/UserRepository.php diff --git a/webapp/migrations/Version20251026092516.php b/webapp/migrations/Version20251026092516.php new file mode 100644 index 0000000000..c0a838e108 --- /dev/null +++ b/webapp/migrations/Version20251026092516.php @@ -0,0 +1,66 @@ + 'clarid', + 'contest' => 'cid', + 'language' => 'langid', + 'problem' => 'probid', + 'submission' => 'submitid', + 'team' => 'teamid', + 'team_affiliation' => 'affilid', + 'team_category' => 'categoryid', + 'user' => ['userid', 'username'], + ]; + + public function getDescription(): string + { + return 'Make auditlog IDs reference external IDs'; + } + + public function up(Schema $schema): void + { + // Note: this migration only works for entities that still exist. For others, it will not work but there is nothing we can do + $this->addSql('ALTER TABLE auditlog CHANGE cid cid VARCHAR(255) DEFAULT NULL COMMENT \'External contest ID associated to this entry\', CHANGE dataid dataid VARCHAR(64) DEFAULT NULL COMMENT \'(External) identifier in reference table\''); + $this->addSql('UPDATE auditlog INNER JOIN contest ON CAST(contest.cid AS CHAR) = auditlog.cid SET auditlog.cid = contest.externalid'); + foreach (static::ENTITIES as $table => $fields) { + if (!is_array($fields)) { + $fields = [$fields]; + } + foreach ($fields as $field) { + $this->addSql(sprintf( + 'UPDATE auditlog INNER JOIN %s ON CAST(%s.%s AS CHAR) = auditlog.dataid AND auditlog.datatype = "%s" SET auditlog.dataid = %s.externalid', + $table, $table, $field, $table, $table + )); + } + } + } + + public function down(Schema $schema): void + { + // Note: this migration only works for entities that still exist. For others, it will not work but there is nothing we can do + $this->addSql('UPDATE auditlog INNER JOIN contest ON contest.externalid = auditlog.cid SET auditlog.cid = contest.cid'); + foreach (static::ENTITIES as $table => $fields) { + $field = is_array($fields) ? $fields[0] : $fields; + $this->addSql(sprintf( + 'UPDATE auditlog INNER JOIN %s ON %s.externalid = auditlog.dataid AND auditlog.datatype = "%s" SET auditlog.dataid = %s.%s', + $table, $table, $table, $table, $field + )); + } + $this->addSql('ALTER TABLE auditlog CHANGE cid cid INT UNSIGNED DEFAULT NULL COMMENT \'Contest ID associated to this entry\', CHANGE dataid dataid VARCHAR(64) DEFAULT NULL COMMENT \'Identifier in reference table\''); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/src/Controller/API/ClarificationController.php b/webapp/src/Controller/API/ClarificationController.php index a502b64c82..6f9df8f8b6 100644 --- a/webapp/src/Controller/API/ClarificationController.php +++ b/webapp/src/Controller/API/ClarificationController.php @@ -249,7 +249,7 @@ public function addAction( $this->em->persist($clarification); $this->em->flush(); - $this->dj->auditlog('clarification', $clarification->getClarid(), 'added', null, null, $contestId); + $this->dj->auditlog('clarification', $clarification->getExternalid(), 'added', null, null, $contest->getExternalid()); $this->eventLogService->log('clarification', $clarification->getClarid(), 'create', $contestId); // Refresh the clarification since the event log service will have unloaded it. diff --git a/webapp/src/Controller/API/JudgehostController.php b/webapp/src/Controller/API/JudgehostController.php index e9b184606f..e36edf2825 100644 --- a/webapp/src/Controller/API/JudgehostController.php +++ b/webapp/src/Controller/API/JudgehostController.php @@ -442,8 +442,9 @@ public function updateJudgingAction( $judgingId = $judging->getJudgingid(); $contestId = $judging->getSubmission()->getContest()->getCid(); - $this->dj->auditlog('judging', $judgingId, 'judged', - 'compiler-error', $judgehost->getHostname(), $contestId); + $contestExernalid = $judging->getSubmission()->getContest()->getExternalid(); + $this->dj->auditlog('judging', (string)$judgingId, 'judged', + 'compiler-error', $judgehost->getHostname(), $contestExernalid); $this->maybeUpdateActiveJudging($judging); $this->em->flush(); @@ -905,9 +906,9 @@ protected function giveBackJudging(int $judgingId, ?Judgehost $judgehost): void $this->em->flush(); } - $this->dj->auditlog('judging', $judgingId, 'given back' + $this->dj->auditlog('judging', (string)$judgingId, 'given back' . ($judgehost === null ? '' : ' for judgehost ' . $judgehost->getHostname()), null, - $judgehost?->getHostname(), $judging->getContest()->getCid()); + $judgehost?->getHostname(), $judging->getContest()->getExternalid()); } } @@ -1127,7 +1128,7 @@ private function addSingleJudgingRun( $this->balloonService->updateBalloons($contest, $submission, $judging); } - $this->dj->auditlog('judging', $judging->getJudgingid(), 'judged', $result, $hostname); + $this->dj->auditlog('judging', (string)$judging->getJudgingid(), 'judged', $result, $hostname); } // Send an event for an endtime (and max runtime update). diff --git a/webapp/src/Controller/API/LanguageController.php b/webapp/src/Controller/API/LanguageController.php index 5a861e2eac..525b1cb5ab 100644 --- a/webapp/src/Controller/API/LanguageController.php +++ b/webapp/src/Controller/API/LanguageController.php @@ -124,7 +124,7 @@ public function updateExecutableActions(Request $request, string $id): void $language->getCompileExecutable()->setImmutableExecutable($immutableExecutable); $this->em->flush(); - $this->dj->auditlog('executable', $language->getLangid(), 'updated'); + $this->dj->auditlog('executable', $language->getExternalid(), 'updated'); } #[IsGranted('ROLE_ADMIN')] diff --git a/webapp/src/Controller/API/ProblemController.php b/webapp/src/Controller/API/ProblemController.php index 479c96b105..d00ad05f86 100644 --- a/webapp/src/Controller/API/ProblemController.php +++ b/webapp/src/Controller/API/ProblemController.php @@ -281,7 +281,7 @@ public function unlinkProblemAction(Request $request, string $id): Response } $this->em->remove($contestProblem); - $id = [$contestProblem->getCid(), $contestProblem->getProbid()]; + $id = [$contestProblem->getExternalId(), $contestProblem->getExternalId()]; $this->dj->auditlog('contest_problem', implode(', ', $id), 'deleted'); $this->eventLogService->log('problem', $contestProblem->getProbid(), EventLogService::ACTION_DELETE, $cid, @@ -354,7 +354,7 @@ public function linkProblemAction( $this->em->persist($contestProblem); $this->em->flush(); - $fullId = [$contestProblem->getCid(), $contestProblem->getProbid()]; + $fullId = [$contestProblem->getExternalId(), $contestProblem->getExternalId()]; $this->dj->auditlog('contest_problem', implode(', ', $fullId), 'added'); $this->eventLogService->log('problem', $contestProblem->getProbid(), EventLogService::ACTION_CREATE, $cid, diff --git a/webapp/src/Controller/API/UserController.php b/webapp/src/Controller/API/UserController.php index e3135ce267..2315fca0be 100644 --- a/webapp/src/Controller/API/UserController.php +++ b/webapp/src/Controller/API/UserController.php @@ -410,7 +410,7 @@ protected function addOrUpdateUser(AddUser $addUser, Request $request): Response $this->em->persist($user); $this->em->flush(); - $this->dj->auditlog('user', $user->getUserid(), 'added'); + $this->dj->auditlog('user', $user->getExternalid(), 'added'); return $this->renderCreateData($request, $user, 'user', $user->getUserid()); } diff --git a/webapp/src/Controller/BaseController.php b/webapp/src/Controller/BaseController.php index 00b17e4424..1cba1929ad 100644 --- a/webapp/src/Controller/BaseController.php +++ b/webapp/src/Controller/BaseController.php @@ -8,6 +8,7 @@ use App\Entity\Contest; use App\Entity\ContestProblem; use App\Entity\ExternalIdFromInternalIdInterface; +use App\Entity\HasExternalIdInterface; use App\Entity\Problem; use App\Entity\RankCache; use App\Entity\ScoreCache; @@ -151,7 +152,12 @@ protected function saveEntity( } } - $this->dj->auditlog($auditLogType, $id, $isNewEntity ? 'added' : 'updated'); + if ($entity instanceof HasExternalIdInterface) { + $dataid = $entity->getExternalId(); + } else { + $dataid = $id; + } + $this->dj->auditlog($auditLogType, $dataid, $isNewEntity ? 'added' : 'updated'); } /** @@ -216,7 +222,12 @@ protected function commitDeleteEntity(object $entity, array $primaryKeyData): vo // Add an audit log entry. $auditLogType = Utils::tableForEntity($entity); - $this->dj->auditlog($auditLogType, implode(', ', $primaryKeyData), 'deleted'); + if ($entity instanceof HasExternalIdInterface) { + $dataid = $entity->getExternalId(); + } else { + $dataid = implode(', ', $primaryKeyData); + } + $this->dj->auditlog($auditLogType, $dataid, 'deleted'); // Trigger the delete event. We need to do this before deleting the entity to make // sure we can still find the entity in the table. diff --git a/webapp/src/Controller/Jury/AuditLogController.php b/webapp/src/Controller/Jury/AuditLogController.php index ad000bc413..ec1d670958 100644 --- a/webapp/src/Controller/Jury/AuditLogController.php +++ b/webapp/src/Controller/Jury/AuditLogController.php @@ -79,7 +79,7 @@ public function indexAction( $cid = $logline->getCid(); if ($cid) { - $data['where']['value'] = "c" . $cid; + $data['where']['value'] = $cid; $data['where']['sortvalue'] = $cid; $data['where']['link'] = $this->generateUrl('jury_contest', ['contestId' => $cid]); } else { @@ -148,11 +148,6 @@ private function generateDatatypeUrl(string $type, int|string|null $id): ?string case 'team_category': return $this->generateUrl('jury_team_category', ['categoryId' => $id]); case 'user': - // Pre 6.1, usernames were stored instead of numeric IDs. - if (!is_numeric($id)) { - $user = $this->em->getRepository(User::class)->findOneBy(['username'=>$id]); - $id = $user->getUserId(); - } return $this->generateUrl('jury_user', ['userId' => $id]); case 'testcase': // For testcase audit logs, the ID is actually the problem ID diff --git a/webapp/src/Controller/Jury/ClarificationController.php b/webapp/src/Controller/Jury/ClarificationController.php index 4eda5224e2..4e4b9b3096 100644 --- a/webapp/src/Controller/Jury/ClarificationController.php +++ b/webapp/src/Controller/Jury/ClarificationController.php @@ -410,7 +410,7 @@ protected function processSubmittedClarification( $this->em->flush(); $clarId = $clarification->getClarId(); - $this->dj->auditlog('clarification', $clarId, 'added', null, null, $cid); + $this->dj->auditlog('clarification', $clarification->getExternalid(), 'added', null, null, $contest->getExternalid()); $this->eventLogService->log('clarification', $clarId, 'create', (int)$cid); // Reload clarification to make sure we have a fresh one after calling the event log service. $clarification = $this->em->getRepository(Clarification::class)->find($clarId); diff --git a/webapp/src/Controller/Jury/ContestController.php b/webapp/src/Controller/Jury/ContestController.php index b8c4c5e419..50d441a8b0 100644 --- a/webapp/src/Controller/Jury/ContestController.php +++ b/webapp/src/Controller/Jury/ContestController.php @@ -305,10 +305,10 @@ public function indexAction(Request $request): Response ]); } - #[Route(path: '/{contestId<\d+>}', name: 'jury_contest')] - public function viewAction(Request $request, int $contestId): Response + #[Route(path: '/{contestId}', name: 'jury_contest')] + public function viewAction(Request $request, string $contestId): Response { - $contest = $this->em->getRepository(Contest::class)->find($contestId); + $contest = $this->em->getRepository(Contest::class)->findByExternalId($contestId); if (!$contest) { throw new NotFoundHttpException(sprintf('Contest with ID %s not found', $contestId)); } @@ -401,7 +401,7 @@ public function toggleSubmitAction( } $this->em->flush(); - $this->dj->auditlog('contest', $contestId, $label, $value ? 'yes' : 'no'); + $this->dj->auditlog('contest', $contest->getExternalid(), $label, $value ? 'yes' : 'no'); return $this->redirectToLocalReferrer( $router, $request, @@ -810,7 +810,7 @@ public function finalizeAction(Request $request, int $contestId): Response if ($form->isSubmitted() && $form->isValid()) { $contest->setFinalizetime(Utils::now()); $this->em->flush(); - $this->dj->auditlog('contest', $contest->getCid(), 'finalized', + $this->dj->auditlog('contest', $contest->getExternalid(), 'finalized', $contest->getFinalizecomment()); return $this->redirectToRoute('jury_contest', ['contestId' => $contest->getCid()]); } @@ -846,7 +846,7 @@ public function doNowAction(Request $request, int $contestId, string $time): Res $now = (int)floor(Utils::now()); $nowstring = date('Y-m-d H:i:s ', $now) . date_default_timezone_get(); - $this->dj->auditlog('contest', $contest->getCid(), $time . ' now', $nowstring); + $this->dj->auditlog('contest', $contest->getExternalid(), $time . ' now', $nowstring); // Special case delay/resume start (only sets/unsets starttime_undefined). $maxSeconds = Contest::STARTTIME_UPDATE_MIN_SECONDS_BEFORE; @@ -1018,7 +1018,7 @@ private function doLock(int $contestId, bool $locked): Response throw new NotFoundHttpException(sprintf('Contest with ID %s not found.', $contestId)); } - $this->dj->auditlog('contest', $contest->getCid(), $locked ? 'lock' : 'unlock'); + $this->dj->auditlog('contest', $contest->getExternalid(), $locked ? 'lock' : 'unlock'); $contest->setIsLocked($locked); $this->em->flush(); diff --git a/webapp/src/Controller/Jury/ImportExportController.php b/webapp/src/Controller/Jury/ImportExportController.php index a7c127c345..3a746571da 100644 --- a/webapp/src/Controller/Jury/ImportExportController.php +++ b/webapp/src/Controller/Jury/ImportExportController.php @@ -168,7 +168,7 @@ public function indexAction(Request $request): Response ); $allMessages = array_merge($allMessages, $messages); if ($newProblem) { - $this->dj->auditlog('problem', $newProblem->getProbid(), 'upload zip', + $this->dj->auditlog('problem', $newProblem->getExternalid(), 'upload zip', $clientName); } else { $this->postMessages($allMessages); diff --git a/webapp/src/Controller/Jury/InternalErrorController.php b/webapp/src/Controller/Jury/InternalErrorController.php index 04642dab40..f7148b1ef5 100644 --- a/webapp/src/Controller/Jury/InternalErrorController.php +++ b/webapp/src/Controller/Jury/InternalErrorController.php @@ -151,7 +151,7 @@ public function handleAction(Request $request, ?Profiler $profiler, int $errorId ->getSingleResult(); if ($action === 'ignore') { $internalError->setStatus(InternalErrorStatusType::STATUS_IGNORED); - $this->dj->auditlog('internal_error', $internalError->getErrorid(), + $this->dj->auditlog('internal_error', (string)$internalError->getErrorid(), sprintf('internal error: %s', InternalErrorStatusType::STATUS_IGNORED)); $this->em->flush(); return $this->redirectToRoute('jury_internal_error', ['errorId' => $internalError->getErrorid()]); @@ -175,7 +175,7 @@ public function handleAction(Request $request, ?Profiler $profiler, int $errorId ); $this->em->flush(); - $this->dj->auditlog('internal_error', $internalError->getErrorid(), + $this->dj->auditlog('internal_error', (string)$internalError->getErrorid(), sprintf('internal error: %s', InternalErrorStatusType::STATUS_RESOLVED)); $affectedJudgings = $internalError->getAffectedJudgings(); diff --git a/webapp/src/Controller/Jury/JudgehostController.php b/webapp/src/Controller/Jury/JudgehostController.php index 3679c196b2..3437bdab24 100644 --- a/webapp/src/Controller/Jury/JudgehostController.php +++ b/webapp/src/Controller/Jury/JudgehostController.php @@ -325,7 +325,7 @@ public function enableAction(RouterInterface $router, Request $request, int $jud $judgehost = $this->em->getRepository(Judgehost::class)->find($judgehostid); $judgehost->setEnabled(true); $this->em->flush(); - $this->dj->auditlog('judgehost', $judgehost->getJudgehostid(), 'marked enabled'); + $this->dj->auditlog('judgehost', (string)$judgehost->getJudgehostid(), 'marked enabled'); return $this->redirectToLocalReferrer($router, $request, $this->generateUrl('jury_judgehosts')); } @@ -337,7 +337,7 @@ public function disableAction(RouterInterface $router, Request $request, int $ju $judgehost = $this->em->getRepository(Judgehost::class)->find($judgehostid); $judgehost->setEnabled(false); $this->em->flush(); - $this->dj->auditlog('judgehost', $judgehost->getJudgehostid(), 'marked disabled'); + $this->dj->auditlog('judgehost', (string)$judgehost->getJudgehostid(), 'marked disabled'); return $this->redirectToLocalReferrer($router, $request, $this->generateUrl('jury_judgehosts')); } diff --git a/webapp/src/Controller/Jury/LanguageController.php b/webapp/src/Controller/Jury/LanguageController.php index a4e577d133..f023def0a7 100644 --- a/webapp/src/Controller/Jury/LanguageController.php +++ b/webapp/src/Controller/Jury/LanguageController.php @@ -232,7 +232,7 @@ public function toggleSubmitAction(Request $request, string $langId): Response $language->setAllowSubmit($request->request->getBoolean('value')); $this->em->flush(); - $this->dj->auditlog('language', $langId, 'set allow submit', + $this->dj->auditlog('language', $language->getExternalid(), 'set allow submit', $request->request->getBoolean('value') ? 'yes' : 'no'); return $this->redirectToRoute('jury_language', ['langId' => $langId]); } @@ -256,7 +256,7 @@ public function toggleJudgeAction( $this->dj->unblockJudgeTasksForLanguage($language->getLangid()); } - $this->dj->auditlog('language', $langId, 'set allow judge', + $this->dj->auditlog('language', $language->getExternalid(), 'set allow judge', $request->request->getBoolean('value') ? 'yes' : 'no'); return $this->redirectToLocalReferrer( $router, @@ -277,7 +277,7 @@ public function toggleFilterCompilerFlagsAction(Request $request, string $langId $language->setFilterCompilerFiles($enabled); $this->em->flush(); - $this->dj->auditlog('language', $langId, 'set filter compiler flags', + $this->dj->auditlog('language', $language->getExternalid(), 'set filter compiler flags', $request->request->getBoolean('value') ? 'yes' : 'no'); return $this->redirectToRoute('jury_language', ['langId' => $langId]); } diff --git a/webapp/src/Controller/Jury/ProblemController.php b/webapp/src/Controller/Jury/ProblemController.php index e10cc3f6be..541a0dacc1 100644 --- a/webapp/src/Controller/Jury/ProblemController.php +++ b/webapp/src/Controller/Jury/ProblemController.php @@ -433,10 +433,10 @@ public function exportAction(int $problemId): StreamedResponse * @throws NoResultException * @throws NonUniqueResultException */ - #[Route(path: '/{probId<\d+>}', name: 'jury_problem')] - public function viewAction(Request $request, SubmissionService $submissionService, int $probId): Response + #[Route(path: '/{probId}', name: 'jury_problem')] + public function viewAction(Request $request, SubmissionService $submissionService, string $probId): Response { - $problem = $this->em->getRepository(Problem::class)->find($probId); + $problem = $this->em->getRepository(Problem::class)->findByExternalId($probId); if (!$problem) { throw new NotFoundHttpException(sprintf('Problem with ID %s not found', $probId)); } @@ -637,7 +637,7 @@ public function testcasesAction(Request $request, int $probId): Response } } - $this->dj->auditlog('testcase', $probId, 'updated', + $this->dj->auditlog('testcase', $problem->getExternalid(), 'updated', sprintf('%s rank %d', $type, $rank)); $message = sprintf('Updated %s for testcase %d with file %s (%s)', @@ -739,7 +739,7 @@ public function testcasesAction(Request $request, int $probId): Response } $this->em->persist($newTestcase); - $this->dj->auditlog('testcase', $probId, 'added', sprintf("rank %d", $maxrank)); + $this->dj->auditlog('testcase', $problem->getExternalid(), 'added', sprintf("rank %d", $maxrank)); $inFile = $request->files->get('add_input'); $outFile = $request->files->get('add_output'); @@ -866,7 +866,7 @@ public function moveTestcaseAction(int $probId, int $rank, string $direction): R $other->setRank($currentRank); }); - $this->dj->auditlog('testcase', $probId, 'switch rank', + $this->dj->auditlog('testcase', $problem->getExternalid(), 'switch rank', sprintf("%d <=> %d", $current->getRank(), $other->getRank())); } @@ -981,7 +981,7 @@ public function editAction(Request $request, int $probId): Response if ($this->importProblemService->importZippedProblem( $zip, $clientName, $problem, $contest, $messages )) { - $this->dj->auditlog('problem', $problem->getProbid(), 'upload zip', $clientName); + $this->dj->auditlog('problem', $problem->getExternalid(), 'upload zip', $clientName); } else { $this->postMessages($messages); return $this->redirectToRoute('jury_problem', ['probId' => $problem->getProbid()]); @@ -1220,7 +1220,7 @@ public function toggleSubmitAction( } $this->em->flush(); - $id = [$contestProblem->getCid(), $contestProblem->getProbid()]; + $id = [$contestProblem->getExternalId(), $contestProblem->getExternalId()]; $this->dj->auditlog('contest_problem', implode(', ', $id), $label, $value ? 'yes' : 'no'); return $this->redirectToLocalReferrer($router, $request, $this->generateUrl('jury_problems')); } diff --git a/webapp/src/Controller/Jury/SubmissionController.php b/webapp/src/Controller/Jury/SubmissionController.php index 3097765723..ce3fd24bac 100644 --- a/webapp/src/Controller/Jury/SubmissionController.php +++ b/webapp/src/Controller/Jury/SubmissionController.php @@ -1118,7 +1118,7 @@ public function updateStatusAction( // FIXME: We should also delete/recreate any dependent judging(runs). $eventLogService->log('submission', $submission->getSubmitid(), ($valid ? 'create' : 'delete'), $submission->getContest()->getCid(), null, null, $valid); - $this->dj->auditlog('submission', $submission->getSubmitid(), + $this->dj->auditlog('submission', $submission->getExternalid(), 'marked ' . ($valid ? 'valid' : 'invalid')); $contest = $this->em->getRepository(Contest::class)->find($contestId); $team = $this->em->getRepository(Team::class)->find($teamId); @@ -1155,7 +1155,7 @@ public function verifyAction( ->setVerifyComment($comment); $this->em->flush(); - $this->dj->auditlog('judging', $judging->getJudgingid(), + $this->dj->auditlog('judging', (string)$judging->getJudgingid(), $verified ? 'set verified' : 'set unverified'); if ((bool)$this->config->get('verification_required')) { @@ -1221,7 +1221,7 @@ public function verifyShadowDifferenceAction( ->setVerifyComment($comment); $this->em->flush(); - $this->dj->auditlog('external_judgement', $judgement->getExtjudgementid(), + $this->dj->auditlog('external_judgement', (string)$judgement->getExtjudgementid(), $verified ? 'set verified' : 'set unverified'); }); @@ -1267,7 +1267,7 @@ protected function processClaim( $auditLogType = 'judging'; $auditLogId = $judging->getJudgingid(); } - $this->dj->auditlog($auditLogType, $auditLogId, $action . 'ed'); + $this->dj->auditlog($auditLogType, (string)$auditLogId, $action . 'ed'); if ($action === 'claim') { return $this->redirectToRoute('jury_submission', ['submitId' => $judging->getSubmission()->getSubmitid()]); diff --git a/webapp/src/Controller/Jury/TeamController.php b/webapp/src/Controller/Jury/TeamController.php index 9798bab14b..aa2fbc9e46 100644 --- a/webapp/src/Controller/Jury/TeamController.php +++ b/webapp/src/Controller/Jury/TeamController.php @@ -227,16 +227,16 @@ public function indexAction(): Response ]); } - #[Route(path: '/{teamId<\d+>}', name: 'jury_team')] + #[Route(path: '/{teamId}', name: 'jury_team')] public function viewAction( Request $request, - int $teamId, + string $teamId, ScoreboardService $scoreboardService, SubmissionService $submissionService, #[MapQueryParameter] - ?int $cid = null, + ?string $cid = null, ): Response { - $team = $this->em->getRepository(Team::class)->find($teamId); + $team = $this->em->getRepository(Team::class)->findByExternalId($teamId); if (!$team) { throw new NotFoundHttpException(sprintf('Team with ID %s not found', $teamId)); } @@ -294,7 +294,7 @@ public function viewAction( } $restrictionText = implode(', ', $restrictionTexts); } - $restrictions->teamId = $teamId; + $restrictions->teamId = $team->getTeamid(); [$submissions, $submissionCounts] = $submissionService->getSubmissionList( $this->dj->getCurrentContests(honorCookie: true), @@ -370,7 +370,7 @@ public function deleteMultipleAction(Request $request): Response } #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/add', name: 'jury_team_add')] + #[Route(path: '/add', name: 'jury_team_add', priority: 1)] public function addAction(Request $request): Response { $team = new Team(); diff --git a/webapp/src/Controller/Jury/UserController.php b/webapp/src/Controller/Jury/UserController.php index f696d82ae1..7ee386bc9a 100644 --- a/webapp/src/Controller/Jury/UserController.php +++ b/webapp/src/Controller/Jury/UserController.php @@ -175,10 +175,10 @@ public function indexAction(): Response ]); } - #[Route(path: '/{userId<\d+>}', name: 'jury_user')] - public function viewAction(Request $request, int $userId, SubmissionService $submissionService): Response + #[Route(path: '/{userId}', name: 'jury_user')] + public function viewAction(Request $request, string $userId, SubmissionService $submissionService): Response { - $user = $this->em->getRepository(User::class)->find($userId); + $user = $this->em->getRepository(User::class)->findByExternalId($userId); if (!$user) { throw new NotFoundHttpException(sprintf('User with ID %s not found', $userId)); } @@ -345,7 +345,7 @@ public function generatePasswordsAction(Request $request): Response if ($doit) { $newpass = Utils::generatePassword(false); $user->setPlainPassword($newpass); - $this->dj->auditlog('user', $user->getUserid(), 'set password'); + $this->dj->auditlog('user', $user->getExternalid(), 'set password'); $changes[] = [ 'type' => $role, 'fullname' => $user->getName(), diff --git a/webapp/src/Controller/Team/ClarificationController.php b/webapp/src/Controller/Team/ClarificationController.php index b02dfb1576..d1d4771bf3 100644 --- a/webapp/src/Controller/Team/ClarificationController.php +++ b/webapp/src/Controller/Team/ClarificationController.php @@ -255,8 +255,8 @@ private function newClarificationHelper( $this->em->persist($newClarification); $this->em->flush(); - $this->dj->auditlog('clarification', $newClarification->getClarid(), 'added', null, null, - $contest->getCid()); + $this->dj->auditlog('clarification', $newClarification->getExternalid(), 'added', null, null, + $contest->getExternalid()); $this->eventLogService->log('clarification', $newClarification->getClarid(), 'create', $contest->getCid()); $this->addFlash('success', 'Clarification sent to the jury'); diff --git a/webapp/src/Controller/Team/ScoreboardController.php b/webapp/src/Controller/Team/ScoreboardController.php index c22325471a..ca90b607b0 100644 --- a/webapp/src/Controller/Team/ScoreboardController.php +++ b/webapp/src/Controller/Team/ScoreboardController.php @@ -67,7 +67,7 @@ public function teamAction(Request $request, string $teamId): Response } $team = $this->em->getRepository(Team::class)->findByExternalId($teamId); - if ($team && $team->getCategory() && !$team->getCategory()->getVisible() && $teamId !== $this->dj->getUser()->getTeamId()) { + if ($team && $team->getCategory() && !$team->getCategory()->getVisible() && $teamId !== $this->dj->getUser()->getTeam()->getExternalid()) { $team = null; } $showFlags = (bool)$this->config->get('show_flags'); diff --git a/webapp/src/Entity/AuditLog.php b/webapp/src/Entity/AuditLog.php index 416620d9bf..922e556632 100644 --- a/webapp/src/Entity/AuditLog.php +++ b/webapp/src/Entity/AuditLog.php @@ -33,9 +33,9 @@ class AuditLog #[ORM\Column( nullable: true, - options: ['comment' => 'Contest ID associated to this entry', 'unsigned' => true] + options: ['comment' => 'External contest ID associated to this entry', 'unsigned' => true] )] - private ?int $cid = null; + private ?string $cid = null; #[ORM\Column( nullable: true, @@ -52,7 +52,7 @@ class AuditLog #[ORM\Column( length: 64, nullable: true, - options: ['comment' => 'Identifier in reference table'] + options: ['comment' => '(External) identifier in reference table'] )] private ?string $dataid = null; @@ -81,13 +81,13 @@ public function getLogtime(): string|float return $this->logtime; } - public function setCid(?int $cid): AuditLog + public function setCid(?string $cid): AuditLog { $this->cid = $cid; return $this; } - public function getCid(): ?int + public function getCid(): ?string { return $this->cid; } diff --git a/webapp/src/Entity/Contest.php b/webapp/src/Entity/Contest.php index 75dd6c7b25..a237143ea3 100644 --- a/webapp/src/Entity/Contest.php +++ b/webapp/src/Entity/Contest.php @@ -6,6 +6,7 @@ use App\DataTransferObject\ContestState; use App\DataTransferObject\FileWithName; use App\DataTransferObject\ImageFile; +use App\Repository\ContestRepository; use App\Utils\FreezeData; use App\Utils\Utils; use App\Validator\Constraints\Identifier; @@ -28,7 +29,7 @@ /** * Contests that will be run with this install. */ -#[ORM\Entity] +#[ORM\Entity(repositoryClass: ContestRepository::class)] #[ORM\Table(options: [ 'collation' => 'utf8mb4_unicode_ci', 'charset' => 'utf8mb4', diff --git a/webapp/src/Entity/User.php b/webapp/src/Entity/User.php index e829805659..9f3f7724f2 100644 --- a/webapp/src/Entity/User.php +++ b/webapp/src/Entity/User.php @@ -2,6 +2,7 @@ namespace App\Entity; use App\Controller\API\AbstractRestController as ARC; +use App\Repository\UserRepository; use App\Utils\Utils; use DateTime; use Doctrine\Common\Collections\ArrayCollection; @@ -18,7 +19,7 @@ /** * Users that have access to DOMjudge. */ -#[ORM\Entity] +#[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\Table(options: [ 'collation' => 'utf8mb4_unicode_ci', 'charset' => 'utf8mb4', diff --git a/webapp/src/Repository/ContestRepository.php b/webapp/src/Repository/ContestRepository.php new file mode 100644 index 0000000000..cf564357fe --- /dev/null +++ b/webapp/src/Repository/ContestRepository.php @@ -0,0 +1,17 @@ + + */ +class ContestRepository extends EntityRepository +{ + /** + * @use FindByExternalidTrait + */ + use FindByExternalidTrait; +} diff --git a/webapp/src/Repository/TeamRepository.php b/webapp/src/Repository/TeamRepository.php index 53f94b61e2..4c0f46de49 100644 --- a/webapp/src/Repository/TeamRepository.php +++ b/webapp/src/Repository/TeamRepository.php @@ -2,7 +2,6 @@ namespace App\Repository; -use App\Entity\Language; use App\Entity\Team; use Doctrine\ORM\EntityRepository; diff --git a/webapp/src/Repository/UserRepository.php b/webapp/src/Repository/UserRepository.php new file mode 100644 index 0000000000..5ddde5aefd --- /dev/null +++ b/webapp/src/Repository/UserRepository.php @@ -0,0 +1,17 @@ + + */ +class UserRepository extends EntityRepository +{ + /** + * @use FindByExternalidTrait + */ + use FindByExternalidTrait; +} diff --git a/webapp/src/Security/UserStateUpdater.php b/webapp/src/Security/UserStateUpdater.php index 981e9e079b..89ab53eaa5 100644 --- a/webapp/src/Security/UserStateUpdater.php +++ b/webapp/src/Security/UserStateUpdater.php @@ -47,7 +47,7 @@ public function updateUserState(AuthenticationSuccessEvent $event): void // Otherwise, we would log every API call and we do not want that. if ($firewallName === 'main') { $ip = $this->requestStack->getMainRequest()->getClientIp(); - $this->dj->auditlog('user', $user->getUserid(), 'logged on on ' . $ip, null, $user->getUserName()); + $this->dj->auditlog('user', $user->getExternalid(), 'logged on on ' . $ip, null, $user->getUserName()); } } } diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index 134a991603..e942f88ccb 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -150,7 +150,7 @@ public function getCurrentContests( } $now = Utils::now(); $qb = $this->em->createQueryBuilder(); - $qb->select('c')->from(Contest::class, 'c', 'c.cid'); + $qb->select('c')->from(Contest::class, 'c', 'c.externalid'); if (isset($onlyOfTeam)) { $qb->leftJoin('c.teams', 'ct') ->leftJoin('c.team_categories', 'tc') @@ -504,11 +504,11 @@ public function withAllRoles(callable $callable, ?UserInterface $user = null): v */ public function auditlog( string $datatype, - mixed $dataid, + string|null $dataid, string $action, mixed $extraInfo = null, ?string $forceUsername = null, - string|int|null $cid = null + string|null $cid = null ): void { if (!empty($forceUsername)) { $user = $forceUsername; @@ -516,10 +516,6 @@ public function auditlog( $user = $this->getUser() ? $this->getUser()->getUsername() : null; } - if (gettype($cid) == 'string') { - $cid = (int) $cid; - } - $auditLog = new AuditLog(); $auditLog ->setLogtime(Utils::now()) diff --git a/webapp/src/Service/ImportExportService.php b/webapp/src/Service/ImportExportService.php index c4110ab2ea..89361a26ba 100644 --- a/webapp/src/Service/ImportExportService.php +++ b/webapp/src/Service/ImportExportService.php @@ -894,7 +894,7 @@ protected function importGroupData( foreach ($allCategories as $category) { $this->em->persist($category); $this->em->flush(); - $this->dj->auditlog('team_category', $category->getCategoryid(), 'replaced', + $this->dj->auditlog('team_category', $category->getExternalid(), 'replaced', 'imported from tsv / json'); } @@ -1004,7 +1004,7 @@ protected function importOrganizationData( foreach ($allOrganizations as $organization) { $this->em->persist($organization); $this->em->flush(); - $this->dj->auditlog('team_affiliation', $organization->getAffilid(), 'replaced', + $this->dj->auditlog('team_affiliation', $organization->getExternalid(), 'replaced', 'imported from tsv / json'); } @@ -1370,21 +1370,21 @@ protected function importTeamData(array $teamData, ?string &$message, ?array &$s $this->em->persist($affiliation); $this->em->flush(); $this->dj->auditlog('team_affiliation', - $affiliation->getAffilid(), + $affiliation->getExternalid(), 'added', 'imported from tsv / json'); } foreach ($createdCategories as $category) { $this->em->persist($category); $this->em->flush(); - $this->dj->auditlog('team_category', $category->getCategoryid(), + $this->dj->auditlog('team_category', $category->getExternalid(), 'added', 'imported from tsv'); } foreach ($allTeams as $team) { $this->em->persist($team); $this->em->flush(); - $this->dj->auditlog('team', $team->getTeamid(), 'replaced', 'imported from tsv'); + $this->dj->auditlog('team', $team->getExternalid(), 'replaced', 'imported from tsv'); } if ($contest = $this->dj->getCurrentContest()) { @@ -1481,7 +1481,7 @@ protected function importAccountData( ->setExternalid((string)$teamId) ->setName($teamId . ' - auto-create during import'); $this->em->persist($team); - $this->dj->auditlog('team', $team->getTeamid(), + $this->dj->auditlog('team', $team->getExternalid(), 'added', 'imported from tsv'); } } @@ -1532,14 +1532,15 @@ protected function importAccountData( $this->em->flush(); foreach ($allUsers as $user) { - $this->dj->auditlog('user', $user->getUserid(), 'replaced', 'imported from tsv'); + $this->dj->auditlog('user', $user->getExternalid(), 'replaced', 'imported from tsv'); } if ($contest = $this->dj->getCurrentContest()) { foreach ($newTeams as $newTeam) { + /** @var Team $team */ $team = $newTeam['team']; $action = $newTeam['action']; - $this->dj->auditlog('team', $team->getTeamid(), 'replaced', + $this->dj->auditlog('team', $team->getExternalid(), 'replaced', 'imported from tsv, autocreated for judge'); $this->eventLogService->log('team', $team->getTeamid(), $action, $contest->getCid()); } diff --git a/webapp/src/Service/ImportProblemService.php b/webapp/src/Service/ImportProblemService.php index 929285b210..840759d717 100644 --- a/webapp/src/Service/ImportProblemService.php +++ b/webapp/src/Service/ImportProblemService.php @@ -883,7 +883,7 @@ public function importProblemFromRequest(Request $request, ?int $contestId = nul ); $allMessages = array_merge($allMessages, $messages); if ($newProblem) { - $this->dj->auditlog('problem', $newProblem->getProbid(), 'upload zip', $clientName); + $this->dj->auditlog('problem', $newProblem->getExternalid(), 'upload zip', $clientName); $probId = $newProblem->getExternalid(); } else { $errors = array_merge($errors, $messages); diff --git a/webapp/src/Service/RejudgingService.php b/webapp/src/Service/RejudgingService.php index 629dfc3418..5cf79ae67b 100644 --- a/webapp/src/Service/RejudgingService.php +++ b/webapp/src/Service/RejudgingService.php @@ -232,7 +232,7 @@ public function finishRejudging(Rejudging $rejudging, string $action, ?callable ->getQuery() ->getResult(); - $this->dj->auditlog('rejudging', $rejudgingId, $action . 'ing rejudge', '(start)'); + $this->dj->auditlog('rejudging', (string)$rejudgingId, $action . 'ing rejudge', '(start)'); // Add missing state events for all active contests. We do this here // and disable doing it in the loop when calling EventLogService::log, @@ -400,7 +400,7 @@ public function finishRejudging(Rejudging $rejudging, string $action, ?callable ->setValid($action === self::ACTION_APPLY); $this->em->flush(); - $this->dj->auditlog('rejudging', $rejudgingId, $action . 'ing rejudge', '(end)'); + $this->dj->auditlog('rejudging', (string)$rejudgingId, $action . 'ing rejudge', '(end)'); return true; } diff --git a/webapp/src/Service/ScoreboardService.php b/webapp/src/Service/ScoreboardService.php index eec5a835ce..67a95931ac 100644 --- a/webapp/src/Service/ScoreboardService.php +++ b/webapp/src/Service/ScoreboardService.php @@ -606,7 +606,7 @@ public function refreshCache(Contest $contest, ?callable $progressReporter = nul { Utils::extendMaxExecutionTime(300); - $this->dj->auditlog('contest', $contest->getCid(), 'refresh scoreboard cache'); + $this->dj->auditlog('contest', $contest->getExternalid(), 'refresh scoreboard cache'); if ($progressReporter === null) { $progressReporter = static function (int $progress, string $log, ?string $message = null) { diff --git a/webapp/src/Service/SubmissionService.php b/webapp/src/Service/SubmissionService.php index b303005066..8275154350 100644 --- a/webapp/src/Service/SubmissionService.php +++ b/webapp/src/Service/SubmissionService.php @@ -770,8 +770,8 @@ public function submitSolution( $submission->getSubmitid(), $team->getTeamid(), $language->getLangid(), $problem->getProblem()->getProbid())); - $this->dj->auditlog('submission', $submission->getSubmitid(), 'added', - 'via ' . $source->value, null, $contest->getCid()); + $this->dj->auditlog('submission', $submission->getExternalid(), 'added', + 'via ' . $source->value, null, $contest->getExternalid()); if (Utils::difftime((float)$contest->getEndtime(), $submitTime) <= 0) { $this->logger->info( From 3c8895dc5889525722ea4071588038520e126b76 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Sun, 26 Oct 2025 11:14:24 +0100 Subject: [PATCH 06/32] Change jury balloon pages to use external IDs instead of internal IDs Part of #3024 --- webapp/src/Controller/Jury/BalloonController.php | 10 +++++----- webapp/src/Service/BalloonService.php | 8 ++++---- webapp/templates/jury/balloons.html.twig | 10 +++++----- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/webapp/src/Controller/Jury/BalloonController.php b/webapp/src/Controller/Jury/BalloonController.php index 9098772310..1520e54c2f 100644 --- a/webapp/src/Controller/Jury/BalloonController.php +++ b/webapp/src/Controller/Jury/BalloonController.php @@ -88,7 +88,7 @@ public function indexAction(BalloonService $balloonService): Response $filteredAffiliations = $this->em->createQueryBuilder() ->from(TeamAffiliation::class, 'a') ->select('a') - ->where('a.affilid IN (:affilIds)') + ->where('a.externalid IN (:affilIds)') ->setParameter('affilIds', $filters['affiliation-id']) ->getQuery() ->getResult(); @@ -123,7 +123,7 @@ public function indexAction(BalloonService $balloonService): Response $filteredCategories = $this->em->createQueryBuilder() ->from(TeamCategory::class, 'c') ->select('c') - ->where('c.categoryid IN (:categories)') + ->where('c.externalid IN (:categories)') ->setParameter('categories', $filters['category-id']) ->getQuery() ->getResult(); @@ -131,7 +131,7 @@ public function indexAction(BalloonService $balloonService): Response $availableCategories = $this->em->createQueryBuilder() ->from(TeamCategory::class, 'c') ->select('c') - ->where('c.categoryid NOT IN (:categories)') + ->where('c.externalid NOT IN (:categories)') ->setParameter('categories', $filters['category-id']) ->getQuery() ->getResult(); @@ -145,11 +145,11 @@ public function indexAction(BalloonService $balloonService): Response } $defaultCategories = $this->em->createQueryBuilder() ->from(TeamCategory::class, 'c') - ->select('c.categoryid') + ->select('c.externalid') ->where('c.visible = true') ->getQuery() ->getArrayResult(); - $defaultCategories = array_column($defaultCategories, "categoryid"); + $defaultCategories = array_column($defaultCategories, "externalid"); return $this->render('jury/balloons.html.twig', [ 'refresh' => [ diff --git a/webapp/src/Service/BalloonService.php b/webapp/src/Service/BalloonService.php index eb80ffe0f7..20e30c784a 100644 --- a/webapp/src/Service/BalloonService.php +++ b/webapp/src/Service/BalloonService.php @@ -99,11 +99,11 @@ public function collectBalloonTable(Contest $contest, bool $todo = false): array $query = $em->createQueryBuilder() ->select('b', 's.submittime', 'p.probid', - 't.teamid', 's', 't', 't.location', - 'c.categoryid AS categoryid', 'c.name AS catname', - 'co.cid', 'co.shortname', + 't.externalid as teamid', 's', 't', 't.location', + 'c.externalid AS categoryid', 'c.name AS catname', + 'co.externalid as cid', 'co.shortname', 'cp.shortname AS probshortname', 'cp.color', - 'a.affilid AS affilid', 'a.shortname AS affilshort') + 'a.externalid AS affilid', 'a.shortname AS affilshort') ->from(Balloon::class, 'b') ->leftJoin('b.submission', 's') ->leftJoin('b.problem', 'p') diff --git a/webapp/templates/jury/balloons.html.twig b/webapp/templates/jury/balloons.html.twig index 38d87c88fe..6a14c36ebb 100644 --- a/webapp/templates/jury/balloons.html.twig +++ b/webapp/templates/jury/balloons.html.twig @@ -37,8 +37,8 @@ @@ -54,12 +54,12 @@ {% for contest,subject in subjects %} @@ -78,7 +75,7 @@
    To: {% if clar.to_team is defined %} - {{ clar.to_teamname }} {{ clar.to_team | entityIdBadge('t') }} + {{ clar.to_teamname }} {{ clar.to_team | entityIdBadge('t') }} {% elseif clar.from_team is defined %} Jury {% else %} @@ -92,12 +89,12 @@ {{ clar.queue }} - -
    + + @@ -126,7 +123,7 @@
    - + {% if claimed %} {% if origclar.jurymember_is_me %} @@ -142,7 +139,7 @@
    - + {% if origclar.answered %} @@ -108,12 +105,12 @@ {% if queues | length > 1 %}
    - + {% for qk,qv in queues %} - + {% endfor %}
    From fefba55fc24a55dde660d3936e8322be319a3274 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Sun, 26 Oct 2025 15:58:25 +0100 Subject: [PATCH 08/32] Change config checker to use external IDs instead of internal IDs Part of #3024 --- webapp/src/Service/CheckConfigService.php | 27 ++++++++++++----------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/webapp/src/Service/CheckConfigService.php b/webapp/src/Service/CheckConfigService.php index 3c0dacf18d..e722c0fa1f 100644 --- a/webapp/src/Service/CheckConfigService.php +++ b/webapp/src/Service/CheckConfigService.php @@ -107,8 +107,8 @@ public function runAll(): array $this->stopwatch->stopSection('Teams'); $this->stopwatch->openSection(); - $results['External identifiers'] = $this->checkAllExternalIdentifiers(); - $this->stopwatch->stopSection('External identifiers'); + $results['Identifiers'] = $this->checkAllExternalIdentifiers(); + $this->stopwatch->stopSection('Identifiers'); return $results; } @@ -491,7 +491,7 @@ public function checkContestActive(): ConfigCheckItem $this->stopwatch->stop(__FUNCTION__); $desc = ''; foreach ($contests as $contest) { - $desc .= ' - c' . $contest->getCid() . ' (' . $contest->getShortname() . ")\n"; + $desc .= ' - `' . $contest->getExternalid() . '` (' . $contest->getShortname() . ")\n"; } return new ConfigCheckItem( caption: 'Active contests', @@ -509,7 +509,7 @@ public function checkContestsValidate(): ConfigCheckItem $contesterrors = $cperrors = []; $result = 'O'; foreach ($contests as $contest) { - $cid = $contest->getCid(); + $cid = $contest->getExternalid(); $errors = $this->validator->validate($contest); if (count($errors)) { $result = 'E'; @@ -527,7 +527,7 @@ public function checkContestsValidate(): ConfigCheckItem $desc = ''; foreach ($contesterrors as $cid => $errors) { - $desc .= "Contest: c$cid: "; + $desc .= "Contest: `$cid`: "; if (count($errors) == 0 && empty($cperrors[$cid])) { $desc .= "no errors\n"; } else { @@ -559,7 +559,7 @@ public function checkContestBanners(): ConfigCheckItem foreach ($contests as $contest) { if ($cid = $contest->getExternalid()) { $bannerpath = $this->dj->assetPath($cid, 'contest', true); - $contestName = 'c' . $contest->getCid() . ' (' . $contest->getShortname() . ')'; + $contestName = $contest->getExternalid() . ' (' . $contest->getShortname() . ')'; if ($bannerpath) { if (($filesize = filesize($bannerpath)) > 2 * 1024 * 1024) { $result = 'W'; @@ -598,7 +598,7 @@ public function checkProblemsValidate(): ConfigCheckItem $problemerrors = $moreproblemerrors = []; $result = 'O'; foreach ($problems as $problem) { - $probid = $problem->getProbid(); + $probid = $problem->getExternalid(); $errors = $this->validator->validate($problem); if (count($errors)) { $result = 'E'; @@ -638,20 +638,21 @@ public function checkProblemsValidate(): ConfigCheckItem ->select('tc.testcaseid', 'tc.ranknumber', 'length(tcc.output) as output_size' ) ->from(Testcase::class, 'tc') ->join('tc.content', 'tcc') - ->andWhere('tc.problem = :probid') + ->join('tc.problem', 'p') + ->andWhere('p.externalid = :probid') ->setParameter('probid', $probid) ->getQuery() ->getResult(); if (count($tcs_size) === 0) { $result = 'E'; - $moreproblemerrors[$probid] .= sprintf(" - No testcases for `p%s`\n", $probid); + $moreproblemerrors[$probid] .= sprintf(" - No testcases for `%s`\n", $probid); } else { $problem_output_limit = 1024 * ($problem->getOutputLimit() ?: $output_limit); foreach ($tcs_size as $row) { if ($row['output_size'] > $problem_output_limit) { $result = 'E'; $moreproblemerrors[$probid] .= sprintf( - " - Testcase `%s` for `p%s` exceeds output limit of `%s`\n", + " - Testcase `%s` for `%s` exceeds output limit of `%s`\n", $row['rank'], $probid, $problem_output_limit ); } @@ -662,7 +663,7 @@ public function checkProblemsValidate(): ConfigCheckItem if (!$contestProblem->getAllowJudge()) { $result = 'E'; $moreproblemerrors[$probid] .= sprintf( - " - `p%s` is disabled in contest `%s`\n", + " - `%s` is disabled in contest `%s`\n", $probid, $contestProblem->getContest()->getName() ); } @@ -671,7 +672,7 @@ public function checkProblemsValidate(): ConfigCheckItem $desc = ''; foreach ($problemerrors as $probid => $errors) { - $desc .= " - Problem `p$probid`:\n"; + $desc .= " - Problem `$probid`:\n"; if (count($errors) > 0 || !empty($moreproblemerrors[$probid])) { /* @phpstan-ignore-next-line */ $desc .= (string)$errors . " " . @@ -969,7 +970,7 @@ protected function checkExternalIdentifiers(string $class): ConfigCheckItem $getter = sprintf('get%s', ucfirst($column)); $routeParams[$param] = $entity->{$getter}(); } - $description .= sprintf(" - [%s %s](%s) does not have an external ID\n", + $description .= sprintf(" - [%s %s](%s) does not have an ID\n", ucfirst(str_replace('_', ' ', $inflector->tableize($entityType))), htmlspecialchars(implode(', ', $metadata->getIdentifierValues($entity))), $this->router->generate($route, $routeParams) From ea354b6a36d23b52c25c1b716260f4a630487740 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Sun, 26 Oct 2025 16:28:26 +0100 Subject: [PATCH 09/32] Change jury contest pages to use external IDs instead of internal IDs Part of #3024 --- .../src/Controller/Jury/ContestController.php | 125 +++++++++--------- .../Controller/Jury/JudgeRemainingTrait.php | 21 +-- .../Controller/Jury/LanguageController.php | 2 +- .../src/Controller/Jury/ProblemController.php | 6 +- .../Controller/Jury/RejudgingController.php | 3 +- .../Jury/TeamCategoryController.php | 6 +- .../Type/AbstractExternalIdEntityType.php | 2 +- webapp/src/Form/Type/ContestProblemType.php | 3 +- webapp/src/Form/Type/ContestType.php | 6 +- webapp/templates/jury/contest.html.twig | 57 ++++---- webapp/templates/jury/contest_edit.html.twig | 2 +- .../templates/jury/contest_finalize.html.twig | 4 +- webapp/templates/jury/contests.html.twig | 10 +- .../jury/partials/contest_toggle.html.twig | 2 +- .../jury/partials/problem_toggle.html.twig | 2 +- 15 files changed, 124 insertions(+), 127 deletions(-) diff --git a/webapp/src/Controller/Jury/ContestController.php b/webapp/src/Controller/Jury/ContestController.php index 50d441a8b0..36a1e0dfb3 100644 --- a/webapp/src/Controller/Jury/ContestController.php +++ b/webapp/src/Controller/Jury/ContestController.php @@ -81,8 +81,7 @@ public function indexAction(Request $request): Response ->getQuery()->getResult(); $table_fields = [ - 'cid' => ['title' => 'CID', 'sort' => true], - 'externalid' => ['title' => "external ID", 'sort' => true], + 'externalid' => ['title' => "ID", 'sort' => true], 'shortname' => ['title' => 'shortname', 'sort' => true], 'name' => ['title' => 'name', 'sort' => true], 'scoreboard_type' => ['title' => 'scoreboard type', 'sort' => true], @@ -151,7 +150,7 @@ public function indexAction(Request $request): Response 'icon' => 'file-' . $contest->getContestProblemsetType(), 'title' => 'view contest problemset document', 'link' => $this->generateUrl('jury_contest_problemset', [ - 'cid' => $contest->getCid(), + 'cid' => $contest->getExternalid(), ]) ]; } else { @@ -170,14 +169,14 @@ public function indexAction(Request $request): Response 'icon' => 'edit', 'title' => 'edit this contest', 'link' => $this->generateUrl('jury_contest_edit', [ - 'contestId' => $contest->getCid(), + 'contestId' => $contest->getExternalid(), ]) ]; $contestactions[] = [ 'icon' => 'trash-alt', 'title' => 'delete this contest', 'link' => $this->generateUrl('jury_contest_delete', [ - 'contestId' => $contest->getCid(), + 'contestId' => $contest->getExternalid(), ]), 'ajaxModal' => true, ]; @@ -275,13 +274,13 @@ public function indexAction(Request $request): Response if (!$contest->getEnabled()) { $styles[] = 'disabled'; } - if (in_array($contest->getCid(), array_keys($currentContests))) { + if (in_array($contest->getExternalid(), array_keys($currentContests))) { $styles[] = 'highlight'; } $contests_table[] = [ 'data' => $contestdata, 'actions' => $contestactions, - 'link' => $this->generateUrl('jury_contest', ['contestId' => $contest->getCid()]), + 'link' => $this->generateUrl('jury_contest', ['contestId' => $contest->getExternalid()]), 'cssclass' => implode(' ', $styles), ]; } @@ -368,7 +367,7 @@ public function toggleSubmitAction( string $contestId, string $type ): Response { - $contest = $this->em->getRepository(Contest::class)->find($contestId); + $contest = $this->em->getRepository(Contest::class)->findByExternalId($contestId); if (!$contest) { throw new NotFoundHttpException(sprintf('Contest with ID %s not found', $contestId)); } @@ -409,10 +408,10 @@ public function toggleSubmitAction( ); } - #[Route(path: '/{contestId<\d+>}/remove-interval/{intervalId}', name: 'jury_contest_remove_interval', methods: ['POST'])] - public function removeIntervalAction(int $contestId, int $intervalId): RedirectResponse + #[Route(path: '/{contestId}/remove-interval/{intervalId}', name: 'jury_contest_remove_interval', methods: ['POST'])] + public function removeIntervalAction(string $contestId, int $intervalId): RedirectResponse { - $contest = $this->em->getRepository(Contest::class)->find($contestId); + $contest = $this->em->getRepository(Contest::class)->findByExternalId($contestId); if (!$contest) { throw new NotFoundHttpException(sprintf('Contest with ID %s not found', $contestId)); } @@ -437,14 +436,14 @@ public function removeIntervalAction(int $contestId, int $intervalId): RedirectR $this->addFlash('scoreboard_refresh', 'After removing a removed time interval, it is ' . 'necessary to recalculate any cached scoreboards.'); - return $this->redirectToRoute('jury_contest', ['contestId' => $contest->getCid()]); + return $this->redirectToRoute('jury_contest', ['contestId' => $contest->getExternalid()]); } #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/{contestId<\d+>}/edit', name: 'jury_contest_edit')] - public function editAction(Request $request, int $contestId): Response + #[Route(path: '/{contestId}/edit', name: 'jury_contest_edit')] + public function editAction(Request $request, string $contestId): Response { - $contest = $this->em->getRepository(Contest::class)->find($contestId); + $contest = $this->em->getRepository(Contest::class)->findByExternalId($contestId); if (!$contest) { throw new NotFoundHttpException(sprintf('Contest with ID %s not found', $contestId)); } @@ -578,10 +577,10 @@ public function editAction(Request $request, int $contestId): Response } #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/{contestId<\d+>}/delete', name: 'jury_contest_delete')] - public function deleteAction(Request $request, int $contestId): Response + #[Route(path: '/{contestId}/delete', name: 'jury_contest_delete')] + public function deleteAction(Request $request, string $contestId): Response { - $contest = $this->em->getRepository(Contest::class)->find($contestId); + $contest = $this->em->getRepository(Contest::class)->findByExternalId($contestId); if (!$contest) { throw new NotFoundHttpException(sprintf('Contest with ID %s not found', $contestId)); } @@ -595,13 +594,10 @@ public function deleteAction(Request $request, int $contestId): Response } #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/{contestId<\d+>}/problems/{probId<\d+>}/delete', name: 'jury_contest_problem_delete')] - public function deleteProblemAction(Request $request, int $contestId, int $probId): Response + #[Route(path: '/{contestId}/problems/{probId}/delete', name: 'jury_contest_problem_delete')] + public function deleteProblemAction(Request $request, string $contestId, string $probId): Response { - $contestProblem = $this->em->getRepository(ContestProblem::class)->find([ - 'contest' => $contestId, - 'problem' => $probId - ]); + $contestProblem = $this->em->getRepository(ContestProblem::class)->findByProblemAndContest($contestId, $probId); if (!$contestProblem) { throw new NotFoundHttpException( sprintf('Contest problem with contest ID %s and problem ID %s not found', @@ -618,7 +614,7 @@ public function deleteProblemAction(Request $request, int $contestId, int $probI } #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/add', name: 'jury_contest_add')] + #[Route(path: '/add', name: 'jury_contest_add', priority: 1)] public function addAction(Request $request): Response { $contest = new Contest(); @@ -631,7 +627,7 @@ public function addAction(Request $request): Response if ($response = $this->processAddFormForExternalIdEntity( $form, $contest, - fn () => $this->generateUrl('jury_contest', ['contestId' => $contest->getcid()]), + fn () => $this->generateUrl('jury_contest', ['contestId' => $contest->getExternalid()]), function () use ($form, $contest) { $response = $this->checkTimezones($form); if ($response !== null) { @@ -675,12 +671,12 @@ function () use ($form, $contest) { ]); } - #[Route(path: '/{contestId<\d+>}/prefetch', name: 'jury_contest_prefetch')] - public function prefetchAction(Request $request, int $contestId): Response + #[Route(path: '/{contestId}/prefetch', name: 'jury_contest_prefetch')] + public function prefetchAction(Request $request, string $contestId): Response { - $contest = $this->em->getRepository(Contest::class)->find($contestId); + $contest = $this->em->getRepository(Contest::class)->findByExternalId($contestId); if ($contest === null) { - throw new BadRequestHttpException("Contest with cid=$contestId not found."); + throw new BadRequestHttpException("Contest with ID $contestId not found."); } $judgehosts = $this->em->getRepository(Judgehost::class)->findBy([ 'enabled' => true, @@ -756,11 +752,11 @@ public function prefetchAction(Request $request, int $contestId): Response } #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/{contestId<\d+>}/finalize', name: 'jury_contest_finalize')] - public function finalizeAction(Request $request, int $contestId): Response + #[Route(path: '/{contestId}/finalize', name: 'jury_contest_finalize')] + public function finalizeAction(Request $request, string $contestId): Response { /** @var Contest $contest */ - $contest = $this->em->getRepository(Contest::class)->find($contestId); + $contest = $this->em->getRepository(Contest::class)->findByExternalId($contestId); $blockers = []; if (Utils::difftime((float)$contest->getEndtime(), Utils::now()) > 0) { $blockers[] = sprintf('Contest not ended yet (will end at %s)', @@ -771,24 +767,24 @@ public function finalizeAction(Request $request, int $contestId): Response $submissionIds = array_map(fn(array $data) => $data['submitid'], $this->em->createQueryBuilder() ->from(Submission::class, 's') ->join('s.judgings', 'j', Join::WITH, 'j.valid = 1') - ->select('s.submitid') + ->select('s.externalid') ->andWhere('s.contest = :contest') ->andWhere('s.valid = true') ->andWhere('j.result IS NULL') ->setParameter('contest', $contest) - ->orderBy('s.submitid') + ->orderBy('s.externalid') ->getQuery() ->getResult() ); if (count($submissionIds) > 0) { - $blockers[] = 'Unjudged submissions found: s' . implode(', s', $submissionIds); + $blockers[] = 'Unjudged submissions found: ' . implode(', ', $submissionIds); } /** @var int[] $clarificationIds */ $clarificationIds = array_map(fn(array $data) => $data['clarid'], $this->em->createQueryBuilder() ->from(Clarification::class, 'c') - ->select('c.clarid') + ->select('c.externalid') ->andWhere('c.contest = :contest') ->andWhere('c.answered = false') ->setParameter('contest', $contest) @@ -812,7 +808,7 @@ public function finalizeAction(Request $request, int $contestId): Response $this->em->flush(); $this->dj->auditlog('contest', $contest->getExternalid(), 'finalized', $contest->getFinalizecomment()); - return $this->redirectToRoute('jury_contest', ['contestId' => $contest->getCid()]); + return $this->redirectToRoute('jury_contest', ['contestId' => $contest->getExternalid()]); } } @@ -824,14 +820,14 @@ public function finalizeAction(Request $request, int $contestId): Response } #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/{contestId<\d+>}/{time}/doNow', name: 'jury_contest_donow')] - public function doNowAction(Request $request, int $contestId, string $time): Response + #[Route(path: '/{contestId}/{time}/doNow', name: 'jury_contest_donow')] + public function doNowAction(Request $request, string $contestId, string $time): Response { $times = ['activate', 'start', 'freeze', 'end', 'unfreeze', 'finalize', 'deactivate']; $start_actions = ['delay_start', 'resume_start']; $actions = array_merge($times, $start_actions); - $contest = $this->em->getRepository(Contest::class)->find($contestId); + $contest = $this->em->getRepository(Contest::class)->findByExternalId($contestId); if (!$contest) { throw new NotFoundHttpException(sprintf('Contest with ID %s not found', $contestId)); } @@ -841,7 +837,7 @@ public function doNowAction(Request $request, int $contestId, string $time): Res } if ($time === 'finalize') { - return $this->redirectToRoute('jury_contest_finalize', ['contestId' => $contest->getCid()]); + return $this->redirectToRoute('jury_contest_finalize', ['contestId' => $contest->getExternalid()]); } $now = (int)floor(Utils::now()); @@ -919,10 +915,10 @@ public function doNowAction(Request $request, int $contestId, string $time): Res return $this->redirectToRoute('jury_contests'); } - #[Route(path: '/{contestId<\d+>}/request-remaining', name: 'jury_contest_request_remaining')] - public function requestRemainingRunsWholeContestAction(int $contestId): RedirectResponse + #[Route(path: '/{contestId}/request-remaining', name: 'jury_contest_request_remaining')] + public function requestRemainingRunsWholeContestAction(string $contestId): RedirectResponse { - $contest = $this->em->getRepository(Contest::class)->find($contestId); + $contest = $this->em->getRepository(Contest::class)->findByExternalId($contestId); if (!$contest) { throw new NotFoundHttpException(sprintf('Contest with ID %s not found', $contestId)); } @@ -930,13 +926,10 @@ public function requestRemainingRunsWholeContestAction(int $contestId): Redirect return $this->redirectToRoute('jury_contest', ['contestId' => $contestId]); } - #[Route(path: '/{contestId<\d+>}/problems/{probId<\d+>}/request-remaining', name: 'jury_contest_problem_request_remaining')] - public function requestRemainingRunsContestProblemAction(int $contestId, int $probId): RedirectResponse + #[Route(path: '/{contestId}/problems/{probId}/request-remaining', name: 'jury_contest_problem_request_remaining')] + public function requestRemainingRunsContestProblemAction(string $contestId, string $probId): RedirectResponse { - $contestProblem = $this->em->getRepository(ContestProblem::class)->find([ - 'contest' => $contestId, - 'problem' => $probId - ]); + $contestProblem = $this->em->getRepository(ContestProblem::class)->findByProblemAndContest($contestId, $probId); if (!$contestProblem) { throw new NotFoundHttpException( sprintf('Contest problem with contest ID %s and problem ID %s not found', @@ -987,23 +980,23 @@ private function checkTimezones(FormInterface $form): ?Response } #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/{contestId<\d+>}/lock', name: 'jury_contest_lock')] - public function lockAction(Request $request, int $contestId): Response + #[Route(path: '/{contestId}/lock', name: 'jury_contest_lock')] + public function lockAction(Request $request, string $contestId): Response { return $this->doLock($contestId, true); } #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/{contestId<\d+>}/unlock', name: 'jury_contest_unlock')] - public function unlockAction(Request $request, int $contestId): Response + #[Route(path: '/{contestId}/unlock', name: 'jury_contest_unlock')] + public function unlockAction(Request $request, string $contestId): Response { return $this->doLock($contestId, false); } - #[Route(path: '/{contestId<\d+>}/samples.zip', name: 'jury_contest_samples_data_zip')] - public function samplesDataZipAction(Request $request, int $contestId): Response + #[Route(path: '/{contestId}/samples.zip', name: 'jury_contest_samples_data_zip')] + public function samplesDataZipAction(Request $request, string $contestId): Response { - $contest = $this->em->getRepository(Contest::class)->find($contestId); + $contest = $this->em->getRepository(Contest::class)->findByExternalId($contestId); if (!$contest) { throw new NotFoundHttpException(sprintf('Contest with ID %s not found', $contestId)); } @@ -1011,9 +1004,9 @@ public function samplesDataZipAction(Request $request, int $contestId): Response return $this->dj->getSamplesZipForContest($contest); } - private function doLock(int $contestId, bool $locked): Response + private function doLock(string $contestId, bool $locked): Response { - $contest = $this->em->getRepository(Contest::class)->find($contestId); + $contest = $this->em->getRepository(Contest::class)->findByExternalId($contestId); if (!$contest) { throw new NotFoundHttpException(sprintf('Contest with ID %s not found.', $contestId)); } @@ -1030,25 +1023,25 @@ private function doLock(int $contestId, bool $locked): Response return $this->redirectToRoute('jury_contest', ['contestId' => $contestId]); } - #[Route(path: '/{contestId<\d+>}/{type}-scoreboard.zip', name: 'jury_scoreboard_data_zip')] + #[Route(path: '/{contestId}/{type}-scoreboard.zip', name: 'jury_scoreboard_data_zip')] public function scoreboardDataZipAction( - int $contestId, + string $contestId, string $type, RequestStack $requestStack, Request $request, ScoreboardService $scoreboardService ): Response { - $contest = $this->em->getRepository(Contest::class)->find($contestId); + $contest = $this->em->getRepository(Contest::class)->findByExternalId($contestId); if (!$contest) { throw new NotFoundHttpException(sprintf('Contest with ID %s not found', $contestId)); } return $this->dj->getScoreboardZip($request, $requestStack, $contest, $scoreboardService, $type === 'unfrozen'); } - #[Route(path: '/{cid<\d+>}/problemset', name: 'jury_contest_problemset')] - public function viewProblemsetAction(int $cid): StreamedResponse + #[Route(path: '/{cid}/problemset', name: 'jury_contest_problemset')] + public function viewProblemsetAction(string $cid): StreamedResponse { - $contest = $this->em->getRepository(Contest::class)->find($cid); + $contest = $this->em->getRepository(Contest::class)->findByExternalId($cid); if (!$contest) { throw new NotFoundHttpException(sprintf('Contest with ID %s not found', $cid)); } diff --git a/webapp/src/Controller/Jury/JudgeRemainingTrait.php b/webapp/src/Controller/Jury/JudgeRemainingTrait.php index 8466616198..e1419be140 100644 --- a/webapp/src/Controller/Jury/JudgeRemainingTrait.php +++ b/webapp/src/Controller/Jury/JudgeRemainingTrait.php @@ -79,7 +79,7 @@ protected function judgeRemainingJudgings(array $judgings): void } } - public function judgeRemaining(int $contestId = -1, string $categoryId = '', string|int $probId = '', string $langId = ''): void + public function judgeRemaining(?string $contestId = null, ?string $categoryId = null, ?string $probId = null, ?string $langId = null): void { $query = $this->em->createQueryBuilder() ->from(Judging::class, 'j') @@ -90,24 +90,27 @@ public function judgeRemaining(int $contestId = -1, string $categoryId = '', str ->andWhere('j.valid = true') ->andWhere('j.result != :compiler_error') ->setParameter('compiler_error', 'compiler-error'); - if ($contestId > -1) { + if ($contestId !== null) { $query - ->andWhere('s.contest = :contestId') + ->join('s.contest', 'c') + ->andWhere('c.externalid = :contestId') ->setParameter('contestId', $contestId); } - if ($categoryId > -1) { + if ($categoryId !== null) { $query - ->andWhere('tc.categoryid = :categoryId') + ->andWhere('tc.externalid = :categoryId') ->setParameter('categoryId', $categoryId); } - if ($probId !== '') { + if ($probId !== null) { $query - ->andWhere('s.problem = :probId') + ->join('s.problem', 'p') + ->andWhere('p.externalid = :probId') ->setParameter('probId', $probId); } - if ($langId !== '') { + if ($langId !== null) { $query - ->andWhere('s.language = :langId') + ->join('s.language', 'l') + ->andWhere('l.externalid = :langId') ->setParameter('langId', $langId); } $judgings = $query->getQuery()->getResult(); diff --git a/webapp/src/Controller/Jury/LanguageController.php b/webapp/src/Controller/Jury/LanguageController.php index f023def0a7..592e3539d4 100644 --- a/webapp/src/Controller/Jury/LanguageController.php +++ b/webapp/src/Controller/Jury/LanguageController.php @@ -333,7 +333,7 @@ public function requestRemainingRunsWholeLanguageAction(string $langId): Redirec if (!$language) { throw new NotFoundHttpException(sprintf('Language with ID %s not found', $langId)); } - $contestId = $this->dj->getCurrentContest()->getCid(); + $contestId = $this->dj->getCurrentContest()->getExternalid(); $this->judgeRemaining(contestId: $contestId, langId: $langId); return $this->redirectToRoute('jury_language', ['langId' => $langId]); } diff --git a/webapp/src/Controller/Jury/ProblemController.php b/webapp/src/Controller/Jury/ProblemController.php index 541a0dacc1..765354ae7e 100644 --- a/webapp/src/Controller/Jury/ProblemController.php +++ b/webapp/src/Controller/Jury/ProblemController.php @@ -525,8 +525,8 @@ public function viewAction(Request $request, SubmissionService $submissionServic return $this->render('jury/problem.html.twig', $data); } - #[Route(path: '/{probId<\d+>}/statement', name: 'jury_problem_statement')] - public function viewTextAction(int $probId): StreamedResponse + #[Route(path: '/{probId}/statement', name: 'jury_problem_statement')] + public function viewTextAction(string $probId): StreamedResponse { $problem = $this->em->getRepository(Problem::class)->find($probId); if (!$problem) { @@ -1183,7 +1183,7 @@ public function requestRemainingRunsWholeProblemAction(string $probId): Redirect if (!$problem) { throw new NotFoundHttpException(sprintf('Problem with ID %s not found', $probId)); } - $contestId = $this->dj->getCurrentContest()->getCid(); + $contestId = $this->dj->getCurrentContest()->getExternalid(); $this->judgeRemaining(contestId: $contestId, probId: $probId); return $this->redirectToRoute('jury_problem', ['probId' => $probId]); } diff --git a/webapp/src/Controller/Jury/RejudgingController.php b/webapp/src/Controller/Jury/RejudgingController.php index da05c10a79..4e734e6a96 100644 --- a/webapp/src/Controller/Jury/RejudgingController.php +++ b/webapp/src/Controller/Jury/RejudgingController.php @@ -700,7 +700,7 @@ public function createAction(Request $request): Response // These are the tables that we can deal with. $tablemap = [ - 'contest' => 's.contest', + 'contest' => 'c.externalid', 'judgehost' => 'jt.judgehost', 'language' => 's.language', 'problem' => 's.problem', @@ -737,6 +737,7 @@ public function createAction(Request $request): Response ->from(Judging::class, 'j') ->leftJoin('j.submission', 's') ->leftJoin('s.rejudging', 'r') + ->leftJoin('s.contest', 'c') ->leftJoin('s.team', 't') ->leftJoin('j.runs', 'jr') ->leftJoin('jr.judgetask', 'jt') diff --git a/webapp/src/Controller/Jury/TeamCategoryController.php b/webapp/src/Controller/Jury/TeamCategoryController.php index 19f845c94f..12a47f3ce2 100644 --- a/webapp/src/Controller/Jury/TeamCategoryController.php +++ b/webapp/src/Controller/Jury/TeamCategoryController.php @@ -124,8 +124,8 @@ public function indexAction(): Response * @throws NoResultException * @throws NonUniqueResultException */ - #[Route(path: '/{categoryId<\d+>}', name: 'jury_team_category')] - public function viewAction(Request $request, SubmissionService $submissionService, int $categoryId): Response + #[Route(path: '/{categoryId}', name: 'jury_team_category')] + public function viewAction(Request $request, SubmissionService $submissionService, string $categoryId): Response { $teamCategory = $this->em->getRepository(TeamCategory::class)->find($categoryId); if (!$teamCategory) { @@ -256,7 +256,7 @@ public function requestRemainingRunsWholeTeamCategoryAction(string $categoryId): if (!$category) { throw new NotFoundHttpException(sprintf('Team category with ID %s not found', $categoryId)); } - $contestId = $this->dj->getCurrentContest()->getCid(); + $contestId = $this->dj->getCurrentContest()->getExternalid(); $this->judgeRemaining(contestId: $contestId, categoryId: $categoryId); return $this->redirectToRoute('jury_team_category', ['categoryId' => $categoryId]); } diff --git a/webapp/src/Form/Type/AbstractExternalIdEntityType.php b/webapp/src/Form/Type/AbstractExternalIdEntityType.php index c79a8fbb92..2f934376f1 100644 --- a/webapp/src/Form/Type/AbstractExternalIdEntityType.php +++ b/webapp/src/Form/Type/AbstractExternalIdEntityType.php @@ -24,7 +24,7 @@ public function __construct(protected readonly EventLogService $eventLogService) protected function addExternalIdField(FormBuilderInterface $builder, string $entity): void { $builder->add('externalid', TextType::class, [ - 'label' => 'External ID', + 'label' => 'ID', 'help' => 'Leave empty to generate automatically.', 'required' => false, 'empty_data' => '', diff --git a/webapp/src/Form/Type/ContestProblemType.php b/webapp/src/Form/Type/ContestProblemType.php index e3c89cdd1d..a191ef2f6e 100644 --- a/webapp/src/Form/Type/ContestProblemType.php +++ b/webapp/src/Form/Type/ContestProblemType.php @@ -21,7 +21,8 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $builder->add('problem', EntityType::class, [ 'class' => Problem::class, 'required' => true, - 'choice_label' => fn(Problem $problem) => sprintf('p%d - %s', $problem->getProbid(), $problem->getName()), + 'choice_label' => fn(Problem $problem) => sprintf('%s - %s', $problem->getExternalid(), $problem->getName()), + 'choice_value' => 'externalid', 'query_builder' => fn(EntityRepository $er) => $er ->createQueryBuilder('p') ->orderBy('p.probid'), diff --git a/webapp/src/Form/Type/ContestType.php b/webapp/src/Form/Type/ContestType.php index bcf1769bcc..419f4be719 100644 --- a/webapp/src/Form/Type/ContestType.php +++ b/webapp/src/Form/Type/ContestType.php @@ -124,6 +124,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'class' => TeamCategory::class, 'multiple' => true, 'choice_label' => fn(TeamCategory $category) => $category->getName(), + 'choice_value' => 'externalid', 'help' => 'List of team categories that will receive medals for this contest.', ]); foreach (['gold', 'silver', 'bronze'] as $medalType) { @@ -158,7 +159,8 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'class' => Team::class, 'multiple' => true, - 'choice_label' => fn(Team $team) => sprintf('%s (t%d)', $team->getEffectiveName(), $team->getTeamid()), + 'choice_label' => fn(Team $team) => sprintf('%s (%s)', $team->getEffectiveName(), $team->getExternalid()), + 'choice_value' => 'externalid', 'help' => 'List of teams participating in the contest, in case it is not open to all teams.', ]); $builder->add('teamCategories', EntityType::class, [ @@ -166,6 +168,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'class' => TeamCategory::class, 'multiple' => true, 'choice_label' => fn(TeamCategory $category) => $category->getName(), + 'choice_value' => 'externalid', 'help' => 'List of team categories participating in the contest, in case it is not open to all teams.', ]); $builder->add('enabled', ChoiceType::class, [ @@ -215,6 +218,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'class' => Language::class, 'multiple' => true, 'choice_label' => fn(Language $language) => sprintf('%s (%s)', $language->getName(), $language->getExternalid()), + 'choice_value' => 'externalid', 'help' => 'List of languages that can be used in this contest. Leave empty to allow all languages that are enabled globally.', ]); diff --git a/webapp/templates/jury/contest.html.twig b/webapp/templates/jury/contest.html.twig index d18aa5bb5b..462dc5ae0e 100644 --- a/webapp/templates/jury/contest.html.twig +++ b/webapp/templates/jury/contest.html.twig @@ -1,7 +1,7 @@ {% extends "jury/base.html.twig" %} {% import "jury/jury_macros.twig" as macros %} -{% block title %}Contest {{ contest.cid }} - {{ parent() }}{% endblock %} +{% block title %}Contest {{ contest.externalid }} - {{ parent() }}{% endblock %} {% block extrahead %} {{ parent() }} @@ -38,12 +38,7 @@
    - - - - - - + @@ -70,14 +65,14 @@ @@ -97,7 +92,7 @@ @@ -218,7 +213,7 @@ @@ -227,7 +222,7 @@ @@ -236,7 +231,7 @@ @@ -398,8 +393,8 @@ {% for problem in problems %} - {% set link = path('jury_problem', {'probId': problem.probid}) %} - + {% set link = path('jury_problem', {'probId': problem.externalId}) %} + @@ -425,14 +420,14 @@
    CIDc{{ contest.cid }}
    External IDID {{ contest.externalid }}
    {% if data.show_button %} {% set button_label = type ~ " now" %} - {{ button(path('jury_contest_donow', {'contestId': contest.cid, 'time': type}), button_label, 'primary btn-sm timebutton') }} + {{ button(path('jury_contest_donow', {'contestId': contest.externalid, 'time': type}), button_label, 'primary btn-sm timebutton') }} {% endif %} {% if data.extra_button is defined %} - {{ button(path('jury_contest_donow', {'contestId': contest.cid, 'time': data.extra_button.type}), data.extra_button.label, 'primary btn-sm timebutton') }} + {{ button(path('jury_contest_donow', {'contestId': contest.externalid, 'time': data.extra_button.type}), data.extra_button.label, 'primary btn-sm timebutton') }} {% endif %} {% if type == 'finalize' %} {% if contest.finalizetime %} - {{ button(path('jury_contest_finalize', {'contestId': contest.cid}), 'Update finalization', 'secondary btn-sm timebutton') }} + {{ button(path('jury_contest_finalize', {'contestId': contest.externalid}), 'Update finalization', 'secondary btn-sm timebutton') }} {% endif %} {% endif %}
    Problemset document - + @@ -148,7 +143,7 @@
    Public static scoreboard ZIP - + Download
    Jury (unfrozen) static scoreboard ZIP - + Download
    Sample data ZIP - + Download
    @@ -321,7 +316,7 @@
    {{ removedInterval.starttime | printtimediff(removedInterval.endtime) }}
    p{{ problem.probid }}p{{ problem.externalId }} {{ problem.problem.name }} {{ problem.shortname }} {{ problem.points }} {% if problem.problem.problemstatementType %} - + {% endif %} {% if is_granted('ROLE_ADMIN') and not contest.isLocked %} - @@ -440,7 +435,7 @@ {% if is_granted('ROLE_ADMIN') %} - + {% endif %} @@ -456,16 +451,16 @@
    {%- if is_granted('ROLE_ADMIN') -%} {% if contest.isLocked %} - {{ button(path('jury_contest_unlock', {'contestId': contest.cid}), 'Unlock', 'danger', 'unlock') }} + {{ button(path('jury_contest_unlock', {'contestId': contest.externalid}), 'Unlock', 'danger', 'unlock') }} {% else %} - {{ button(path('jury_contest_edit', {'contestId': contest.cid}), 'Edit', 'primary', 'edit') }} - {{ button(path('jury_contest_delete', {'contestId': contest.cid}), 'Delete', 'danger', 'trash-alt', true) }} - {{ button(path('jury_contest_lock', {'contestId': contest.cid}), 'Lock', 'secondary', 'lock') }} + {{ button(path('jury_contest_edit', {'contestId': contest.externalid}), 'Edit', 'primary', 'edit') }} + {{ button(path('jury_contest_delete', {'contestId': contest.externalid}), 'Delete', 'danger', 'trash-alt', true) }} + {{ button(path('jury_contest_lock', {'contestId': contest.externalid}), 'Lock', 'secondary', 'lock') }} {% endif %} - {{ button(path('jury_contest_request_remaining', {'contestId': contest.cid}), 'Judge remaining testcases', 'secondary', 'gavel') }} + {{ button(path('jury_contest_request_remaining', {'contestId': contest.externalid}), 'Judge remaining testcases', 'secondary', 'gavel') }} {% endif %} - {{ button(path('jury_contest_prefetch', {'contestId': contest.cid}), 'Heat up judgehosts with contest data', 'secondary', 'download') }} - {% include 'jury/partials/rejudge_form.html.twig' with {table: 'contest', id: contest.cid, buttonClass: 'btn-secondary'} %} + {{ button(path('jury_contest_prefetch', {'contestId': contest.externalid}), 'Heat up judgehosts with contest data', 'secondary', 'download') }} + {% include 'jury/partials/rejudge_form.html.twig' with {table: 'contest', id: contest.externalid, buttonClass: 'btn-secondary'} %}
    {% endblock %} diff --git a/webapp/templates/jury/contest_edit.html.twig b/webapp/templates/jury/contest_edit.html.twig index 3391454125..b78d71ec4e 100644 --- a/webapp/templates/jury/contest_edit.html.twig +++ b/webapp/templates/jury/contest_edit.html.twig @@ -1,7 +1,7 @@ {% extends "jury/base.html.twig" %} {% import "jury/jury_macros.twig" as macros %} -{% block title %}Edit contest {{ contest.cid }} - {{ parent() }}{% endblock %} +{% block title %}Edit contest {{ contest.externalid }} - {{ parent() }}{% endblock %} {% block extrahead %} {{ parent() }} diff --git a/webapp/templates/jury/contest_finalize.html.twig b/webapp/templates/jury/contest_finalize.html.twig index 3f185353f8..684f6c85d3 100644 --- a/webapp/templates/jury/contest_finalize.html.twig +++ b/webapp/templates/jury/contest_finalize.html.twig @@ -26,7 +26,7 @@ - + @@ -47,7 +47,7 @@ {{ form_row(form.b) }} {{ form_row(form.finalizecomment) }} {{ form_widget(form.finalize) }} - {{ button(path('jury_contest', {'contestId': contest.cid}), 'Cancel', 'secondary') }} + {{ button(path('jury_contest', {'contestId': contest.externalid}), 'Cancel', 'secondary') }} {{ form_end(form) }} {% endif %} diff --git a/webapp/templates/jury/contests.html.twig b/webapp/templates/jury/contests.html.twig index 78db62934b..57aeddcfdf 100644 --- a/webapp/templates/jury/contests.html.twig +++ b/webapp/templates/jury/contests.html.twig @@ -19,11 +19,11 @@
    - {{ contest.name }} ({{ contest.shortname }} - c{{ contest.cid }}) + {{ contest.name }} ({{ contest.shortname }} - {{ contest.externalid }}) {% if contest.locked %} {% endif %} - +
    {% if not contest.starttimeEnabled and contest.finalizetime is not empty %} @@ -46,10 +46,10 @@
    {% endif %} @@ -73,7 +73,7 @@ {{ upcoming_contest.name }} ({{ upcoming_contest.shortname }}); active from {{ upcoming_contest.activatetime | printtime('D d M Y H:i:s T') }}

    - {{ button(path('jury_contest_donow', {'contestId': upcoming_contest.cid, 'time': 'activate'}), 'Activate now', 'primary') }} + {{ button(path('jury_contest_donow', {'contestId': upcoming_contest.externalid, 'time': 'activate'}), 'Activate now', 'primary') }} {% endif %} {% endfor %} diff --git a/webapp/templates/jury/partials/contest_toggle.html.twig b/webapp/templates/jury/partials/contest_toggle.html.twig index 8d23d4b9ea..607890d296 100644 --- a/webapp/templates/jury/partials/contest_toggle.html.twig +++ b/webapp/templates/jury/partials/contest_toggle.html.twig @@ -1,4 +1,4 @@ - + diff --git a/webapp/templates/jury/partials/problem_toggle.html.twig b/webapp/templates/jury/partials/problem_toggle.html.twig index 448061b3f7..a4002d2963 100644 --- a/webapp/templates/jury/partials/problem_toggle.html.twig +++ b/webapp/templates/jury/partials/problem_toggle.html.twig @@ -1,4 +1,4 @@ - + From 2c22819ac7380a40de122000ca0d94f37b2b0574 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Sun, 26 Oct 2025 16:38:17 +0100 Subject: [PATCH 10/32] Change jury executable pages to use external IDs instead of internal IDs Part of #3024 --- webapp/templates/jury/executable.html.twig | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/webapp/templates/jury/executable.html.twig b/webapp/templates/jury/executable.html.twig index 75e85d9a9e..4fee495d7f 100644 --- a/webapp/templates/jury/executable.html.twig +++ b/webapp/templates/jury/executable.html.twig @@ -48,21 +48,21 @@ {% endif %} {% if executable.type == 'compare' %} {% for problem in executable.problemsCompare %} - - p{{ problem.probid }} {{ problem | problemBadgeForContest }} + + {{ problem.externalid }} {{ problem | problemBadgeForContest }} {% set used = true %} {% endfor %} {% elseif executable.type == 'run' %} {% for problem in executable.problemsRun %} - - p{{ problem.probid }} {{ problem | problemBadgeForContest }} + + {{ problem.externalid }} {{ problem | problemBadgeForContest }} {% set used = true %} {% endfor %} {% elseif executable.type == 'compile' %} {% for language in executable.languages %} - + {{ language | entityIdBadge }} {% set used = true %} From d01e3937b66ce06eafdd314873b9e2e5bf3930f9 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Sun, 26 Oct 2025 16:42:27 +0100 Subject: [PATCH 11/32] Update entity ID badge to display only one ID Part of #3024 --- webapp/src/Controller/Jury/UserController.php | 1 - webapp/src/Twig/TwigExtension.php | 8 +------- webapp/templates/jury/analysis/contest_overview.html.twig | 2 +- webapp/templates/jury/analysis/languages.html.twig | 2 +- webapp/templates/jury/clarification.html.twig | 4 ++-- webapp/templates/jury/contest.html.twig | 2 +- webapp/templates/jury/entity_id_badge.html.twig | 4 ++-- webapp/templates/jury/export/clarifications.html.twig | 4 ++-- webapp/templates/jury/jury_macros.twig | 2 +- webapp/templates/jury/partials/balloon_list.html.twig | 2 +- .../templates/jury/partials/clarification_list.html.twig | 4 ++-- webapp/templates/jury/partials/submission_list.html.twig | 2 +- webapp/templates/jury/submission.html.twig | 8 ++++---- webapp/templates/jury/team_affiliation.html.twig | 2 +- webapp/templates/jury/team_category.html.twig | 2 +- webapp/templates/jury/user.html.twig | 2 +- 16 files changed, 22 insertions(+), 29 deletions(-) diff --git a/webapp/src/Controller/Jury/UserController.php b/webapp/src/Controller/Jury/UserController.php index 7ee386bc9a..10545acf02 100644 --- a/webapp/src/Controller/Jury/UserController.php +++ b/webapp/src/Controller/Jury/UserController.php @@ -105,7 +105,6 @@ public function indexAction(): Response if ($u->getTeam()) { $userdata['teamid'] = [ 'value' => $u->getTeam(), - 'idPrefix' => 't', ]; $userdata['team'] = [ 'value' => $u->getTeamName(), diff --git a/webapp/src/Twig/TwigExtension.php b/webapp/src/Twig/TwigExtension.php index 07fd305a48..d0e3f301bc 100644 --- a/webapp/src/Twig/TwigExtension.php +++ b/webapp/src/Twig/TwigExtension.php @@ -1295,20 +1295,14 @@ public function printWarningContent(ExternalSourceWarning $warning): string /** * Get the entity ID badge to display for the given entity. - * - * When we are in a data source mode that uses external ID's, those will be used and the - * internal ID will be shown in a tooltip. - * - * @param string $idPrefix The prefix to use for the internal ID, if any. */ - public function entityIdBadge(BaseApiEntity $entity, string $idPrefix = ''): string + public function entityIdBadge(BaseApiEntity $entity): string { $propertyAccessor = PropertyAccess::createPropertyAccessor(); $metadata = $this->em->getClassMetadata($entity::class); $primaryKeyColumn = $metadata->getIdentifierColumnNames()[0]; $data = [ - 'idPrefix' => $idPrefix, 'id' => $propertyAccessor->getValue($entity, $primaryKeyColumn), 'externalId' => null, ]; diff --git a/webapp/templates/jury/analysis/contest_overview.html.twig b/webapp/templates/jury/analysis/contest_overview.html.twig index ddcf887523..b3fcd8df3c 100644 --- a/webapp/templates/jury/analysis/contest_overview.html.twig +++ b/webapp/templates/jury/analysis/contest_overview.html.twig @@ -152,7 +152,7 @@ $(function() { {% set id=t.externalid %} {% set link = path('analysis_team', {'team':id}) %} - + diff --git a/webapp/templates/jury/analysis/languages.html.twig b/webapp/templates/jury/analysis/languages.html.twig index 2128589c1a..c299264038 100644 --- a/webapp/templates/jury/analysis/languages.html.twig +++ b/webapp/templates/jury/analysis/languages.html.twig @@ -41,7 +41,7 @@ diff --git a/webapp/templates/jury/partials/clarification_list.html.twig b/webapp/templates/jury/partials/clarification_list.html.twig index 83853ed88b..4c85ee403b 100644 --- a/webapp/templates/jury/partials/clarification_list.html.twig +++ b/webapp/templates/jury/partials/clarification_list.html.twig @@ -39,7 +39,7 @@ {%- set recipient = 'All' %} {%- else %} {%- set recipient = clarification.recipient.effectiveName %} - {%- set recipientBadge = clarification.recipient | entityIdBadge('t') %} + {%- set recipientBadge = clarification.recipient | entityIdBadge %} {%- endif %} {%- else %} {%- set sender = clarification.sender.effectiveName %} - {%- set senderBadge = clarification.sender | entityIdBadge('t') %} + {%- set senderBadge = clarification.sender | entityIdBadge %} {% else %} From e1970f3d9a4adb4b8e16e0936f8cc92d5b4e5f8a Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Thu, 30 Oct 2025 11:19:47 +0100 Subject: [PATCH 12/32] Fix refreshing scoreboard with external IDs Part of #3024 --- webapp/src/Controller/Jury/JuryMiscController.php | 2 +- webapp/src/Service/ScoreboardService.php | 2 +- webapp/templates/jury/refresh_cache.html.twig | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/webapp/src/Controller/Jury/JuryMiscController.php b/webapp/src/Controller/Jury/JuryMiscController.php index 9f077427e6..55f0dca4a7 100644 --- a/webapp/src/Controller/Jury/JuryMiscController.php +++ b/webapp/src/Controller/Jury/JuryMiscController.php @@ -215,7 +215,7 @@ public function refreshCacheAction(Request $request, ScoreboardService $scoreboa $contests = [$cid => $contests[$cid]]; } elseif ($request->cookies->has('domjudge_cid') && ($contest = $this->dj->getCurrentContest())) { - $contests = [$contest->getCid() => $contest]; + $contests = [$contest->getExternalid() => $contest]; } if ($request->isXmlHttpRequest() && $request->isMethod('POST')) { diff --git a/webapp/src/Service/ScoreboardService.php b/webapp/src/Service/ScoreboardService.php index 67a95931ac..89d4a11344 100644 --- a/webapp/src/Service/ScoreboardService.php +++ b/webapp/src/Service/ScoreboardService.php @@ -657,7 +657,7 @@ public function refreshCache(Contest $contest, ?callable $progressReporter = nul $log .= ', '; } $first = false; - $log .= sprintf('t%d', $team->getTeamid()); + $log .= $team->getExternalid(); $progress = (int)round($index / count($teams) * 100); $progressReporter($progress, $log); diff --git a/webapp/templates/jury/refresh_cache.html.twig b/webapp/templates/jury/refresh_cache.html.twig index c9216004f7..e7525f59d5 100644 --- a/webapp/templates/jury/refresh_cache.html.twig +++ b/webapp/templates/jury/refresh_cache.html.twig @@ -32,7 +32,7 @@

    {% if current_contest is not empty %} - + {% endif %}
    {% for clarification in clarifications %} - + From 15fccb61e8431e8faa6b6756b9ab269b29c4ec16 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Thu, 30 Oct 2025 11:33:29 +0100 Subject: [PATCH 15/32] Fix freeze display for balloons page with external ID Part of #3024 --- webapp/templates/jury/balloons.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/templates/jury/balloons.html.twig b/webapp/templates/jury/balloons.html.twig index 6a14c36ebb..32bf0c7a2e 100644 --- a/webapp/templates/jury/balloons.html.twig +++ b/webapp/templates/jury/balloons.html.twig @@ -19,7 +19,7 @@

    Balloons - {{ current_contest.name }}

    {% if isfrozen %} -
    Scoreboard of c{{ current_contest.cid }} ({{ current_contest.shortname }}) is now frozen.
    +
    Scoreboard of {{ current_contest.externalid }} ({{ current_contest.shortname }}) is now frozen.
    {% endif %}
    From 5222b7a1931ac2d856df1608d52caeaa38bd500c Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Thu, 30 Oct 2025 11:36:57 +0100 Subject: [PATCH 16/32] Change jury misc pages to use external IDs instead of internal IDs Part of #3024 --- .../Controller/Jury/JuryMiscController.php | 28 +++++++++---------- webapp/src/Service/DOMJudgeService.php | 2 +- .../templates/jury/check_judgings.html.twig | 4 +-- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/webapp/src/Controller/Jury/JuryMiscController.php b/webapp/src/Controller/Jury/JuryMiscController.php index 55f0dca4a7..df6d62a455 100644 --- a/webapp/src/Controller/Jury/JuryMiscController.php +++ b/webapp/src/Controller/Jury/JuryMiscController.php @@ -85,7 +85,7 @@ public function ajaxDataAction(Request $request, string $datatype): JsonResponse if ($datatype === 'affiliations') { $affiliations = $qb->from(TeamAffiliation::class, 'a') - ->select('a.affilid', 'a.name', 'a.shortname') + ->select('a.externalid', 'a.name', 'a.shortname') ->where($qb->expr()->like('a.name', '?1')) ->orWhere($qb->expr()->like('a.shortname', '?1')) ->orWhere($qb->expr()->eq('a.affilid', '?2')) @@ -95,9 +95,9 @@ public function ajaxDataAction(Request $request, string $datatype): JsonResponse ->getResult(); $results = array_map(function (array $affiliation) { - $displayname = $affiliation['name'] . " (" . $affiliation['affilid'] . ")"; + $displayname = $affiliation['name'] . " (" . $affiliation['externalid'] . ")"; return [ - 'id' => $affiliation['affilid'], + 'id' => $affiliation['externalid'], 'text' => $displayname, ]; }, $affiliations); @@ -117,7 +117,7 @@ public function ajaxDataAction(Request $request, string $datatype): JsonResponse throw new AccessDeniedHttpException('Permission denied'); } elseif ($datatype === 'problems') { $problems = $qb->from(Problem::class, 'p') - ->select('p.probid', 'p.name') + ->select('p.externalid', 'p.name') ->where($qb->expr()->like('p.name', '?1')) ->orWhere($qb->expr()->eq('p.probid', '?2')) ->orderBy('p.name', 'ASC') @@ -126,15 +126,15 @@ public function ajaxDataAction(Request $request, string $datatype): JsonResponse ->getResult(); $results = array_map(function (array $problem) { - $displayname = $problem['name'] . " (p" . $problem['probid'] . ")"; + $displayname = $problem['name'] . " (" . $problem['externalid'] . ")"; return [ - 'id' => $problem['probid'], + 'id' => $problem['externalid'], 'text' => $displayname, ]; }, $problems); } elseif ($datatype === 'teams') { $teams = $qb->from(Team::class, 't') - ->select('t.teamid', 't.display_name', 't.name', 'COALESCE(t.display_name, t.name) AS order') + ->select('t.externalid', 't.display_name', 't.name', 'COALESCE(t.display_name, t.name) AS order') ->where($qb->expr()->like('t.name', '?1')) ->orWhere($qb->expr()->like('t.display_name', '?1')) ->orWhere($qb->expr()->eq('t.teamid', '?2')) @@ -144,9 +144,9 @@ public function ajaxDataAction(Request $request, string $datatype): JsonResponse ->getResult(); $results = array_map(function (array $team) { - $displayname = ($team['display_name'] ?? $team['name']) . " (t" . $team['teamid'] . ")"; + $displayname = ($team['display_name'] ?? $team['name']) . " (" . $team['externalid'] . ")"; return [ - 'id' => $team['teamid'], + 'id' => $team['externalid'], 'text' => $displayname, ]; }, $teams); @@ -169,7 +169,7 @@ public function ajaxDataAction(Request $request, string $datatype): JsonResponse }, $languages); } elseif ($datatype === 'contests') { $query = $qb->from(Contest::class, 'c') - ->select('c.cid', 'c.name', 'c.shortname') + ->select('c.externalid', 'c.name', 'c.shortname') ->where($qb->expr()->like('c.name', '?1')) ->orWhere($qb->expr()->like('c.shortname', '?1')) ->orWhere($qb->expr()->eq('c.cid', '?2')) @@ -187,9 +187,9 @@ public function ajaxDataAction(Request $request, string $datatype): JsonResponse $contests = $query->getResult(); $results = array_map(function (array $contest) { - $displayname = $contest['name'] . " (" . $contest['shortname'] . " - c" . $contest['cid'] . ")"; + $displayname = $contest['name'] . " (" . $contest['shortname'] . " - " . $contest['externalid'] . ")"; return [ - 'id' => $contest['cid'], + 'id' => $contest['externalid'], 'text' => $displayname, ]; }, $contests); @@ -288,7 +288,7 @@ public function judgingVerifierAction(Request $request): Response $judging = $submission->getJudgings()->first(); /** @var string[] $expectedResults */ $expectedResults = $submission->getExpectedResults(); - $submissionId = $submission->getSubmitid(); + $submissionId = $submission->getExternalid(); $submissionFiles = $submission->getFiles(); $result = mb_strtoupper($judging->getResult()); @@ -340,7 +340,7 @@ public function judgingVerifierAction(Request $request): Response 'nomatch' => $nomatch, 'earlier' => $earlier, 'problems' => $problems, - 'contestId' => $this->dj->getCurrentContest()?->getCid(), + 'contestId' => $this->dj->getCurrentContest()?->getExternalid(), 'verifyMultiple' => $verifyMultiple, ]); } diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index e942f88ccb..5d475c7d65 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -360,7 +360,7 @@ public function getUpdates(): array if ($this->checkRole('jury')) { if ($contest) { $clarifications = $this->em->createQueryBuilder() - ->select('clar.clarid', 'clar.body') + ->select('clar.externalid', 'clar.body') ->from(Clarification::class, 'clar') ->andWhere('clar.contest = :contest') ->andWhere('clar.sender is not null') diff --git a/webapp/templates/jury/check_judgings.html.twig b/webapp/templates/jury/check_judgings.html.twig index 561005f515..504e00de8a 100644 --- a/webapp/templates/jury/check_judgings.html.twig +++ b/webapp/templates/jury/check_judgings.html.twig @@ -40,7 +40,7 @@ {% for submitId,result in results %} {% set link = path('jury_submission', {'submitId': submitId}) %}
    - + {%- for submission in submissions %} {%- if rejudging is defined %} - {%- set link = path('jury_submission', {submitId: submission.submitid, rejudgingid: rejudging.rejudgingid}) %} + {%- set link = path('jury_submission', {submitId: submission.externalid, rejudgingid: rejudging.rejudgingid}) %} {%- else %} - {%- set link = path('jury_submission', {submitId: submission.submitid}) %} + {%- set link = path('jury_submission', {submitId: submission.externalid}) %} {%- endif %} {% if submission.team.affiliation %} - {% set affilid = submission.team.affiliation.affilid %} + {% set affilid = submission.team.affiliation.externalid %} {% else %} {% set affilid = '' %} {% endif %} @@ -109,14 +109,11 @@ {% endif %} {%- if showContest %} - + {%- endif %} {% endif %} {%- if rejudging is defined %} - {%- endif %} @@ -205,9 +202,9 @@ {% if not showExternalResult or not showExternalTestcases %} {% set link = path('jury_problem', {'probId': problem.externalId}) %} - + diff --git a/webapp/templates/jury/partials/problem_toggle.html.twig b/webapp/templates/jury/partials/problem_toggle.html.twig index a4002d2963..1b741e6899 100644 --- a/webapp/templates/jury/partials/problem_toggle.html.twig +++ b/webapp/templates/jury/partials/problem_toggle.html.twig @@ -1,4 +1,4 @@ - + diff --git a/webapp/templates/jury/problem.html.twig b/webapp/templates/jury/problem.html.twig index 0679104872..1b29b98ce6 100644 --- a/webapp/templates/jury/problem.html.twig +++ b/webapp/templates/jury/problem.html.twig @@ -1,7 +1,7 @@ {% extends "jury/base.html.twig" %} {% import "jury/jury_macros.twig" as macros %} -{% block title %}Problem {{ problem.probid }} - {{ parent() }}{% endblock %} +{% block title %}Problem {{ problem.externalid }} - {{ parent() }}{% endblock %} {% block extrahead %} {{ parent() }} @@ -19,10 +19,6 @@
    Contest IDc{{ contest.cid }}c{{ contest.externalid }}
    Contest name {% if data.show_button %} {% set button_label = type ~ " now" %} - {{ button(path('jury_contest_donow', {'contestId': contest.cid, 'time': type}), button_label, 'primary btn-sm') }} + {{ button(path('jury_contest_donow', {'contestId': contest.externalid, 'time': type}), button_label, 'primary btn-sm') }} {% endif %} {% if data.extra_button is defined %} - {{ button(path('jury_contest_donow', {'contestId': contest.cid, 'time': data.extra_button.type}), data.extra_button.label, 'primary btn-sm') }} + {{ button(path('jury_contest_donow', {'contestId': contest.externalid, 'time': data.extra_button.type}), data.extra_button.label, 'primary btn-sm') }} {% endif %}
    {{ t | entityIdBadge('t') }}{{ t | entityIdBadge }} {% if t.affiliation %}{{ t.affiliation.name }}{% else %}-{% endif %} {{ t.effectiveName }} {{ misc.team_stats[id].total_submitted }} / {{ misc.team_stats[id].total_accepted }}
    - {{ team.team | entityIdBadge('t') }} + {{ team.team | entityIdBadge }} diff --git a/webapp/templates/jury/clarification.html.twig b/webapp/templates/jury/clarification.html.twig index cf87e3a7bb..1168028fe1 100644 --- a/webapp/templates/jury/clarification.html.twig +++ b/webapp/templates/jury/clarification.html.twig @@ -34,7 +34,7 @@
    From: {% if clar.from_team is defined %} - {{ clar.from_teamname }} {{ clar.from_team | entityIdBadge('t') }} + {{ clar.from_teamname }} {{ clar.from_team | entityIdBadge }} {% else %} Jury {% if clar.from_jurymember is defined %} @@ -75,7 +75,7 @@
    To: {% if clar.to_team is defined %} - {{ clar.to_teamname }} {{ clar.to_team | entityIdBadge('t') }} + {{ clar.to_teamname }} {{ clar.to_team | entityIdBadge }} {% elseif clar.from_team is defined %} Jury {% else %} diff --git a/webapp/templates/jury/contest.html.twig b/webapp/templates/jury/contest.html.twig index 462dc5ae0e..860ea95bfa 100644 --- a/webapp/templates/jury/contest.html.twig +++ b/webapp/templates/jury/contest.html.twig @@ -178,7 +178,7 @@ {% else %} {% for team in contest.teams %} - {{ team.effectiveName }} {{ team | entityIdBadge('t') }} + {{ team.effectiveName }} {{ team | entityIdBadge }}
    {% endfor %} diff --git a/webapp/templates/jury/entity_id_badge.html.twig b/webapp/templates/jury/entity_id_badge.html.twig index 8e304dc748..df4d37a1f7 100644 --- a/webapp/templates/jury/entity_id_badge.html.twig +++ b/webapp/templates/jury/entity_id_badge.html.twig @@ -1,9 +1,9 @@ - + {% if label is defined and label | length %} {{ label }} {% elseif externalId is not null %} {{ externalId }} {% else %} - {{ idPrefix }}{{ id }} + {{ id }} {% endif %} diff --git a/webapp/templates/jury/export/clarifications.html.twig b/webapp/templates/jury/export/clarifications.html.twig index 871033868e..52352d8ae1 100644 --- a/webapp/templates/jury/export/clarifications.html.twig +++ b/webapp/templates/jury/export/clarifications.html.twig @@ -68,14 +68,14 @@
    {{ clarification.submitTime | printtime(null, contest) }} {% if clarification.sender %} - {{ clarification.sender.effectiveName }} {{ clarification.sender | entityIdBadge('t') }} + {{ clarification.sender.effectiveName }} {{ clarification.sender | entityIdBadge }} {% else %} Jury ({{ clarification.juryMember }}) {% endif %} {% if clarification.recipient and clarification.sender is empty %} - {{ clarification.recipient.effectiveName }} {{ clarification.recipient | entityIdBadge('t') }} + {{ clarification.recipient.effectiveName }} {{ clarification.recipient | entityIdBadge }} {% elseif clarification.sender %} Jury {% else %} diff --git a/webapp/templates/jury/jury_macros.twig b/webapp/templates/jury/jury_macros.twig index 9df0ba8c2e..4c3af41c5e 100644 --- a/webapp/templates/jury/jury_macros.twig +++ b/webapp/templates/jury/jury_macros.twig @@ -147,7 +147,7 @@ {{- badge|problemBadge }} {% endfor %} {% elseif (column.render | default('')) == "entity_id_badge" %} - {% if item.value %}{{- item.value|entityIdBadge(item.idPrefix|default('')) -}}{% endif %} + {% if item.value %}{{- item.value|entityIdBadge -}}{% endif %} {% else %} {%- if column.raw|default(false) %}{{- (item.value|default(item.default|default('')))|raw -}}{% else %}{{- (item.value|default(item.default|default(''))) -}}{% endif -%} {% endif %} diff --git a/webapp/templates/jury/partials/balloon_list.html.twig b/webapp/templates/jury/partials/balloon_list.html.twig index 4195b59bb6..462dc796ca 100644 --- a/webapp/templates/jury/partials/balloon_list.html.twig +++ b/webapp/templates/jury/partials/balloon_list.html.twig @@ -35,7 +35,7 @@ {{ balloon.data.time | printtime }} {{ balloon.data.contestproblem | problemBadge }} - {{ balloon.data.team | entityIdBadge('t') }} + {{ balloon.data.team | entityIdBadge }} {{ balloon.data.team.effectiveName | u.truncate(teamname_max_length, '…') }} {{ balloon.data.affiliation }} @@ -53,7 +53,7 @@ {{ senderBadge | raw }} diff --git a/webapp/templates/jury/partials/submission_list.html.twig b/webapp/templates/jury/partials/submission_list.html.twig index 4493e5867d..fb0cf38dc3 100644 --- a/webapp/templates/jury/partials/submission_list.html.twig +++ b/webapp/templates/jury/partials/submission_list.html.twig @@ -124,7 +124,7 @@ - {{ submission.team | entityIdBadge('t') }} + {{ submission.team | entityIdBadge }} diff --git a/webapp/templates/jury/submission.html.twig b/webapp/templates/jury/submission.html.twig index 18f31720e9..7b151d46b9 100644 --- a/webapp/templates/jury/submission.html.twig +++ b/webapp/templates/jury/submission.html.twig @@ -135,7 +135,7 @@ - {{ submission.team.effectiveName }} {{ submission.team | entityIdBadge('t') }} + {{ submission.team.effectiveName }} {{ submission.team | entityIdBadge }} @@ -143,7 +143,7 @@ - {{ submission.user.username }} {{ submission.user | entityIdBadge('u') }} + {{ submission.user.username }} {{ submission.user | entityIdBadge }} {% endif %} @@ -153,7 +153,7 @@ {{ submission.contest.shortname }} - {{ submission.contest | entityIdBadge('c') }} + {{ submission.contest | entityIdBadge }} {% endif %} @@ -166,7 +166,7 @@ {% else %} {{ submission.problem.name }} {% endif %} - {{ submission.problem | entityIdBadge('p') }} + {{ submission.problem | entityIdBadge }} diff --git a/webapp/templates/jury/team_affiliation.html.twig b/webapp/templates/jury/team_affiliation.html.twig index 1de78a3446..25df6c8053 100644 --- a/webapp/templates/jury/team_affiliation.html.twig +++ b/webapp/templates/jury/team_affiliation.html.twig @@ -92,7 +92,7 @@ {% for team in teamAffiliation.teams %}
    - {{ team | entityIdBadge('t') }} + {{ team | entityIdBadge }} {{ team.effectiveName }} diff --git a/webapp/templates/jury/team_category.html.twig b/webapp/templates/jury/team_category.html.twig index 22b2790610..d5f129f2de 100644 --- a/webapp/templates/jury/team_category.html.twig +++ b/webapp/templates/jury/team_category.html.twig @@ -81,7 +81,7 @@ {% for team in teamCategory.teams %}
    - {{ team | entityIdBadge('t') }} + {{ team | entityIdBadge }} {{ team.effectiveName }} diff --git a/webapp/templates/jury/user.html.twig b/webapp/templates/jury/user.html.twig index a8a071a8f3..3f58734a71 100644 --- a/webapp/templates/jury/user.html.twig +++ b/webapp/templates/jury/user.html.twig @@ -79,7 +79,7 @@ {% if user.team %} - {{ user.team.effectiveName }} {{ user.team | entityIdBadge('t') }} + {{ user.team.effectiveName }} {{ user.team | entityIdBadge }}
    {{ clarification.clarId }}{{ clarification.externalid }} {{ clarification.submitTime | printtime(null, contest) }} {% if clarification.sender %} From 7fb7b92a878872acd941b9e8788e29fe5f2478c9 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Thu, 30 Oct 2025 11:26:31 +0100 Subject: [PATCH 14/32] Change jury internal error pages to use external IDs instead of internal IDs Part of #3024 --- webapp/src/Controller/Jury/InternalErrorController.php | 8 +++++--- webapp/templates/jury/internal_error.html.twig | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/webapp/src/Controller/Jury/InternalErrorController.php b/webapp/src/Controller/Jury/InternalErrorController.php index f7148b1ef5..919d569415 100644 --- a/webapp/src/Controller/Jury/InternalErrorController.php +++ b/webapp/src/Controller/Jury/InternalErrorController.php @@ -7,6 +7,7 @@ use App\Entity\InternalError; use App\Entity\Judgehost; use App\Entity\JudgeTask; +use App\Entity\Language; use App\Entity\Problem; use App\Service\DOMJudgeService; use App\Service\EventLogService; @@ -102,8 +103,8 @@ public function viewAction(int $errorId): Response $affectedLink = $affectedText = null; switch ($disabled['kind']) { case 'problem': - $affectedLink = $this->generateUrl('jury_problem', ['probId' => $disabled['probid']]); $problem = $this->em->getRepository(Problem::class)->find($disabled['probid']); + $affectedLink = $this->generateUrl('jury_problem', ['probId' => $problem->getExternalid()]); $affectedText = $problem->getName(); break; case 'judgehost': @@ -117,8 +118,9 @@ public function viewAction(int $errorId): Response } break; case 'language': - $affectedLink = $this->generateUrl('jury_language', ['langId' => $disabled['langid']]); - $affectedText = $disabled['langid']; + $language = $this->em->getRepository(Language::class)->find($disabled['langid']); + $affectedLink = $this->generateUrl('jury_language', ['langId' => $language->getExternalid()]); + $affectedText = $language->getName(); break; case 'executable': $affectedLink = $this->generateUrl('jury_executable', ['execId' => $disabled['execid']]); diff --git a/webapp/templates/jury/internal_error.html.twig b/webapp/templates/jury/internal_error.html.twig index 4565c5c751..82bcce10c7 100644 --- a/webapp/templates/jury/internal_error.html.twig +++ b/webapp/templates/jury/internal_error.html.twig @@ -92,8 +92,8 @@
    Related contest - - c{{ internalError.contest.cid }} + + {{ internalError.contest.externalid }}
    s{{ submitId }}{{ submitId }} {% if result.contestProblem is defined %} @@ -111,7 +111,7 @@ After this compare the maximum runtime for Accepted solutions and tune those against expected Time Limit Exceeded solutions.
    {% for p in problems %} - {% set link = path('analysis_problem', {'probid': p.probid, 'view': 'hidden'}) %} + {% set link = path('analysis_problem', {'probid': p.externalId, 'view': 'hidden'}) %}
    {{ p | problemBadge }} {% endfor %} {% endif %} From b3f4661533d409a910572466a1e1fb233136cd12 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Thu, 30 Oct 2025 11:47:21 +0100 Subject: [PATCH 17/32] Make sure we save the audit log with a string Part of #3024 --- webapp/src/Controller/BaseController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/Controller/BaseController.php b/webapp/src/Controller/BaseController.php index 1cba1929ad..797a574b7c 100644 --- a/webapp/src/Controller/BaseController.php +++ b/webapp/src/Controller/BaseController.php @@ -157,7 +157,7 @@ protected function saveEntity( } else { $dataid = $id; } - $this->dj->auditlog($auditLogType, $dataid, $isNewEntity ? 'added' : 'updated'); + $this->dj->auditlog($auditLogType, (string)$dataid, $isNewEntity ? 'added' : 'updated'); } /** From 1a12659d1437d0696a075d37ad0ce6e6e894dca6 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Thu, 30 Oct 2025 11:47:33 +0100 Subject: [PATCH 18/32] Change jury language pages to use external IDs instead of internal IDs Part of #3024 --- .../Controller/Jury/LanguageController.php | 33 +++++++++---------- .../Controller/Jury/RejudgingController.php | 3 +- webapp/templates/jury/language.html.twig | 14 +++----- webapp/templates/jury/language_edit.html.twig | 13 +------- .../jury/partials/language_toggle.html.twig | 2 +- .../jury/partials/submission_list.html.twig | 19 +++++------ 6 files changed, 32 insertions(+), 52 deletions(-) diff --git a/webapp/src/Controller/Jury/LanguageController.php b/webapp/src/Controller/Jury/LanguageController.php index 592e3539d4..b2aeccc6d3 100644 --- a/webapp/src/Controller/Jury/LanguageController.php +++ b/webapp/src/Controller/Jury/LanguageController.php @@ -52,8 +52,7 @@ public function indexAction(): Response ->orderBy('lang.name', 'ASC') ->getQuery()->getResult(); $table_fields = [ - 'langid' => ['title' => 'ID', 'sort' => true], - 'externalid' => ['title' => 'external ID', 'sort' => true], + 'externalid' => ['title' => 'ID', 'sort' => true], 'name' => ['title' => 'name', 'sort' => true, 'default_sort' => true], 'entrypoint' => ['title' => 'entry point', 'sort' => true], 'allowjudge' => ['title' => 'allow judge', 'sort' => true], @@ -80,14 +79,14 @@ public function indexAction(): Response 'icon' => 'edit', 'title' => 'edit this language', 'link' => $this->generateUrl('jury_language_edit', [ - 'langId' => $lang->getLangid() + 'langId' => $lang->getExternalid() ]) ]; $langactions[] = [ 'icon' => 'trash-alt', 'title' => 'delete this language', 'link' => $this->generateUrl('jury_language_delete', [ - 'langId' => $lang->getLangid(), + 'langId' => $lang->getExternalid(), ]), 'ajaxModal' => true, ]; @@ -126,14 +125,14 @@ public function indexAction(): Response $enabled_languages[] = [ 'data' => $langdata, 'actions' => $langactions, - 'link' => $this->generateUrl('jury_language', ['langId' => $lang->getLangid()]), + 'link' => $this->generateUrl('jury_language', ['langId' => $lang->getExternalid()]), 'cssclass' => '', ]; } else { $disabled_languages[] = [ 'data' => $langdata, 'actions' => $langactions, - 'link' => $this->generateUrl('jury_language', ['langId' => $lang->getLangid()]), + 'link' => $this->generateUrl('jury_language', ['langId' => $lang->getExternalid()]), 'cssclass' => 'disabled', ]; } @@ -145,10 +144,8 @@ public function indexAction(): Response ]); } - // Note that the add action appears before the view action to make sure - // /add is not seen as a language. #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/add', name: 'jury_language_add')] + #[Route(path: '/add', name: 'jury_language_add', priority: 1)] public function addAction(Request $request): Response { $language = new Language(); @@ -159,7 +156,7 @@ public function addAction(Request $request): Response if ($response = $this->processAddFormForExternalIdEntity( $form, $language, - fn() => $this->generateUrl('jury_language', ['langId' => $language->getLangid()]), + fn() => $this->generateUrl('jury_language', ['langId' => $language->getExternalid()]), function () use ($language) { // Normalize extensions if ($language->getExtensions()) { @@ -187,7 +184,7 @@ function () use ($language) { #[Route(path: '/{langId}', name: 'jury_language')] public function viewAction(Request $request, SubmissionService $submissionService, string $langId): Response { - $language = $this->em->getRepository(Language::class)->find($langId); + $language = $this->em->getRepository(Language::class)->findByExternalId($langId); if (!$language) { throw new NotFoundHttpException(sprintf('Language with ID %s not found', $langId)); } @@ -207,7 +204,7 @@ public function viewAction(Request $request, SubmissionService $submissionServic 'showExternalResult' => $this->dj->shadowMode(), 'refresh' => [ 'after' => 15, - 'url' => $this->generateUrl('jury_language', ['langId' => $language->getLangid()]), + 'url' => $this->generateUrl('jury_language', ['langId' => $language->getExternalid()]), 'ajax' => true, ], ]; @@ -224,7 +221,7 @@ public function viewAction(Request $request, SubmissionService $submissionServic #[Route(path: '/{langId}/toggle-submit', name: 'jury_language_toggle_submit')] public function toggleSubmitAction(Request $request, string $langId): Response { - $language = $this->em->getRepository(Language::class)->find($langId); + $language = $this->em->getRepository(Language::class)->findByExternalId($langId); if (!$language) { throw new NotFoundHttpException(sprintf('Language with ID %s not found', $langId)); } @@ -243,7 +240,7 @@ public function toggleJudgeAction( Request $request, string $langId ): Response { - $language = $this->em->getRepository(Language::class)->find($langId); + $language = $this->em->getRepository(Language::class)->findByExternalId($langId); if (!$language) { throw new NotFoundHttpException(sprintf('Language with ID %s not found', $langId)); } @@ -268,7 +265,7 @@ public function toggleJudgeAction( #[Route(path: '/{langId}/toggle-filter-compiler-flags', name: 'jury_language_toggle_filter_compiler_files')] public function toggleFilterCompilerFlagsAction(Request $request, string $langId): Response { - $language = $this->em->getRepository(Language::class)->find($langId); + $language = $this->em->getRepository(Language::class)->findByExternalId($langId); if (!$language) { throw new NotFoundHttpException(sprintf('Language with ID %s not found', $langId)); } @@ -286,7 +283,7 @@ public function toggleFilterCompilerFlagsAction(Request $request, string $langId #[Route(path: '/{langId}/edit', name: 'jury_language_edit')] public function editAction(Request $request, string $langId): Response { - $language = $this->em->getRepository(Language::class)->find($langId); + $language = $this->em->getRepository(Language::class)->findByExternalId($langId); if (!$language) { throw new NotFoundHttpException(sprintf('Language with ID %s not found', $langId)); } @@ -317,7 +314,7 @@ public function editAction(Request $request, string $langId): Response #[Route(path: '/{langId}/delete', name: 'jury_language_delete')] public function deleteAction(Request $request, string $langId): Response { - $language = $this->em->getRepository(Language::class)->find($langId); + $language = $this->em->getRepository(Language::class)->findByExternalId($langId); if (!$language) { throw new NotFoundHttpException(sprintf('Language with ID %s not found', $langId)); } @@ -329,7 +326,7 @@ public function deleteAction(Request $request, string $langId): Response #[Route(path: '/{langId}/request-remaining', name: 'jury_language_request_remaining')] public function requestRemainingRunsWholeLanguageAction(string $langId): RedirectResponse { - $language = $this->em->getRepository(Language::class)->find($langId); + $language = $this->em->getRepository(Language::class)->findByExternalId($langId); if (!$language) { throw new NotFoundHttpException(sprintf('Language with ID %s not found', $langId)); } diff --git a/webapp/src/Controller/Jury/RejudgingController.php b/webapp/src/Controller/Jury/RejudgingController.php index 4e734e6a96..39442917f3 100644 --- a/webapp/src/Controller/Jury/RejudgingController.php +++ b/webapp/src/Controller/Jury/RejudgingController.php @@ -702,7 +702,7 @@ public function createAction(Request $request): Response $tablemap = [ 'contest' => 'c.externalid', 'judgehost' => 'jt.judgehost', - 'language' => 's.language', + 'language' => 'l.externalid', 'problem' => 's.problem', 'submission' => 's.submitid', 'team' => 's.team', @@ -738,6 +738,7 @@ public function createAction(Request $request): Response ->leftJoin('j.submission', 's') ->leftJoin('s.rejudging', 'r') ->leftJoin('s.contest', 'c') + ->leftJoin('s.language', 'l') ->leftJoin('s.team', 't') ->leftJoin('j.runs', 'jr') ->leftJoin('jr.judgetask', 'jt') diff --git a/webapp/templates/jury/language.html.twig b/webapp/templates/jury/language.html.twig index 974a996a4e..e3cc81e4e2 100644 --- a/webapp/templates/jury/language.html.twig +++ b/webapp/templates/jury/language.html.twig @@ -1,7 +1,7 @@ {% extends "jury/base.html.twig" %} {% import "jury/jury_macros.twig" as macros %} -{% block title %}Language {{ language.langid }} - {{ parent() }}{% endblock %} +{% block title %}Language {{ language.externalid }} - {{ parent() }}{% endblock %} {% block extrahead %} {{ parent() }} @@ -19,10 +19,6 @@ - - - - @@ -106,11 +102,11 @@
    {%- if is_granted('ROLE_ADMIN') -%} - {{ button(path('jury_language_edit', {'langId': language.langid}), 'Edit', 'primary', 'edit') }} - {{ button(path('jury_language_delete', {'langId': language.langid}), 'Delete', 'danger', 'trash-alt', true) }} - {{ button(path('jury_language_request_remaining', {'langId': language.langid}), 'Judge remaining testcases', 'secondary', 'gavel') }} + {{ button(path('jury_language_edit', {'langId': language.externalid}), 'Edit', 'primary', 'edit') }} + {{ button(path('jury_language_delete', {'langId': language.externalid}), 'Delete', 'danger', 'trash-alt', true) }} + {{ button(path('jury_language_request_remaining', {'langId': language.externalid}), 'Judge remaining testcases', 'secondary', 'gavel') }} {% endif %} - {% include 'jury/partials/rejudge_form.html.twig' with {table: 'language', id: language.langid, buttonClass: 'btn-secondary'} %} + {% include 'jury/partials/rejudge_form.html.twig' with {table: 'language', id: language.externalid, buttonClass: 'btn-secondary'} %}

    Recent submissions

    diff --git a/webapp/templates/jury/language_edit.html.twig b/webapp/templates/jury/language_edit.html.twig index 78afbf22fe..8c8a085e52 100644 --- a/webapp/templates/jury/language_edit.html.twig +++ b/webapp/templates/jury/language_edit.html.twig @@ -10,18 +10,7 @@ {% block content %} -

    Edit language {{ language.langid }}

    - -
    -
    -
    ID{{ language.langid }}
    External ID {{ language.externalid }}
    - - - - -
    Language ID{{ language.langid }}
    - - +

    Edit language {{ language.externalid }}

    {% include 'jury/partials/language_form.html.twig' %} diff --git a/webapp/templates/jury/partials/language_toggle.html.twig b/webapp/templates/jury/partials/language_toggle.html.twig index 1335bd45d2..1f968e474d 100644 --- a/webapp/templates/jury/partials/language_toggle.html.twig +++ b/webapp/templates/jury/partials/language_toggle.html.twig @@ -1,4 +1,4 @@ - + diff --git a/webapp/templates/jury/partials/submission_list.html.twig b/webapp/templates/jury/partials/submission_list.html.twig index fb0cf38dc3..a67d7f750a 100644 --- a/webapp/templates/jury/partials/submission_list.html.twig +++ b/webapp/templates/jury/partials/submission_list.html.twig @@ -88,13 +88,13 @@
    - s{{ submission.submitid }} - {% if shadowMode() and submission.externalid %} - ({{ submission.externalid }}) - {% endif %} + {{ submission.externalid }} c{{ submission.contest.cid }}{{ submission.contest.externalid }} @@ -148,7 +145,7 @@ + {{ submission.oldResult | printValidJuryResult }} {%- if rejudging is defined %} - {%- set claimLink = path('jury_submission', claimArg | merge({submitId: submission.submitid, rejudgingid: rejudging.rejudgingid})) %} + {%- set claimLink = path('jury_submission', claimArg | merge({submitId: submission.externalid, rejudgingid: rejudging.rejudgingid})) %} {%- else %} - {%- set claimLink = path('jury_submission', claimArg | merge({submitId: submission.submitid})) %} + {%- set claimLink = path('jury_submission', claimArg | merge({submitId: submission.externalid})) %} {%- endif %} {%- if claim -%} Date: Thu, 30 Oct 2025 12:15:33 +0100 Subject: [PATCH 19/32] Change jury problem pages to use external IDs instead of internal IDs Part of #3024 --- .../src/Controller/Jury/ProblemController.php | 133 +++++++++--------- .../Controller/Jury/RejudgingController.php | 3 +- webapp/src/Form/Type/ProblemType.php | 1 + webapp/templates/jury/contest.html.twig | 2 +- .../jury/partials/problem_toggle.html.twig | 2 +- webapp/templates/jury/problem.html.twig | 30 ++-- webapp/templates/jury/problem_edit.html.twig | 8 +- .../jury/problem_testcases.html.twig | 6 +- 8 files changed, 91 insertions(+), 94 deletions(-) diff --git a/webapp/src/Controller/Jury/ProblemController.php b/webapp/src/Controller/Jury/ProblemController.php index 765354ae7e..2f165e68d5 100644 --- a/webapp/src/Controller/Jury/ProblemController.php +++ b/webapp/src/Controller/Jury/ProblemController.php @@ -76,10 +76,9 @@ public function indexAction(): Response ->getQuery()->getResult(); $table_fields = [ - 'probid' => ['title' => 'ID', 'sort' => true, 'default_sort' => true], - 'externalid' => ['title' => 'external ID', 'sort' => true], + 'externalid' => ['title' => 'ID', 'sort' => true], 'name' => ['title' => 'name', 'sort' => true], - 'badges' => ['title' => '', 'sort' => false], + 'badges' => ['title' => '', 'sort' => true], 'num_contests' => ['title' => '# contests', 'sort' => true], 'timelimit' => ['title' => 'time limit', 'sort' => true], 'memlimit' => ['title' => 'memory limit', 'sort' => true], @@ -88,6 +87,12 @@ public function indexAction(): Response 'type' => ['title' => 'type', 'sort' => true], ]; + if ($this->dj->getCurrentContest() !== null) { + $table_fields['badges']['default_sort'] = true; + } else { + $table_fields['badges']['externalid'] = true; + } + $this->addSelectAllCheckbox($table_fields, 'problems'); $contestCountData = $this->em->createQueryBuilder() @@ -112,7 +117,7 @@ public function indexAction(): Response $problemdata = []; $problemactions = []; - $this->addEntityCheckbox($problemdata, $p, $p->getProbid(), 'problem-checkbox', fn(Problem $problem) => !$problem->isLocked()); + $this->addEntityCheckbox($problemdata, $p, $p->getExternalid(), 'problem-checkbox', fn(Problem $problem) => !$problem->isLocked()); // Get whatever fields we can from the problem object itself. foreach ($table_fields as $k => $v) { @@ -127,7 +132,7 @@ public function indexAction(): Response 'icon' => 'file-' . $p->getProblemstatementType(), 'title' => 'view problem statement', 'link' => $this->generateUrl('jury_problem_statement', [ - 'probId' => $p->getProbid(), + 'probId' => $p->getExternalid(), ]) ]; } else { @@ -137,7 +142,7 @@ public function indexAction(): Response 'icon' => 'save', 'title' => 'export problem as zip-file', 'link' => $this->generateUrl('jury_export_problem', [ - 'problemId' => $p->getProbid(), + 'problemId' => $p->getExternalid(), ]) ]; @@ -146,7 +151,7 @@ public function indexAction(): Response 'icon' => 'edit', 'title' => 'edit this problem', 'link' => $this->generateUrl('jury_problem_edit', [ - 'probId' => $p->getProbid(), + 'probId' => $p->getExternalid(), ]) ]; @@ -161,7 +166,7 @@ public function indexAction(): Response 'icon' => 'trash-alt', 'title' => 'delete this problem', 'link' => $this->generateUrl('jury_problem_delete', [ - 'probId' => $p->getProbid(), + 'probId' => $p->getExternalid(), ]), 'ajaxModal' => true, ]; @@ -206,7 +211,10 @@ public function indexAction(): Response if ($this->dj->getCurrentContest() !== null) { $badges = array_filter($contestProblems, fn($cp) => $cp->getCid() === $this->dj->getCurrentContest()->getCid()); } - $problemdata['badges'] = ['value' => $badges]; + $problemdata['badges'] = [ + 'value' => $badges, + 'sortvalue' => implode(', ', array_map(fn(ContestProblem $problem) => $problem->getShortname(), $badges)), + ]; // merge in the rest of the data $problemdata = array_merge($problemdata, [ @@ -218,7 +226,7 @@ public function indexAction(): Response $data_to_add = [ 'data' => $problemdata, 'actions' => $problemactions, - 'link' => $this->generateUrl('jury_problem', ['probId' => $p->getProbid()]), + 'link' => $this->generateUrl('jury_problem', ['probId' => $p->getExternalid()]), ]; if ($badges) { $problems_table_current[] = $data_to_add; @@ -246,16 +254,13 @@ public function problemsetAction(StatisticsService $stats): Response $this->dj->getTwigDataForProblemsAction($stats, teamId: $teamId, forJury: true)); } - #[Route(path: '/{probId<\d+>}/samples.zip', name: 'jury_problem_sample_zip')] - public function sampleZipAction(int $probId): StreamedResponse + #[Route(path: '/{probId}/samples.zip', name: 'jury_problem_sample_zip')] + public function sampleZipAction(string $probId): StreamedResponse { $contest = $this->dj->getCurrentContest(); - $contestProblem = $this->em->getRepository(ContestProblem::class)->find([ - 'problem' => $probId, - 'contest' => $contest, - ]); + $contestProblem = $this->em->getRepository(ContestProblem::class)->findByProblemAndContest($contest, $probId); if (!$contestProblem) { - throw new NotFoundHttpException(sprintf('Problem p%d not found or not available', $probId)); + throw new NotFoundHttpException(sprintf('Problem %s not found or not available', $probId)); } return $this->dj->getSamplesZipStreamedResponse($contestProblem); } @@ -264,8 +269,8 @@ public function sampleZipAction(int $probId): StreamedResponse * @throws NonUniqueResultException */ #[IsGranted('ROLE_JURY')] - #[Route(path: '/{problemId<\d+>}/export', name: 'jury_export_problem')] - public function exportAction(int $problemId): StreamedResponse + #[Route(path: '/{problemId}/export', name: 'jury_export_problem')] + public function exportAction(string $problemId): StreamedResponse { // This might take a while. Utils::extendMaxExecutionTime(300); @@ -275,7 +280,7 @@ public function exportAction(int $problemId): StreamedResponse ->leftJoin('p.contest_problems', 'cp', Join::WITH, 'cp.contest = :contest') ->leftJoin('p.problemStatementContent', 'content') ->select('p', 'cp', 'content') - ->andWhere('p.probid = :problemId') + ->andWhere('p.externalid = :problemId') ->setParameter('problemId', $problemId) ->setParameter('contest', $this->dj->getCurrentContest()) ->getQuery() @@ -412,7 +417,7 @@ public function exportAction(int $problemId): StreamedResponse // since we can't change the filename to something unique, since // that could break e.g. Java sources, even if _we_ support this // by default. - $directory = sprintf('submissions/%s/s%d/', $problemResult, $solution->getSubmitid()); + $directory = sprintf('submissions/%s/%s/', $problemResult, $solution->getExternalid()); /** @var SubmissionFile $source */ foreach ($solution->getFiles() as $source) { $zip->addFromString($directory . $source->getFilename(), $source->getSourcecode()); @@ -423,7 +428,7 @@ public function exportAction(int $problemId): StreamedResponse if ($contestProblem && $contestProblem->getShortname()) { $zipFilename = sprintf('%s.zip', $contestProblem->getShortname()); } else { - $zipFilename = sprintf('p%d.zip', $problem->getProbid()); + $zipFilename = sprintf('%s.zip', $problem->getExternalid()); } return Utils::streamZipFile($tempFilename, $zipFilename); @@ -446,7 +451,7 @@ public function viewAction(Request $request, SubmissionService $submissionServic /** @var ContestProblem $contestProblem */ if ($contestProblem->getContest()->isLocked()) { if (!$request->isXmlHttpRequest()) { - $this->addFlash('warning', 'Cannot edit problem, it belongs to locked contest c' . $contestProblem->getContest()->getCid()); + $this->addFlash('warning', 'Cannot edit problem, it belongs to locked contest ' . $contestProblem->getContest()->getExternalid()); } $lockedProblem = true; } @@ -511,7 +516,7 @@ public function viewAction(Request $request, SubmissionService $submissionServic 'lockedProblem' => $lockedProblem, 'refresh' => [ 'after' => 15, - 'url' => $this->generateUrl('jury_problem', ['probId' => $problem->getProbid()]), + 'url' => $this->generateUrl('jury_problem', ['probId' => $problem->getExternalid()]), 'ajax' => true, ], ]; @@ -528,7 +533,7 @@ public function viewAction(Request $request, SubmissionService $submissionServic #[Route(path: '/{probId}/statement', name: 'jury_problem_statement')] public function viewTextAction(string $probId): StreamedResponse { - $problem = $this->em->getRepository(Problem::class)->find($probId); + $problem = $this->em->getRepository(Problem::class)->findByExternalId($probId); if (!$problem) { throw new NotFoundHttpException(sprintf('Problem with ID %s not found', $probId)); } @@ -536,10 +541,10 @@ public function viewTextAction(string $probId): StreamedResponse return $problem->getProblemStatementStreamedResponse(); } - #[Route(path: '/{probId<\d+>}/testcases', name: 'jury_problem_testcases')] - public function testcasesAction(Request $request, int $probId): Response + #[Route(path: '/{probId}/testcases', name: 'jury_problem_testcases')] + public function testcasesAction(Request $request, string $probId): Response { - $problem = $this->em->getRepository(Problem::class)->find($probId); + $problem = $this->em->getRepository(Problem::class)->findByExternalId($probId); if (!$problem) { throw new NotFoundHttpException(sprintf('Problem with ID %s not found', $probId)); } @@ -548,7 +553,7 @@ public function testcasesAction(Request $request, int $probId): Response foreach ($problem->getContestProblems() as $contestproblem) { /** @var ContestProblem $contestproblem */ if ($contestproblem->getContest()->isLocked()) { - $lockedContests[] = 'c' . $contestproblem->getContest()->getCid(); + $lockedContests[] = $contestproblem->getContest()->getExternalid(); break; } } @@ -571,7 +576,7 @@ public function testcasesAction(Request $request, int $probId): Response if (!empty($lockedContests)) { $this->addFlash('danger', 'Cannot edit problem / testcases, it belongs to locked contest(s) ' . join(', ', $lockedContests)); - return $this->redirectToRoute('jury_problem', ['probId' => $problem->getProbid()]); + return $this->redirectToRoute('jury_problem', ['probId' => $problem->getExternalid()]); } $messages = []; $maxrank = 0; @@ -803,10 +808,10 @@ public function testcasesAction(Request $request, int $probId): Response } #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/{probId<\d+>}/testcases/{rank<\d+>}/move/{direction}', name: 'jury_problem_testcase_move')] - public function moveTestcaseAction(int $probId, int $rank, string $direction): Response + #[Route(path: '/{probId}/testcases/{rank<\d+>}/move/{direction}', name: 'jury_problem_testcase_move')] + public function moveTestcaseAction(string $probId, int $rank, string $direction): Response { - $problem = $this->em->getRepository(Problem::class)->find($probId); + $problem = $this->em->getRepository(Problem::class)->findByExternalId($probId); if (!$problem) { throw new NotFoundHttpException(sprintf('Problem with ID %s not found', $probId)); } @@ -815,7 +820,7 @@ public function moveTestcaseAction(int $probId, int $rank, string $direction): R /** @var ContestProblem $contestProblem */ if ($contestProblem->getContest()->isLocked()) { $this->addFlash('danger', 'Cannot edit problem, it belongs to locked contest c' . $contestProblem->getContest()->getCid()); - return $this->redirectToRoute('jury_problem', ['probId' => $problem->getProbid()]); + return $this->redirectToRoute('jury_problem', ['probId' => $problem->getExternalid()]); } } @@ -876,28 +881,29 @@ public function moveTestcaseAction(int $probId, int $rank, string $direction): R /** * @throws NonUniqueResultException */ - #[Route(path: '/{probId<\d+>}/testcases/{rank<\d+>}/fetch/{type}', name: 'jury_problem_testcase_fetch')] - public function fetchTestcaseAction(int $probId, int $rank, string $type): Response + #[Route(path: '/{probId}/testcases/{rank<\d+>}/fetch/{type}', name: 'jury_problem_testcase_fetch')] + public function fetchTestcaseAction(string $probId, int $rank, string $type): Response { /** @var Testcase|null $testcase */ $testcase = $this->em->createQueryBuilder() ->from(Testcase::class, 'tc') ->join('tc.content', 'tcc') + ->join('tc.problem', 'p') ->select('tc', 'tcc') - ->andWhere('tc.problem = :problem') + ->andWhere('p.externalid = :problem') ->andWhere('tc.ranknumber = :ranknumber') ->setParameter('problem', $probId) ->setParameter('ranknumber', $rank) ->getQuery() ->getOneOrNullResult(); if (!$testcase) { - throw new NotFoundHttpException(sprintf('Testcase with rank %d for problem %d not found', $rank, $probId)); + throw new NotFoundHttpException(sprintf('Testcase with rank %d for problem %s not found', $rank, $probId)); } if ($type === 'image') { $extension = $testcase->getImageType(); $mimetype = sprintf('image/%s', $extension); - $filename = sprintf('p%d.t%d.%s', $probId, $rank, $extension); + $filename = sprintf('%s.t%d.%s', $probId, $rank, $extension); } else { $extension = Testcase::EXTENSION_MAPPING[$type]; $mimetype = 'text/plain'; @@ -930,10 +936,10 @@ public function fetchTestcaseAction(int $probId, int $rank, string $type): Respo } #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/{probId<\d+>}/edit', name: 'jury_problem_edit')] - public function editAction(Request $request, int $probId): Response + #[Route(path: '/{probId}/edit', name: 'jury_problem_edit')] + public function editAction(Request $request, string $probId): Response { - $problem = $this->em->getRepository(Problem::class)->find($probId); + $problem = $this->em->getRepository(Problem::class)->findByExternalId($probId); if (!$problem) { throw new NotFoundHttpException(sprintf('Problem with ID %s not found', $probId)); } @@ -941,8 +947,8 @@ public function editAction(Request $request, int $probId): Response foreach ($problem->getContestProblems() as $contestProblem) { /** @var ContestProblem $contestProblem */ if ($contestProblem->getContest()->isLocked()) { - $this->addFlash('danger', 'Cannot edit problem, it belongs to locked contest c' . $contestProblem->getContest()->getCid()); - return $this->redirectToRoute('jury_problem', ['probId' => $problem->getProbid()]); + $this->addFlash('danger', 'Cannot edit problem, it belongs to locked contest ' . $contestProblem->getContest()->getExternalid()); + return $this->redirectToRoute('jury_problem', ['probId' => $problem->getExternalid()]); } } @@ -952,7 +958,7 @@ public function editAction(Request $request, int $probId): Response if ($form->isSubmitted() && $form->isValid()) { $this->saveEntity($problem, $problem->getProbid(), false); - return $this->redirectToRoute('jury_problem', ['probId' => $problem->getProbid()]); + return $this->redirectToRoute('jury_problem', ['probId' => $problem->getExternalid()]); } $data = []; @@ -984,7 +990,7 @@ public function editAction(Request $request, int $probId): Response $this->dj->auditlog('problem', $problem->getExternalid(), 'upload zip', $clientName); } else { $this->postMessages($messages); - return $this->redirectToRoute('jury_problem', ['probId' => $problem->getProbid()]); + return $this->redirectToRoute('jury_problem', ['probId' => $problem->getExternalid()]); } } catch (Exception $e) { $messages['danger'][] = $e->getMessage(); @@ -995,7 +1001,7 @@ public function editAction(Request $request, int $probId): Response } $this->postMessages($messages); - return $this->redirectToRoute('jury_problem', ['probId' => $problem->getProbid()]); + return $this->redirectToRoute('jury_problem', ['probId' => $problem->getExternalid()]); } return $this->render('jury/problem_edit.html.twig', [ @@ -1012,7 +1018,7 @@ public function deleteMultipleAction(Request $request): Response return $this->deleteMultiple( $request, Problem::class, - 'probid', + 'externalid', 'jury_problems', 'No problems could be deleted (they might be locked).', fn(Problem $problem) => !$problem->isLocked() @@ -1020,10 +1026,10 @@ public function deleteMultipleAction(Request $request): Response } #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/{probId<\d+>}/delete', name: 'jury_problem_delete')] - public function deleteAction(Request $request, int $probId): Response + #[Route(path: '/{probId}/delete', name: 'jury_problem_delete')] + public function deleteAction(Request $request, string $probId): Response { - $problem = $this->em->getRepository(Problem::class)->find($probId); + $problem = $this->em->getRepository(Problem::class)->findByExternalId($probId); if (!$problem) { throw new NotFoundHttpException(sprintf('Problem with ID %s not found', $probId)); } @@ -1031,7 +1037,7 @@ public function deleteAction(Request $request, int $probId): Response foreach ($problem->getContestProblems() as $contestProblem) { /** @var ContestProblem $contestProblem */ if ($contestProblem->getContest()->isLocked()) { - $this->addFlash('danger', 'Cannot delete problem, it belongs to locked contest c' . $contestProblem->getContest()->getCid()); + $this->addFlash('danger', 'Cannot delete problem, it belongs to locked contest ' . $contestProblem->getContest()->getExternalid()); return $this->redirectToRoute('jury_problem', ['probId' => $probId]); } } @@ -1061,7 +1067,7 @@ public function deleteAttachmentAction(Request $request, int $attachmentId): Res } $problem = $attachment->getProblem(); - $probId = $problem->getProbid(); + $probId = $problem->getExternalid(); foreach ($problem->getContestProblems() as $contestProblem) { /** @var ContestProblem $contestProblem */ @@ -1086,8 +1092,8 @@ public function deleteTestcaseAction(Request $request, int $testcaseId): Respons foreach ($problem->getContestProblems() as $contestProblem) { /** @var ContestProblem $contestProblem */ if ($contestProblem->getContest()->isLocked()) { - $this->addFlash('danger', 'Cannot edit problem, it belongs to locked contest c' . $contestProblem->getContest()->getCid()); - return $this->redirectToRoute('jury_problem', ['probId' => $problem->getProbid()]); + $this->addFlash('danger', 'Cannot edit problem, it belongs to locked contest ' . $contestProblem->getContest()->getExternalid()); + return $this->redirectToRoute('jury_problem', ['probId' => $problem->getExternalid()]); } } $testcase->setDeleted(true); @@ -1103,12 +1109,12 @@ public function deleteTestcaseAction(Request $request, int $testcaseId): Respons } } $this->em->flush(); - $this->addFlash('danger', sprintf('Testcase %d removed from problem %s. Consider rejudging the problem.', $testcaseId, $problem->getProbid())); - return $this->redirectToRoute('jury_problem_testcases', ['probId' => $problem->getProbid()]); + $this->addFlash('danger', sprintf('Testcase %d removed from problem %s. Consider rejudging the problem.', $testcaseId, $problem->getExternalid())); + return $this->redirectToRoute('jury_problem_testcases', ['probId' => $problem->getExternalid()]); } #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/add', name: 'jury_problem_add')] + #[Route(path: '/add', name: 'jury_problem_add', priority: 1)] public function addAction(Request $request): Response { $problem = new Problem(); @@ -1119,7 +1125,7 @@ public function addAction(Request $request): Response if ($response = $this->processAddFormForExternalIdEntity( $form, $problem, - fn() => $this->generateUrl('jury_problem', ['probId' => $problem->getProbid()]) + fn() => $this->generateUrl('jury_problem', ['probId' => $problem->getExternalid()]) )) { return $response; } @@ -1176,10 +1182,10 @@ private function addTestcasesToZip(array $testcases, ZipArchive $zip, bool $isSa } } - #[Route(path: '/{probId<\d+>}/request-remaining', name: 'jury_problem_request_remaining')] + #[Route(path: '/{probId}/request-remaining', name: 'jury_problem_request_remaining')] public function requestRemainingRunsWholeProblemAction(string $probId): RedirectResponse { - $problem = $this->em->getRepository(Problem::class)->find($probId); + $problem = $this->em->getRepository(Problem::class)->findByExternalId($probId); if (!$problem) { throw new NotFoundHttpException(sprintf('Problem with ID %s not found', $probId)); } @@ -1196,10 +1202,7 @@ public function toggleSubmitAction( string $probId, string $type ): Response { - $contestProblem = $this->em->getRepository(ContestProblem::class)->find([ - 'contest' => $contestId, - 'problem' => $probId, - ]); + $contestProblem = $this->em->getRepository(ContestProblem::class)->findByProblemAndContest($contestId, $probId); if (!$contestProblem) { throw new NotFoundHttpException(sprintf('Problem with ID %s not found for contest %s', $probId, $contestId)); } diff --git a/webapp/src/Controller/Jury/RejudgingController.php b/webapp/src/Controller/Jury/RejudgingController.php index 39442917f3..9c148fa15c 100644 --- a/webapp/src/Controller/Jury/RejudgingController.php +++ b/webapp/src/Controller/Jury/RejudgingController.php @@ -703,7 +703,7 @@ public function createAction(Request $request): Response 'contest' => 'c.externalid', 'judgehost' => 'jt.judgehost', 'language' => 'l.externalid', - 'problem' => 's.problem', + 'problem' => 'p.externalid', 'submission' => 's.submitid', 'team' => 's.team', 'user' => 's.user', @@ -739,6 +739,7 @@ public function createAction(Request $request): Response ->leftJoin('s.rejudging', 'r') ->leftJoin('s.contest', 'c') ->leftJoin('s.language', 'l') + ->leftJoin('s.problem', 'p') ->leftJoin('s.team', 't') ->leftJoin('j.runs', 'jr') ->leftJoin('jr.judgetask', 'jt') diff --git a/webapp/src/Form/Type/ProblemType.php b/webapp/src/Form/Type/ProblemType.php index 21f7959c79..ec39744da5 100644 --- a/webapp/src/Form/Type/ProblemType.php +++ b/webapp/src/Form/Type/ProblemType.php @@ -99,6 +99,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'class' => Language::class, 'multiple' => true, + 'choice_value' => 'externalid', 'choice_label' => fn(Language $language) => sprintf('%s (%s)', $language->getName(), $language->getExternalid()), 'help' => 'List of languages that can be used for this problem. Leave empty to allow all languages that are enabled for this contest.', ]); diff --git a/webapp/templates/jury/contest.html.twig b/webapp/templates/jury/contest.html.twig index 860ea95bfa..b7bd586e70 100644 --- a/webapp/templates/jury/contest.html.twig +++ b/webapp/templates/jury/contest.html.twig @@ -394,7 +394,7 @@ {% for problem in problems %}
    p{{ problem.externalId }}{{ problem.externalId }} {{ problem.problem.name }} {{ problem.shortname }} {{ problem.points }}
    - - - - @@ -33,8 +29,8 @@ {% else %} {{ problem.testcases.count }} {% endif %} - details - {%- if is_granted('ROLE_ADMIN') and not lockedProblem -%} + details + {%- if is_granted('ROLE_ADMIN') and not lockedProblem %} / edit {% endif %} @@ -68,7 +64,7 @@ - + @@ -168,10 +164,10 @@ {% for contestProblem in problem.contestProblems %} - {% set link = path('jury_contest', {'contestId': contestProblem.cid}) %} + {% set link = path('jury_contest', {'contestId': contestProblem.contest.externalid}) %}
    IDp{{ problem.probid }}
    External ID {{ problem.externalid }}
    Problem statement - + @@ -124,7 +120,7 @@ {% else %} {% endif %} @@ -137,13 +133,13 @@
    {%- if is_granted('ROLE_ADMIN') -%} {%- if not lockedProblem -%} - {{ button(path('jury_problem_edit', {'probId': problem.probid}), 'Edit', 'primary', 'edit') }} - {{ button(path('jury_problem_delete', {'probId': problem.probid}), 'Delete', 'danger', 'trash-alt', true) }} + {{ button(path('jury_problem_edit', {'probId': problem.externalid}), 'Edit', 'primary', 'edit') }} + {{ button(path('jury_problem_delete', {'probId': problem.externalid}), 'Delete', 'danger', 'trash-alt', true) }} {% endif %} - {{ button(path('jury_problem_request_remaining', {'probId': problem.probid}), 'Judge remaining testcases', 'secondary', 'gavel') }} + {{ button(path('jury_problem_request_remaining', {'probId': problem.externalid}), 'Judge remaining testcases', 'secondary', 'gavel') }} {% endif %} - {{ button(path('jury_export_problem', {'problemId': problem.probid}), 'Export', 'secondary', 'download') }} - {% include 'jury/partials/rejudge_form.html.twig' with {table: 'problem', id: problem.probid, buttonClass: 'btn-secondary'} %} + {{ button(path('jury_export_problem', {'problemId': problem.externalid}), 'Export', 'secondary', 'download') }} + {% include 'jury/partials/rejudge_form.html.twig' with {table: 'problem', id: problem.externalid, buttonClass: 'btn-secondary'} %}

    Contests

    @@ -157,7 +153,7 @@ class="data-table table table-hover table-striped table-sm">
    CIDID Contest
    shortname
    Contest
    name
    Problem
    shortname
    - c{{ contestProblem.cid }} + {{ contestProblem.contest.externalid }} {{ contestProblem.contest.shortname }} diff --git a/webapp/templates/jury/problem_edit.html.twig b/webapp/templates/jury/problem_edit.html.twig index 0c1e7efa3c..13b04caa48 100644 --- a/webapp/templates/jury/problem_edit.html.twig +++ b/webapp/templates/jury/problem_edit.html.twig @@ -1,7 +1,7 @@ {% extends "jury/base.html.twig" %} {% import "jury/jury_macros.twig" as macros %} -{% block title %}Edit problem {{ problem.probid }} - {{ parent() }}{% endblock %} +{% block title %}Edit problem {{ problem.externalid }} - {{ parent() }}{% endblock %} {% block content %} @@ -10,17 +10,13 @@
    - - - - diff --git a/webapp/templates/jury/problem_testcases.html.twig b/webapp/templates/jury/problem_testcases.html.twig index 8a54121fb8..24808df9a1 100644 --- a/webapp/templates/jury/problem_testcases.html.twig +++ b/webapp/templates/jury/problem_testcases.html.twig @@ -1,7 +1,7 @@ {% extends "jury/base.html.twig" %} {% import "jury/jury_macros.twig" as macros %} -{% block title %}Testcases for p{{ problem.probid }} - {{ parent() }}{% endblock %} +{% block title %}Testcases for p{{ problem.externalid }} - {{ parent() }}{% endblock %} {% block extrahead %} {{ parent() }} @@ -10,10 +10,10 @@ {% block content %} -

    Testcases for p{{ problem.probid }} - {{ problem.name }}

    +

    Testcases for {{ problem.externalid }} - {{ problem.name }}

    From 6702b9c14133f9bc8b56ce91c4a29451e5b8fe7a Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Thu, 30 Oct 2025 12:22:09 +0100 Subject: [PATCH 20/32] Change jury queue task pages to use external IDs instead of internal IDs Part of #3024 --- webapp/src/Controller/Jury/QueueTaskController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/Controller/Jury/QueueTaskController.php b/webapp/src/Controller/Jury/QueueTaskController.php index b7e505df62..0e9c505df7 100644 --- a/webapp/src/Controller/Jury/QueueTaskController.php +++ b/webapp/src/Controller/Jury/QueueTaskController.php @@ -79,7 +79,7 @@ public function indexAction(): Response $queueTaskData['priority']['value'] = static::PRIORITY_MAP[$queueTaskData['priority']['value']]; // Add some links. - $queueTaskData['team.name']['link'] = $this->generateUrl('jury_team', ['teamId' => $queueTask->getTeam()->getTeamid()]); + $queueTaskData['team.name']['link'] = $this->generateUrl('jury_team', ['teamId' => $queueTask->getTeam()->getExternalid()]); $queueTaskData['judgingid']['link'] = $this->generateUrl('jury_submission_by_judging', ['jid' => $queueTask->getJudging()->getJudgingid()]); // Format start time. From d590c34d1efc5414a7966f57b4984d8f6adc421f Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Thu, 30 Oct 2025 12:28:34 +0100 Subject: [PATCH 21/32] Change jury rejudging pages to use external IDs instead of internal IDs Part of #3024 --- webapp/src/Controller/Jury/RejudgingController.php | 9 +++++---- webapp/src/Form/Type/RejudgingType.php | 5 +++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/webapp/src/Controller/Jury/RejudgingController.php b/webapp/src/Controller/Jury/RejudgingController.php index 9c148fa15c..4d5ec33806 100644 --- a/webapp/src/Controller/Jury/RejudgingController.php +++ b/webapp/src/Controller/Jury/RejudgingController.php @@ -807,10 +807,10 @@ private function generateFlashMessagesForSkippedJudgings(array $skipped): void /** @var Judging $judging */ foreach ($skipped as $judging) { $submission = $judging->getSubmission(); - $submitid = $submission->getSubmitid(); + $submitid = $submission->getExternalid(); $rejudgingid = $submission->getRejudging()->getRejudgingid(); $msg = sprintf( - 'Skipping submission s%d since it is ' . + 'Skipping submission %s since it is ' . 'already part of rejudging r%d.', $submitid, $rejudgingid @@ -840,7 +840,7 @@ private function getStats(Rejudging $rejudging): array ->leftJoin('j.rejudging', 'r') ->leftJoin('j.submission', 's') ->leftJoin('jt.judgehost', 'jh') - ->select('r.rejudgingid, j.judgingid', 's.submitid', 'jh.hostname', 'j.result', + ->select('r.rejudgingid, j.judgingid', 's.externalid as submitid', 'jh.hostname', 'j.result', 'AVG(jr.runtime) AS runtime_avg', 'COUNT(jr.runtime) AS ntestcases', '(j.endtime - j.starttime) AS duration' ) @@ -899,7 +899,8 @@ private function getStats(Rejudging $rejudging): array ->select('t.ranknumber', 'MAX(jr.runtime) - MIN(jr.runtime) AS spread') ->leftJoin('jr.judging', 'j') ->leftJoin('jr.testcase', 't') - ->andWhere('j.submission = :submitid') + ->leftJoin('j.submission', 's') + ->andWhere('s.externalid = :submitid') ->setParameter('submitid', $submitid) ->groupBy('jr.testcase') ->getQuery() diff --git a/webapp/src/Form/Type/RejudgingType.php b/webapp/src/Form/Type/RejudgingType.php index 5afcda07c6..96f453f0d8 100644 --- a/webapp/src/Form/Type/RejudgingType.php +++ b/webapp/src/Form/Type/RejudgingType.php @@ -53,6 +53,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'multiple' => true, 'choice_label' => 'name', + 'choice_value' => 'externalid', 'query_builder' => fn(EntityRepository $er) => $er ->createQueryBuilder('c') ->where('c.enabled = 1') @@ -64,6 +65,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'class' => Problem::class, 'required' => false, 'choice_label' => 'name', + 'choice_value' => 'externalid', 'choices' => [], ]); $builder->add('languages', EntityType::class, [ @@ -72,6 +74,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'class' => Language::class, 'required' => false, 'choice_label' => 'name', + 'choice_value' => 'externalid', 'query_builder' => fn(EntityRepository $er) => $er ->createQueryBuilder('l') ->where('l.allowSubmit = 1') @@ -83,6 +86,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'class' => Team::class, 'required' => false, 'choice_label' => 'name', + 'choice_value' => 'externalid', 'choices' => [], ]); $builder->add('users', EntityType::class, [ @@ -91,6 +95,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'multiple' => true, 'choice_label' => 'name', + 'choice_value' => 'externalid', 'query_builder' => fn(EntityRepository $er) => $er ->createQueryBuilder('u') ->where('u.enabled = 1') From 02bd5cb352619e444d5112359ebc5adf603ea77e Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Thu, 30 Oct 2025 13:21:20 +0100 Subject: [PATCH 22/32] Change jury scoreboard page to use external IDs instead of internal IDs Part of #3024 --- .../src/Controller/Jury/ContestController.php | 4 +-- .../Jury/TeamAffiliationController.php | 2 +- webapp/src/Controller/Jury/TeamController.php | 9 +++++- .../partials/scoreboard_table.html.twig | 28 +++++++++---------- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/webapp/src/Controller/Jury/ContestController.php b/webapp/src/Controller/Jury/ContestController.php index 36a1e0dfb3..8b14a4796f 100644 --- a/webapp/src/Controller/Jury/ContestController.php +++ b/webapp/src/Controller/Jury/ContestController.php @@ -465,7 +465,7 @@ public function editAction(Request $request, string $contestId): Response if ($problems = $contestData['problems'] ?? null) { $existingProblemIndices = []; foreach ($contest->getProblems() as $index => $problem) { - $existingProblemIndices[$problem->getProbId()] = $index; + $existingProblemIndices[$problem->getExternalId()] = $index; } $indexForNew = $contest->getProblems()->count(); $newProblems = []; @@ -565,7 +565,7 @@ public function editAction(Request $request, string $contestId): Response $this->eventLogService->log('problems', $problem->getProbid(), EventLogService::ACTION_DELETE, $contest->getCid(), null, null, false); } - return $this->redirectToRoute('jury_contest', ['contestId' => $contest->getcid()]); + return $this->redirectToRoute('jury_contest', ['contestId' => $contest->getExternalId()]); } $this->em->refresh($contest); diff --git a/webapp/src/Controller/Jury/TeamAffiliationController.php b/webapp/src/Controller/Jury/TeamAffiliationController.php index ef0ab44ee5..7b03b4646c 100644 --- a/webapp/src/Controller/Jury/TeamAffiliationController.php +++ b/webapp/src/Controller/Jury/TeamAffiliationController.php @@ -131,7 +131,7 @@ public function indexAction( ]); } - #[Route(path: '/{affilId<\d+>}', name: 'jury_team_affiliation')] + #[Route(path: '/{affilId}', name: 'jury_team_affiliation')] public function viewAction(Request $request, ScoreboardService $scoreboardService, int $affilId): Response { $teamAffiliation = $this->em->getRepository(TeamAffiliation::class)->find($affilId); diff --git a/webapp/src/Controller/Jury/TeamController.php b/webapp/src/Controller/Jury/TeamController.php index aa2fbc9e46..fb217378b4 100644 --- a/webapp/src/Controller/Jury/TeamController.php +++ b/webapp/src/Controller/Jury/TeamController.php @@ -5,6 +5,8 @@ use App\Controller\BaseController; use App\DataTransferObject\SubmissionRestriction; use App\Entity\Contest; +use App\Entity\Language; +use App\Entity\Problem; use App\Entity\Role; use App\Entity\Team; use App\Entity\User; @@ -289,7 +291,12 @@ public function viewAction( 'judgehost' => 'judgehost', default => throw new BadRequestHttpException(sprintf('Restriction on %s not allowed.', $key)), }; - $restrictions->$key = is_numeric($value) ? (int)$value : $value; + $restrictionValue = match ($key) { + 'problemId' => $this->em->getRepository(Problem::class)->findByExternalId($value)->getProbid(), + 'languageId' => $this->em->getRepository(Language::class)->findByExternalId($value)->getLangid(), + default => $value, + }; + $restrictions->$key = is_numeric($restrictionValue) ? (int)$restrictionValue : $restrictionValue; $restrictionTexts[] = sprintf('%s %s', $restrictionKeyText, $value); } $restrictionText = implode(', ', $restrictionTexts); diff --git a/webapp/templates/partials/scoreboard_table.html.twig b/webapp/templates/partials/scoreboard_table.html.twig index 15ca1b287a..d5720b9313 100644 --- a/webapp/templates/partials/scoreboard_table.html.twig +++ b/webapp/templates/partials/scoreboard_table.html.twig @@ -94,7 +94,7 @@ {% set target = '_self' %} {% if not static %} {% if jury %} - {% set link = path('jury_problem', {'probId': problem.probid}) %} + {% set link = path('jury_problem', {'probId': problem.externalid}) %} {% elseif problem.problem.problemstatementType is not empty %} {% if public %} {% set link = path('public_problem_statement', {probId: problem.externalid}) %} @@ -188,7 +188,7 @@ {% if score.team.affiliation %} {% set link = null %} {% if jury %} - {% set link = path('jury_team_affiliation', {'affilId': score.team.affiliation.affilid}) %} + {% set link = path('jury_team_affiliation', {'affilId': score.team.affiliation.externalid}) %} {% endif %} {{ score.team.affiliation.country|countryFlag }} @@ -201,7 +201,7 @@ {% if score.team.affiliation %} {% set link = null %} {% if jury %} - {% set link = path('jury_team_affiliation', {'affilId': score.team.affiliation.affilid}) %} + {% set link = path('jury_team_affiliation', {'affilId': score.team.affiliation.externalid}) %} {% endif %} {% set affiliationId = score.team.affiliation.externalid %} @@ -225,10 +225,10 @@ {% set extra = null %} {% if static %} {% set link = '#' %} - {% set extra = 'data-bs-toggle="modal" data-bs-target="#team-modal-' ~ score.team.teamid ~ '"' %} + {% set extra = 'data-bs-toggle="modal" data-bs-target="#team-modal-' ~ score.team.externalid ~ '"' %} {% else %} {% if jury %} - {% set link = path('jury_team', {teamId: score.team.teamid}) %} + {% set link = path('jury_team', {teamId: score.team.externalid}) %} {% elseif public %} {% set link = path('public_team', {teamId: score.team.externalid}) %} {% set extra = 'data-ajax-modal' %} @@ -321,8 +321,8 @@ {% set link = null %} {% set extra = null %} {% if jury %} - {% set restrict = {problemId: problem.probid} %} - {% set link = path('jury_team', {teamId: score.team.teamid, restrict: restrict}) %} + {% set restrict = {problemId: problem.externalid} %} + {% set link = path('jury_team', {teamId: score.team.externalid, restrict: restrict}) %} {% elseif static %} {% set link = '#' %} {% set extra = 'data-submissions-url="' ~ path('public_submissions_data') ~ '"' %} @@ -462,7 +462,7 @@ {% if score.team.affiliation %} {% set link = null %} {% if jury %} - {% set link = path('jury_team_affiliation', {'affilId': score.team.affiliation.affilid}) %} + {% set link = path('jury_team_affiliation', {'affilId': score.team.affiliation.externalid}) %} {% endif %} {{ score.team.affiliation.country|countryFlag }} @@ -475,7 +475,7 @@ {% if score.team.affiliation %} {% set link = null %} {% if jury %} - {% set link = path('jury_team_affiliation', {'affilId': score.team.affiliation.affilid}) %} + {% set link = path('jury_team_affiliation', {'affilId': score.team.affiliation.externalid}) %} {% endif %} {% set affiliationId = score.team.affiliation.externalid %} @@ -499,12 +499,12 @@ {% set extra = null %} {% if static %} {% set link = '#' %} - {% set extra = 'data-bs-toggle="modal" data-bs-target="#team-modal-' ~ score.team.teamid ~ '"' %} + {% set extra = 'data-bs-toggle="modal" data-bs-target="#team-modal-' ~ score.team.externalid ~ '"' %} {% else %} {% if jury %} - {% set link = path('jury_team', {teamId: score.team.teamid}) %} + {% set link = path('jury_team', {teamId: score.team.externalid}) %} {% elseif public %} - {% set link = path('public_team', {teamId: score.team.teamid}) %} + {% set link = path('public_team', {teamId: score.team.externalid}) %} {% set extra = 'data-ajax-modal' %} {% else %} {% set link = path('team_team', {teamId: score.team.externalid}) %} @@ -572,7 +572,7 @@ {% if static %} {% for score in scores %} - {% embed 'partials/modal.html.twig' with {'modalId': 'team-modal-' ~ score.team.teamid} %} + {% embed 'partials/modal.html.twig' with {'modalId': 'team-modal-' ~ score.team.externalid} %} {% block title %}{{ score.team.effectiveName }}{% endblock %} {% block content %} {% include 'partials/team.html.twig' with {size: 6, team: score.team} %} @@ -604,7 +604,7 @@ From 2d9333d5bbbc2095abd86bd21aa23656673b1d0e Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Sat, 29 Nov 2025 15:34:00 +0100 Subject: [PATCH 23/32] Change jury submission pages to use external IDs instead of internal IDs Part of #3024 # Conflicts: # webapp/templates/jury/partials/submission_diff.html.twig # webapp/templates/jury/submission_source.html.twig --- webapp/public/js/domjudge.js | 8 +- .../Controller/Jury/RejudgingController.php | 2 +- .../Controller/Jury/SubmissionController.php | 146 +++++++++--------- .../SubmissionRestriction.php | 78 +++++----- webapp/src/Entity/Submission.php | 3 +- .../src/Form/Type/SubmissionsFilterType.php | 5 + .../src/Repository/SubmissionRepository.php | 17 ++ webapp/src/Service/DOMJudgeService.php | 2 +- webapp/src/Service/SubmissionService.php | 2 +- webapp/src/Twig/TwigExtension.php | 4 +- .../jury/partials/submission_diff.html.twig | 20 +-- webapp/templates/jury/submission.html.twig | 75 +++++---- .../jury/submission_edit_source.html.twig | 4 +- .../jury/submission_source.html.twig | 10 +- webapp/var/log/.gitkeep | 0 15 files changed, 199 insertions(+), 177 deletions(-) create mode 100644 webapp/src/Repository/SubmissionRepository.php mode change 100755 => 100644 webapp/var/log/.gitkeep diff --git a/webapp/public/js/domjudge.js b/webapp/public/js/domjudge.js index 790d7043c3..bc17cfa8fd 100644 --- a/webapp/public/js/domjudge.js +++ b/webapp/public/js/domjudge.js @@ -1403,7 +1403,7 @@ function initDiffEditor(editorId) { 'onDiffSelectChange': (f) => { select.change((e) => { const noDiff = e.target.value === ""; - const submitId = parseInt(e.target.value); + const submitId = e.target.value; f(submitId, noDiff); }); } @@ -1432,10 +1432,10 @@ function initDiffEditor(editorId) { diffTitle.style.display = 'inline'; diffTag.innerText = selected.dataset.tag; diffLink.href = selected.dataset.url; - diffLink.innerText = `s${submitId}`; + diffLink.innerText = submitId; } }; - updateSelect(parseInt(select[0].value), select[0].value === ""); + updateSelect(select[0].value, select[0].value === ""); editor.onDiffSelectChange(updateSelect); } @@ -1559,7 +1559,7 @@ function initDiffEditorTab(editorId, diffId, submissionId, models) { return; } - const submitId = parseInt(editors[editorId].getDiffSelection()); + const submitId = editors[editorId].getDiffSelection(); const noDiff = editors[editorId].getDiffSelection() === ""; if (noDiff) { setIcon('file'); diff --git a/webapp/src/Controller/Jury/RejudgingController.php b/webapp/src/Controller/Jury/RejudgingController.php index 4d5ec33806..bd618d3032 100644 --- a/webapp/src/Controller/Jury/RejudgingController.php +++ b/webapp/src/Controller/Jury/RejudgingController.php @@ -704,7 +704,7 @@ public function createAction(Request $request): Response 'judgehost' => 'jt.judgehost', 'language' => 'l.externalid', 'problem' => 'p.externalid', - 'submission' => 's.submitid', + 'submission' => 's.externalid', 'team' => 's.team', 'user' => 's.user', 'rejudging' => 'j2.rejudging', diff --git a/webapp/src/Controller/Jury/SubmissionController.php b/webapp/src/Controller/Jury/SubmissionController.php index ce3fd24bac..6a6ba30f59 100644 --- a/webapp/src/Controller/Jury/SubmissionController.php +++ b/webapp/src/Controller/Jury/SubmissionController.php @@ -119,25 +119,25 @@ public function indexAction( $contests = $this->dj->getCurrentContests(); if ($contest = $this->dj->getCurrentContest()) { - $contests = [$contest->getCid() => $contest]; + $contests = [$contest->getExternalid() => $contest]; } // Load preselected filters $filtersFromCookie = Utils::jsonDecode((string)$this->dj->getCookie('domjudge_submissionsfilter') ?: '[]'); $formAssociationFields = [ - 'problem_id' => [Problem::class, 'probid'], - 'language_id' => [Language::class, 'langid'], - 'team_id' => [Team::class, 'teamid'], - 'category_id' => [TeamCategory::class, 'categoryid'], - 'affiliation_id' => [TeamAffiliation::class, 'affilid'], + 'problem_id' => Problem::class, + 'language_id' => Language::class, + 'team_id' => Team::class, + 'category_id' => TeamCategory::class, + 'affiliation_id' => TeamAffiliation::class, ]; // Build the filter form. $filtersForForm = ['result' => $filtersFromCookie['result'] ?? []]; $hasFilters = !empty($filtersForForm['result']); - foreach ($formAssociationFields as $field => [$entityClass, $idField]) { - $filtersForForm[$field] = $this->em->getRepository($entityClass)->findBy([$idField => $filtersFromCookie[$field] ?? []]); + foreach ($formAssociationFields as $field => $entityClass) { + $filtersForForm[$field] = $this->em->getRepository($entityClass)->findBy(['externalid' => $filtersFromCookie[$field] ?? []]); $hasFilters = $hasFilters || !empty($filtersForForm[$field]); } $appliedFilters = $filtersForForm; @@ -149,13 +149,13 @@ public function indexAction( if ($form->isSubmitted() && $form->isValid()) { $filtersForCookie = ['result' => $form->get('result')->getData()]; $hasFilters = !empty($filtersForCookie['result']); - foreach ($formAssociationFields as $field => [$entityClass, $idField]) { - $method = 'get' . ucfirst($idField); - $filtersForCookie[$field] = array_map(fn($entity) => $entity->$method(), $form->get($field)->getData()); + $appliedFilters = []; + foreach ($formAssociationFields as $field => $entityClass) { + $filtersForCookie[$field] = array_map(fn($entity) => $entity->getExternalid(), $form->get($field)->getData()); $hasFilters = $hasFilters || !empty($filtersForCookie[$field]); + $appliedFilters[$field] = $form->get($field)->getData(); } $response = $this->dj->setCookie('domjudge_submissionsfilter', Utils::jsonEncode($filtersForCookie), response: $response); - $appliedFilters = $filtersForCookie; } if (!empty($appliedFilters['result'])) { @@ -188,10 +188,10 @@ public function indexAction( $disabledLangs = []; foreach ($submissions as $submission) { if (!$submission->getContestProblem()->getAllowJudge()) { - $disabledProblems[$submission->getProblemId()] = $submission->getProblem()->getName(); + $disabledProblems[$submission->getExternalid()] = $submission->getProblem()->getName(); } if (!$submission->getLanguage()->getAllowJudge()) { - $disabledLangs[$submission->getLanguage()->getLangid()] = $submission->getLanguage()->getName(); + $disabledLangs[$submission->getLanguage()->getExternalid()] = $submission->getLanguage()->getName(); } } @@ -225,10 +225,10 @@ public function indexAction( /** * @throws NonUniqueResultException */ - #[Route(path: '/{submitId<\d+>}', name: 'jury_submission')] + #[Route(path: '/{submitId}', name: 'jury_submission')] public function viewAction( Request $request, - int $submitId, + string $submitId, #[MapQueryParameter(name: 'jid')] ?int $judgingId = null, #[MapQueryParameter(name: 'rejudgingid')] @@ -240,11 +240,17 @@ public function viewAction( // If judging ID is not set but rejudging ID is, try to deduce the judging ID from the database. if (!isset($judgingId) && isset($rejudgingId)) { - $judging = $this->em->getRepository(Judging::class) - ->findOneBy([ - 'submission' => $submitId, - 'rejudging' => $rejudgingId - ]); + /** @var Judging|null $judging */ + $judging = $this->em->createQueryBuilder() + ->from(Judging::class, 'j') + ->select('j') + ->innerJoin('j.submission', 's') + ->andWhere('j.rejudging = :rejudgingId') + ->andWhere('s.externalid = :submitId') + ->setParameter('rejudgingId', $rejudgingId) + ->setParameter('submitId', $submitId) + ->getQuery() + ->getOneOrNullResult(); if ($judging) { $judgingId = $judging->getJudgingid(); } @@ -261,13 +267,13 @@ public function viewAction( ->leftJoin('s.external_judgements', 'ej', Join::WITH, 'ej.valid = 1') ->leftJoin('s.contest_problem', 'cp') ->select('s', 't', 'p', 'l', 'c', 'f', 'cp', 'ej') - ->andWhere('s.submitid = :submitid') + ->andWhere('s.externalid = :submitid') ->setParameter('submitid', $submitId) ->getQuery() ->getOneOrNullResult(); if (!$submission) { - throw new NotFoundHttpException(sprintf('No submission found with ID %d', $submitId)); + throw new NotFoundHttpException(sprintf('No submission found with ID %s', $submitId)); } $judgingData = $this->em->createQueryBuilder() @@ -590,7 +596,7 @@ public function viewAction( // Automatically refresh page while we wait for judging data. $twigData['refresh'] = [ 'after' => 15, - 'url' => $this->generateUrl('jury_submission', ['submitId' => $submission->getSubmitid()]), + 'url' => $this->generateUrl('jury_submission', ['submitId' => $submission->getExternalid()]), ]; } else { $contestProblem = $submission->getContestProblem(); @@ -681,7 +687,7 @@ public function requestFullDebug(Request $request, Judging $jid): RedirectRespon $this->em->flush(); } return $this->redirectToLocalReferrer($this->router, $request, $this->generateUrl('jury_submission', [ - 'submitId' => $jid->getSubmission()->getSubmitid(), + 'submitId' => $jid->getSubmission()->getExternalid(), 'jid' => $jid->getJudgingid(), ])); } @@ -715,7 +721,7 @@ public function requestOutput(Request $request, Judging $jid, JudgingRun $jrid): $this->em->persist($judgeTask); $this->em->flush(); return $this->redirectToLocalReferrer($this->router, $request, $this->generateUrl('jury_submission', [ - 'submitId' => $jid->getSubmission()->getSubmitid(), + 'submitId' => $jid->getSubmission()->getExternalid(), 'jid' => $jid->getJudgingid(), ])); } @@ -724,7 +730,7 @@ public function requestOutput(Request $request, Judging $jid, JudgingRun $jrid): public function viewForJudgingAction(Judging $jid): RedirectResponse { return $this->redirectToRoute('jury_submission', [ - 'submitId' => $jid->getSubmission()->getSubmitid(), + 'submitId' => $jid->getSubmission()->getExternalid(), 'jid' => $jid->getJudgingid(), ]); } @@ -733,7 +739,7 @@ public function viewForJudgingAction(Judging $jid): RedirectResponse public function viewForExternalJudgementAction(ExternalJudgement $externalJudgement): RedirectResponse { return $this->redirectToRoute('jury_submission', [ - 'submitId' => $externalJudgement->getSubmission()->getSubmitid(), + 'submitId' => $externalJudgement->getSubmission()->getExternalid(), ]); } @@ -756,34 +762,18 @@ public function viewForContestExternalIdAction(string $externalContestId, string } return $this->redirectToRoute('jury_submission', [ - 'submitId' => $submission->getSubmitid(), - ]); - } - - #[Route(path: '/by-external-id/{externalId}', name: 'jury_submission_by_external_id')] - public function viewForExternalIdAction(string $externalId): RedirectResponse - { - if (!$this->dj->getCurrentContest()) { - throw new BadRequestHttpException("Cannot determine submission from external ID without selecting a contest."); - } - - $submission = $this->em->getRepository(Submission::class) - ->findOneBy([ - 'contest' => $this->dj->getCurrentContest(), - 'externalid' => $externalId - ]); - - if (!$submission) { - throw new NotFoundHttpException(sprintf('No submission found with external ID %s', $externalId)); - } - - return $this->redirectToRoute('jury_submission', [ - 'submitId' => $submission->getSubmitid(), + 'submitId' => $submission->getExternalid(), ]); } #[Route(path: '/{submission}/runs/{contest}/{run}/team-output', name: 'jury_submission_team_output')] - public function teamOutputAction(Submission $submission, Contest $contest, JudgingRun $run): StreamedResponse + public function teamOutputAction( + #[MapEntity(mapping: ['submission' => 'externalid'])] + Submission $submission, + #[MapEntity(mapping: ['contest' => 'externalid'])] + Contest $contest, + JudgingRun $run, + ): StreamedResponse { if ($run->getJudging()->getSubmission()->getSubmitid() !== $submission->getSubmitid() || $submission->getContest()->getCid() !== $contest->getCid()) { throw new BadRequestHttpException('Integrity problem while fetching team output.'); @@ -792,9 +782,9 @@ public function teamOutputAction(Submission $submission, Contest $contest, Judgi throw new NotFoundHttpException('No team output available (yet).'); } - $filename = sprintf('p%d.t%d.%s.run%d.team%d.out', $submission->getProblem()->getProbid(), $run->getTestcase()->getRank(), + $filename = sprintf('%s.t%d.%s.run%d.%s.out', $submission->getProblem()->getExternalid(), $run->getTestcase()->getRank(), $submission->getContestProblem()->getShortname(), $run->getRunid(), - $submission->getTeam()->getTeamid()); + $submission->getTeam()->getExternalid()); $outputRun = $run->getOutput()->getOutputRun(); return Utils::streamAsBinaryFile($outputRun, $filename); @@ -809,6 +799,7 @@ private function allowEdit(): bool { */ #[Route(path: '/{submission}/source', name: 'jury_submission_source')] public function sourceAction( + #[MapEntity(mapping: ['submission' => 'externalid'])] Submission $submission, #[MapQueryParameter] ?int $fetch = null @@ -840,7 +831,7 @@ public function sourceAction( } /** @var array{ - * submitid: int, + * submitid: string, * tag?: string * } otherSubmissions */ @@ -848,7 +839,7 @@ public function sourceAction( $originalSubmission = $submission->getOriginalSubmission(); if ($originalSubmission) { $otherSubmissions[] = [ - 'submitid' => $originalSubmission->getSubmitid(), + 'submitid' => $originalSubmission->getExternalid(), 'tag' => 'original', ]; /** @var Submission $oldSubmission */ @@ -888,20 +879,21 @@ public function sourceAction( } if ($oldSubmission !== null) { $otherSubmissions[] = [ - 'submitid' => $oldSubmission->getSubmitid(), + 'submitid' => $oldSubmission->getExternalid(), 'tag' => 'previous', ]; } $files_query = array_map(fn($s) => $s['submitid'], $otherSubmissions); - $files_query[] = $submission->getSubmitid(); + $files_query[] = $submission->getExternalid(); /** @var SubmissionFile[] $oldFiles */ $oldFiles = $this->em->createQueryBuilder() ->from(SubmissionFile::class, 'file') ->select('file') - ->andWhere('file.submission in (:submissions)') + ->join('file.submission', 's') + ->andWhere('s.externalid in (:submissions)') ->setParameter('submissions', $files_query) - ->orderBy('file.submission, file.ranknumber') + ->orderBy('s.externalid, file.ranknumber') ->getQuery() ->getResult(); @@ -915,7 +907,7 @@ public function sourceAction( /** @var array $renames */ $renames = []; foreach ($oldFiles as $f) { - $submitId = $f->getSubmission()->getSubmitid(); + $submitId = $f->getSubmission()->getExternalid(); $files[$f->getFilename()] ??= []; $files[$f->getFilename()][$submitId] = [ 'rank' => $f->getRank(), @@ -928,7 +920,7 @@ public function sourceAction( } // Handle file renaming for a single-file submission. - $renamedTo = $renames[$submission->getSubmitid()]; + $renamedTo = $renames[$submission->getExternalid()]; if ($renamedTo !== false) { foreach ($renames as $submitId => $filename) { if ($filename !== false && $filename !== $renamedTo) { @@ -954,13 +946,18 @@ public function sourceAction( } #[Route(path: '/{submission}/edit-source', name: 'jury_submission_edit_source')] - public function editSourceAction(Request $request, Submission $submission, #[MapQueryParameter] ?int $rank = null): Response - { + public function editSourceAction( + Request $request, + #[MapEntity(mapping: ['submission' => 'externalid'])] + Submission $submission, + #[MapQueryParameter] + ?int $rank = null + ): Response { if (!$this->allowEdit()) { $this->addFlash('danger', 'You cannot re-submit code without being a team.'); return $this->redirectToLocalReferrer($this->router, $request, $this->generateUrl( 'jury_submission', - ['submitId' => $submission->getSubmitid()] + ['submitId' => $submission->getExternalid()] )); } @@ -988,6 +985,7 @@ public function editSourceAction(Request $request, Submission $submission, #[Map ->add('problem', EntityType::class, [ 'class' => Problem::class, 'choice_label' => 'name', + 'choice_value' => 'externalid', 'query_builder' => fn(EntityRepository $er) => $er->createQueryBuilder('p') ->join('p.contest_problems', 'cp') ->andWhere('cp.allowSubmit = 1') @@ -998,6 +996,7 @@ public function editSourceAction(Request $request, Submission $submission, #[Map ->add('language', EntityType::class, [ 'class' => Language::class, 'choice_label' => 'name', + 'choice_value' => 'externalid', 'query_builder' => fn(EntityRepository $er) => $er->createQueryBuilder('lang') ->andWhere('lang.allowSubmit = 1') ->orderBy('lang.name') @@ -1058,10 +1057,10 @@ public function editSourceAction(Request $request, Submission $submission, #[Map if (!$submittedSubmission) { $this->addFlash('danger', $message); - return $this->redirectToRoute('jury_submission', ['submitId' => $submission->getSubmitid()]); + return $this->redirectToRoute('jury_submission', ['submitId' => $submission->getExternalid()]); } - return $this->redirectToRoute('jury_submission', ['submitId' => $submittedSubmission->getSubmitid()]); + return $this->redirectToRoute('jury_submission', ['submitId' => $submittedSubmission->getExternalid()]); } $twigData = [ @@ -1098,14 +1097,14 @@ public function requestRemainingRuns(Request $request, int $judgingId): Redirect * @throws DBALException */ #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/{submitId<\d+>}/update-status', name: 'jury_submission_update_status', methods: ['POST'])] + #[Route(path: '/{submitId}/update-status', name: 'jury_submission_update_status', methods: ['POST'])] public function updateStatusAction( EventLogService $eventLogService, ScoreboardService $scoreboardService, Request $request, - int $submitId + string $submitId ): RedirectResponse { - $submission = $this->em->getRepository(Submission::class)->find($submitId); + $submission = $this->em->getRepository(Submission::class)->findByExternalId($submitId); $valid = $request->request->getBoolean('valid'); $submission->setValid($valid); $this->em->flush(); @@ -1126,7 +1125,7 @@ public function updateStatusAction( $scoreboardService->calculateScoreRow($contest, $team, $problem); return $this->redirectToLocalReferrer($this->router, $request, - $this->generateUrl('jury_submission', ['submitId' => $submission->getSubmitid()]) + $this->generateUrl('jury_submission', ['submitId' => $submission->getExternalid()]) ); } @@ -1206,7 +1205,6 @@ public function verifyAction( #[Route(path: '/shadow-difference/{extjudgementid<\d+>}/verify', name: 'jury_shadow_difference_verify', methods: ['POST'])] public function verifyShadowDifferenceAction( - EventLogService $eventLogService, Request $request, int $extjudgementid ): RedirectResponse { @@ -1270,7 +1268,7 @@ protected function processClaim( $this->dj->auditlog($auditLogType, (string)$auditLogId, $action . 'ed'); if ($action === 'claim') { - return $this->redirectToRoute('jury_submission', ['submitId' => $judging->getSubmission()->getSubmitid()]); + return $this->redirectToRoute('jury_submission', ['submitId' => $judging->getSubmission()->getExternalid()]); } else { return $this->redirectToLocalReferrer($this->router, $request, $this->generateUrl('jury_submissions') @@ -1282,7 +1280,7 @@ protected function processClaim( return null; } - #[Route(path: '/{submitId<\d+>}/create-tasks', name: 'jury_submission_create_tasks')] + #[Route(path: '/{submitId}/create-tasks', name: 'jury_submission_create_tasks')] public function createJudgeTasks(string $submitId): RedirectResponse { $this->dj->unblockJudgeTasksForSubmission($submitId); diff --git a/webapp/src/DataTransferObject/SubmissionRestriction.php b/webapp/src/DataTransferObject/SubmissionRestriction.php index 1d66065c2a..268348fd99 100644 --- a/webapp/src/DataTransferObject/SubmissionRestriction.php +++ b/webapp/src/DataTransferObject/SubmissionRestriction.php @@ -2,45 +2,51 @@ namespace App\DataTransferObject; +use App\DataTransferObject\Scoreboard\Problem; +use App\Entity\Language; +use App\Entity\Team; +use App\Entity\TeamAffiliation; +use App\Entity\TeamCategory; + class SubmissionRestriction { /** - * @param int|null $rejudgingId ID of a rejudging to filter on - * @param bool|null $verified If true, only return verified submissions - * If false, only return unverified or unjudged submissions - * @param bool|null $judged If true, only return judged submissions - * If false, only return unjudged submissions - * @param bool|null $judging If true, only return submissions currently being judged - * If false, only return submssions which are already judged or still - * need to be judged - * @param bool|null $rejudgingDifference If true, only return judgings that differ from their - * original result in final verdict. Vice versa if false - * @param int|null $teamId ID of a team to filter on - * @param list|null $teamIds ID's of teams to filter on - * @param int|null $categoryId ID of a category to filter on - * @param list|null $categoryIds ID's of categories to filter on - * @param int|null $affiliationId ID of an affiliation to filter on - * @param list|null $affiliationIds ID's of affiliations to filter on - * @param int|null $problemId ID of a problem to filter on - * @param list|null $problemIds ID's of problems to filter on - * @param int|null $languageId ID of a language to filter on - * @param list|null $languageIds ID's of languages to filter on - * @param string|null $judgehost Hostname of a judgehost to filter on - * @param string|null $oldResult Result of old judging to filter on - * @param string|null $result Result of current judging to filter on - * @param list|null $results Results of current judging to filter on - * @param int|null $userId Filter on specific user - * @param bool|null $visible If true, only return submissions from visible teams - * @param bool|null $externalDifference If true, only return results with a difference with an - * external system - * If false, only return results without a difference with an - * external system - * @param string|null $externalResult Result in the external system - * @param bool|null $externallyJudged If true, only return externally judged submissions - * If false, only return externally unjudged submissions - * @param bool|null $externallyVerified If true, only return verified submissions - * If false, only return unverified or unjudged submissions - * @param bool|null $withExternalId If true, only return submissions with an external ID. + * @param int|null $rejudgingId ID of a rejudging to filter on + * @param bool|null $verified If true, only return verified submissions + * If false, only return unverified or unjudged submissions + * @param bool|null $judged If true, only return judged submissions + * If false, only return unjudged submissions + * @param bool|null $judging If true, only return submissions currently being judged + * If false, only return submssions which are already judged or still + * need to be judged + * @param bool|null $rejudgingDifference If true, only return judgings that differ from their + * original result in final verdict. Vice versa if false + * @param int|null $teamId ID of a team to filter on + * @param list|list|null $teamIds ID's of teams to filter on + * @param int|null $categoryId ID of a category to filter on + * @param list|list|null $categoryIds ID's of categories to filter on + * @param int|null $affiliationId ID of an affiliation to filter on + * @param list|list|null $affiliationIds ID's of affiliations to filter on + * @param int|null $problemId ID of a problem to filter on + * @param list|list|null $problemIds ID's of problems to filter on + * @param int|null $languageId ID of a language to filter on + * @param list|list|null $languageIds ID's of languages to filter on + * @param string|null $judgehost Hostname of a judgehost to filter on + * @param string|null $oldResult Result of old judging to filter on + * @param string|null $result Result of current judging to filter on + * @param list|null $results Results of current judging to filter on + * @param int|null $userId Filter on specific user + * @param bool|null $visible If true, only return submissions from visible teams + * @param bool|null $externalDifference If true, only return results with a difference with an + * external system + * If false, only return results without a difference with an + * external system + * @param string|null $externalResult Result in the external system + * @param bool|null $externallyJudged If true, only return externally judged submissions + * If false, only return externally unjudged submissions + * @param bool|null $externallyVerified If true, only return verified submissions + * If false, only return unverified or unjudged submissions + * @param bool|null $withExternalId If true, only return submissions with an external ID. */ public function __construct( public ?int $rejudgingId = null, diff --git a/webapp/src/Entity/Submission.php b/webapp/src/Entity/Submission.php index 0932fff1bd..909718fd84 100644 --- a/webapp/src/Entity/Submission.php +++ b/webapp/src/Entity/Submission.php @@ -4,6 +4,7 @@ use App\Controller\API\AbstractRestController as ARC; use App\DataTransferObject\FileWithName; +use App\Repository\SubmissionRepository; use App\Utils\Utils; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -16,7 +17,7 @@ /** * All incoming submissions. */ -#[ORM\Entity] +#[ORM\Entity(repositoryClass: SubmissionRepository::class)] #[ORM\Table(options: [ 'collation' => 'utf8mb4_unicode_ci', 'charset' => 'utf8mb4', diff --git a/webapp/src/Form/Type/SubmissionsFilterType.php b/webapp/src/Form/Type/SubmissionsFilterType.php index f962bb7501..ebb4ee439a 100644 --- a/webapp/src/Form/Type/SubmissionsFilterType.php +++ b/webapp/src/Form/Type/SubmissionsFilterType.php @@ -45,6 +45,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void "class" => Problem::class, "required" => false, "choice_label" => "name", + "choice_value" => "externalid", "choices" => $problems, "attr" => ["data-filter-field" => "problem-id"], ]); @@ -54,6 +55,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void "class" => Language::class, "required" => false, "choice_label" => "name", + "choice_value" => "externalid", "query_builder" => fn(EntityRepository $er) => $er ->createQueryBuilder("l") ->where("l.allowSubmit = 1") @@ -66,6 +68,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void "class" => TeamCategory::class, "required" => false, "choice_label" => "name", + "choice_value" => "externalid", "query_builder" => fn(EntityRepository $er) => $er ->createQueryBuilder("tc") ->orderBy("tc.name"), @@ -77,6 +80,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void "class" => TeamAffiliation::class, "required" => false, "choice_label" => "name", + "choice_value" => "externalid", "query_builder" => fn(EntityRepository $er) => $er ->createQueryBuilder("ta") ->orderBy("ta.name"), @@ -114,6 +118,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void "class" => Team::class, "required" => false, "choice_label" => "name", + "choice_value" => "externalid", "choices" => $teams, "attr" => ["data-filter-field" => "team-id"], ]); diff --git a/webapp/src/Repository/SubmissionRepository.php b/webapp/src/Repository/SubmissionRepository.php new file mode 100644 index 0000000000..9e5facfef8 --- /dev/null +++ b/webapp/src/Repository/SubmissionRepository.php @@ -0,0 +1,17 @@ + + */ +class SubmissionRepository extends EntityRepository +{ + /** + * @use FindByExternalidTrait + */ + use FindByExternalidTrait; +} diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index 5d475c7d65..9c8f4b961c 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -1197,7 +1197,7 @@ public function unblockJudgeTasksForSubmission(string $submissionId): void // These are all the judgings that don't have associated judgetasks yet. Check whether we unblocked them. $judgings = $this->helperUnblockJudgeTasks() ->join(Submission::class, 's', Join::WITH, 'j.submission = s.submitid') - ->andWhere('j.submission = :submissionid') + ->andWhere('s.externalid = :submissionid') ->setParameter('submissionid', $submissionId) ->getQuery() ->getResult(); diff --git a/webapp/src/Service/SubmissionService.php b/webapp/src/Service/SubmissionService.php index 8275154350..5c3622c2c1 100644 --- a/webapp/src/Service/SubmissionService.php +++ b/webapp/src/Service/SubmissionService.php @@ -89,7 +89,7 @@ public function getSubmissionList( ->join('s.contest_problem', 'cp') ->join('s.language', 'l') ->andWhere('s.contest IN (:contests)') - ->setParameter('contests', array_keys($contests)) + ->setParameter('contests', $contests) ->orderBy('s.submittime', 'DESC') ->addOrderBy('s.submitid', 'DESC'); diff --git a/webapp/src/Twig/TwigExtension.php b/webapp/src/Twig/TwigExtension.php index d0e3f301bc..b609e2159c 100644 --- a/webapp/src/Twig/TwigExtension.php +++ b/webapp/src/Twig/TwigExtension.php @@ -935,7 +935,7 @@ public function codeEditor( * source: string, * renamedFrom?: string * }> $files */ - public function showDiff(string $editorId, string $diffId, int $submissionId, string $filename, array $files): string + public function showDiff(string $editorId, string $diffId, string $submissionId, string $filename, array $files): string { $editor = << @@ -943,7 +943,7 @@ public function showDiff(string $editorId, string $diffId, int $submissionId, st $(function() { const editorId = '%s'; const diffId = '%s'; - const submissionId = %d; + const submissionId = '%s'; const models = %s; require(['vs/editor/editor.main'], () => { initDiffEditorTab(editorId, diffId, submissionId, models); diff --git a/webapp/templates/jury/partials/submission_diff.html.twig b/webapp/templates/jury/partials/submission_diff.html.twig index de391147c4..11a64bc550 100644 --- a/webapp/templates/jury/partials/submission_diff.html.twig +++ b/webapp/templates/jury/partials/submission_diff.html.twig @@ -4,8 +4,8 @@ {% set extra_css_classes = "show active" %}
    {%- for name, name_files in files %} {% set diff_id = "diff-" ~ name %}
    - {{ showDiff(editor_id, diff_id, submission.submitid, name, name_files) }} + {{ showDiff(editor_id, diff_id, submission.externalid, name, name_files) }}
    {% set extra_css_classes = "" %} {%- endfor %} diff --git a/webapp/templates/jury/submission.html.twig b/webapp/templates/jury/submission.html.twig index 7b151d46b9..f41b5eafce 100644 --- a/webapp/templates/jury/submission.html.twig +++ b/webapp/templates/jury/submission.html.twig @@ -1,7 +1,7 @@ {# @var \App\Entity\ExternalJudgement externalJudgement #} {% extends "jury/base.html.twig" %} -{% block title %}Submission s{{ submission.submitid }} - {{ parent() }}{% endblock %} +{% block title %}Submission {{ submission.externalid }} - {{ parent() }}{% endblock %} {% block extrahead %} {{ parent() }} @@ -75,7 +75,7 @@ {% for type, versions in version_warnings %}

    - This judging did not use the same {{ type}} version for all testcases. + This judging did not use the same {{ type }} version for all testcases. This may lead to unexpected results. Versions used:

    @@ -91,16 +91,16 @@

    - Submission s{{ submission.submitid }} + Submission {{ submission.externalid }} {% if submission.originalSubmission %} - {% set origSubmissionUrl = path('jury_submission', {submitId: submission.originalSubmission.submitid}) %} - (resubmit of s{{ submission.originalSubmission.submitid }}) + {% set origSubmissionUrl = path('jury_submission', {submitId: submission.originalSubmission.externalid}) %} + (resubmit of {{ submission.originalSubmission.externalid }}) {% endif %} {% if submission.resubmissions is not empty %} (resubmitted as {%- for resubmission in submission.resubmissions -%} - {% set resubmissionUrl = path('jury_submission', {submitId: resubmission.submitid}) %} - s{{ resubmission.submitid }} + {% set resubmissionUrl = path('jury_submission', {submitId: resubmission.externalid}) %} + {{ resubmission.externalid }} {%- if not loop.last -%},{%- endif -%} {%- endfor -%} ) @@ -117,16 +117,16 @@ {% else %} {% set action = 'unignore' %} {% endif %} - + onclick="return confirm('Really {{ action }} submission {{ submission.externalid }}?');"/> {% endif %} - {% include 'jury/partials/rejudge_form.html.twig' with {table: 'submission', id: submission.submitid} %} + {% include 'jury/partials/rejudge_form.html.twig' with {table: 'submission', id: submission.externalid} %}

    {% endif %} @@ -134,7 +134,7 @@
    - + {{ submission.team.effectiveName }} {{ submission.team | entityIdBadge }} @@ -142,7 +142,7 @@ {% if submission.user %} - + {{ submission.user.username }} {{ submission.user | entityIdBadge }} @@ -151,7 +151,7 @@ {% if current_contest.cid != submission.contest.cid %} - + {{ submission.contest.shortname }} {{ submission.contest | entityIdBadge }} @@ -160,7 +160,7 @@ - + {% if submission.contestProblem %} {{ submission.contestProblem | problemBadge }} {{ submission.problem.name }} {% else %} @@ -172,7 +172,7 @@ - + {{ submission.language.name }} {{ submission.language | entityIdBadge }} @@ -192,25 +192,20 @@ - + View {{ submission.files | printFiles }} - {% if shadowMode() and submission.externalid %} - {% if external_ccs_submission_url is empty %} - External ID: {{- submission.externalid -}} - {% else %} - {% set externalSubmissionUrl = submission | externalCcsUrl %} - {% if externalSubmissionUrl is not empty %} - - - - View in external CCS - - (ID: {{- submission.externalid -}}) - - {% endif %} + {% if shadowMode() and external_ccs_submission_url is not empty %} + {% set externalSubmissionUrl = submission | externalCcsUrl %} + {% if externalSubmissionUrl is not empty %} + + + + View in external CCS + + {% endif %} {% endif %}
    @@ -288,7 +283,7 @@
    {% for judging in judgings %} - {% set link = path('jury_submission', {submitId: submission.submitid, jid: judging.judgingid}) %} + {% set link = path('jury_submission', {submitId: submission.externalid, jid: judging.judgingid}) %}
    Problem ID{{ problem.probid }}
    Testcases {% if problem.testcases is empty %} no testcases {% else %} - details / edit + details / edit {% endif %}
    {% set link = null %} {% if jury %} - {% set link = path('jury_team_category', {'categoryId': category.categoryid}) %} + {% set link = path('jury_team_category', {'categoryId': category.externalid}) %} {% endif %} {{ category.name }}
    @@ -372,9 +367,9 @@ {%- endif -%} {% endif %} {%- if lastJudging is not null -%} - {% set lastSubmissionLink = path('jury_submission', {submitId: lastSubmission.submitid}) %}{#- + {% set lastSubmissionLink = path('jury_submission', {submitId: lastSubmission.externalid}) %}{#- -#} - (s{{ lastSubmission.submitid }}: {{ lastJudging.result | printResult }}){#- + ({{ lastSubmission.externalid }}: {{ lastJudging.result | printResult }}){#- -#} {%- endif -%} {%- if externalJudgement is not null %} @@ -433,7 +428,7 @@ {% endif %}   {% if not selectedJudging.verified %} -
    {% if selectedJudging.juryMember is not empty %} (claimed by {{ selectedJudging.juryMember }}) @@ -449,7 +444,7 @@   {% if selectedJudging is null or selectedJudging.result is empty %} {%- if not selectedJudging or not selectedJudging.started %} - +
    - previous s{{ lastSubmission.submitid }} + previous {{ lastSubmission.externalid }} {% if lastJudging.verifyComment %} (verify comment: '{{ lastJudging.verifyComment }}') {% endif %} @@ -671,20 +666,20 @@ -#} {% endif %}
    - + - + {% if run.firstJudgingRun is not null %} - +
    - - {% include 'jury/partials/team_form.html.twig' %} {% endblock %} From c2121c574b40604e99246d14d1dcce6e58cd5df1 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Tue, 11 Nov 2025 22:59:03 +0100 Subject: [PATCH 26/32] Change jury user pages to use external IDs instead of internal IDs Part of #3024 --- .../Controller/Jury/RejudgingController.php | 3 +- webapp/src/Controller/Jury/UserController.php | 38 +++++++++---------- webapp/src/Form/Type/UserType.php | 1 + webapp/templates/jury/user.html.twig | 14 +++---- webapp/templates/jury/user_edit.html.twig | 8 +--- 5 files changed, 29 insertions(+), 35 deletions(-) diff --git a/webapp/src/Controller/Jury/RejudgingController.php b/webapp/src/Controller/Jury/RejudgingController.php index ef2682293e..15bbaa31ea 100644 --- a/webapp/src/Controller/Jury/RejudgingController.php +++ b/webapp/src/Controller/Jury/RejudgingController.php @@ -706,7 +706,7 @@ public function createAction(Request $request): Response 'problem' => 'p.externalid', 'submission' => 's.externalid', 'team' => 't.externalid', - 'user' => 's.user', + 'user' => 'u.externalid', 'rejudging' => 'j2.rejudging', ]; @@ -741,6 +741,7 @@ public function createAction(Request $request): Response ->leftJoin('s.language', 'l') ->leftJoin('s.problem', 'p') ->leftJoin('s.team', 't') + ->leftJoin('s.user', 'u') ->leftJoin('j.runs', 'jr') ->leftJoin('jr.judgetask', 'jt') ->select('j', 's', 'r', 't') diff --git a/webapp/src/Controller/Jury/UserController.php b/webapp/src/Controller/Jury/UserController.php index 10545acf02..90cb6a2990 100644 --- a/webapp/src/Controller/Jury/UserController.php +++ b/webapp/src/Controller/Jury/UserController.php @@ -62,7 +62,7 @@ public function indexAction(): Response $table_fields = [ 'username' => ['title' => 'username', 'sort' => true, 'default_sort' => true], - 'externalid' => ['title' => 'external ID', 'sort' => true], + 'externalid' => ['title' => 'ID', 'sort' => true], 'name' => ['title' => 'name', 'sort' => true], 'email' => ['title' => 'email', 'sort' => true], 'user_roles' => ['title' => 'roles', 'sort' => true], @@ -86,7 +86,7 @@ public function indexAction(): Response $userdata = []; $useractions = []; - $this->addEntityCheckbox($userdata, $u, $u->getUserid(), 'user-checkbox', fn(User $user) => $user->getUserid() !== $this->dj->getUser()->getUserid()); + $this->addEntityCheckbox($userdata, $u, $u->getExternalid(), 'user-checkbox', fn(User $user) => $user->getUserid() !== $this->dj->getUser()->getUserid()); // Get whatever fields we can from the user object itself. foreach ($table_fields as $k => $v) { @@ -108,9 +108,9 @@ public function indexAction(): Response ]; $userdata['team'] = [ 'value' => $u->getTeamName(), - 'sortvalue' => $u->getTeam()->getTeamid(), + 'sortvalue' => $u->getTeam()->getExternalid(), 'link' => $this->generateUrl('jury_team', [ - 'teamId' => $u->getTeam()->getTeamid(), + 'teamId' => $u->getTeam()->getExternalid(), ]), 'title' => $u->getTeam()->getEffectiveName(), ]; @@ -138,14 +138,14 @@ public function indexAction(): Response 'icon' => 'edit', 'title' => 'edit this user', 'link' => $this->generateUrl('jury_user_edit', [ - 'userId' => $u->getUserid(), + 'userId' => $u->getExternalid(), ]) ]; $useractions[] = [ 'icon' => 'trash-alt', 'title' => 'delete this user', 'link' => $this->generateUrl('jury_user_delete', [ - 'userId' => $u->getUserid(), + 'userId' => $u->getExternalid(), ]), 'ajaxModal' => true, ]; @@ -163,7 +163,7 @@ public function indexAction(): Response $users_table[] = [ 'data' => $userdata, 'actions' => $useractions, - 'link' => $this->generateUrl('jury_user', ['userId' => $u->getUserid()]), + 'link' => $this->generateUrl('jury_user', ['userId' => $u->getExternalid()]), 'cssclass' => $u->getEnabled() ? '' : 'disabled', ]; } @@ -218,10 +218,10 @@ public function checkPasswordLength(User $user, FormInterface $form): ?Response } #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/{userId<\d+>}/edit', name: 'jury_user_edit')] - public function editAction(Request $request, int $userId): Response + #[Route(path: '/{userId}/edit', name: 'jury_user_edit')] + public function editAction(Request $request, string $userId): Response { - $user = $this->em->getRepository(User::class)->find($userId); + $user = $this->em->getRepository(User::class)->findByExternalId($userId); if (!$user) { throw new NotFoundHttpException(sprintf('User with ID %s not found', $userId)); } @@ -247,7 +247,7 @@ public function editAction(Request $request, int $userId): Response $this->tokenStorage->setToken($token); } - return $this->redirectToRoute('jury_user', ['userId' => $user->getUserid()]); + return $this->redirectToRoute('jury_user', ['userId' => $user->getExternalid()]); } return $this->render('jury/user_edit.html.twig', [ @@ -257,10 +257,10 @@ public function editAction(Request $request, int $userId): Response } #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/{userId<\d+>}/delete', name: 'jury_user_delete')] - public function deleteAction(Request $request, int $userId): Response + #[Route(path: '/{userId}/delete', name: 'jury_user_delete')] + public function deleteAction(Request $request, string $userId): Response { - $user = $this->em->getRepository(User::class)->find($userId); + $user = $this->em->getRepository(User::class)->findByExternalId($userId); if (!$user) { throw new NotFoundHttpException(sprintf('User with ID %s not found', $userId)); } @@ -269,7 +269,7 @@ public function deleteAction(Request $request, int $userId): Response } #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/add', name: 'jury_user_add')] + #[Route(path: '/add', name: 'jury_user_add', priority: 1)] public function addAction( Request $request, #[MapQueryParameter] @@ -286,7 +286,7 @@ public function addAction( if ($response = $this->processAddFormForExternalIdEntity( $form, $user, - fn() => $this->generateUrl('jury_user', ['userId' => $user->getUserid()]), + fn() => $this->generateUrl('jury_user', ['userId' => $user->getExternalid()]), function () use ($user, $form) { if ($errorResult = $this->checkPasswordLength($user, $form)) { return $errorResult; @@ -306,7 +306,7 @@ function () use ($user, $form) { } #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/generate-passwords', name: 'jury_generate_passwords')] + #[Route(path: '/generate-passwords', name: 'jury_generate_passwords', priority: 1)] public function generatePasswordsAction(Request $request): Response { $form = $this->createForm(GeneratePasswordsType::class); @@ -371,7 +371,7 @@ public function generatePasswordsAction(Request $request): Response } #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/reset_login_status', name: 'jury_reset_login_status')] + #[Route(path: '/reset_login_status', name: 'jury_reset_login_status', priority: 1)] public function resetTeamLoginStatus(Request $request): Response { /** @var Role $teamRole */ @@ -392,7 +392,7 @@ public function resetTeamLoginStatus(Request $request): Response } #[IsGranted('ROLE_ADMIN')] - #[Route(path: '/delete-multiple', name: 'jury_user_delete_multiple', methods: ['GET', 'POST'])] + #[Route(path: '/delete-multiple', name: 'jury_user_delete_multiple', methods: ['GET', 'POST'], priority: 1)] public function deleteMultipleAction(Request $request): Response { return $this->deleteMultiple( diff --git a/webapp/src/Form/Type/UserType.php b/webapp/src/Form/Type/UserType.php index 12da3e28c2..04acb1dbfa 100644 --- a/webapp/src/Form/Type/UserType.php +++ b/webapp/src/Form/Type/UserType.php @@ -78,6 +78,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'choice_label' => 'effective_name', 'required' => false, 'placeholder' => '-- no team --', + 'choice_value' => 'externalid', 'choices' => $teams, ]); $builder->add('user_roles', EntityType::class, [ diff --git a/webapp/templates/jury/user.html.twig b/webapp/templates/jury/user.html.twig index 3f58734a71..8a50ebf5a2 100644 --- a/webapp/templates/jury/user.html.twig +++ b/webapp/templates/jury/user.html.twig @@ -1,7 +1,7 @@ {% extends "jury/base.html.twig" %} {% import "jury/jury_macros.twig" as macros %} -{% block title %}User {{ user.userid }} - {{ parent() }}{% endblock %} +{% block title %}User {{ user.externalid }} - {{ parent() }}{% endblock %} {% block extrahead %} {{ parent() }} @@ -18,10 +18,6 @@ - - - - @@ -78,7 +74,7 @@ {% if user.team %} @@ -137,10 +133,10 @@
    {%- if is_granted('ROLE_ADMIN') -%} - {{ button(path('jury_user_edit', {'userId': user.userid}), 'Edit', 'primary', 'edit') }} - {{ button(path('jury_user_delete', {'userId': user.userid}), 'Delete', 'danger', 'trash-alt', true) }} + {{ button(path('jury_user_edit', {'userId': user.externalid}), 'Edit', 'primary', 'edit') }} + {{ button(path('jury_user_delete', {'userId': user.externalid}), 'Delete', 'danger', 'trash-alt', true) }} {% endif %} - {% include 'jury/partials/rejudge_form.html.twig' with {table: 'user', id: user.userid, buttonClass: 'btn-secondary'} %} + {% include 'jury/partials/rejudge_form.html.twig' with {table: 'user', id: user.externalid, buttonClass: 'btn-secondary'} %}

    Submissions for {{ user.name }}

    diff --git a/webapp/templates/jury/user_edit.html.twig b/webapp/templates/jury/user_edit.html.twig index 45477f7969..39338ebdad 100644 --- a/webapp/templates/jury/user_edit.html.twig +++ b/webapp/templates/jury/user_edit.html.twig @@ -1,7 +1,7 @@ {% extends "jury/base.html.twig" %} {% import "jury/jury_macros.twig" as macros %} -{% block title %}Edit user {{ user.userid }} - {{ parent() }}{% endblock %} +{% block title %}Edit user {{ user.externalid }} - {{ parent() }}{% endblock %} {% block extrahead %} {{ parent() }} @@ -10,15 +10,11 @@ {% block content %} -

    Edit user {{ user.userid }}

    +

    Edit user {{ user.externalid }}

    ID{{ user.userid }}
    External ID {{ user.externalid }}
    Team - + {{ user.team.effectiveName }} {{ user.team | entityIdBadge }}
    - - - - From 2a97bfdf3d185226af724fd65df36ea50098124b Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Sat, 29 Nov 2025 16:32:38 +0100 Subject: [PATCH 27/32] Make keyboard shortcuts work with external IDs Part of #3024 --- webapp/public/js/domjudge.js | 20 +-- webapp/src/Controller/BaseController.php | 118 +++++++++++++++++- .../Controller/Jury/AnalysisController.php | 3 +- .../Jury/ClarificationController.php | 22 +++- .../src/Controller/Jury/ContestController.php | 4 + .../Controller/Jury/ExecutableController.php | 6 + .../Controller/Jury/JudgehostController.php | 6 + .../Controller/Jury/LanguageController.php | 4 + .../src/Controller/Jury/ProblemController.php | 4 + .../Controller/Jury/RejudgingController.php | 6 + .../Controller/Jury/SubmissionController.php | 9 +- .../Jury/TeamAffiliationController.php | 4 + .../Jury/TeamCategoryController.php | 4 + webapp/src/Controller/Jury/TeamController.php | 4 + webapp/src/Controller/Jury/UserController.php | 5 + webapp/templates/jury/base.html.twig | 20 ++- 16 files changed, 216 insertions(+), 23 deletions(-) diff --git a/webapp/public/js/domjudge.js b/webapp/public/js/domjudge.js index bc17cfa8fd..2f979eb5d5 100644 --- a/webapp/public/js/domjudge.js +++ b/webapp/public/js/domjudge.js @@ -998,14 +998,16 @@ function initializeKeyboardShortcuts() { var parts = window.location.href.split('/'); var lastPart = parts[parts.length - 1]; var params = lastPart.split('?'); - var currentNumber = parseInt(params[0]); - if (isNaN(currentNumber)) { - return; - } if (key === 'j') { - parts[parts.length - 1] = currentNumber + 1; + if (!window.nextEntity) { + return; + } + parts[parts.length - 1] = window.nextEntity; } else if (key === 'k') { - parts[parts.length - 1] = currentNumber - 1; + if (!window.previousEntity) { + return; + } + parts[parts.length - 1] = window.previousEntity; } if (params.length > 1) { parts[parts.length - 1] += '?' + params[1]; @@ -1039,9 +1041,13 @@ function initializeKeyboardShortcuts() { sequence = ''; return; } - if (e.key >= '0' && e.key <= '9') { + if (/^[a-zA-Z0-9_.-]$/.test(e.key)) { sequence += e.key; box.text(type + sequence); + } else if (e.key === 'Backspace') { + e.preventDefault(); + sequence = sequence.slice(0, -1); + box.text(type + sequence); } else { ignore = false; if (box) { diff --git a/webapp/src/Controller/BaseController.php b/webapp/src/Controller/BaseController.php index e60df13896..a8c77e4703 100644 --- a/webapp/src/Controller/BaseController.php +++ b/webapp/src/Controller/BaseController.php @@ -563,7 +563,7 @@ protected function addEntityCheckbox(array &$data, object $entity, mixed $identi '', $identifierValue, $checkboxClass - ) + ), ]; } } @@ -665,4 +665,120 @@ protected function processAddFormForExternalIdEntity( return null; } + + /** + * Get the previous and next object IDs for navigation. + * + * @param class-string $entityClass Entity class to query + * @param mixed $currentIdValue Current value of the ID field + * @param string $idField Field to return as the ID (e.g., 'externalid', 'submitid') + * @param array $orderBy Sort criteria as field => direction (e.g., ['e.submittime' => 'ASC', 'e.submitid' => 'ASC']) + * @param bool $filterOnContest Whether to filter results by current contests + * + * @return array{previous: string|int|null, next: string|int|null} + */ + protected function getPreviousAndNextObjectIds( + string $entityClass, + mixed $currentIdValue, + string $idField = 'externalid', + array $orderBy = ['e.externalid' => 'ASC'], + bool $filterOnContest = false, + ): array { + $result = ['previous' => null, 'next' => null]; + + // Fetch the current entity once to get field values + $currentEntity = $this->em->getRepository($entityClass)->findOneBy([$idField => $currentIdValue]); + if ($currentEntity === null) { + return $result; + } + + $accessor = PropertyAccess::createPropertyAccessor(); + + // Pre-compute field values for comparison + $fieldValues = []; + foreach (array_keys($orderBy) as $field) { + $fieldName = str_replace('e.', '', $field); + $fieldValues[$field] = $accessor->getValue($currentEntity, $fieldName); + } + + // Build the comparison conditions based on the sort criteria. + // For multi-column ordering, we need: (col1 < val1) OR (col1 = val1 AND col2 < val2) etc. + $buildComparisonConditions = function (string $operator) use ($orderBy, $fieldValues): array { + $conditions = []; + $parameters = []; + $fields = array_keys($orderBy); + $directions = array_values($orderBy); + + for ($i = 0; $i < count($fields); $i++) { + $equalityParts = []; + // Add equality conditions for all previous columns + for ($j = 0; $j < $i; $j++) { + $field = $fields[$j]; + $paramName = 'eq_' . $j; + $equalityParts[] = "$field = :$paramName"; + $parameters[$paramName] = $fieldValues[$field]; + } + + // Add the comparison for this column + $field = $fields[$i]; + $direction = $directions[$i]; + // For "previous": if ASC, we want < ; if DESC, we want > + // For "next": if ASC, we want > ; if DESC, we want < + $compOp = ($operator === 'previous') + ? ($direction === 'ASC' ? '<' : '>') + : ($direction === 'ASC' ? '>' : '<'); + $paramName = 'cmp_' . $i; + $comparisonPart = "$field $compOp :$paramName"; + $parameters[$paramName] = $fieldValues[$field]; + + if (!empty($equalityParts)) { + $conditions[] = '(' . implode(' AND ', $equalityParts) . ' AND ' . $comparisonPart . ')'; + } else { + $conditions[] = '(' . $comparisonPart . ')'; + } + } + + return ['condition' => implode(' OR ', $conditions), 'parameters' => $parameters]; + }; + + foreach (['previous', 'next'] as $direction) { + $qb = $this->em->createQueryBuilder() + ->select("e.$idField") + ->from($entityClass, 'e'); + + // Build and apply the comparison conditions + $comp = $buildComparisonConditions($direction); + if (!empty($comp['condition'])) { + $qb->andWhere($comp['condition']); + foreach ($comp['parameters'] as $param => $value) { + $qb->setParameter($param, $value); + } + } + + // Apply contest filter + if ($filterOnContest && $contest = $this->dj->getCurrentContest()) { + $qb->andWhere('e.contest = :contest') + ->setParameter('contest', $contest); + } + + // Apply ordering (reversed for previous) + foreach ($orderBy as $field => $dir) { + $actualDir = $direction === 'previous' + ? ($dir === 'ASC' ? 'DESC' : 'ASC') + : $dir; + $qb->addOrderBy($field, $actualDir); + } + + $qb->setMaxResults(1); + + try { + $value = $qb->getQuery()->getSingleScalarResult(); + $result[$direction] = $value; + } catch (NoResultException) { + // No previous/next found, leave as null + } + } + + return $result; + } } diff --git a/webapp/src/Controller/Jury/AnalysisController.php b/webapp/src/Controller/Jury/AnalysisController.php index 4887f55b88..8d4bf6c97d 100644 --- a/webapp/src/Controller/Jury/AnalysisController.php +++ b/webapp/src/Controller/Jury/AnalysisController.php @@ -86,8 +86,7 @@ public function indexAction( public function teamAction( #[MapEntity(mapping: ['team' => 'externalid'])] Team $team, - ): Response - { + ): Response { $contest = $this->dj->getCurrentContest(); if ($contest === null) { diff --git a/webapp/src/Controller/Jury/ClarificationController.php b/webapp/src/Controller/Jury/ClarificationController.php index ef681a0e5b..6268b7b081 100644 --- a/webapp/src/Controller/Jury/ClarificationController.php +++ b/webapp/src/Controller/Jury/ClarificationController.php @@ -2,6 +2,7 @@ namespace App\Controller\Jury; +use App\Controller\BaseController; use App\Entity\Clarification; use App\Entity\Contest; use App\Entity\Problem; @@ -20,19 +21,23 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; #[IsGranted('ROLE_CLARIFICATION_RW')] #[Route(path: '/jury/clarifications')] -class ClarificationController extends AbstractController +class ClarificationController extends BaseController { public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly EventLogService $eventLogService - ) {} + EventLogService $eventLogService, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_clarifications')] public function indexAction( @@ -236,6 +241,11 @@ public function viewAction(Request $request, string $id): Response ->getQuery() ->getSingleResult()['jury_member']; + $parameters['previousNext'] = $this->getPreviousAndNextObjectIds( + Clarification::class, + $clarification->getExternalid(), + ); + return $this->render('jury/clarification.html.twig', $parameters); } @@ -429,7 +439,7 @@ protected function processSubmittedClarification( $clarId = $clarification->getClarId(); $this->dj->auditlog('clarification', $clarification->getExternalid(), 'added', null, null, $contest->getExternalid()); - $this->eventLogService->log('clarification', $clarId, 'create', $contest->getCid()); + $this->eventLog->log('clarification', $clarId, 'create', $contest->getCid()); // Reload clarification to make sure we have a fresh one after calling the event log service. $clarification = $this->em->getRepository(Clarification::class)->find($clarId); diff --git a/webapp/src/Controller/Jury/ContestController.php b/webapp/src/Controller/Jury/ContestController.php index 8b14a4796f..6625286ce6 100644 --- a/webapp/src/Controller/Jury/ContestController.php +++ b/webapp/src/Controller/Jury/ContestController.php @@ -352,6 +352,10 @@ public function viewAction(Request $request, string $contestId): Response return $this->render('jury/contest.html.twig', [ 'contest' => $contest, + 'previousNext' => $this->getPreviousAndNextObjectIds( + Contest::class, + $contest->getExternalid(), + ), 'allowRemovedIntervals' => $this->getParameter('removed_intervals'), 'removedIntervalForm' => $form, 'removedIntervals' => $removedIntervals, diff --git a/webapp/src/Controller/Jury/ExecutableController.php b/webapp/src/Controller/Jury/ExecutableController.php index 27a6c32ebc..c2c60bf9c8 100644 --- a/webapp/src/Controller/Jury/ExecutableController.php +++ b/webapp/src/Controller/Jury/ExecutableController.php @@ -368,6 +368,12 @@ public function viewAction( 'uploadForm' => $uploadForm->createView(), 'selected' => $index, 'executable' => $executable, + 'previousNext' => $this->getPreviousAndNextObjectIds( + Executable::class, + $executable->getExecid(), + 'execid', + ['e.execid' => 'ASC'], + ), 'default_compare' => (string)$this->config->get('default_compare'), 'default_run' => (string)$this->config->get('default_run'), 'default_full_debug' => (string)$this->config->get('default_full_debug'), diff --git a/webapp/src/Controller/Jury/JudgehostController.php b/webapp/src/Controller/Jury/JudgehostController.php index 3437bdab24..84c49c4772 100644 --- a/webapp/src/Controller/Jury/JudgehostController.php +++ b/webapp/src/Controller/Jury/JudgehostController.php @@ -280,6 +280,12 @@ public function viewAction(Request $request, int $judgehostid): Response $data = [ 'judgehost' => $judgehost, + 'previousNext' => $this->getPreviousAndNextObjectIds( + Judgehost::class, + $judgehost->getJudgehostid(), + 'judgehostid', + ['e.judgehostid' => 'ASC'], + ), 'status' => $status, 'statusIcon' => $statusIcon, 'judgings' => $judgings, diff --git a/webapp/src/Controller/Jury/LanguageController.php b/webapp/src/Controller/Jury/LanguageController.php index b2aeccc6d3..e714052926 100644 --- a/webapp/src/Controller/Jury/LanguageController.php +++ b/webapp/src/Controller/Jury/LanguageController.php @@ -198,6 +198,10 @@ public function viewAction(Request $request, SubmissionService $submissionServic $data = [ 'language' => $language, + 'previousNext' => $this->getPreviousAndNextObjectIds( + Language::class, + $language->getExternalid(), + ), 'submissions' => $submissions, 'submissionCounts' => $submissionCounts, 'showContest' => count($this->dj->getCurrentContests(honorCookie: true)) > 1, diff --git a/webapp/src/Controller/Jury/ProblemController.php b/webapp/src/Controller/Jury/ProblemController.php index 2f165e68d5..35edbb8dac 100644 --- a/webapp/src/Controller/Jury/ProblemController.php +++ b/webapp/src/Controller/Jury/ProblemController.php @@ -503,6 +503,10 @@ public function viewAction(Request $request, SubmissionService $submissionServic $data = [ 'problem' => $problem, + 'previousNext' => $this->getPreviousAndNextObjectIds( + Problem::class, + $problem->getExternalid(), + ), 'problemAttachmentForm' => $problemAttachmentForm->createView(), 'submissions' => $submissions, 'submissionCounts' => $submissionCounts, diff --git a/webapp/src/Controller/Jury/RejudgingController.php b/webapp/src/Controller/Jury/RejudgingController.php index 15bbaa31ea..9487efe444 100644 --- a/webapp/src/Controller/Jury/RejudgingController.php +++ b/webapp/src/Controller/Jury/RejudgingController.php @@ -392,6 +392,12 @@ public function viewAction( $data = [ 'rejudging' => $rejudging, + 'previousNext' => $this->getPreviousAndNextObjectIds( + Rejudging::class, + $rejudging->getRejudgingid(), + 'rejudgingid', + ['e.rejudgingid' => 'ASC'], + ), 'todo' => $todo, 'done' => $done, 'verdicts' => $verdicts, diff --git a/webapp/src/Controller/Jury/SubmissionController.php b/webapp/src/Controller/Jury/SubmissionController.php index 6a6ba30f59..d3a3cdea74 100644 --- a/webapp/src/Controller/Jury/SubmissionController.php +++ b/webapp/src/Controller/Jury/SubmissionController.php @@ -568,6 +568,12 @@ public function viewAction( $twigData = [ 'submission' => $submission, + 'previousNext' => $this->getPreviousAndNextObjectIds( + Submission::class, + $submission->getExternalid(), + orderBy: ['e.submittime' => 'ASC', 'e.submitid' => 'ASC'], + filterOnContest: true, + ), 'lastSubmission' => $lastSubmission, 'judgings' => $judgings, 'maxRunTimes' => $maxRunTimes, @@ -773,8 +779,7 @@ public function teamOutputAction( #[MapEntity(mapping: ['contest' => 'externalid'])] Contest $contest, JudgingRun $run, - ): StreamedResponse - { + ): StreamedResponse { if ($run->getJudging()->getSubmission()->getSubmitid() !== $submission->getSubmitid() || $submission->getContest()->getCid() !== $contest->getCid()) { throw new BadRequestHttpException('Integrity problem while fetching team output.'); } diff --git a/webapp/src/Controller/Jury/TeamAffiliationController.php b/webapp/src/Controller/Jury/TeamAffiliationController.php index eb5382c617..9dd4c9183b 100644 --- a/webapp/src/Controller/Jury/TeamAffiliationController.php +++ b/webapp/src/Controller/Jury/TeamAffiliationController.php @@ -137,6 +137,10 @@ public function viewAction(Request $request, ScoreboardService $scoreboardServic $data = [ 'teamAffiliation' => $teamAffiliation, + 'previousNext' => $this->getPreviousAndNextObjectIds( + TeamAffiliation::class, + $teamAffiliation->getExternalid(), + ), 'showFlags' => $this->config->get('show_flags'), 'refresh' => [ 'after' => 30, diff --git a/webapp/src/Controller/Jury/TeamCategoryController.php b/webapp/src/Controller/Jury/TeamCategoryController.php index 2c3b7a5262..b3f7cca5d4 100644 --- a/webapp/src/Controller/Jury/TeamCategoryController.php +++ b/webapp/src/Controller/Jury/TeamCategoryController.php @@ -140,6 +140,10 @@ public function viewAction(Request $request, SubmissionService $submissionServic $data = [ 'teamCategory' => $teamCategory, + 'previousNext' => $this->getPreviousAndNextObjectIds( + TeamCategory::class, + $teamCategory->getExternalid(), + ), 'submissions' => $submissions, 'submissionCounts' => $submissionCounts, 'showContest' => count($this->dj->getCurrentContests(honorCookie: true)) > 1, diff --git a/webapp/src/Controller/Jury/TeamController.php b/webapp/src/Controller/Jury/TeamController.php index edd8a6b848..f067547ad9 100644 --- a/webapp/src/Controller/Jury/TeamController.php +++ b/webapp/src/Controller/Jury/TeamController.php @@ -253,6 +253,10 @@ public function viewAction( 'ajax' => true, ], 'team' => $team, + 'previousNext' => $this->getPreviousAndNextObjectIds( + Team::class, + $team->getExternalid(), + ), 'showAffiliations' => (bool)$this->config->get('show_affiliations'), 'showFlags' => (bool)$this->config->get('show_flags'), 'showContest' => count($this->dj->getCurrentContests()) > 1, diff --git a/webapp/src/Controller/Jury/UserController.php b/webapp/src/Controller/Jury/UserController.php index 90cb6a2990..3bb86e8f2f 100644 --- a/webapp/src/Controller/Jury/UserController.php +++ b/webapp/src/Controller/Jury/UserController.php @@ -191,6 +191,11 @@ public function viewAction(Request $request, string $userId, SubmissionService $ return $this->render('jury/user.html.twig', [ 'user' => $user, + 'previousNext' => $this->getPreviousAndNextObjectIds( + User::class, + $user->getExternalid(), + orderBy: ['e.username' => 'ASC'], + ), 'submissions' => $submissions, 'submissionCounts' => $submissionCounts, 'showContest' => count($this->dj->getCurrentContests(honorCookie: true)) > 1, diff --git a/webapp/templates/jury/base.html.twig b/webapp/templates/jury/base.html.twig index c481bae793..a142c44959 100644 --- a/webapp/templates/jury/base.html.twig +++ b/webapp/templates/jury/base.html.twig @@ -27,6 +27,16 @@ require.config({ paths: { vs: '{{ asset('js/monaco/vs') }}'.replace(/\?.*/, '') } }); window.editorThemeFolder = '{{ asset('js/monaco/themes') }}'.replace(/\?.*/, ''); + {% if previousNext is defined %} + + {% endif %}
    User ID{{ user.userid }}
    Username {{ user.username }}