diff --git a/webapp/migrations/Version20250323190305.php b/webapp/migrations/Version20250323190305.php index 126980b53d..edab8b5662 100644 --- a/webapp/migrations/Version20250323190305.php +++ b/webapp/migrations/Version20250323190305.php @@ -14,7 +14,7 @@ final class Version20250323190305 extends AbstractMigration { public function getDescription(): string { - return ''; + return 'Add problem types'; } public function up(Schema $schema): void diff --git a/webapp/migrations/Version20250620082406.php b/webapp/migrations/Version20250620082406.php new file mode 100644 index 0000000000..07c52b9347 --- /dev/null +++ b/webapp/migrations/Version20250620082406.php @@ -0,0 +1,38 @@ +addSql('ALTER TABLE judging CHANGE max_runtime_for_verdict max_runtime_for_verdict NUMERIC(32, 9) UNSIGNED DEFAULT NULL COMMENT \'The maximum runtime for all runs that resulted in the verdict\''); + $this->addSql('ALTER TABLE problem CHANGE types types INT NOT NULL COMMENT \'Bitmask of problem types, default is pass-fail.\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE problem CHANGE types types INT NOT NULL COMMENT \'Bitset of problem types, default is pass-fail.\''); + $this->addSql('ALTER TABLE judging CHANGE max_runtime_for_verdict max_runtime_for_verdict NUMERIC(32, 9) UNSIGNED DEFAULT NULL COMMENT \'The maximum run time for all runs that resulted in the verdict\''); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/migrations/Version20250620090108.php b/webapp/migrations/Version20250620090108.php new file mode 100644 index 0000000000..a7e5f3242c --- /dev/null +++ b/webapp/migrations/Version20250620090108.php @@ -0,0 +1,48 @@ +addSql('CREATE TABLE team_category_team (categoryid INT UNSIGNED NOT NULL COMMENT \'Team category ID\', teamid INT UNSIGNED NOT NULL COMMENT \'Team ID\', INDEX IDX_3A19F9C99B32FD3 (categoryid), INDEX IDX_3A19F9C94DD6ABF3 (teamid), PRIMARY KEY(categoryid, teamid)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE team_category_team ADD CONSTRAINT FK_3A19F9C99B32FD3 FOREIGN KEY (categoryid) REFERENCES team_category (categoryid) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE team_category_team ADD CONSTRAINT FK_3A19F9C94DD6ABF3 FOREIGN KEY (teamid) REFERENCES team (teamid) ON DELETE CASCADE'); + $this->addSql('INSERT INTO team_category_team (categoryid, teamid) SELECT categoryid, teamid FROM team'); + $this->addSql('ALTER TABLE team DROP FOREIGN KEY team_ibfk_1'); + $this->addSql('DROP INDEX categoryid ON team'); + $this->addSql('ALTER TABLE team DROP categoryid'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE team ADD categoryid INT UNSIGNED DEFAULT NULL COMMENT \'Team category ID\''); + $this->addSql('ALTER TABLE team ADD CONSTRAINT team_ibfk_1 FOREIGN KEY (categoryid) REFERENCES team_category (categoryid) ON DELETE CASCADE'); + $this->addSql('CREATE INDEX categoryid ON team (categoryid)'); + $this->addSql('UPDATE team SET categoryid = (SELECT MIN(categoryid) from team_category_team WHERE team_category_team.teamid = team.teamid)'); + $this->addSql('ALTER TABLE team_category_team DROP FOREIGN KEY FK_3A19F9C99B32FD3'); + $this->addSql('ALTER TABLE team_category_team DROP FOREIGN KEY FK_3A19F9C94DD6ABF3'); + $this->addSql('DROP TABLE team_category_team'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/src/Command/ScoreboardMergeCommand.php b/webapp/src/Command/ScoreboardMergeCommand.php index 254035ca7e..aab4e0b63d 100644 --- a/webapp/src/Command/ScoreboardMergeCommand.php +++ b/webapp/src/Command/ScoreboardMergeCommand.php @@ -220,7 +220,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $teamObj->setAffiliation($affiliations[$organizationName]); } - $teamObj->setCategory($category); + $teamObj->addCategory($category); $oldid = $team['id']; $newid = $nextTeamId++; $teamObj->setTeamid($newid); diff --git a/webapp/src/Controller/API/MetricsController.php b/webapp/src/Controller/API/MetricsController.php index 0b9e53da0d..3eb6f14ea2 100644 --- a/webapp/src/Controller/API/MetricsController.php +++ b/webapp/src/Controller/API/MetricsController.php @@ -79,7 +79,8 @@ public function prometheusAction(): Response ->select('t', 'u') ->from(Team::class, 't') ->leftJoin('t.users', 'u') - ->join('t.category', 'cat') + // TODO: category type + ->join('t.categories', 'cat') ->andWhere('cat.visible = true') ->getQuery() ->getResult(); @@ -89,7 +90,8 @@ public function prometheusAction(): Response ->select('u') ->from(User::class, 'u') ->leftJoin('u.team', 't') - ->join('t.category', 'cat') + // TODO: category type + ->join('t.categories', 'cat') ->andWhere('cat.visible = true') ->getQuery() ->getResult(); @@ -134,7 +136,8 @@ public function prometheusAction(): Response ->from(Team::class, 't') ->leftJoin('t.users', 'u') ->leftJoin('t.contests', 'c') - ->join('t.category', 'cat') + // TODO: category type + ->join('t.categories', 'cat') ->leftJoin('cat.contests', 'cc') ->andWhere('c.cid = :cid OR cc.cid = :cid') ->andWhere('cat.visible = true') @@ -154,7 +157,8 @@ public function prometheusAction(): Response ->from(User::class, 'u') ->leftJoin('u.team', 't') ->leftJoin('t.contests', 'c') - ->join('t.category', 'cat') + // TODO: category type + ->join('t.categories', 'cat') ->leftJoin('cat.contests', 'cc') ->andWhere('c.cid = :cid OR cc.cid = :cid') ->andWhere('cat.visible = true') @@ -227,7 +231,8 @@ public function prometheusAction(): Response ->join('b.submission', 's') ->join('s.contest', 'c') ->join('s.team', 't') - ->join('t.category', 'cat') + // TODO: category type + ->join('t.categories', 'cat') ->andWhere('b.done = false') ->andWhere('c.cid = :cid') ->andWhere('cat.visible = true') diff --git a/webapp/src/Controller/API/ScoreboardController.php b/webapp/src/Controller/API/ScoreboardController.php index 51f0c23a3c..63029e30e7 100644 --- a/webapp/src/Controller/API/ScoreboardController.php +++ b/webapp/src/Controller/API/ScoreboardController.php @@ -171,7 +171,7 @@ public function getScoreboardAction( $scoreIsInSeconds = (bool)$this->config->get('score_in_seconds'); foreach ($scoreboard->getScores() as $teamScore) { - if ($teamScore->team->getCategory()->getSortorder() !== $sortorder) { + if ($teamScore->team->getSortorder() !== $sortorder) { continue; } diff --git a/webapp/src/Controller/API/SubmissionController.php b/webapp/src/Controller/API/SubmissionController.php index 419c1b12df..f1816f94cf 100644 --- a/webapp/src/Controller/API/SubmissionController.php +++ b/webapp/src/Controller/API/SubmissionController.php @@ -462,7 +462,8 @@ protected function getQueryBuilder(Request $request): QueryBuilder if (!$this->dj->checkrole('api_reader') && !$this->dj->checkrole('judgehost')) { $queryBuilder - ->join('t.category', 'cat'); + // TODO: category type + ->join('t.categories', 'cat'); if ($this->dj->checkrole('team')) { $queryBuilder ->andWhere('cat.visible = 1 OR s.team = :team') diff --git a/webapp/src/Controller/API/TeamController.php b/webapp/src/Controller/API/TeamController.php index db5e8f017e..6f8ab3d148 100644 --- a/webapp/src/Controller/API/TeamController.php +++ b/webapp/src/Controller/API/TeamController.php @@ -305,14 +305,15 @@ protected function getQueryBuilder(Request $request): QueryBuilder $queryBuilder = $this->em->createQueryBuilder() ->from(Team::class, 't') ->leftJoin('t.affiliation', 'ta') - ->leftJoin('t.category', 'tc') + // TODO: category type + ->leftJoin('t.categories', 'tc') ->leftJoin('t.contests', 'c') ->leftJoin('tc.contests', 'cc') ->select('t, ta'); if ($request->query->has('category')) { $queryBuilder - ->andWhere('t.category = :category') + ->andWhere('tc.categoryid = :category') ->setParameter('category', $request->query->get('category')); } diff --git a/webapp/src/Controller/Jury/ContestController.php b/webapp/src/Controller/Jury/ContestController.php index 84f9a2a729..1d26dd6cd6 100644 --- a/webapp/src/Controller/Jury/ContestController.php +++ b/webapp/src/Controller/Jury/ContestController.php @@ -216,7 +216,7 @@ public function indexAction(Request $request): Response ->select('COUNT(DISTINCT t.teamid)') ->from(Team::class, 't') ->leftJoin('t.contests', 'c') - ->join('t.category', 'cat') + ->join('t.categories', 'cat') ->leftJoin('cat.contests', 'cc') ->andWhere('c.cid = :cid OR cc.cid = :cid') ->setParameter('cid', $contest->getCid()) diff --git a/webapp/src/Controller/Jury/ImportExportController.php b/webapp/src/Controller/Jury/ImportExportController.php index 6f07dfd71a..64246fc16a 100644 --- a/webapp/src/Controller/Jury/ImportExportController.php +++ b/webapp/src/Controller/Jury/ImportExportController.php @@ -487,7 +487,7 @@ protected function getResultsHtml( 'rank' => null, ]; foreach ($teams as $team) { - if (!isset($categories[$team->getCategory()->getCategoryid()]) || $team->getCategory()->getSortorder() !== $sortOrder) { + if ($team->getHidden() || $team->getSortorder() !== $sortOrder) { continue; } diff --git a/webapp/src/Controller/Jury/JudgeRemainingTrait.php b/webapp/src/Controller/Jury/JudgeRemainingTrait.php index 466a652e26..8600006586 100644 --- a/webapp/src/Controller/Jury/JudgeRemainingTrait.php +++ b/webapp/src/Controller/Jury/JudgeRemainingTrait.php @@ -80,7 +80,7 @@ public function judgeRemaining(int $contestId = -1, string $categoryId = '', str ->select('j') ->join('j.submission', 's') ->join('s.team', 't') - ->join('t.category', 'tc') + ->join('t.categories', 'tc') ->andWhere('j.valid = true') ->andWhere('j.result != :compiler_error') ->setParameter('compiler_error', 'compiler-error'); diff --git a/webapp/src/Controller/Jury/SubmissionController.php b/webapp/src/Controller/Jury/SubmissionController.php index 8afa8582f4..15dff9daaf 100644 --- a/webapp/src/Controller/Jury/SubmissionController.php +++ b/webapp/src/Controller/Jury/SubmissionController.php @@ -1145,7 +1145,7 @@ public function verifyAction( if (!$judging->getContest()->isOpenToAllTeams()) { $teamsQueryBuilder ->leftJoin('t.contests', 'c') - ->join('t.category', 'cat') + ->join('t.categories', 'cat') ->leftJoin('cat.contests', 'cc') ->andWhere('c.cid = :cid OR cc.cid = :cid') ->setParameter('cid', $judging->getContest()->getCid()); diff --git a/webapp/src/Controller/Jury/TeamController.php b/webapp/src/Controller/Jury/TeamController.php index 87399655e9..8ee8cba867 100644 --- a/webapp/src/Controller/Jury/TeamController.php +++ b/webapp/src/Controller/Jury/TeamController.php @@ -51,7 +51,7 @@ public function indexAction(): Response ->from(Team::class, 't') ->leftJoin('t.contests', 'c') ->leftJoin('t.affiliation', 'a') - ->leftJoin('t.category', 'cat') + ->leftJoin('t.categories', 'cat') ->leftJoin('cat.contests', 'cc') ->orderBy('cat.sortorder', 'ASC') ->addOrderBy('t.name', 'ASC') @@ -93,7 +93,8 @@ public function indexAction(): Response 'externalid' => ['title' => 'external ID', 'sort' => true], 'label' => ['title' => 'label', 'sort' => true,], 'effective_name' => ['title' => 'name', 'sort' => true,], - 'category' => ['title' => 'category', 'sort' => true,], + 'category' => ['title' => 'sort order category', 'sort' => true,], + 'num_categories' => ['title' => '# categories', 'sort' => true,], 'affiliation' => ['title' => 'affiliation', 'sort' => true,], 'num_contests' => ['title' => '# contests', 'sort' => true,], 'ip_address' => ['title' => 'last IP', 'sort' => true,], @@ -122,6 +123,9 @@ public function indexAction(): Response } } + $teamdata['category'] = ['value' => $t->getSortOrderCategory()]; + $teamdata['num_categories'] = ['value' => $t->getCategories()->count()]; + // Add some elements for the solved status. $num_solved = 0; $num_submitted = 0; @@ -189,8 +193,8 @@ public function indexAction(): Response foreach ($t->getContests() as $c) { $teamContests[$c->getCid()] = true; } - if ($t->getCategory()) { - foreach ($t->getCategory()->getContests() as $c) { + foreach ($t->getCategories() as $category) { + foreach ($category->getContests() as $c) { $teamContests[$c->getCid()] = true; } } @@ -212,7 +216,8 @@ public function indexAction(): Response 'data' => $teamdata, 'actions' => $teamactions, 'link' => $this->generateUrl('jury_team', ['teamId' => $t->getTeamId()]), - 'cssclass' => ($t->getCategory() ? ("category" . $t->getCategory()->getCategoryId()) : '') . + // TODO: category type + 'cssclass' => ($t->getCategories()->first() ? ("category" . $t->getCategories()->first()->getCategoryId()) : '') . ($t->getEnabled() ? '' : ' disabled'), ]; } diff --git a/webapp/src/Controller/PublicController.php b/webapp/src/Controller/PublicController.php index 9d368ad964..fb2ed8f0db 100644 --- a/webapp/src/Controller/PublicController.php +++ b/webapp/src/Controller/PublicController.php @@ -168,7 +168,7 @@ public function teamAction(Request $request, int $teamId): Response { /** @var Team|null $team */ $team = $this->em->getRepository(Team::class)->find($teamId); - if ($team && $team->getCategory() && !$team->getCategory()->getVisible()) { + if ($team?->getHidden()) { $team = null; } $showFlags = (bool)$this->config->get('show_flags'); diff --git a/webapp/src/Controller/SecurityController.php b/webapp/src/Controller/SecurityController.php index 15bc302dff..bb7534dbb9 100644 --- a/webapp/src/Controller/SecurityController.php +++ b/webapp/src/Controller/SecurityController.php @@ -133,7 +133,7 @@ public function registerAction( ->setExternalid(Uuid::uuid4()->toString()) ->addUser($user) ->setName($teamName) - ->setCategory($teamCategory) + ->addCategory($teamCategory) ->setInternalComments('Registered by ' . $this->dj->getClientIp() . ' on ' . date('r')); if ($this->config->get('show_affiliations')) { diff --git a/webapp/src/Controller/Team/ScoreboardController.php b/webapp/src/Controller/Team/ScoreboardController.php index 069e5db60c..2e487e3835 100644 --- a/webapp/src/Controller/Team/ScoreboardController.php +++ b/webapp/src/Controller/Team/ScoreboardController.php @@ -68,7 +68,7 @@ public function teamAction(Request $request, int $teamId): Response /** @var Team|null $team */ $team = $this->em->getRepository(Team::class)->find($teamId); - if ($team && $team->getCategory() && !$team->getCategory()->getVisible() && $teamId !== $this->dj->getUser()->getTeamId()) { + if ($team?->getHidden() && $teamId !== $this->dj->getUser()->getTeamId()) { $team = null; } $showFlags = (bool)$this->config->get('show_flags'); diff --git a/webapp/src/DataFixtures/DefaultData/TeamFixture.php b/webapp/src/DataFixtures/DefaultData/TeamFixture.php index 7b2fc0fda5..05b50604e3 100644 --- a/webapp/src/DataFixtures/DefaultData/TeamFixture.php +++ b/webapp/src/DataFixtures/DefaultData/TeamFixture.php @@ -23,7 +23,7 @@ public function load(ObjectManager $manager): void ->setName('DOMjudge') ->setExternalid('domjudge') ->setLabel('domjudge') - ->setCategory($this->getReference(TeamCategoryFixture::SYSTEM_REFERENCE, TeamCategory::class)); + ->addCategory($this->getReference(TeamCategoryFixture::SYSTEM_REFERENCE, TeamCategory::class)); $manager->persist($team); } else { $this->logger->info('Team DOMjudge already exists, not created'); diff --git a/webapp/src/DataFixtures/ExampleData/TeamFixture.php b/webapp/src/DataFixtures/ExampleData/TeamFixture.php index 122fc9fdc4..e7048333dc 100644 --- a/webapp/src/DataFixtures/ExampleData/TeamFixture.php +++ b/webapp/src/DataFixtures/ExampleData/TeamFixture.php @@ -21,7 +21,7 @@ public function load(ObjectManager $manager): void ->setLabel('exteam') ->setName('Example teamname') ->setAffiliation($this->getReference(TeamAffiliationFixture::AFFILIATION_REFERENCE, TeamAffiliation::class)) - ->setCategory($this->getReference(TeamCategoryFixture::PARTICIPANTS_REFERENCE, TeamCategory::class)); + ->addCategory($this->getReference(TeamCategoryFixture::PARTICIPANTS_REFERENCE, TeamCategory::class)); $manager->persist($team); $manager->flush(); diff --git a/webapp/src/DataFixtures/Test/CreateTeamWithTwoTeamAffiliationsFixture.php b/webapp/src/DataFixtures/Test/CreateTeamWithTwoTeamAffiliationsFixture.php new file mode 100644 index 0000000000..6da99e8081 --- /dev/null +++ b/webapp/src/DataFixtures/Test/CreateTeamWithTwoTeamAffiliationsFixture.php @@ -0,0 +1,28 @@ +setExternalid('teamwithtwogroups') + ->setIcpcid('teamwithtwogroups') + ->setLabel('teamwithtwogroups') + ->setName('Team with two groups') + ->setAffiliation($manager->getRepository(TeamAffiliation::class)->findOneBy(['externalid' => 'utrecht'])) + ->addCategory($manager->getRepository(TeamCategory::class)->findOneBy(['externalid' => 'participants'])) + ->addCategory($manager->getRepository(TeamCategory::class)->findOneBy(['externalid' => 'observers'])); + + $manager->persist($team); + $manager->flush(); + } +} diff --git a/webapp/src/DataFixtures/Test/RejudgingFirstToSolveFixture.php b/webapp/src/DataFixtures/Test/RejudgingFirstToSolveFixture.php index a5fb6c9f06..8dc42bfcdf 100644 --- a/webapp/src/DataFixtures/Test/RejudgingFirstToSolveFixture.php +++ b/webapp/src/DataFixtures/Test/RejudgingFirstToSolveFixture.php @@ -17,8 +17,10 @@ public function load(ObjectManager $manager): void { $team1 = $manager->getRepository(Team::class)->findOneBy(['name' => 'Example teamname']); $team2 = (new Team()) - ->setName('Another team') - ->setCategory($team1->getCategory()); + ->setName('Another team'); + foreach ($team1->getCategories() as $category) { + $team2->addCategory($category); + } $manager->persist($team2); diff --git a/webapp/src/Entity/ExternalSourceWarning.php b/webapp/src/Entity/ExternalSourceWarning.php index b223b6277b..b46bd123ee 100644 --- a/webapp/src/Entity/ExternalSourceWarning.php +++ b/webapp/src/Entity/ExternalSourceWarning.php @@ -40,7 +40,7 @@ class ExternalSourceWarning scale: 9, options: ['comment' => 'Time this warning happened last', 'unsigned' => true] )] - private float $lastTime; + private string|float $lastTime; #[ORM\Column(options: ['comment' => 'Type of the entity for this warning'])] private string $entityType; @@ -81,12 +81,12 @@ public function setLastEventId(?string $lastEventId): ExternalSourceWarning return $this; } - public function getLastTime(): float + public function getLastTime(): string|float { return $this->lastTime; } - public function setLastTime(float $lastTime): ExternalSourceWarning + public function setLastTime(string|float $lastTime): ExternalSourceWarning { $this->lastTime = $lastTime; return $this; diff --git a/webapp/src/Entity/RankCache.php b/webapp/src/Entity/RankCache.php index d1f25640eb..0971575ca4 100644 --- a/webapp/src/Entity/RankCache.php +++ b/webapp/src/Entity/RankCache.php @@ -19,8 +19,8 @@ #[ORM\Index(columns: ['cid', 'points_public', 'totaltime_public', 'totalruntime_public'], name: 'order_public')] #[ORM\Index(columns: ['cid'], name: 'cid')] #[ORM\Index(columns: ['teamid'], name: 'teamid')] -#[ORM\Index(columns: ['sort_key_public'], name: 'sortKeyPublic')] -#[ORM\Index(columns: ['sort_key_restricted'], name: 'sortKeyRestricted')] +#[ORM\Index(columns: ['sort_key_public'], name: 'sortKeyPublic', options: ['lengths' => [768]])] +#[ORM\Index(columns: ['sort_key_restricted'], name: 'sortKeyRestricted', options: ['lengths' => [768]])] class RankCache { #[ORM\Column(options: [ diff --git a/webapp/src/Entity/Team.php b/webapp/src/Entity/Team.php index b08b485446..9787112426 100644 --- a/webapp/src/Entity/Team.php +++ b/webapp/src/Entity/Team.php @@ -21,7 +21,6 @@ #[ORM\Entity] #[ORM\Table(options: ['collation' => 'utf8mb4_unicode_ci', 'charset' => 'utf8mb4'])] #[ORM\Index(columns: ['affilid'], name: 'affilid')] -#[ORM\Index(columns: ['categoryid'], name: 'categoryid')] #[ORM\UniqueConstraint(name: 'externalid', columns: ['externalid'], options: ['lengths' => [190]])] #[ORM\UniqueConstraint(name: 'label', columns: ['label'])] #[UniqueEntity(fields: 'externalid')] @@ -142,10 +141,12 @@ class Team extends BaseApiEntity implements #[Serializer\Exclude] private ?TeamAffiliation $affiliation = null; - #[ORM\ManyToOne(inversedBy: 'teams')] - #[ORM\JoinColumn(name: 'categoryid', referencedColumnName: 'categoryid', onDelete: 'CASCADE')] + /** + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: TeamCategory::class, mappedBy: 'teams', cascade: ['persist'])] #[Serializer\Exclude] - private ?TeamCategory $category = null; + private Collection $categories; /** * @var Collection @@ -436,15 +437,25 @@ public function getAffiliationId(): ?string return $this->getAffiliation()?->getExternalid(); } - public function setCategory(?TeamCategory $category = null): Team + public function addCategory(TeamCategory $category): Team { - $this->category = $category; + $this->categories[] = $category; + $category->addTeam($this); return $this; } - public function getCategory(): ?TeamCategory + public function removeCategory(TeamCategory $category): void + { + $this->categories->removeElement($category); + $category->removeTeam($this); + } + + /** + * @return Collection + */ + public function getCategories(): Collection { - return $this->category; + return $this->categories; } #[Serializer\VirtualProperty] @@ -452,12 +463,18 @@ public function getCategory(): ?TeamCategory #[Serializer\Type('bool')] public function getHidden(): bool { - return !$this->getCategory() || !$this->getCategory()->getVisible(); + foreach ($this->getCategories() as $category) { + if ($category->getVisible()) { + return false; + } + } + return true; } public function __construct() { $this->contests = new ArrayCollection(); + $this->categories = new ArrayCollection(); $this->users = new ArrayCollection(); $this->submissions = new ArrayCollection(); $this->sent_clarifications = new ArrayCollection(); @@ -569,7 +586,7 @@ public function getUnreadClarifications(): Collection #[Serializer\Type('array')] public function getGroupIds(): array { - return $this->getCategory() ? [$this->getCategory()->getExternalid()] : []; + return $this->categories->map(fn(TeamCategory $category) => $category->getExternalid())->toArray(); } #[OA\Property(nullable: true)] @@ -619,9 +636,18 @@ public function validate(ExecutionContextInterface $context): void public function inContest(Contest $contest): bool { - return $contest->isOpenToAllTeams() || - $this->getContests()->contains($contest) || - ($this->getCategory() !== null && $this->getCategory()->inContest($contest)); + if ($contest->isOpenToAllTeams()) { + return true; + } + if ($this->getContests()->contains($contest)) { + return true; + } + foreach ($this->getCategories() as $category) { + if ($category->inContest($contest)) { + return true; + } + } + return false; } public function getAssetProperties(): array @@ -661,4 +687,15 @@ public function getPhotoForApi(): array { return array_filter([$this->photoForApi]); } + + public function getSortOrderCategory(): ?TeamCategory + { + // TODO: category type + return $this->categories->first() ?: null; + } + + public function getSortOrder(): ?int + { + return $this->getSortOrderCategory()?->getSortorder(); + } } diff --git a/webapp/src/Entity/TeamCategory.php b/webapp/src/Entity/TeamCategory.php index 0ae161ff87..96bf89bd84 100644 --- a/webapp/src/Entity/TeamCategory.php +++ b/webapp/src/Entity/TeamCategory.php @@ -91,7 +91,9 @@ class TeamCategory extends BaseApiEntity implements /** * @var Collection */ - #[ORM\OneToMany(mappedBy: 'category', targetEntity: Team::class)] + #[ORM\ManyToMany(targetEntity: Team::class, inversedBy: 'categories', cascade: ['persist'])] + #[ORM\JoinColumn(name: 'categoryid', referencedColumnName: 'categoryid', onDelete: 'CASCADE')] + #[ORM\InverseJoinColumn(name: 'teamid', referencedColumnName: 'teamid', onDelete: 'CASCADE')] #[Serializer\Exclude] private Collection $teams; @@ -220,6 +222,11 @@ public function addTeam(Team $team): TeamCategory return $this; } + public function removeTeam(Team $team): void + { + $this->teams->removeElement($team); + } + /** * @return Collection */ diff --git a/webapp/src/Form/Type/RejudgingType.php b/webapp/src/Form/Type/RejudgingType.php index 5afcda07c6..6050cddfd0 100644 --- a/webapp/src/Form/Type/RejudgingType.php +++ b/webapp/src/Form/Type/RejudgingType.php @@ -181,7 +181,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void if (!$selectAllTeams) { $teamsQueryBuilder ->leftJoin('t.contests', 'c') - ->join('t.category', 'cat') + ->join('t.categories', 'cat') ->leftJoin('cat.contests', 'cc') ->andWhere('c IN (:contests) OR cc IN (:contests)') ->setParameter('contests', $contests); diff --git a/webapp/src/Form/Type/SubmissionsFilterType.php b/webapp/src/Form/Type/SubmissionsFilterType.php index f962bb7501..74951f4f43 100644 --- a/webapp/src/Form/Type/SubmissionsFilterType.php +++ b/webapp/src/Form/Type/SubmissionsFilterType.php @@ -101,7 +101,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void if (!$selectAllTeams) { $teamsQueryBuilder ->leftJoin("t.contests", "c") - ->join("t.category", "cat") + ->join("t.categories", "cat") ->leftJoin("cat.contests", "cc") ->andWhere("c IN (:contests) OR cc IN (:contests)") ->setParameter(":contests", $contests); diff --git a/webapp/src/Form/Type/TeamType.php b/webapp/src/Form/Type/TeamType.php index 5a5cdde162..eaddc686ef 100644 --- a/webapp/src/Form/Type/TeamType.php +++ b/webapp/src/Form/Type/TeamType.php @@ -65,8 +65,12 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'help' => 'If provided, will display this instead of the team name in certain places, like the scoreboard.', ]); - $builder->add('category', EntityType::class, [ - 'class' => TeamCategory::class, + $builder->add('categories', EntityType::class, [ + 'class' => TeamCategory::class, + 'required' => false, + 'choice_label' => 'name', + 'multiple' => true, + 'by_reference' => false, ]); $builder->add('publicdescription', TextareaType::class, [ 'label' => 'Public description', diff --git a/webapp/src/Service/AwardService.php b/webapp/src/Service/AwardService.php index 4dd15073f7..5e5b87343a 100644 --- a/webapp/src/Service/AwardService.php +++ b/webapp/src/Service/AwardService.php @@ -17,11 +17,12 @@ protected function loadAwards(Contest $contest, Scoreboard $scoreboard): void $group_winners = $problem_winners = $problem_shortname = []; $groups = []; foreach ($scoreboard->getTeamsInDescendingOrder() as $team) { + // TODO: category type $teamid = $team->getExternalid(); if ($scoreboard->isBestInCategory($team)) { - $catId = $team->getCategory()->getExternalid(); + $catId = $team->getSortOrderCategory()?->getExternalid(); $group_winners[$catId][] = $teamid; - $groups[$catId] = $team->getCategory()->getName(); + $groups[$catId] = $team->getSortOrderCategory()?->getName(); } foreach ($scoreboard->getProblems() as $problem) { $shortname = $problem->getShortname(); @@ -65,8 +66,8 @@ protected function loadAwards(Contest $contest, Scoreboard $scoreboard): void foreach ($scoreboard->getScores() as $teamScore) { // If we are checking a new sort order, reset the number of skipped teams - if ($teamScore->team->getCategory()->getSortorder() !== $currentSortOrder) { - $currentSortOrder = $teamScore->team->getCategory()->getSortorder(); + if ($teamScore->team->getSortOrder() !== $currentSortOrder) { + $currentSortOrder = $teamScore->team->getSortorder(); $skippedTeams = 0; } @@ -79,7 +80,7 @@ protected function loadAwards(Contest $contest, Scoreboard $scoreboard): void $overall_winners[] = $teamid; } if ($contest->getMedalsEnabled()) { - if ($contest->getMedalCategories()->contains($teamScore->team->getCategory())) { + if ($teamScore->team->getSortOrderCategory() && $contest->getMedalCategories()->contains($teamScore->team->getSortOrderCategory())) { if ($rank - $skippedTeams <= $contest->getGoldMedals()) { $medal_winners['gold'][] = $teamid; } elseif ($rank - $skippedTeams <= $contest->getGoldMedals() + $contest->getSilverMedals()) { diff --git a/webapp/src/Service/BalloonService.php b/webapp/src/Service/BalloonService.php index ff93e2fddf..e463ef854d 100644 --- a/webapp/src/Service/BalloonService.php +++ b/webapp/src/Service/BalloonService.php @@ -117,7 +117,8 @@ public function collectBalloonTable(Contest $contest, bool $todo = false): array ->leftJoin('b.contest', 'co') ->leftJoin('p.contest_problems', 'cp', Join::WITH, 'co.cid = cp.contest AND p.probid = cp.problem') ->leftJoin('b.team', 't') - ->leftJoin('t.category', 'c') + // TODO: category type + ->leftJoin('t.categories', 'c') ->leftJoin('t.affiliation', 'a') ->andWhere('co.cid = :cid') ->setParameter('cid', $contest->getCid()) diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index 132beee23c..0a40fd7e94 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -1395,7 +1395,7 @@ public function loadTeam(string $teamId, Contest $contest): Team $queryBuilder = $this->em->createQueryBuilder() ->from(Team::class, 't') ->select('t') - ->leftJoin('t.category', 'tc') + ->leftJoin('t.categories', 'tc') ->leftJoin('t.contests', 'c') ->leftJoin('tc.contests', 'cc') ->andWhere('t.externalid = :team') diff --git a/webapp/src/Service/ExternalContestSourceService.php b/webapp/src/Service/ExternalContestSourceService.php index 719aee8e30..946458f73f 100644 --- a/webapp/src/Service/ExternalContestSourceService.php +++ b/webapp/src/Service/ExternalContestSourceService.php @@ -1102,7 +1102,7 @@ protected function validateAndUpdateTeam(Event $event, EventData $data): void $category->setName($data->groupIds[0]); $this->em->persist($category); } - $team->setCategory($category); + $team->addCategory($category); } $this->removeWarning($event->type, $data->id, ExternalSourceWarning::TYPE_ENTITY_NOT_FOUND); diff --git a/webapp/src/Service/ICPCCmsService.php b/webapp/src/Service/ICPCCmsService.php index ff5c308deb..2e90d5e23a 100644 --- a/webapp/src/Service/ICPCCmsService.php +++ b/webapp/src/Service/ICPCCmsService.php @@ -110,7 +110,7 @@ public function importTeams(string $token, string $contest, ?string &$message = $team = new Team(); $team ->setName($teamData['teamName']) - ->setCategory($participants) + ->addCategory($participants) ->setAffiliation($affiliation) ->setEnabled($enabled) ->setInternalComments('Status: ' . $teamData['status']) @@ -132,7 +132,7 @@ public function importTeams(string $token, string $contest, ?string &$message = $username = sprintf("team%04d", $team->getTeamid()); $team ->setName($teamData['teamName']) - ->setCategory($participants) + ->addCategory($participants) ->setAffiliation($affiliation) ->setEnabled($enabled) ->setInternalComments('Status: ' . $teamData['status']) diff --git a/webapp/src/Service/ImportExportService.php b/webapp/src/Service/ImportExportService.php index 2039c7bf06..a6ce1d9daa 100644 --- a/webapp/src/Service/ImportExportService.php +++ b/webapp/src/Service/ImportExportService.php @@ -431,7 +431,8 @@ public function getTeamData(): array /** @var Team[] $teams */ $teams = $this->em->createQueryBuilder() ->from(Team::class, 't') - ->join('t.category', 'c') + // TODO: category type + ->join('t.categories', 'c') ->select('t') ->where('c.visible = 1') ->getQuery() @@ -442,7 +443,7 @@ public function getTeamData(): array $data[] = [ $team->getExternalid(), $team->getIcpcId(), - $team->getCategory()->getExternalid(), + $team->getSortOrderCategory()?->getExternalid(), $team->getEffectiveName(), $team->getAffiliation() ? $team->getAffiliation()->getName() : '', $team->getAffiliation() ? $team->getAffiliation()->getShortname() : '', @@ -515,7 +516,7 @@ public function getResultsData( $skippedTeams = 0; foreach ($scoreboard->getScores() as $teamScore) { - if ($teamScore->team->getCategory()->getSortorder() !== $sortOrder) { + if ($teamScore->team->getSortorder() !== $sortOrder) { continue; } $maxTime = -1; @@ -528,7 +529,7 @@ public function getResultsData( $numPoints = $teamScore->numPoints; $skip = false; - if (!$contest->getMedalCategories()->contains($teamScore->team->getCategory())) { + if (!$teamScore->team->getSortOrderCategory() || !$contest->getMedalCategories()->contains($teamScore->team->getSortOrderCategory())) { $skip = true; $skippedTeams++; } @@ -583,12 +584,12 @@ public function getResultsData( $rank = null; } - $categoryId = $teamScore->team->getCategory()->getCategoryid(); - if (isset($groupWinners[$categoryId])) { + $categoryId = $teamScore->team->getSortOrderCategory()?->getCategoryid(); + if ($categoryId && isset($groupWinners[$categoryId])) { $groupWinner = null; } else { $groupWinners[$categoryId] = true; - $groupWinner = $teamScore->team->getCategory()->getName(); + $groupWinner = $teamScore->team->getSortOrderCategory()?->getName(); } $data[] = new ResultRow( @@ -985,7 +986,7 @@ protected function importTeamsTsv(array $content, ?string &$message = null): int 'team' => [ 'teamid' => $teamId, 'icpcid' => $teamIcpcId, - 'categoryid' => @$line[2], + 'categoryids' => isset($line[2]) ? [$line[2]] : [], 'name' => @$line[3], ], 'team_affiliation' => [ @@ -1016,7 +1017,7 @@ public function importTeamsJson(array $data, ?string &$message = null, ?array &$ 'teamid' => $team['id'] ?? null, 'icpcid' => $team['icpc_id'] ?? null, 'label' => $team['label'] ?? null, - 'categoryid' => $team['group_ids'][0] ?? null, + 'categoryids' => $team['group_ids'] ?? [], 'name' => $team['name'] ?? '', 'display_name' => $team['display_name'] ?? null, 'publicdescription' => $team['public_description'] ?? $team['members'] ?? '', @@ -1189,11 +1190,12 @@ protected function importTeamData(array $teamData, ?string &$message, ?array &$s $teamItem['team']['affiliation'] = $teamAffiliation; unset($teamItem['team']['affilid']); - if (!empty($teamItem['team']['categoryid'])) { - $teamCategory = $this->em->getRepository(TeamCategory::class)->findOneBy(['externalid' => $teamItem['team']['categoryid']]); + $teamCategories = []; + foreach ($teamItem['team']['categoryids'] ?? [] as $categoryid) { + $teamCategory = $this->em->getRepository(TeamCategory::class)->findOneBy(['externalid' => $categoryid]); if (!$teamCategory) { foreach ($createdCategories as $createdCategory) { - if ($createdCategory->getExternalid() === $teamItem['team']['categoryid']) { + if ($createdCategory->getExternalid() === $categoryid) { $teamCategory = $createdCategory; break; } @@ -1202,8 +1204,8 @@ protected function importTeamData(array $teamData, ?string &$message, ?array &$s if (!$teamCategory) { $teamCategory = new TeamCategory(); $teamCategory - ->setExternalid($teamItem['team']['categoryid']) - ->setName($teamItem['team']['categoryid'] . ' - auto-create during import'); + ->setExternalid($categoryid) + ->setName($categoryid . ' - auto-create during import'); $errors = $this->validator->validate($teamCategory); if ($errors->count()) { @@ -1222,9 +1224,10 @@ protected function importTeamData(array $teamData, ?string &$message, ?array &$s $createdCategories[] = $teamCategory; } } + $teamCategories[] = $teamCategory; } - $teamItem['team']['category'] = $teamCategory; - unset($teamItem['team']['categoryid']); + $teamItem['team']['categories'] = $teamCategories; + unset($teamItem['team']['categoryids']); // Determine if we need to set the team ID manually or automatically if (empty($teamItem['team']['teamid'])) { @@ -1349,15 +1352,21 @@ protected function importAccountData( $allUsers = []; foreach ($accountData as $index => $accountItem) { if (!empty($accountItem['team'])) { - $team = $this->em->getRepository(Team::class)->findOneBy([ - 'name' => $accountItem['team']['name'], - 'category' => $accountItem['team']['category'] - ]); + $team = $this->em->createQueryBuilder() + ->select('t') + ->from(Team::class, 't') + ->join('t.categories', 'c') + ->andWhere('t.name = :name') + ->andWhere('c.categoryid = :category') + ->setParameter('name', $accountItem['team']['name']) + ->setParameter('category', $accountItem['team']['category']) + ->getQuery() + ->getOneOrNullResult(); if ($team === null) { $team = new Team(); $team ->setName($accountItem['team']['name']) - ->setCategory($accountItem['team']['category']) + ->addCategory($accountItem['team']['category']) ->setExternalid($accountItem['team']['externalid']) ->setPublicDescription($accountItem['team']['publicdescription'] ?? null); $action = EventLogService::ACTION_CREATE; diff --git a/webapp/src/Service/RejudgingService.php b/webapp/src/Service/RejudgingService.php index 629dfc3418..56bc9528f7 100644 --- a/webapp/src/Service/RejudgingService.php +++ b/webapp/src/Service/RejudgingService.php @@ -369,7 +369,7 @@ public function finishRejudging(Rejudging $rejudging, string $action, ?callable if (!$contest->isOpenToAllTeams()) { $queryBuilder ->leftJoin('t.contests', 'c') - ->join('t.category', 'cat') + ->join('t.categories', 'cat') ->leftJoin('cat.contests', 'cc') ->andWhere('c.cid = :cid OR cc.cid = :cid') ->setParameter('cid', $contest->getCid()); diff --git a/webapp/src/Service/ScoreboardService.php b/webapp/src/Service/ScoreboardService.php index 7ba4ae3267..8436db27af 100644 --- a/webapp/src/Service/ScoreboardService.php +++ b/webapp/src/Service/ScoreboardService.php @@ -131,7 +131,7 @@ public function calculateTeamRank( } $restricted = ($jury || $freezeData->showFinal(false)); $variant = $restricted ? 'Restricted' : 'Public'; - $sortOrder = $team->getCategory()->getSortorder(); + $sortOrder = $team->getSortOrder(); $sortKey = $this->em->createQueryBuilder() ->from(RankCache::class, 'r') @@ -152,7 +152,8 @@ public function calculateTeamRank( $better = $this->em->createQueryBuilder() ->from(RankCache::class, 'r') ->join('r.team', 't') - ->join('t.category', 'tc') + // TODO: category type + ->join('t.categories', 'tc') ->select('COUNT(t.teamid)') ->andWhere('r.sortKey'.$variant.' > :sortKey') ->andWhere('r.contest = :contest') @@ -190,7 +191,7 @@ public function calculateScoreRow( [ $contest->getCid(), $team->getTeamid(), $problem->getProbid() ] ); - if (!$team->getCategory()) { + if (!$team->getSortOrderCategory()) { $this->logger->warning( "Team '%d' has no category, skipping", [ $team->getTeamid() ] @@ -354,7 +355,7 @@ public function calculateScoreRow( $params = [ 'cid' => $contest->getCid(), 'probid' => $problem->getProbid(), - 'teamSortOrder' => $team->getCategory()->getSortorder(), + 'teamSortOrder' => $team->getSortorder(), /** @phpstan-ignore-next-line $absSubmitTime is always set when $correctJury is true */ 'submitTime' => $absSubmitTime, 'correctResult' => Judging::RESULT_CORRECT, @@ -377,6 +378,8 @@ public function calculateScoreRow( LEFT JOIN external_judgement ej USING (submitid) LEFT JOIN external_judgement ej2 ON ej2.submitid = s.submitid AND ej2.starttime > ej.starttime LEFT JOIN team t USING(teamid) + # TODO: category type + LEFT JOIN team_category_team tcc USING (teamid) LEFT JOIN team_category tc USING (categoryid) WHERE s.valid = 1 AND (ej.result IS NULL OR ej.result = :correctResult '. @@ -389,6 +392,8 @@ public function calculateScoreRow( SELECT count(*) FROM submission s LEFT JOIN judging j ON (s.submitid=j.submitid AND j.valid=1) LEFT JOIN team t USING (teamid) + # TODO: category type + LEFT JOIN team_category_team tcc USING (teamid) LEFT JOIN team_category tc USING (categoryid) WHERE s.valid = 1 AND (j.judgingid IS NULL OR j.result IS NULL OR j.result = :correctResult '. @@ -621,7 +626,7 @@ public function refreshCache(Contest $contest, ?callable $progressReporter = nul if (!$contest->isOpenToAllTeams()) { $queryBuilder ->leftJoin('t.contests', 'c') - ->join('t.category', 'cat') + ->join('t.categories', 'cat') ->leftJoin('cat.contests', 'cc') ->andWhere('c.cid = :cid OR cc.cid = :cid') ->setParameter('cid', $contest->getCid()); @@ -831,12 +836,13 @@ public function getFilterValues(Contest $contest, bool $jury): array ->from(TeamAffiliation::class, 'a') ->select('a') ->join('a.teams', 't') - ->andWhere('t.category IN (:categories)') + ->join('t.categories', 'tc') + ->andWhere('tc.categoryid IN (:categories)') ->setParameter('categories', $categories); if (!$contest->isOpenToAllTeams()) { $queryBuilder ->leftJoin('t.contests', 'c') - ->join('t.category', 'cat') + ->join('t.categories', 'cat') ->leftJoin('cat.contests', 'cc') ->andWhere('c = :contest OR cc = :contest') ->setParameter('contest', $contest); @@ -950,7 +956,8 @@ protected function getTeamsInOrder(Contest $contest, bool $jury = false, ?Filter { $queryBuilder = $this->em->createQueryBuilder() ->from(Team::class, 't', 't.teamid') - ->innerJoin('t.category', 'tc') + // TODO: category type + ->innerJoin('t.categories', 'tc') ->leftJoin(RankCache::class, 'r', Join::WITH, 'r.team = t AND r.contest = :rcid') ->leftJoin('t.affiliation', 'ta') ->select('t, tc, ta', 'COALESCE(t.display_name, t.name) AS HIDDEN effectivename') @@ -960,7 +967,7 @@ protected function getTeamsInOrder(Contest $contest, bool $jury = false, ?Filter if (!$contest->isOpenToAllTeams()) { $queryBuilder ->leftJoin('t.contests', 'c') - ->join('t.category', 'cat') + ->join('t.categories', 'cat') ->leftJoin('cat.contests', 'cc') ->andWhere('c.cid = :cid OR cc.cid = :cid') ->setParameter('cid', $contest->getCid()); @@ -988,7 +995,7 @@ protected function getTeamsInOrder(Contest $contest, bool $jury = false, ?Filter if ($filter->categories) { $queryBuilder - ->andWhere('t.category IN (:categories)') + ->andWhere('tc.categoryid IN (:categories)') ->setParameter('categories', $filter->categories); } diff --git a/webapp/src/Service/StatisticsService.php b/webapp/src/Service/StatisticsService.php index afd7d25075..403bcfa467 100644 --- a/webapp/src/Service/StatisticsService.php +++ b/webapp/src/Service/StatisticsService.php @@ -56,7 +56,7 @@ public function getTeams(Contest $contest, string $filter): array return $this->applyFilter($this->em->createQueryBuilder() ->select('t', 'ts', 'j', 'lang', 'a') ->from(Team::class, 't') - ->join('t.category', 'tc') + ->join('t.categories', 'tc') ->leftJoin('t.affiliation', 'a') ->join('t.submissions', 'ts') ->join('ts.language', 'l') @@ -71,7 +71,7 @@ public function getTeams(Contest $contest, string $filter): array ->from(Team::class, 't') ->leftJoin('t.contests', 'c') ->leftJoin('t.affiliation', 'a') - ->join('t.category', 'tc') + ->join('t.categories', 'tc') ->leftJoin('tc.contests', 'cc') ->join('t.submissions', 'ts') ->join('ts.language', 'l') @@ -257,7 +257,7 @@ public function getTeamStats(Contest $contest, Team $team): array ->join('s.problem', 'p') ->join('j.runs', 'jr') ->join('s.team', 'team') - ->join('team.category', 'tc') + ->join('team.categories', 'tc') // ->andWhere('j.valid = true') ->andWhere('s.contest = :contest') ->andWhere('s.team = :team') @@ -342,7 +342,7 @@ public function getProblemStats( ->join('s.judgings', 'sj') ->join('j.runs', 'jr') ->join('s.team', 'team') - ->join('team.category', 'tc') + ->join('team.categories', 'tc') ->andWhere('j.valid = true') ->andWhere('j.result IS NOT NULL') ->andWhere('s.contest = :contest') @@ -430,7 +430,7 @@ public function getGroupedProblemsStats( ->join('j.submission', 's') ->join('s.problem', 'p') ->join('s.team', 'team') - ->join('team.category', 'tc') + ->join('team.categories', 'tc') ->andWhere('j.valid = true') ->andWhere('j.result IS NOT NULL') ->andWhere('s.contest = :contest') @@ -685,7 +685,7 @@ protected function getTeamNumSubmissions(Contest $contest, string $filter): arra ->select('t.teamid as teamid, count(t.teamid) as num_submissions') ->from(Submission::class, 's') ->join('s.team', 't') - ->join('t.category', 'tc') + ->join('t.categories', 'tc') ->andWhere('s.contest = :contest'), $filter) ->groupBy('t.teamid') ->setParameter('contest', $contest) diff --git a/webapp/src/Service/SubmissionService.php b/webapp/src/Service/SubmissionService.php index 7dc699d43e..43ca69717c 100644 --- a/webapp/src/Service/SubmissionService.php +++ b/webapp/src/Service/SubmissionService.php @@ -86,6 +86,7 @@ public function getSubmissionList( ->from(Submission::class, 's') ->select('s', 'j', 'cp') ->join('s.team', 't') + ->join('t.categories', 'tc') ->join('s.contest_problem', 'cp') ->andWhere('s.contest IN (:contests)') ->setParameter('contests', array_keys($contests)) @@ -214,13 +215,13 @@ public function getSubmissionList( if (isset($restrictions->categoryId)) { $queryBuilder - ->andWhere('t.category = :categoryid') + ->andWhere('tc.categoryid = :categoryid') ->setParameter('categoryid', $restrictions->categoryId); } if (!empty($restrictions->categoryIds)) { $queryBuilder - ->andWhere('t.category IN (:categoryids)') + ->andWhere('tc.categoryid IN (:categoryids)') ->setParameter('categoryids', $restrictions->categoryIds); } @@ -238,7 +239,7 @@ public function getSubmissionList( if (isset($restrictions->visible)) { $queryBuilder - ->innerJoin('t.category', 'cat') + ->innerJoin('t.categories', 'cat') ->andWhere('cat.visible = true'); } @@ -368,7 +369,6 @@ public function getSubmissionList( $counts['inContest'] = (clone $queryBuilder) ->select('COUNT(s.submitid)') ->join('s.contest', 'c') - ->join('t.category', 'tc') ->andWhere('s.submittime BETWEEN c.starttime AND c.endtime') ->andWhere('tc.visible = true') ->getQuery() diff --git a/webapp/src/Utils/Scoreboard/Scoreboard.php b/webapp/src/Utils/Scoreboard/Scoreboard.php index aaea69c269..8b7031aa5d 100644 --- a/webapp/src/Utils/Scoreboard/Scoreboard.php +++ b/webapp/src/Utils/Scoreboard/Scoreboard.php @@ -178,7 +178,7 @@ protected function calculateScoreboard(): void $previousTeamId = null; foreach ($this->scores as $teamScore) { $teamId = $teamScore->team->getTeamid(); - $teamSortOrder = $teamScore->team->getCategory()->getSortorder(); + $teamSortOrder = $teamScore->team->getSortorder(); // rank, team name, total correct, total time if ($teamSortOrder != $prevSortOrder) { $prevSortOrder = $teamSortOrder; @@ -198,7 +198,7 @@ protected function calculateScoreboard(): void // Keep summary statistics for the bottom row of our table. // The numberOfPoints summary is useful only if they're all 1-point problems. - $sortOrder = $teamScore->team->getCategory()->getSortorder(); + $sortOrder = $teamScore->team->getSortorder(); $this->summary->addNumberOfPoints($sortOrder, $teamScore->numPoints); $teamAffiliation = $teamScore->team->getAffiliation(); if ($teamAffiliation) { @@ -274,7 +274,7 @@ public function getUsedCategories(?array $limitToTeamIds = null): array continue; } - $category = $score->team->getCategory(); + $category = $score->team->getSortOrderCategory(); if ($category) { $usedCategories[$category->getCategoryid()] = $category; } @@ -298,9 +298,9 @@ public function hasCategoryColors(?array $limitToTeamIds = null): bool continue; } - if ($score->team->getCategory() && - $score->team->getCategory()->getColor()) { - $colors[$score->team->getCategory()->getColor()] = 1; + // TODO: category type + if ($score->team->getSortOrderCategory()?->getColor()) { + $colors[$score->team->getSortOrderCategory()->getColor()] = 1; } else { $colors['transparent'] = 1; } @@ -325,16 +325,16 @@ public function isBestInCategory(Team $team, ?array $limitToTeamIds = null): boo continue; } - $categoryId = $score->team->getCategory()->getCategoryid(); - if (!isset($this->bestInCategoryData[$categoryId])) { + $categoryId = $score->team->getSortOrderCategory()?->getCategoryid(); + if ($categoryId && !isset($this->bestInCategoryData[$categoryId])) { $this->bestInCategoryData[$categoryId] = $score->team->getTeamid(); } } } - $categoryId = $team->getCategory()->getCategoryid(); + $categoryId = $team->getSortOrderCategory()?->getCategoryid(); // Only check the scores when the team has points. - if ($this->scores[$team->getTeamid()]->numPoints > 0) { + if ($categoryId && $this->scores[$team->getTeamid()]->numPoints > 0) { // If the rank of this team is equal to the best team for this // category, this team is best in that category. return $this->scores[$this->bestInCategoryData[$categoryId]]->rank === @@ -362,7 +362,7 @@ public function isFastestSubmission(Team $team, ContestProblem $problem): bool if (!$item->isCorrect) { return false; } - $sortorder = $team->getCategory()->getSortorder(); + $sortorder = $team->getSortorder(); $bestTime = $this->summary->getProblem($problem->getProbid())->getBestRuntime($sortorder); return $item->runtime == $bestTime; } diff --git a/webapp/templates/jury/partials/team_form.html.twig b/webapp/templates/jury/partials/team_form.html.twig index 81d3f9e508..fe29a7e3c8 100644 --- a/webapp/templates/jury/partials/team_form.html.twig +++ b/webapp/templates/jury/partials/team_form.html.twig @@ -6,7 +6,7 @@ {{ form_row(form.label) }} {{ form_row(form.name) }} {{ form_row(form.displayName) }} - {{ form_row(form.category) }} + {{ form_row(form.categories) }} {{ form_row(form.publicdescription) }} {{ form_row(form.affiliation) }} {{ form_row(form.penalty) }} diff --git a/webapp/templates/jury/team.html.twig b/webapp/templates/jury/team.html.twig index 8db6ef1f1b..8ced325188 100644 --- a/webapp/templates/jury/team.html.twig +++ b/webapp/templates/jury/team.html.twig @@ -110,14 +110,20 @@
- + diff --git a/webapp/templates/partials/scoreboard_table.html.twig b/webapp/templates/partials/scoreboard_table.html.twig index 6b2da2f314..5d29a83c5d 100644 --- a/webapp/templates/partials/scoreboard_table.html.twig +++ b/webapp/templates/partials/scoreboard_table.html.twig @@ -119,13 +119,13 @@ {% set medalCount = 0 %} {% for score in scores %} {% set classes = [] %} - {% if score.team.category.sortorder != previousSortOrder %} + {% if score.team.sortorder != previousSortOrder %} {% if previousSortOrder != -1 %} {# Output summary of previous sort order #} {% include 'partials/scoreboard_summary.html.twig' with {sortOrder: previousSortOrder} %} {% endif %} {% set classes = classes | merge(['sortorderswitch']) %} - {% set previousSortOrder = score.team.category.sortorder %} + {% set previousSortOrder = score.team.sortorder %} {% set previousTeam = null %} {% endif %} @@ -140,7 +140,8 @@ {% set classes = classes | merge(['scorethisisme']) %} {% set color = '#FFFF99' %} {% else %} - {% set color = score.team.category.color %} + {# TODO: category type #} + {% set color = score.team.sortOrderCategory.color %} {% endif %} {% if enable_ranking %} @@ -227,7 +228,8 @@ {% if usedCategories | length > 1 and scoreboard.bestInCategory(score.team, limitToTeamIds) %} - {{ score.team.category.name }} + {# TODO: category type #} + {{ score.team.sortOrderCategory.name }} {% endif %} {{ score.team.effectiveName }} @@ -384,9 +386,9 @@ {% set medalCount = 0 %} {% for score in scores %} {% set classes = [] %} - {% if score.team.category.sortorder != previousSortOrder %} + {% if score.team.sortorder != previousSortOrder %} {% set classes = classes | merge(['sortorderswitch']) %} - {% set previousSortOrder = score.team.category.sortorder %} + {% set previousSortOrder = score.team.sortorder %} {% set previousTeam = null %} {% endif %} @@ -401,7 +403,8 @@ {% set classes = classes | merge(['scorethisisme']) %} {% set color = '#FFFF99' %} {% else %} - {% set color = score.team.category.color %} + {# TODO: category type #} + {% set color = score.team.sortOrderCategory.color %} {% endif %} {% if enable_ranking %} @@ -475,7 +478,8 @@ {% if false and usedCategories | length > 1 and scoreboard.bestInCategory(score.team, limitToTeamIds) %} - {{ score.team.category.name }} + {# TODO: category type #} + {{ score.team.sortOrdercCategory.name }} {% endif %} {{ score.team.effectiveName }} diff --git a/webapp/templates/partials/team.html.twig b/webapp/templates/partials/team.html.twig index 6bb371d8a0..8e6620aa92 100644 --- a/webapp/templates/partials/team.html.twig +++ b/webapp/templates/partials/team.html.twig @@ -12,8 +12,18 @@ - - + + {% if team.publicdescription is not empty %} diff --git a/webapp/tests/Unit/Controller/API/TeamControllerTest.php b/webapp/tests/Unit/Controller/API/TeamControllerTest.php index 9fb775e9ba..ba036af6e4 100644 --- a/webapp/tests/Unit/Controller/API/TeamControllerTest.php +++ b/webapp/tests/Unit/Controller/API/TeamControllerTest.php @@ -3,6 +3,7 @@ namespace App\Tests\Unit\Controller\API; use App\DataFixtures\Test\AddLocationToTeamFixture; +use App\DataFixtures\Test\CreateTeamWithTwoTeamAffiliationsFixture; use App\Entity\Team; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -25,9 +26,23 @@ class TeamControllerTest extends BaseTestCase 'photo' => null, 'location' => ['description' => 'Utrecht'], ], + 'teamwithtwogroups' => [ + 'group_ids' => ['participants', 'observers'], + 'affiliation' => 'Utrecht University', + 'nationality' => 'NLD', + 'id' => 'teamwithtwogroups', + 'icpc_id' => 'teamwithtwogroups', + 'name' => 'Team with two groups', + 'display_name' => null, + 'members' => null, + 'photo' => null, + ], ]; - protected static array $fixtures = [AddLocationToTeamFixture::class]; + protected static array $fixtures = [ + AddLocationToTeamFixture::class, + CreateTeamWithTwoTeamAffiliationsFixture::class, + ]; protected array $expectedAbsent = ['4242', 'nonexistent']; diff --git a/webapp/tests/Unit/Controller/Jury/TeamControllerTest.php b/webapp/tests/Unit/Controller/Jury/TeamControllerTest.php index 89d30a05fd..eb130cc5a2 100644 --- a/webapp/tests/Unit/Controller/Jury/TeamControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/TeamControllerTest.php @@ -26,7 +26,7 @@ class TeamControllerTest extends JuryControllerTestCase protected static array $addEntitiesCount = ['contests']; protected static array $addEntities = [['name' => 'New Team', 'displayName' => 'New Team Display Name', - 'category' => '3', + 'categories' => ['3', '2'], 'publicdescription' => 'Some members', 'penalty' => '0', 'location' => 'The first room', @@ -37,7 +37,7 @@ class TeamControllerTest extends JuryControllerTestCase 'icpcid' => ''], ['name' => 'Another Team', 'displayName' => 'Another Team Display Name', - 'category' => '1', + 'categories' => ['1'], 'publicdescription' => 'More members', 'penalty' => '20', 'location' => 'Another room', @@ -47,7 +47,7 @@ class TeamControllerTest extends JuryControllerTestCase 'newUsername' => 'linkeduser'], ['name' => 'Team linked to existing user', 'displayName' => 'Third team display name', - 'category' => '1', + 'categories' => ['1'], 'publicdescription' => 'Members of this team', 'penalty' => '0', 'enabled' => '1', diff --git a/webapp/tests/Unit/Integration/QueuetaskIntegrationTest.php b/webapp/tests/Unit/Integration/QueuetaskIntegrationTest.php index e39e47e3e0..b2010e8e97 100644 --- a/webapp/tests/Unit/Integration/QueuetaskIntegrationTest.php +++ b/webapp/tests/Unit/Integration/QueuetaskIntegrationTest.php @@ -105,7 +105,7 @@ protected function setUp(): void $this->teams[$i] = new Team(); $this->teams[$i] ->setName(self::CONTEST_NAME . ' team ' . $i) - ->setCategory($category); + ->addCategory($category); $this->em->persist($this->teams[$i]); } $this->em->flush(); diff --git a/webapp/tests/Unit/Integration/ScoreboardIntegrationTest.php b/webapp/tests/Unit/Integration/ScoreboardIntegrationTest.php index 8ff63433d4..c26f6e840c 100644 --- a/webapp/tests/Unit/Integration/ScoreboardIntegrationTest.php +++ b/webapp/tests/Unit/Integration/ScoreboardIntegrationTest.php @@ -121,7 +121,7 @@ protected function setUp(): void $this->teams[$i] = new Team(); $this->teams[$i] ->setName(self::CONTEST_NAME.' team '.$i) - ->setCategory($category); + ->addCategory($category); $this->em->persist($this->teams[$i]); } diff --git a/webapp/tests/Unit/Service/AwardServiceTest.php b/webapp/tests/Unit/Service/AwardServiceTest.php index b4a33fd7e9..51c6737bf2 100644 --- a/webapp/tests/Unit/Service/AwardServiceTest.php +++ b/webapp/tests/Unit/Service/AwardServiceTest.php @@ -63,7 +63,7 @@ protected function setUp(): void $team = (new Team()) ->setName('Team ' . $teamLetter) ->setExternalid('team_' . $teamLetter) - ->setCategory($category) + ->addCategory($category) ->setAffiliation(); // No affiliation needed $reflectedProblem = new ReflectionClass(Team::class); $teamIdProperty = $reflectedProblem->getProperty('teamid'); diff --git a/webapp/tests/Unit/Service/ImportExportServiceTest.php b/webapp/tests/Unit/Service/ImportExportServiceTest.php index e7cc24a9e5..b5d803d963 100644 --- a/webapp/tests/Unit/Service/ImportExportServiceTest.php +++ b/webapp/tests/Unit/Service/ImportExportServiceTest.php @@ -654,8 +654,8 @@ protected function testImportAccounts(int $importCount, ?string $message, bool $ self::assertEquals($data['team']['name'], $team->getName()); } if (isset($data['team']['category'])) { - self::assertNotNull($team->getCategory()); - self::assertEquals($data['team']['category'], $team->getCategory()->getName()); + self::assertNotNull($team->getCategories()->first()); + self::assertEquals($data['team']['category'], $team->getCategories()->first()->getName()); } if (isset($data['team']['members'])) { self::assertEquals($data['team']['members'], $team->getPublicDescription()); @@ -750,7 +750,7 @@ public function testImportTeamsTsv(): void self::assertEquals($data['label'], $team->getLabel()); self::assertEquals($data['name'], $team->getName()); self::assertNull($team->getLocation()); - self::assertEquals($data['category']['externalid'], $team->getCategory()->getExternalid()); + self::assertEquals($data['category']['externalid'], $team->getCategories()->first()->getExternalid()); self::assertEquals($data['affiliation']['externalid'], $team->getAffiliation()->getExternalid()); self::assertEquals($data['affiliation']['shortname'], $team->getAffiliation()->getShortname()); self::assertEquals($data['affiliation']['name'], $team->getAffiliation()->getName()); @@ -782,9 +782,20 @@ public function testImportTeamsJson(): void "id": "13", "icpc_id": "123456", "label": "0", - "group_ids": ["26"], + "group_ids": ["24", "26"], "name": "Team with label 0", "organization_id": "INST-44" +}, { + "id": "14", + "icpc_id": "112233", + "group_ids": [], + "name": "Team with empty groups", + "organization_id": "INST-45" +}, { + "id": "15", + "icpc_id": "445566", + "name": "Team with no groups", + "organization_id": "INST-46" }] EOF; @@ -795,36 +806,40 @@ public function testImportTeamsJson(): void 'label' => 'team1', 'name' => '¡i¡i¡', 'location' => 'AUD 10', - 'category' => [ - 'externalid' => '24', - ], - 'affiliation' => [ - 'externalid' => 'INST-42', - ], + 'categories' => ['24'], + 'affiliation' => 'INST-42', ], [ 'externalid' => '12', 'icpcid' => '447837', 'label' => null, 'name' => 'Pleading not FAUlty', 'location' => null, - 'category' => [ - 'externalid' => '25', - ], - 'affiliation' => [ - 'externalid' => 'INST-43', - ], + 'categories' => ['25'], + 'affiliation' => 'INST-43', ], [ 'externalid' => '13', 'icpcid' => '123456', 'label' => '0', 'name' => 'Team with label 0', 'location' => null, - 'category' => [ - 'externalid' => '26', - ], - 'affiliation' => [ - 'externalid' => 'INST-44', - ], + 'categories' => ['24', '26'], + 'affiliation' => 'INST-44', + ], [ + 'externalid' => '14', + 'icpcid' => '112233', + 'label' => null, + 'name' => 'Team with empty groups', + 'location' => null, + 'categories' => [], + 'affiliation' => 'INST-45', + ], [ + 'externalid' => '15', + 'icpcid' => '445566', + 'label' => null, + 'name' => 'Team with no groups', + 'location' => null, + 'categories' => [], + 'affiliation' => 'INST-46', ], ]; @@ -850,8 +865,9 @@ public function testImportTeamsJson(): void self::assertEquals($data['label'], $team->getLabel()); self::assertEquals($data['location'], $team->getLocation()); self::assertEquals($data['name'], $team->getName()); - self::assertEquals($data['category']['externalid'], $team->getCategory()->getExternalid()); - self::assertEquals($data['affiliation']['externalid'], $team->getAffiliation()->getExternalid()); + $categoryIds = $team->getCategories()->map(fn(TeamCategory $category) => $category->getExternalid())->toArray(); + self::assertEquals($data['categories'], $categoryIds); + self::assertEquals($data['affiliation'], $team->getAffiliation()->getExternalid()); } } @@ -1232,7 +1248,7 @@ public function testGetResultsData(bool $full, bool $honors, string $dataSet, st ->setIcpcid($teamData['icpc_id']) ->setName($teamData['name']) ->setDisplayName($teamData['display_name']) - ->setCategory($groupsById[$teamData['group_ids'][0]]); + ->addCategory($groupsById[$teamData['group_ids'][0]]); $em->persist($team); $em->flush(); $teamsById[$team->getExternalid()] = $team; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy

CategoryCategories - {% if team.category %} - - {{ team.category.name }} - - {% else %} + {% if team.categories.empty %} - + {% else %} + {% endif %}
{{ team.effectiveName }}
Category{{ team.category.name }}Categories + {% if team.categories.empty %} + - + {% else %} +
    + {% for category in team.categories %} +
  • {{ category.name }}
  • + {% endfor %} +
+ {% endif %} +