diff --git a/webapp/src/Command/ScoreboardMergeCommand.php b/webapp/src/Command/ScoreboardMergeCommand.php index c75cd5f8bb..254035ca7e 100644 --- a/webapp/src/Command/ScoreboardMergeCommand.php +++ b/webapp/src/Command/ScoreboardMergeCommand.php @@ -5,6 +5,7 @@ use App\Entity\Contest; use App\Entity\ContestProblem; use App\Entity\Problem; +use App\Entity\RankCache; use App\Entity\ScoreCache; use App\Entity\Team; use App\Entity\TeamAffiliation; @@ -14,6 +15,7 @@ use App\Service\ScoreboardService; use App\Utils\FreezeData; use App\Utils\Scoreboard\Scoreboard; +use App\Utils\Utils; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -131,6 +133,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $problems = []; $problemNameToIdMap = []; $scoreCache = []; + /** @var RankCache[] $rankCache */ + $rankCache = []; + $penaltyTime = (int)$this->config->get('penalty_time'); + $scoreIsInSeconds = (bool)$this->config->get('score_in_seconds'); + $timeOfLastCorrect = []; $affiliations = []; $firstSolve = []; $contest = (new Contest()) @@ -261,6 +268,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $id = count($problems); $problemObj = (new Problem()) ->setProbid($id) + ->setExternalid((string)$id) ->setName($name); $contestProblemObj = (new ContestProblem()) ->setProblem($problemObj) @@ -280,10 +288,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $scoreCacheObj ->setSolveTimePublic($problem['time'] * 60) ->setSolveTimeRestricted($problem['time'] * 60); - if ( - $firstSolve[$name] === null or - $problem['time'] * 60 < $firstSolve[$name] - ) { + if ($firstSolve[$name] === null || + $problem['time'] * 60 < $firstSolve[$name]) { $firstSolve[$name] = $problem['time'] * 60; } } @@ -302,14 +308,64 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($scoreCacheObj->getSolveTimeRestricted() == $firstSolve[$scoreCacheObj->getProblem()->getName()]) { $scoreCacheObj->setIsFirstToSolve(true); } + + $teamId = $scoreCacheObj->getTeam()->getTeamid(); + if (isset($rankCache[$teamId])) { + $rankCacheObj = $rankCache[$teamId]; + } else { + $rankCacheObj = (new RankCache()) + ->setTeam($scoreCacheObj->getTeam()); + $rankCache[$teamId] = $rankCacheObj; + } + + $problem = $problems[$scoreCacheObj->getProblem()->getProbid()]; + if ($scoreCacheObj->getIsCorrectRestricted()) { + $rankCacheObj->setPointsRestricted($rankCacheObj->getPointsRestricted() + $problem->getPoints()); + $solveTime = Utils::scoretime( + (float)$scoreCacheObj->getSolvetimeRestricted(), + $scoreIsInSeconds + ); + $penalty = Utils::calcPenaltyTime($scoreCacheObj->getIsCorrectRestricted(), + $scoreCacheObj->getSubmissionsRestricted(), + $penaltyTime, $scoreIsInSeconds); + $rankCacheObj->setTotaltimeRestricted($rankCacheObj->getTotaltimeRestricted() + $solveTime + $penalty); + $rankCacheObj->setTotalruntimeRestricted($rankCacheObj->getTotalruntimeRestricted() + $scoreCacheObj->getRuntimeRestricted()); + $timeOfLastCorrect[$teamId] = max( + $timeOfLastCorrect[$teamId] ?? 0, + Utils::scoretime( + (float)$scoreCacheObj->getSolvetimeRestricted(), + $scoreIsInSeconds + ), + ); + } } + foreach ($rankCache as $rankCacheObj) { + $teamId = $rankCacheObj->getTeam()->getTeamid(); + $rankCacheObj->setSortKeyRestricted(ScoreboardService::getICPCScoreKey( + $rankCacheObj->getPointsRestricted(), + $rankCacheObj->getTotaltimeRestricted(), $timeOfLastCorrect[$teamId] ?? 0 + )); + } + + usort($teams, function (Team $a, Team $b) use ($rankCache) { + $rankCacheA = $rankCache[$a->getTeamid()]; + $rankCacheB = $rankCache[$b->getTeamid()]; + $rankCacheSort = $rankCacheB->getSortKeyRestricted() <=> $rankCacheA->getSortKeyRestricted(); + if ($rankCacheSort === 0) { + return $a->getEffectiveName() <=> $b->getEffectiveName(); + } + + return $rankCacheSort; + }); + $scoreboard = new Scoreboard( $contest, $teams, [$category], $problems, $scoreCache, + array_values($rankCache), $freezeData, false, (int)$this->config->get('penalty_time'), diff --git a/webapp/src/Controller/API/JudgehostController.php b/webapp/src/Controller/API/JudgehostController.php index a190281bfb..4745c5aa72 100644 --- a/webapp/src/Controller/API/JudgehostController.php +++ b/webapp/src/Controller/API/JudgehostController.php @@ -1656,9 +1656,7 @@ public function getJudgeTasksAction(Request $request): array $judgetasks = [['type' => 'try_again']]; } } - if (!empty($judgetasks)) { - return $judgetasks; - } + return $judgetasks; } if ($this->config->get('enable_parallel_judging')) { diff --git a/webapp/src/Controller/API/PrintController.php b/webapp/src/Controller/API/PrintController.php index 901015a04b..3587954c11 100644 --- a/webapp/src/Controller/API/PrintController.php +++ b/webapp/src/Controller/API/PrintController.php @@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; diff --git a/webapp/src/Controller/Jury/ProblemController.php b/webapp/src/Controller/Jury/ProblemController.php index 0198ad9f80..34174f312b 100644 --- a/webapp/src/Controller/Jury/ProblemController.php +++ b/webapp/src/Controller/Jury/ProblemController.php @@ -593,8 +593,10 @@ public function testcasesAction(Request $request, int $probId): Response $content = file_get_contents($file->getRealPath()); if ($type === 'image') { if (mime_content_type($file->getRealPath()) === 'image/svg+xml') { + $originalContent = $content; $content = Utils::sanitizeSvg($content); if ($content === false) { + $imageType = Utils::getImageType($originalContent, $error); $this->addFlash('danger', sprintf('image: %s', $error)); return $this->redirectToRoute('jury_problem_testcases', ['probId' => $probId]); } diff --git a/webapp/src/Entity/Contest.php b/webapp/src/Entity/Contest.php index cbc2b3dd39..7cb98b5e3c 100644 --- a/webapp/src/Entity/Contest.php +++ b/webapp/src/Entity/Contest.php @@ -936,6 +936,9 @@ public function removeLanguage(Language $language): void $this->languages->removeElement($language); } + /** + * @return Collection + */ public function getLanguages(): Collection { return $this->languages; diff --git a/webapp/src/Entity/Language.php b/webapp/src/Entity/Language.php index 9cb78d3b41..ecaed5e89a 100644 --- a/webapp/src/Entity/Language.php +++ b/webapp/src/Entity/Language.php @@ -447,6 +447,9 @@ public function removeContest(Contest $contest): Language return $this; } + /** + * @return Collection + */ public function getContests(): Collection { return $this->contests; @@ -466,6 +469,9 @@ public function removeProblem(Problem $problem): Language return $this; } + /** + * @return Collection + */ public function getProblems(): Collection { return $this->problems; diff --git a/webapp/src/Entity/Problem.php b/webapp/src/Entity/Problem.php index edc47ab3be..343e7ec4f3 100644 --- a/webapp/src/Entity/Problem.php +++ b/webapp/src/Entity/Problem.php @@ -108,6 +108,9 @@ class Problem extends BaseApiEntity implements public const TYPE_INTERACTIVE = 8; public const TYPE_SUBMIT_ANSWER = 16; + /** + * @var array + */ private array $typesToString = [ self::TYPE_PASS_FAIL => 'pass-fail', self::TYPE_SCORING => 'scoring', @@ -292,8 +295,12 @@ public function getSpecialCompareArgs(): ?string return $this->special_compare_args; } + /** + * @param list $types + */ public function setTypesAsString(array $types): Problem { + /** @var array $stringToTypes */ $stringToTypes = array_flip($this->typesToString); $typeConstants = []; foreach ($types as $type) { @@ -320,6 +327,9 @@ public function getTypesAsString(): string return implode(', ', $typeStrings); } + /** + * @return list + */ public function getTypes(): array { $ret = []; @@ -331,6 +341,9 @@ public function getTypes(): array return $ret; } + /** + * @param array $types + */ public function setTypes(array $types): Problem { $types = array_unique($types); diff --git a/webapp/src/Service/ConfigurationService.php b/webapp/src/Service/ConfigurationService.php index a5c6c92ab2..30f6df2ccb 100644 --- a/webapp/src/Service/ConfigurationService.php +++ b/webapp/src/Service/ConfigurationService.php @@ -410,6 +410,7 @@ public function addOptions(ConfigurationSpecification $item): ConfigurationSpeci * identifier key/value pairs. The identifiers try to adhere to * https://ccs-specs.icpc.io/draft/contest_api#known-judgement-types * + * @param list $groups * @return array */ public function getVerdicts(array $groups = ['final']): array diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index 26a7d5b4c9..132beee23c 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -718,6 +718,7 @@ public function openZipFile(string $filename): ZipArchive * @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 bool $asTeam Print the file as the team associated with the user + * @return array{0: bool, 1: string} */ public function printUserFile( string $filename, @@ -1543,7 +1544,7 @@ public function getScoreboardZip( } /** - * @return array{'backgroundColors', array} + * @return array{backgroundColors: array} */ public function getScoreboardCategoryColorCss(): array { $backgroundColors = array_map(fn($x) => ( $x->getColor() ?? '#FFFFFF' ), $this->em->getRepository(TeamCategory::class)->findAll()); diff --git a/webapp/src/Service/ImportProblemService.php b/webapp/src/Service/ImportProblemService.php index 7cc0c3a9a4..d9a4d05921 100644 --- a/webapp/src/Service/ImportProblemService.php +++ b/webapp/src/Service/ImportProblemService.php @@ -996,7 +996,11 @@ private function searchAndAddValidator(ZipArchive $zip, ?array &$messages, strin return true; } - // Returns true iff the yaml could be parsed correctly. + /** + * Returns true iff the yaml could be parsed correctly. + * + * @param array{danger: string[], info: string[]} $messages + */ public static function parseYaml(bool|string $problemYaml, array &$messages, string &$validationMode, PropertyAccessor $propertyAccessor, Problem $problem): bool { if ($problemYaml === false) { diff --git a/webapp/src/Utils/Scoreboard/Scoreboard.php b/webapp/src/Utils/Scoreboard/Scoreboard.php index b0e53fce1b..aaea69c269 100644 --- a/webapp/src/Utils/Scoreboard/Scoreboard.php +++ b/webapp/src/Utils/Scoreboard/Scoreboard.php @@ -31,6 +31,7 @@ class Scoreboard * @param TeamCategory[] $categories * @param ContestProblem[] $problems * @param ScoreCache[] $scoreCache + * @param RankCache[] $rankCache */ public function __construct( protected readonly Contest $contest, diff --git a/webapp/src/Utils/Scoreboard/SingleTeamScoreboard.php b/webapp/src/Utils/Scoreboard/SingleTeamScoreboard.php index 9acae1c400..50bd5d3d0f 100644 --- a/webapp/src/Utils/Scoreboard/SingleTeamScoreboard.php +++ b/webapp/src/Utils/Scoreboard/SingleTeamScoreboard.php @@ -21,6 +21,7 @@ class SingleTeamScoreboard extends Scoreboard /** * @param ContestProblem[] $problems * @param ScoreCache[] $scoreCache + * @param RankCache[] $rankCache */ public function __construct( Contest $contest, diff --git a/webapp/tests/Unit/Service/ImportProblemServiceTest.php b/webapp/tests/Unit/Service/ImportProblemServiceTest.php index 4f6368c30a..ccbd60c3cd 100644 --- a/webapp/tests/Unit/Service/ImportProblemServiceTest.php +++ b/webapp/tests/Unit/Service/ImportProblemServiceTest.php @@ -14,7 +14,7 @@ protected function setUp(): void self::bootKernel(); } - public function testEmptyYaml() + public function testEmptyYaml(): void { $yaml = ''; $messages = []; @@ -26,7 +26,7 @@ public function testEmptyYaml() $this->assertEquals('Unknown name', $problem->getName()); } - public function testMinimalYamlTest() + public function testMinimalYamlTest(): void { $yaml = <<assertEquals(null, $problem->getSpecialCompareArgs()); } - public function testTypesYamlTest() + public function testTypesYamlTest(): void { foreach ([ 'pass-fail', @@ -89,7 +89,7 @@ public function testTypesYamlTest() } } - public function testUnknownProblemType() + public function testUnknownProblemType(): void { $yaml = <<assertStringContainsString('Unknown problem type', $messagesString); } - public function testInvalidProblemType() { + public function testInvalidProblemType(): void + { foreach ([ 'pass-fail scoring', 'submit-answer multi-pass', @@ -126,7 +127,7 @@ public function testInvalidProblemType() { } } - public function testValidatorFlags() + public function testValidatorFlags(): void { $yaml = <<assertEquals('float_tolerance 1E-6', $problem->getSpecialCompareArgs()); } - public function testCustomValidation() + public function testCustomValidation(): void { foreach (['custom', 'custom interactive', 'custom multi-pass'] as $mode) { $yaml = <<assertEquals(1234*1024, $problem->getMemlimit()); } - public function testOutputLimit() + public function testOutputLimit(): void { $yaml = <<assertEquals(4223*1024, $problem->getOutputlimit()); } - public function testMultipassLimit() + public function testMultipassLimit(): void { $yaml = <<assertEquals(7, $problem->getMultipassLimit()); } - public function testMaximalProblem() { + public function testMaximalProblem(): void + { $yaml = <<assertEquals('special flags', $problem->getSpecialCompareArgs()); } - public function testMultipleLanguages() { + public function testMultipleLanguages(): void + { $yaml = <<assertEquals('english', $problem->getName()); } - public function testKattisExample() + public function testKattisExample(): void { $yaml = <<