diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ebaabc10d3..d20d5914f8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -10,41 +10,6 @@ parameters: count: 1 path: webapp/src/Controller/API/AbstractRestController.php - - - message: "#^Method App\\\\Controller\\\\API\\\\AccessController\\:\\:getStatusAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/AccessController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\AwardsController\\:\\:getAwardsData\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/AwardsController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\AwardsController\\:\\:listAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/AwardsController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\AwardsController\\:\\:singleAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/AwardsController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\BalloonController\\:\\:listAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/BalloonController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\ContestController\\:\\:getContestStateAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/ContestController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\ContestController\\:\\:getStatusAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/ContestController.php - - message: "#^Method App\\\\Controller\\\\API\\\\GeneralInfoController\\:\\:addProblemAction\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -55,21 +20,6 @@ parameters: count: 1 path: webapp/src/Controller/API/GeneralInfoController.php - - - message: "#^Method App\\\\Controller\\\\API\\\\GeneralInfoController\\:\\:getInfoAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/GeneralInfoController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\GeneralInfoController\\:\\:getStatusAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/GeneralInfoController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\GeneralInfoController\\:\\:getVersionAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/GeneralInfoController.php - - message: "#^Method App\\\\Controller\\\\API\\\\GeneralInfoController\\:\\:updateConfigurationAction\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -85,76 +35,16 @@ parameters: count: 1 path: webapp/src/Controller/API/JudgehostController.php - - - message: "#^Method App\\\\Controller\\\\API\\\\JudgehostController\\:\\:getExecutableFiles\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/JudgehostController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\JudgehostController\\:\\:getFilesAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/JudgehostController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\JudgehostController\\:\\:getJudgeTasksAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/JudgehostController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\JudgehostController\\:\\:getJudgehostsAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/JudgehostController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\JudgehostController\\:\\:getJudgetasks\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/JudgehostController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\JudgehostController\\:\\:getSourceFiles\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/JudgehostController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\JudgehostController\\:\\:getTestcaseFiles\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/JudgehostController.php - - message: "#^Method App\\\\Controller\\\\API\\\\JudgehostController\\:\\:getVersionCommands\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: webapp/src/Controller/API/JudgehostController.php - - - message: "#^Method App\\\\Controller\\\\API\\\\JudgehostController\\:\\:serializeJudgeTasks\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/JudgehostController.php - - message: "#^Method App\\\\Controller\\\\API\\\\JudgehostController\\:\\:updateJudgeHostAction\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: webapp/src/Controller/API/JudgehostController.php - - - message: "#^Method App\\\\Controller\\\\API\\\\JudgementTypeController\\:\\:getJudgementTypes\\(\\) has parameter \\$filteredOn with no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/JudgementTypeController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\JudgementTypeController\\:\\:getJudgementTypes\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/JudgementTypeController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\JudgementTypeController\\:\\:listAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/JudgementTypeController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\JudgementTypeController\\:\\:singleAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/JudgementTypeController.php - - message: "#^Method App\\\\Controller\\\\API\\\\ProblemController\\:\\:addProblemAction\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -169,17 +59,6 @@ parameters: message: "#^Method App\\\\Controller\\\\API\\\\ProblemController\\:\\:transformObject\\(\\) has parameter \\$object with no value type specified in iterable type array\\.$#" count: 1 path: webapp/src/Controller/API/ProblemController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\ScoreboardController\\:\\:getScoreboardAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/ScoreboardController.php - - - - message: "#^Method App\\\\Controller\\\\API\\\\SubmissionController\\:\\:getSubmissionSourceCodeAction\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Controller/API/SubmissionController.php - - message: "#^Method App\\\\FosRestBundle\\\\FlattenExceptionHandler\\:\\:serializeToJson\\(\\) has parameter \\$type with no value type specified in iterable type array\\.$#" count: 1 @@ -191,21 +70,7 @@ parameters: path: webapp/src/FosRestBundle/FlattenExceptionHandler.php - - message: "#^Method App\\\\Helpers\\\\OrdinalArray\\:\\:__construct\\(\\) has parameter \\$items with no value type specified in iterable type Traversable\\.$#" - count: 1 - path: webapp/src/Helpers/OrdinalArray.php - - - message: "#^Method App\\\\Helpers\\\\OrdinalArray\\:\\:__construct\\(\\) has parameter \\$items with no value type specified in iterable type array\\.$#" - count: 1 - path: webapp/src/Helpers/OrdinalArray.php - - - - message: "#^Method App\\\\Helpers\\\\OrdinalArray\\:\\:__construct\\(\\) has parameter \\$items with no value type specified in iterable type array\\|Traversable\\.$#" - count: 1 - path: webapp/src/Helpers/OrdinalArray.php - - - message: "#^Method App\\\\Service\\\\ExternalContestSourceService\\:\\:addOrUpdateWarning\\(\\) has parameter \\$content with no value type specified in iterable type array\\.$#" count: 1 path: webapp/src/Service/ExternalContestSourceService.php diff --git a/webapp/config/packages/nelmio_api_doc.yaml b/webapp/config/packages/nelmio_api_doc.yaml index 7d6df2b8f0..337c7d731b 100644 --- a/webapp/config/packages/nelmio_api_doc.yaml +++ b/webapp/config/packages/nelmio_api_doc.yaml @@ -125,208 +125,6 @@ nelmio_api_doc: schema: type: string schemas: - ImageList: - type: array - nullable: true - items: - type: object - properties: - href: - type: string - mime: - type: string - hash: - type: string - filename: - type: string - width: - type: integer - height: - type: integer - Banner: - properties: - banner: - $ref: "#/components/schemas/ImageList" - Logo: - properties: - logo: - $ref: "#/components/schemas/ImageList" - Photo: - properties: - photo: - $ref: "#/components/schemas/ImageList" - ContestProblem: - properties: - id: - type: string - label: - type: string - short_name: - type: string - name: - type: string - ordinal: - type: integer - rgb: - type: string - color: - type: string - time_limit: - type: number - format: float - test_data_count: - type: integer - statement: - $ref: "#/components/schemas/StatementList" - Files: - properties: - files: - $ref: "#/components/schemas/ArchiveList" - ArchiveList: - type: array - items: - type: object - properties: - href: - type: string - mime: - type: string - StatementList: - type: array - items: - type: object - properties: - href: - type: string - mime: - type: string - filename: - type: string - SourceCodeList: - type: array - items: - type: object - properties: - id: - type: string - submission_id: - type: string - filename: - type: string - description: Original file name - source: - type: string - description: Base64-encoded source code - JudgementType: - type: object - properties: - id: - type: string - name: - type: string - penalty: - type: boolean - solved: - type: boolean - JudgementExtraFields: - properties: - judgement_type_id: - type: string - nullable: true - judgehost: - type: string - max_run_time: - type: number - format: float - nullable: true - RunExtraFields: - properties: - judgement_type_id: - type: string - Scoreboard: - type: object - properties: - event_id: - type: string - time: - type: string - contest_time: - type: string - state: - $ref: "#/components/schemas/ContestState" - rows: - type: array - items: - type: object - properties: - rank: - type: integer - team_id: - type: string - score: - type: object - properties: - num_solved: - type: integer - total_time: - type: integer - problems: - type: array - items: - type: object - properties: - label: - type: string - problem_id: - type: string - num_judged: - type: integer - num_pending: - type: integer - solved: - type: boolean - time: - type: integer - first_to_solve: - type: boolean - ContestState: - type: object - properties: - started: - type: string - nullable: true - format: date-time - ended: - type: string - nullable: true - format: date-time - frozen: - type: string - nullable: true - format: date-time - thawed: - type: string - nullable: true - format: date-time - finalized: - type: string - nullable: true - format: date-time - end_of_updates: - type: string - nullable: true - format: date-time - Award: - type: object - properties: - id: - type: string - citation: - type: string - team_ids: - type: array - items: - type: string AddUser: required: - username @@ -356,40 +154,6 @@ nelmio_api_doc: type: array items: type: string - Balloon: - type: object - properties: - balloonid: - type: integer - time: - type: string - problem: - type: string - contestproblem: - $ref: "#/components/schemas/ContestProblem" - team: - type: string - teamid: - type: integer - location: - type: string - nullable: true - affiliation: - type: string - nullable: true - affiliationid: - type: integer - nullable: true - category: - type: string - total: - type: array - items: - $ref: "#/components/schemas/ContestProblem" - awards: - type: string - done: - type: boolean ClarificationPost: type: object required: [text] diff --git a/webapp/migrations/Version20200111104415.php b/webapp/migrations/Version20200111104415.php index 3f118c7ea4..861f19d6e9 100644 --- a/webapp/migrations/Version20200111104415.php +++ b/webapp/migrations/Version20200111104415.php @@ -34,7 +34,7 @@ public function up(Schema $schema): void $allConfig = $configService->all(); foreach ($allConfig as $name => $value) { - if ($value == ($specs[$name]['default_value'] ?? null)) { + if ($value == ($specs[$name]->default_value ?? null)) { $this->addSql( 'DELETE FROM configuration WHERE name = :name', ['name' => $name] diff --git a/webapp/src/Controller/API/AccessController.php b/webapp/src/Controller/API/AccessController.php index f79bc0044e..4cd8203b87 100644 --- a/webapp/src/Controller/API/AccessController.php +++ b/webapp/src/Controller/API/AccessController.php @@ -2,11 +2,14 @@ namespace App\Controller\API; +use App\DataTransferObject\Access; +use App\DataTransferObject\AccessEndpoint; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; use Exception; use FOS\RestBundle\Controller\Annotations as Rest; +use Nelmio\ApiDocBundle\Annotation\Model; use OpenApi\Attributes as OA; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\HttpFoundation\Request; @@ -26,37 +29,17 @@ class AccessController extends AbstractRestController * * @throws NoResultException * @throws NonUniqueResultException + * + * @return Access */ #[IsGranted('ROLE_API_READER')] #[Rest\Get('')] #[OA\Response( response: 200, description: 'Access information for the given contest', - content: new OA\JsonContent( - properties: [ - new OA\Property( - property: 'capabilities', - type: 'array', - items: new OA\Items(type: 'string') - ), - new OA\Property( - property: 'endpoints', - type: 'array', - items: new OA\Items( - properties: [ - new OA\Property(property: 'type', type: 'string'), - new OA\Property( - property: 'properties', - type: 'array', - items: new OA\Items(type: 'string') - ) - ], - type: 'object')) - ], - type: 'object' - ) + content: new OA\JsonContent(ref: new Model(type: Access::class)) )] - public function getStatusAction(Request $request): array + public function getStatusAction(Request $request): Access { // Get the contest ID to make sure the contest exists $this->getContestId($request); @@ -109,12 +92,12 @@ public function getStatusAction(Request $request): array $capabilities[] = 'admin_clar'; } - return [ - 'capabilities' => $capabilities, - 'endpoints' => [ - [ - 'type' => 'contest', - 'properties' => [ + return new Access( + capabilities: $capabilities, + endpoints: [ + new AccessEndpoint( + type: 'contest', + properties: [ 'id', 'name', 'formal_name', @@ -123,19 +106,19 @@ public function getStatusAction(Request $request): array 'scoreboard_freeze_duration', 'penalty_time', ], - ], - [ - 'type' => 'judgement-types', - 'properties' => [ + ), + new AccessEndpoint( + type: 'judgement-types', + properties: [ 'id', 'name', 'penalty', 'solved', ], - ], - [ - 'type' => 'languages', - 'properties' => [ + ), + new AccessEndpoint( + type: 'languages', + properties: [ 'id', 'name', 'entry_point_required', @@ -145,10 +128,10 @@ public function getStatusAction(Request $request): array // 'compiler.command', // 'runner.command', ], - ], - [ - 'type' => 'problems', - 'properties' => [ + ), + new AccessEndpoint( + type: 'problems', + properties: [ 'id', 'label', 'name', @@ -159,23 +142,23 @@ public function getStatusAction(Request $request): array 'test_data_count', 'statement', ], - ], - [ - 'type' => 'groups', - 'properties' => [ + ), + new AccessEndpoint( + type: 'groups', + properties: [ 'id', 'icpc_id', 'name', 'hidden', ], - ], - [ - 'type' => 'organizations', - 'properties' => $organizationProperties, - ], - [ - 'type' => 'teams', - 'properties' => [ + ), + new AccessEndpoint( + type: 'organizations', + properties: $organizationProperties, + ), + new AccessEndpoint( + type: 'teams', + properties: [ 'id', 'icpc_id', 'name', @@ -183,10 +166,10 @@ public function getStatusAction(Request $request): array 'organization_id', 'group_ids', ] - ], - [ - 'type' => 'state', - 'properties' => [ + ), + new AccessEndpoint( + type: 'state', + properties: [ 'started', 'frozen', 'ended', @@ -194,14 +177,14 @@ public function getStatusAction(Request $request): array 'finalized', 'end_of_updates', ], - ], - [ - 'type' => 'submissions', - 'properties' => $submissionsProperties, - ], - [ - 'type' => 'judgements', - 'properties' => [ + ), + new AccessEndpoint( + type: 'submissions', + properties: $submissionsProperties, + ), + new AccessEndpoint( + type: 'judgements', + properties: [ 'id', 'submission_id', 'judgement_type_id', @@ -211,10 +194,10 @@ public function getStatusAction(Request $request): array 'end_contest_time', 'max_run_time', ], - ], - [ - 'type' => 'runs', - 'properties' => [ + ), + new AccessEndpoint( + type: 'runs', + properties: [ 'id', 'judgement_id', 'ordinal', @@ -223,17 +206,17 @@ public function getStatusAction(Request $request): array 'contest_time', 'run_time', ], - ], - [ - 'type' => 'awards', - 'properties' => [ + ), + new AccessEndpoint( + type: 'awards', + properties: [ 'id', 'citation', 'team_ids', ], - ], + ), ], - ]; + ); } protected function getQueryBuilder(Request $request): QueryBuilder diff --git a/webapp/src/Controller/API/AwardsController.php b/webapp/src/Controller/API/AwardsController.php index 9e5c287f53..45c90b5a57 100644 --- a/webapp/src/Controller/API/AwardsController.php +++ b/webapp/src/Controller/API/AwardsController.php @@ -2,16 +2,19 @@ namespace App\Controller\API; +use App\DataTransferObject\Award; use App\Entity\Contest; use App\Service\AwardService; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; use App\Service\EventLogService; use App\Service\ScoreboardService; +use App\Utils\Scoreboard\Scoreboard; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; use Exception; use FOS\RestBundle\Controller\Annotations as Rest; +use Nelmio\ApiDocBundle\Annotation\Model; use OpenApi\Attributes as OA; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -41,6 +44,8 @@ public function __construct( * Get all the awards standings for this contest. * * @throws Exception + * + * @return Award[] */ #[Rest\Get('')] #[OA\Response( @@ -48,12 +53,13 @@ public function __construct( description: 'Returns the current teams qualifying for each award', content: new OA\JsonContent( type: 'array', - items: new OA\Items(ref: '#/components/schemas/Award') + items: new OA\Items(ref: new Model(type: Award::class)) ) )] - public function listAction(Request $request): ?array + public function listAction(Request $request): array { - return $this->getAwardsData($request); + [$contest, $scoreboard] = $this->getContestAndScoreboard($request); + return $this->awards->getAwards($contest, $scoreboard); } /** @@ -65,12 +71,13 @@ public function listAction(Request $request): ?array #[OA\Response( response: 200, description: 'Returns the award for this contest', - content: new OA\JsonContent(ref: '#/components/schemas/Award') + content: new OA\JsonContent(ref: new Model(type: Award::class)) )] #[OA\Parameter(ref: '#/components/parameters/id')] - public function singleAction(Request $request, string $id): array + public function singleAction(Request $request, string $id): Award { - $award = $this->getAwardsData($request, $id); + [$contest, $scoreboard] = $this->getContestAndScoreboard($request); + $award = $this->awards->getAward($contest, $scoreboard, $id); if ($award === null) { throw new NotFoundHttpException(sprintf('Object with ID \'%s\' not found', $id)); @@ -80,9 +87,9 @@ public function singleAction(Request $request, string $id): array } /** - * Get the awards data for the given request and optional award ID. + * @return array{Contest, Scoreboard} */ - protected function getAwardsData(Request $request, string $requestedType = null): ?array + protected function getContestAndScoreboard(Request $request): array { $public = !$this->dj->checkrole('api_reader'); if ($this->dj->checkrole('api_reader') && $request->query->has('public')) { @@ -97,7 +104,7 @@ protected function getAwardsData(Request $request, string $requestedType = null) } $scoreboard = $this->scoreboardService->getScoreboard($contest, !$public, null, true); - return $this->awards->getAwards($contest, $scoreboard, $requestedType); + return [$contest, $scoreboard]; } protected function getQueryBuilder(Request $request): QueryBuilder diff --git a/webapp/src/Controller/API/BalloonController.php b/webapp/src/Controller/API/BalloonController.php index 57f7eadccb..1c5d1ad4e1 100644 --- a/webapp/src/Controller/API/BalloonController.php +++ b/webapp/src/Controller/API/BalloonController.php @@ -2,6 +2,7 @@ namespace App\Controller\API; +use App\DataTransferObject\Balloon; use App\Entity\Contest; use App\Entity\Team; use App\Service\BalloonService; @@ -9,6 +10,7 @@ use Doctrine\ORM\QueryBuilder; use Exception; use FOS\RestBundle\Controller\Annotations as Rest; +use Nelmio\ApiDocBundle\Annotation\Model; use OpenApi\Attributes as OA; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpFoundation\Request; @@ -29,6 +31,7 @@ class BalloonController extends AbstractRestController * Get all the balloons for this contest. * * @throws NonUniqueResultException + * @return Balloon[] */ #[Rest\Get('')] #[OA\Response( @@ -36,7 +39,7 @@ class BalloonController extends AbstractRestController description: 'Returns the balloons for this contest.', content: new OA\JsonContent( type: 'array', - items: new OA\Items(ref: '#/components/schemas/Balloon') + items: new OA\Items(ref: new Model(type: Balloon::class)) ) )] #[OA\Parameter( @@ -54,13 +57,30 @@ public function listAction( /** @var Contest $contest */ $contest = $this->em->getRepository(Contest::class)->find($this->getContestId($request)); $balloonsData = $balloonService->collectBalloonTable($contest, $todo); - foreach ($balloonsData as &$b) { + $balloons = []; + foreach ($balloonsData as $b) { /** @var Team $team */ $team = $b['data']['team']; - $b['data']['team'] = "t" . $team->getTeamid() . ": " . $team->getEffectiveName(); + $teamName = "t" . $team->getTeamid() . ": " . $team->getEffectiveName(); + $balloons[] = new Balloon( + balloonid: $b['data']['balloonid'], + time: $b['data']['time'], + problem: $b['data']['problem'], + contestproblem: $b['data']['contestproblem'], + team: $teamName, + teamid: $team->getTeamid(), + location: $b['data']['location'], + affiliation: $b['data']['affiliation'], + affiliationid: $b['data']['affiliationid'], + category: $b['data']['category'], + categoryid: $b['data']['categoryid'], + total: $b['data']['total'], + awards: $b['data']['awards'], + done: $b['data']['done'], + ); } unset($b); - return array_column($balloonsData, 'data'); + return $balloons; } /** diff --git a/webapp/src/Controller/API/ContestController.php b/webapp/src/Controller/API/ContestController.php index 8709cec921..8cfc621fc9 100644 --- a/webapp/src/Controller/API/ContestController.php +++ b/webapp/src/Controller/API/ContestController.php @@ -2,6 +2,8 @@ namespace App\Controller\API; +use App\DataTransferObject\ContestState; +use App\DataTransferObject\ContestStatus; use App\Entity\Contest; use App\Entity\ContestProblem; use App\Entity\Event; @@ -122,12 +124,7 @@ public function addContestAction(Request $request): string description: 'Returns all contests visible to the user (all contests for privileged users, active contests otherwise)', content: new OA\JsonContent( type: 'array', - items: new OA\Items( - allOf: [ - new OA\Schema(ref: new Model(type: Contest::class)), - new OA\Schema(ref: '#/components/schemas/Banner'), - ] - ) + items: new OA\Items(ref: new Model(type: Contest::class)) ) )] #[OA\Parameter(ref: '#/components/parameters/idlist')] @@ -150,12 +147,7 @@ public function listAction(Request $request): Response #[OA\Response( response: 200, description: 'Returns the given contest', - content: new OA\JsonContent( - allOf: [ - new OA\Schema(ref: new Model(type: Contest::class)), - new OA\Schema(ref: '#/components/schemas/Banner'), - ] - ) + content: new OA\JsonContent(ref: new Model(type: Contest::class)) )] #[OA\Parameter(ref: '#/components/parameters/cid')] public function singleAction(Request $request, string $cid): Response @@ -312,10 +304,7 @@ public function setBannerAction(Request $request, string $cid, ValidatorInterfac #[OA\Response( response: 200, description: 'Contest start time changed successfully', - content: new OA\JsonContent(allOf: [ - new OA\Schema(ref: new Model(type: Contest::class)), - new OA\Schema(ref: '#/components/schemas/Banner'), - ]) + content: new OA\JsonContent(ref: new Model(type: Contest::class)) )] public function changeStartTimeAction( Request $request, @@ -444,9 +433,9 @@ public function getContestYamlAction(Request $request, string $cid): StreamedRes #[OA\Response( response: 200, description: 'The contest state', - content: new OA\JsonContent(ref: '#/components/schemas/ContestState') + content: new OA\JsonContent(ref: new Model(type: ContestState::class)) )] - public function getContestStateAction(Request $request, string $cid): ?array + public function getContestStateAction(Request $request, string $cid): ContestState { $contest = $this->getContestWithId($request, $cid); $inactiveAllowed = $this->isGranted('ROLE_API_READER'); @@ -766,25 +755,9 @@ public function getEventFeedAction( #[OA\Response( response: 200, description: 'General status information for the given contest', - content: new OA\JsonContent( - properties: [ - new OA\Property( - property: 'num_submissions', - type: 'integer' - ), - new OA\Property( - property: 'num_queued', - type: 'integer' - ), - new OA\Property( - property: 'num_judging', - type: 'integer' - ), - ], - type: 'object' - ) + content: new OA\JsonContent(ref: new Model(type: ContestStatus::class)) )] - public function getStatusAction(Request $request, string $cid): array + public function getStatusAction(Request $request, string $cid): ContestStatus { return $this->dj->getContestStats($this->getContestWithId($request, $cid)); } diff --git a/webapp/src/Controller/API/GeneralInfoController.php b/webapp/src/Controller/API/GeneralInfoController.php index d2555439c8..838a3735aa 100644 --- a/webapp/src/Controller/API/GeneralInfoController.php +++ b/webapp/src/Controller/API/GeneralInfoController.php @@ -2,6 +2,10 @@ namespace App\Controller\API; +use App\DataTransferObject\ApiInfo; +use App\DataTransferObject\ApiVersion; +use App\DataTransferObject\DomJudgeApiInfo; +use App\DataTransferObject\ExtendedContestStatus; use App\Entity\Contest; use App\Entity\User; use App\Service\CheckConfigService; @@ -16,6 +20,7 @@ use FOS\RestBundle\Controller\AbstractFOSRestController; use FOS\RestBundle\Controller\Annotations as Rest; use InvalidArgumentException; +use JMS\Serializer\SerializerInterface; use Nelmio\ApiDocBundle\Annotation\Model; use OpenApi\Attributes as OA; use Psr\Log\LoggerInterface; @@ -59,14 +64,11 @@ public function __construct( #[OA\Response( response: 200, description: 'The current API version information', - content: new OA\JsonContent( - properties: [new OA\Property(property: 'api_version', type: 'integer')], - type: 'object' - ) + content: new OA\JsonContent(ref: new Model(type: ApiVersion::class)) )] - public function getVersionAction(): array + public function getVersionAction(): ApiVersion { - return ['api_version' => static::API_VERSION]; + return new ApiVersion(static::API_VERSION); } /** @@ -77,74 +79,36 @@ public function getVersionAction(): array #[OA\Response( response: 200, description: 'Information about the API and DOMjudge', - content: new OA\JsonContent( - properties: [ - new OA\Property( - property: 'version', - description: 'Version of the CCS Specs Contest API the API adheres to', - type: 'string' - ), - new OA\Property( - property: 'version_url', - description: 'URL with the specification of the Contest API', - type: 'string' - ), - new OA\Property( - property: 'domjudge', - description: 'DOMjudge information', - properties: [ - new OA\Property( - property: 'api_version', - description: 'Version of the API', - type: 'integer' - ), - new OA\Property( - property: 'domjudge_version', - description: 'Version of DOMjudge', - type: 'string' - ), - new OA\Property( - property: 'environment', - description: 'Environment DOMjudge is running in', - type: 'string' - ), - new OA\Property( - property: 'doc_url', - description: 'URL to DOMjudge API docs', - type: 'string' - ), - ], - type: 'object' - ), - ], - type: 'object' - ) + content: new OA\JsonContent(ref: new Model(type: ApiInfo::class)) )] public function getInfoAction( #[MapQueryParameter] bool $strict = false - ): array { - $result = [ - 'version' => self::CCS_SPEC_API_VERSION, - 'version_url' => self::CCS_SPEC_API_URL, - 'name' => 'DOMjudge', - ]; + ): ApiInfo { + $domjudge = null; if (!$strict) { - $result['domjudge'] = [ - 'api_version' => static::API_VERSION, - 'version' => $this->getParameter('domjudge.version'), - 'environment' => $this->getParameter('kernel.environment'), - 'doc_url' => $this->router->generate('app.swagger_ui', [], RouterInterface::ABSOLUTE_URL), - ]; + $domjudge = new DomJudgeApiInfo( + apiversion: static::API_VERSION, + version: $this->getParameter('domjudge.version'), + environment: $this->getParameter('kernel.environment'), + docUrl: $this->router->generate('app.swagger_ui', [], RouterInterface::ABSOLUTE_URL) + ); } - return $result; + return new ApiInfo( + version: self::CCS_SPEC_API_VERSION, + versionUrl: self::CCS_SPEC_API_URL, + name: 'DOMjudge', + domjudge: $domjudge + ); } /** * Get general status information * @throws NoResultException * @throws NonUniqueResultException + * + * @return ExtendedContestStatus[] */ #[IsGranted('ROLE_API_READER')] #[Rest\Get('/status')] @@ -153,15 +117,7 @@ public function getInfoAction( description: 'General status information for the currently active contests', content: new OA\JsonContent( type: 'array', - items: new OA\Items( - properties: [ - new OA\Property(property: 'cid', type: 'integer'), - new OA\Property(property: 'num_submissions', type: 'integer'), - new OA\Property(property: 'num_queued', type: 'integer'), - new OA\Property(property: 'num_judging', type: 'integer'), - ], - type: 'object' - ) + items: new OA\Items(ref: new Model(type: ExtendedContestStatus::class)) ) )] public function getStatusAction(): array @@ -174,10 +130,11 @@ public function getStatusAction(): array $result = []; foreach ($contests as $contest) { $contestStats = $this->dj->getContestStats($contest); - $contestStats['cid'] = + $result[] = new ExtendedContestStatus( $this->config->get('data_source') === DOMJudgeService::DATA_SOURCE_LOCAL - ? $contest->getCid() : $contest->getExternalid(); - $result[] = $contestStats; + ? (string)$contest->getCid() : $contest->getExternalid(), + $contestStats + ); } return $result; @@ -296,7 +253,7 @@ public function updateConfigurationAction(Request $request): JsonResponse|array description: 'Result of the various checks performed, errors found.', content: new OA\JsonContent(type: 'object') )] - public function getConfigCheckAction(): JsonResponse + public function getConfigCheckAction(SerializerInterface $serializer): Response { $result = $this->checkConfigService->runAll(); @@ -308,18 +265,17 @@ public function getConfigCheckAction(): JsonResponse $aggregate = 200; foreach ($result as &$cat) { foreach ($cat as &$test) { - if ($test['result'] == 'E') { + if ($test->result == 'E') { $aggregate = max($aggregate, 260); - } elseif ($test['result'] == 'W') { + } elseif ($test->result == 'W') { $aggregate = max($aggregate, 250); } - unset($test['escape']); } unset($test); } unset($cat); - return $this->json($result, $aggregate); + return new Response($serializer->serialize($result, 'json'), $aggregate); } /** diff --git a/webapp/src/Controller/API/JudgehostController.php b/webapp/src/Controller/API/JudgehostController.php index 714847bd68..00e4806725 100644 --- a/webapp/src/Controller/API/JudgehostController.php +++ b/webapp/src/Controller/API/JudgehostController.php @@ -2,6 +2,7 @@ namespace App\Controller\API; +use App\DataTransferObject\JudgehostFile; use App\Doctrine\DBAL\Types\JudgeTaskType; use App\Entity\Contest; use App\Entity\DebugPackage; @@ -73,6 +74,8 @@ public function __construct( /** * Get judgehosts. + * + * @return Judgehost[] */ #[IsGranted('ROLE_JURY')] #[Rest\Get('')] @@ -1152,13 +1155,14 @@ private function maybeUpdateActiveJudging(Judging $judging): void /** * Get files for a given type and id. * @throws NonUniqueResultException + * @return JudgehostFile[] */ #[IsGranted(new Expression("is_granted('ROLE_JURY') or is_granted('ROLE_JUDGEHOST')"))] #[Rest\Get('/get_files/{type}/{id<\d+>}')] #[OA\Response( response: 200, description: 'The files for the submission, testcase or script.', - content: new OA\JsonContent(ref: '#/components/schemas/SourceCodeList') + content: new OA\JsonContent(ref: new Model(type: JudgehostFile::class)) )] #[OA\Parameter(ref: '#/components/parameters/id')] public function getFilesAction( @@ -1336,6 +1340,9 @@ public function checkVersions(Request $request, string $judgetaskid): array return []; } + /** + * @return JudgehostFile[] + */ private function getSourceFiles(string $id): array { $queryBuilder = $this->em->createQueryBuilder() @@ -1354,14 +1361,17 @@ private function getSourceFiles(string $id): array $result = []; foreach ($files as $file) { - $result[] = [ - 'filename' => $file->getFilename(), - 'content' => base64_encode($file->getSourcecode()), - ]; + $result[] = new JudgehostFile( + filename: $file->getFilename(), + content: base64_encode($file->getSourcecode()), + ); } return $result; } + /** + * @return JudgehostFile[] + */ private function getExecutableFiles(string $id): array { $queryBuilder = $this->em->createQueryBuilder() @@ -1380,15 +1390,18 @@ private function getExecutableFiles(string $id): array $result = []; foreach ($files as $file) { - $result[] = [ - 'filename' => $file->getFilename(), - 'content' => base64_encode($file->getFileContent()), - 'is_executable' => $file->isExecutable(), - ]; + $result[] = new JudgehostFile( + filename: $file->getFilename(), + content: base64_encode($file->getFileContent()), + isExecutable: $file->isExecutable(), + ); } return $result; } + /** + * @return JudgehostFile[] + */ private function getTestcaseFiles(string $id): array { $queryBuilder = $this->em->createQueryBuilder() @@ -1406,16 +1419,18 @@ private function getTestcaseFiles(string $id): array $result = []; foreach (['input', 'output'] as $k) { - $result[] = [ - 'filename' => $k, - 'content' => base64_encode($inout[$k]), - ]; + $result[] = new JudgehostFile( + filename: $k, + content: base64_encode($inout[$k]), + ); } return $result; } /** * Fetch work tasks. + * + * @return JudgeTask[] */ #[IsGranted(new Expression("is_granted('ROLE_JUDGEHOST')"))] #[Rest\Post('/fetch-work')] @@ -1623,6 +1638,7 @@ public function getJudgeTasksAction(Request $request): array /** * @param JudgeTask[] $judgeTasks + * @return JudgeTask[] * @throws Exception */ private function serializeJudgeTasks(array $judgeTasks, Judgehost $judgehost): array @@ -1708,6 +1724,9 @@ private function serializeJudgeTasks(array $judgeTasks, Judgehost $judgehost): a return $partialJudgeTasks; } + /** + * @return JudgeTask[]|null + */ private function getJudgetasks(string|int|null $jobId, int $max_batchsize, Judgehost $judgehost): ?array { if ($jobId === null) { diff --git a/webapp/src/Controller/API/JudgementController.php b/webapp/src/Controller/API/JudgementController.php index 770eac1d94..a0f3cd7b88 100644 --- a/webapp/src/Controller/API/JudgementController.php +++ b/webapp/src/Controller/API/JudgementController.php @@ -2,8 +2,8 @@ namespace App\Controller\API; +use App\DataTransferObject\JudgingWrapper; use App\Entity\Judging; -use App\Helpers\JudgingWrapper; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; use App\Service\EventLogService; @@ -58,12 +58,7 @@ public function __construct( description: 'Returns all the judgements for this contest', content: new OA\JsonContent( type: 'array', - items: new OA\Items( - allOf: [ - new OA\Schema(ref: new Model(type: Judging::class)), - new OA\Schema(ref: '#/components/schemas/JudgementExtraFields'), - ] - ) + items: new OA\Items(ref: new Model(type: JudgingWrapper::class)) ) )] #[OA\Parameter(ref: '#/components/parameters/idlist')] @@ -94,12 +89,7 @@ public function listAction(Request $request): Response #[OA\Response( response: 200, description: 'Returns the given judgement for this contest', - content: new OA\JsonContent( - allOf: [ - new OA\Schema(ref: new Model(type: Judging::class)), - new OA\Schema(ref: '#/components/schemas/JudgementExtraFields'), - ] - ) + content: new OA\JsonContent(ref: new Model(type: JudgingWrapper::class)) )] #[OA\Parameter(ref: '#/components/parameters/id')] public function singleAction(Request $request, string $id): Response diff --git a/webapp/src/Controller/API/JudgementTypeController.php b/webapp/src/Controller/API/JudgementTypeController.php index 94a8c452dc..c8b02b54d1 100644 --- a/webapp/src/Controller/API/JudgementTypeController.php +++ b/webapp/src/Controller/API/JudgementTypeController.php @@ -2,10 +2,12 @@ namespace App\Controller\API; +use App\DataTransferObject\JudgementType; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\QueryBuilder; use Exception; use FOS\RestBundle\Controller\Annotations as Rest; +use Nelmio\ApiDocBundle\Annotation\Model; use OpenApi\Attributes as OA; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -21,6 +23,7 @@ class JudgementTypeController extends AbstractRestController * Get all the judgement types for this contest. * * @throws NonUniqueResultException + * @return JudgementType[] */ #[Rest\Get('')] #[OA\Response( @@ -28,7 +31,7 @@ class JudgementTypeController extends AbstractRestController description: 'Returns all the judgement types for this contest', content: new OA\JsonContent( type: 'array', - items: new OA\Items(ref: '#/components/schemas/JudgementType') + items: new OA\Items(ref: new Model(type: JudgementType::class)) ) )] #[OA\Parameter(ref: '#/components/parameters/idlist')] @@ -59,10 +62,10 @@ public function listAction(Request $request): array #[OA\Response( response: 200, description: 'Returns the given judgement type for this contest', - content: new OA\JsonContent(ref: '#/components/schemas/JudgementType') + content: new OA\JsonContent(ref: new Model(type: JudgementType::class)) )] #[OA\Parameter(ref: '#/components/parameters/id')] - public function singleAction(Request $request, string $id): array + public function singleAction(Request $request, string $id): JudgementType { // Call getContestId to make sure we have an active contest. $this->getContestId($request); @@ -77,6 +80,10 @@ public function singleAction(Request $request, string $id): array /** * Get the judgement types, optionally filtered on the given IDs. + * + * @param string[]|null $filteredOn + * + * @return JudgementType[] */ protected function getJudgementTypes(array $filteredOn = null): array { @@ -96,12 +103,12 @@ protected function getJudgementTypes(array $filteredOn = null): array if ($filteredOn !== null && !in_array($label, $filteredOn)) { continue; } - $result[] = [ - 'id' => (string)$label, - 'name' => str_replace('-', ' ', $name), - 'penalty' => (bool)$penalty, - 'solved' => (bool)$solved, - ]; + $result[] = new JudgementType( + id: $label, + name: str_replace('-', ' ', $name), + penalty: (bool)$penalty, + solved: $solved, + ); } return $result; } diff --git a/webapp/src/Controller/API/MetricsController.php b/webapp/src/Controller/API/MetricsController.php index 182207560a..2024e0be3f 100644 --- a/webapp/src/Controller/API/MetricsController.php +++ b/webapp/src/Controller/API/MetricsController.php @@ -2,6 +2,7 @@ namespace App\Controller\API; +use App\DataTransferObject\SubmissionRestriction; use App\Entity\Submission; use App\Service\DOMJudgeService; use App\Service\SubmissionService; @@ -100,7 +101,10 @@ public function prometheusAction(): Response // Get submissions stats for the contest. /** @var Submission[] $submissions */ - [$submissions, $submissionCounts] = $this->submissionService->getSubmissionList([$contest->getCid() => $contest], ['visible' => true], 0); + [$submissions, $submissionCounts] = $this->submissionService->getSubmissionList( + [$contest->getCid() => $contest], + new SubmissionRestriction(visible: true) + ); foreach ($submissionCounts as $kind => $count) { $m['submissions_' . $kind]->set((int)$count, $labels); } diff --git a/webapp/src/Controller/API/OrganizationController.php b/webapp/src/Controller/API/OrganizationController.php index 0d2ef6b8bf..d3e63f4dcb 100644 --- a/webapp/src/Controller/API/OrganizationController.php +++ b/webapp/src/Controller/API/OrganizationController.php @@ -54,12 +54,7 @@ public function __construct( description: 'Returns all the organizations for this contest', content: new OA\JsonContent( type: 'array', - items: new OA\Items( - allOf: [ - new OA\Schema(ref: new Model(type: TeamAffiliation::class)), - new OA\Schema(ref: '#/components/schemas/Logo'), - ] - ) + items: new OA\Items(ref: new Model(type: TeamAffiliation::class)) ) )] #[OA\Parameter(ref: '#/components/parameters/idlist')] @@ -83,12 +78,7 @@ public function listAction(Request $request): Response #[OA\Response( response: 200, description: 'Returns the given organization for this contest', - content: new OA\JsonContent( - allOf: [ - new OA\Schema(ref: new Model(type: TeamAffiliation::class)), - new OA\Schema(ref: '#/components/schemas/Logo'), - ] - ) + content: new OA\JsonContent(ref: new Model(type: TeamAffiliation::class)) )] #[OA\Parameter(ref: '#/components/parameters/id')] public function singleAction(Request $request, string $id): Response diff --git a/webapp/src/Controller/API/ProblemController.php b/webapp/src/Controller/API/ProblemController.php index 0192af43e9..4791d85ab9 100644 --- a/webapp/src/Controller/API/ProblemController.php +++ b/webapp/src/Controller/API/ProblemController.php @@ -2,11 +2,11 @@ namespace App\Controller\API; +use App\DataTransferObject\ContestProblemArray; +use App\DataTransferObject\ContestProblemWrapper; use App\Entity\Contest; use App\Entity\ContestProblem; use App\Entity\Problem; -use App\Helpers\ContestProblemWrapper; -use App\Helpers\OrdinalArray; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; use App\Service\EventLogService; @@ -16,8 +16,8 @@ use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\QueryBuilder; use FOS\RestBundle\Controller\Annotations as Rest; +use Nelmio\ApiDocBundle\Annotation\Model; use OpenApi\Attributes as OA; -use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -25,6 +25,7 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Yaml\Yaml; #[Rest\Route('/contests/{cid}/problems')] @@ -111,7 +112,7 @@ public function addProblemsAction(Request $request): array description: 'Returns all the problems for this contest', content: new OA\JsonContent( type: 'array', - items: new OA\Items(ref: '#/components/schemas/ContestProblem') + items: new OA\Items(ref: new Model(type: ContestProblem::class)) ) )] #[OA\Parameter(ref: '#/components/parameters/idlist')] @@ -133,7 +134,7 @@ public function listAction(Request $request): Response $objects = array_map($this->transformObject(...), $objects); - $ordinalArray = new OrdinalArray($objects); + $ordinalArray = new ContestProblemArray($objects); $objects = $ordinalArray->getItems(); if ($request->query->has('ids')) { @@ -143,7 +144,7 @@ public function listAction(Request $request): Response $objects = []; foreach ($ordinalArray->getItems() as $item) { /** @var ContestProblemWrapper|ContestProblem $contestProblem */ - $contestProblem = $item->getItem(); + $contestProblem = $item->getContestProblemWrapper(); if ($contestProblem instanceof ContestProblemWrapper) { $contestProblem = $contestProblem->getContestProblem(); } @@ -291,7 +292,7 @@ public function unlinkProblemAction(Request $request, string $id): Response #[OA\Response( response: 200, description: 'Returns the linked problem for this contest', - content: new OA\JsonContent(ref: '#/components/schemas/ContestProblem') + content: new OA\JsonContent(ref: new Model(type: ContestProblem::class)) )] #[OA\Parameter(ref: '#/components/parameters/id')] public function linkProblemAction(Request $request, string $id): Response @@ -372,17 +373,17 @@ public function linkProblemAction(Request $request, string $id): Response #[OA\Response( response: 200, description: 'Returns the given problem for this contest', - content: new OA\JsonContent(ref: '#/components/schemas/ContestProblem') + content: new OA\JsonContent(ref: new Model(type: ContestProblem::class)) )] #[OA\Parameter(ref: '#/components/parameters/id')] public function singleAction(Request $request, string $id): Response { - $ordinalArray = new OrdinalArray($this->listActionHelper($request)); + $ordinalArray = new ContestProblemArray($this->listActionHelper($request)); $object = null; foreach ($ordinalArray->getItems() as $item) { /** @var ContestProblemWrapper|ContestProblem $contestProblem */ - $contestProblem = $item->getItem(); + $contestProblem = $item->getContestProblemWrapper(); if ($contestProblem instanceof ContestProblemWrapper) { $contestProblem = $contestProblem->getContestProblem(); } diff --git a/webapp/src/Controller/API/RunController.php b/webapp/src/Controller/API/RunController.php index 961027d0ad..9d9d503aac 100644 --- a/webapp/src/Controller/API/RunController.php +++ b/webapp/src/Controller/API/RunController.php @@ -2,8 +2,8 @@ namespace App\Controller\API; +use App\DataTransferObject\JudgingRunWrapper; use App\Entity\JudgingRun; -use App\Helpers\JudgingRunWrapper; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; use App\Service\EventLogService; @@ -57,12 +57,7 @@ public function __construct( description: 'Returns all the runs for this contest', content: new OA\JsonContent( type: 'array', - items: new OA\Items( - allOf: [ - new OA\Schema(ref: new Model(type: JudgingRun::class)), - new OA\Schema(ref: '#/components/schemas/RunExtraFields'), - ] - ) + items: new OA\Items(ref: new Model(type: JudgingRunWrapper::class)) ) )] #[OA\Parameter(ref: '#/components/parameters/idlist')] @@ -104,12 +99,7 @@ public function listAction(Request $request): Response #[OA\Response( response: 200, description: 'Returns the given run for this contest', - content: new OA\JsonContent( - allOf: [ - new OA\Schema(ref: new Model(type: JudgingRun::class)), - new OA\Schema(ref: '#/components/schemas/RunExtraFields'), - ] - ) + content: new OA\JsonContent(ref: new Model(type: JudgingRunWrapper::class)) )] #[OA\Parameter(ref: '#/components/parameters/id')] public function singleAction(Request $request, string $id): Response diff --git a/webapp/src/Controller/API/ScoreboardController.php b/webapp/src/Controller/API/ScoreboardController.php index 026dc51a57..2d6f3162aa 100644 --- a/webapp/src/Controller/API/ScoreboardController.php +++ b/webapp/src/Controller/API/ScoreboardController.php @@ -2,6 +2,10 @@ namespace App\Controller\API; +use App\DataTransferObject\Scoreboard\Problem; +use App\DataTransferObject\Scoreboard\Row; +use App\DataTransferObject\Scoreboard\Score; +use App\DataTransferObject\Scoreboard\Scoreboard; use App\Entity\Contest; use App\Entity\Event; use App\Entity\TeamCategory; @@ -16,6 +20,7 @@ use Doctrine\ORM\QueryBuilder; use Exception; use FOS\RestBundle\Controller\Annotations as Rest; +use Nelmio\ApiDocBundle\Annotation\Model; use OpenApi\Attributes as OA; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; @@ -48,7 +53,7 @@ public function __construct( #[OA\Response( response: 200, description: 'Returns the scoreboard', - content: new OA\JsonContent(ref: '#/components/schemas/Scoreboard') + content: new OA\JsonContent(ref: new Model(type: Scoreboard::class)) )] #[OA\Parameter( name: 'allteams', @@ -102,7 +107,7 @@ public function getScoreboardAction( ?int $sortorder = null, #[MapQueryParameter] bool $strict = false, - ): array { + ): Scoreboard { $filter = new Filter(); if ($category) { $filter->categories = [$category]; @@ -144,18 +149,15 @@ public function getScoreboardAction( $scoreboard = $this->scoreboardService->getScoreboard($contest, !$public, $filter, !$allTeams); - $results = []; + $results = new Scoreboard(); if ($event) { // Build up scoreboard results. - $results = [ - 'time' => Utils::absTime($event->getEventtime()), - 'contest_time' => Utils::relTime($event->getEventtime() - $contest->getStarttime()), - 'state' => $contest->getState(), - 'rows' => [], - ]; - if (!$strict) { - $results['event_id'] = (string)$event->getEventid(); - } + $results = new Scoreboard( + eventId: (string)$event->getEventid(), + time: Utils::absTime($event->getEventtime()), + contestTime: Utils::relTime($event->getEventtime() - $contest->getStarttime()), + state: $contest->getState(), + ); } // Return early if there's nothing to display yet. @@ -169,56 +171,55 @@ public function getScoreboardAction( if ($teamScore->team->getCategory()->getSortorder() !== $sortorder) { continue; } - $row = [ - 'rank' => $teamScore->rank, - 'team_id' => $teamScore->team->getApiId($this->eventLogService), - 'score' => [ - 'num_solved' => $teamScore->numPoints, - ], - 'problems' => [], - ]; if ($contest->getRuntimeAsScoreTiebreaker()) { - $row['score']['total_runtime'] = $teamScore->totalRuntime; + $score = new Score( + numSolved: $teamScore->numPoints, + totalRuntime: $teamScore->totalRuntime, + ); } else { - $row['score']['total_time'] = $teamScore->totalTime; + $score = new Score( + numSolved: $teamScore->numPoints, + totalTime: $teamScore->totalTime, + ); } + $problems = []; foreach ($scoreboard->getMatrix()[$teamScore->team->getTeamid()] as $problemId => $matrixItem) { $contestProblem = $scoreboard->getProblems()[$problemId]; - $problem = [ - 'label' => $contestProblem->getShortname(), - 'problem_id' => $contestProblem->getApiId($this->eventLogService), - 'num_judged' => $matrixItem->numSubmissions, - 'num_pending' => $matrixItem->numSubmissionsPending, - 'solved' => $matrixItem->isCorrect, - ]; + $problem = new Problem( + label: $contestProblem->getShortname(), + problemId: $contestProblem->getApiId($this->eventLogService), + numJudged: $matrixItem->numSubmissions, + numPending: $matrixItem->numSubmissionsPending, + solved: $matrixItem->isCorrect, + ); if ($contest->getRuntimeAsScoreTiebreaker()) { - $problem['fastest_submission'] = $matrixItem->isCorrect && $scoreboard->isFastestSubmission($teamScore->team, $contestProblem); + $problem->fastestSubmission = $matrixItem->isCorrect && $scoreboard->isFastestSubmission($teamScore->team, $contestProblem); if ($matrixItem->isCorrect) { - $problem['runtime'] = $matrixItem->runtime; + $problem->runtime = $matrixItem->runtime; } } else { - $problem['first_to_solve'] = $matrixItem->isCorrect && $scoreboard->solvedFirst($teamScore->team, $contestProblem); + $problem->firstToSolve = $matrixItem->isCorrect && $scoreboard->solvedFirst($teamScore->team, $contestProblem); if ($matrixItem->isCorrect) { - $problem['time'] = Utils::scoretime($matrixItem->time, $scoreIsInSeconds); + $problem->time = Utils::scoretime($matrixItem->time, $scoreIsInSeconds); } } - $row['problems'][] = $problem; + $problems[] = $problem; } - usort($row['problems'], fn($a, $b) => $a['label'] <=> $b['label']); + usort($problems, fn(Problem $a, Problem $b) => $a->label <=> $b->label); - if ($strict) { - foreach ($row['problems'] as $key => $data) { - unset($row['problems'][$key]['label']); - unset($row['problems'][$key]['first_to_solve']); - } - } + $row = new Row( + rank: $teamScore->rank, + teamId: $teamScore->team->getApiId($this->eventLogService), + score: $score, + problems: $problems, + ); - $results['rows'][] = $row; + $results->rows[] = $row; } return $results; diff --git a/webapp/src/Controller/API/SubmissionController.php b/webapp/src/Controller/API/SubmissionController.php index 7da7bcfeb7..aae06c7689 100644 --- a/webapp/src/Controller/API/SubmissionController.php +++ b/webapp/src/Controller/API/SubmissionController.php @@ -2,6 +2,7 @@ namespace App\Controller\API; +use App\DataTransferObject\SourceCode; use App\Entity\Contest; use App\Entity\ContestProblem; use App\Entity\Language; @@ -62,12 +63,7 @@ public function __construct( description: 'Returns all the submissions for this contest', content: new OA\JsonContent( type: 'array', - items: new OA\Items( - allOf: [ - new OA\Schema(ref: new Model(type: Submission::class)), - new OA\Schema(ref: '#/components/schemas/Files'), - ] - ) + items: new OA\Items(ref: new Model(type: Submission::class)) ) )] #[OA\Parameter(ref: '#/components/parameters/idlist')] @@ -91,12 +87,7 @@ public function listAction(Request $request): Response #[OA\Response( response: 200, description: 'Returns the given submission for this contest', - content: new OA\JsonContent( - allOf: [ - new OA\Schema(ref: new Model(type: Submission::class)), - new OA\Schema(ref: '#/components/schemas/Files'), - ] - ) + content: new OA\JsonContent(ref: new Model(type: Submission::class)) )] #[OA\Parameter(ref: '#/components/parameters/id')] public function singleAction(Request $request, string $id): Response @@ -208,12 +199,7 @@ public function singleAction(Request $request, string $id): Response #[OA\Response( response: 200, description: 'When submitting was successful', - content: new OA\JsonContent( - allOf: [ - new OA\Schema(ref: new Model(type: Submission::class)), - new OA\Schema(ref: '#/components/schemas/Files'), - ] - ) + content: new OA\JsonContent(ref: new Model(type: Submission::class)) )] public function addSubmissionAction(Request $request, ?string $id): Response { @@ -500,13 +486,14 @@ public function getSubmissionFilesAction(Request $request, string $id): Response /** * Get the source code of all the files for the given submission. * @throws NonUniqueResultException + * @return SourceCode[] */ #[IsGranted(new Expression("is_granted('ROLE_JURY') or is_granted('ROLE_JUDGEHOST')"))] #[Rest\Get('contests/{cid}/submissions/{id}/source-code')] #[OA\Response( response: 200, description: 'The files for the submission', - content: new OA\JsonContent(ref: '#/components/schemas/SourceCodeList') + content: new OA\JsonContent(ref: new Model(type: SourceCode::class)) )] #[OA\Parameter(ref: '#/components/parameters/id')] public function getSubmissionSourceCodeAction(Request $request, string $id): array @@ -530,12 +517,12 @@ public function getSubmissionSourceCodeAction(Request $request, string $id): arr $result = []; foreach ($files as $file) { - $result[] = [ - 'id' => (string)$file->getSubmitfileid(), - 'submission_id' => (string)$file->getSubmission()->getSubmitid(), - 'filename' => $file->getFilename(), - 'source' => base64_encode($file->getSourcecode()), - ]; + $result[] = new SourceCode( + id: (string)$file->getSubmitfileid(), + submissionId: (string)$file->getSubmission()->getSubmitid(), + filename: $file->getFilename(), + source: base64_encode($file->getSourcecode()), + ); } return $result; } diff --git a/webapp/src/Controller/API/TeamController.php b/webapp/src/Controller/API/TeamController.php index dbc8ec7fdd..3e9563662d 100644 --- a/webapp/src/Controller/API/TeamController.php +++ b/webapp/src/Controller/API/TeamController.php @@ -55,12 +55,7 @@ public function __construct( description: 'Returns all the teams for this contest', content: new OA\JsonContent( type: 'array', - items: new OA\Items( - allOf: [ - new OA\Schema(ref: new Model(type: Team::class)), - new OA\Schema(ref: '#/components/schemas/Photo'), - ] - ) + items: new OA\Items(ref: new Model(type: Team::class)) ) )] #[OA\Parameter(ref: '#/components/parameters/idlist')] @@ -96,12 +91,7 @@ public function listAction(Request $request): Response #[OA\Response( response: 200, description: 'Returns the given team for this contest', - content: new OA\JsonContent( - allOf: [ - new OA\Schema(ref: new Model(type: Team::class)), - new OA\Schema(ref: '#/components/schemas/Photo'), - ] - ) + content: new OA\JsonContent(ref: new Model(type: Team::class)) )] #[OA\Parameter(ref: '#/components/parameters/id')] public function singleAction(Request $request, string $id): Response diff --git a/webapp/src/Controller/Jury/BalloonController.php b/webapp/src/Controller/Jury/BalloonController.php index 3963d07828..5726d5032f 100644 --- a/webapp/src/Controller/Jury/BalloonController.php +++ b/webapp/src/Controller/Jury/BalloonController.php @@ -156,7 +156,7 @@ public function indexAction(BalloonService $balloonService): Response 'url' => $this->generateUrl('jury_balloons'), 'ajax' => true ], - 'isfrozen' => isset($contest->getState()['frozen']), + 'isfrozen' => isset($contest->getState()->frozen), 'hasFilters' => !$this->areDefault($filters, $defaultCategories), 'filteredAffiliations' => $filteredAffiliations, 'filteredLocations' => $filteredLocations, diff --git a/webapp/src/Controller/Jury/ConfigController.php b/webapp/src/Controller/Jury/ConfigController.php index cd598306ee..b9c34dbb11 100644 --- a/webapp/src/Controller/Jury/ConfigController.php +++ b/webapp/src/Controller/Jury/ConfigController.php @@ -80,8 +80,8 @@ public function indexAction(EventLogService $eventLogService, Request $request): $categories = []; foreach ($specs as $spec) { - if (!in_array($spec['category'], $categories)) { - $categories[] = $spec['category']; + if (!in_array($spec->category, $categories)) { + $categories[] = $spec->category; } } $allData = []; @@ -89,7 +89,7 @@ public function indexAction(EventLogService $eventLogService, Request $request): foreach ($categories as $category) { $data = []; foreach ($specs as $specName => $spec) { - if ($spec['category'] !== $category) { + if ($spec->category !== $category) { continue; } if (isset($errors[$specName]) && $activeCategory === null) { @@ -97,16 +97,16 @@ public function indexAction(EventLogService $eventLogService, Request $request): } $data[] = [ 'name' => $specName, - 'type' => $spec['type'], + 'type' => $spec->type, 'value' => isset($options[$specName]) ? $options[$specName]->getValue() : - $spec['default_value'], - 'description' => $spec['description'], - 'options' => $spec['options'] ?? null, - 'key_options' => $spec['key_options'] ?? null, - 'value_options' => $spec['value_options'] ?? null, - 'key_placeholder' => $spec['key_placeholder'] ?? '', - 'value_placeholder' => $spec['value_placeholder'] ?? '', + $spec->defaultValue, + 'description' => $spec->description, + 'options' => $spec->options, + 'key_options' => $spec->keyOptions, + 'value_options' => $spec->valueOptions, + 'key_placeholder' => $spec->keyPlaceholder ?? '', + 'value_placeholder' => $spec->valuePlaceholder ?? '', ]; } $allData[] = [ diff --git a/webapp/src/Controller/Jury/LanguageController.php b/webapp/src/Controller/Jury/LanguageController.php index 79adfcc726..2f36c011b6 100644 --- a/webapp/src/Controller/Jury/LanguageController.php +++ b/webapp/src/Controller/Jury/LanguageController.php @@ -3,6 +3,7 @@ namespace App\Controller\Jury; use App\Controller\BaseController; +use App\DataTransferObject\SubmissionRestriction; use App\Entity\Judging; use App\Entity\Language; use App\Entity\Submission; @@ -175,11 +176,10 @@ public function viewAction(Request $request, SubmissionService $submissionServic throw new NotFoundHttpException(sprintf('Language with ID %s not found', $langId)); } - $restrictions = ['langid' => $language->getLangid()]; /** @var Submission[] $submissions */ [$submissions, $submissionCounts] = $submissionService->getSubmissionList( $this->dj->getCurrentContests(honorCookie: true), - $restrictions + new SubmissionRestriction(languageId: $language->getLangid()) ); $data = [ diff --git a/webapp/src/Controller/Jury/ProblemController.php b/webapp/src/Controller/Jury/ProblemController.php index aaed115c6a..8661870b61 100644 --- a/webapp/src/Controller/Jury/ProblemController.php +++ b/webapp/src/Controller/Jury/ProblemController.php @@ -3,6 +3,7 @@ namespace App\Controller\Jury; use App\Controller\BaseController; +use App\DataTransferObject\SubmissionRestriction; use App\Entity\Contest; use App\Entity\ContestProblem; use App\Entity\Judging; @@ -430,11 +431,10 @@ public function viewAction(Request $request, SubmissionService $submissionServic return $this->redirectToRoute('jury_problem', ['probId' => $probId]); } - $restrictions = ['probid' => $problem->getProbid()]; /** @var Submission[] $submissions */ [$submissions, $submissionCounts] = $submissionService->getSubmissionList( $this->dj->getCurrentContests(honorCookie: true), - $restrictions + new SubmissionRestriction(problemId: $problem->getProbid()), ); $data = [ diff --git a/webapp/src/Controller/Jury/RejudgingController.php b/webapp/src/Controller/Jury/RejudgingController.php index 5bc7d12601..d08b239129 100644 --- a/webapp/src/Controller/Jury/RejudgingController.php +++ b/webapp/src/Controller/Jury/RejudgingController.php @@ -3,6 +3,7 @@ namespace App\Controller\Jury; use App\Controller\BaseController; +use App\DataTransferObject\SubmissionRestriction; use App\Entity\Contest; use App\Entity\Judgehost; use App\Entity\JudgeTask; @@ -322,21 +323,21 @@ public function viewAction( } } - $restrictions = ['rejudgingid' => $rejudgingId]; + $restrictions = new SubmissionRestriction(rejudgingId: $rejudgingId); if ($viewTypes[$view] == 'unverified') { - $restrictions['verified'] = false; + $restrictions->verified = false; } if ($viewTypes[$view] == 'unjudged') { - $restrictions['judged'] = false; + $restrictions->judged = false; } if ($viewTypes[$view] == 'diff') { - $restrictions['rejudgingdiff'] = true; + $restrictions->rejudgingDifference = true; } if ($oldverdict !== 'all') { - $restrictions['old_result'] = $oldverdict; + $restrictions->oldResult = $oldverdict; } if ($newverdict !== 'all') { - $restrictions['result'] = $newverdict; + $restrictions->result = $newverdict; } /** @var Submission[] $submissions */ diff --git a/webapp/src/Controller/Jury/ShadowDifferencesController.php b/webapp/src/Controller/Jury/ShadowDifferencesController.php index e8ab8dd25b..0fe08bdfbb 100644 --- a/webapp/src/Controller/Jury/ShadowDifferencesController.php +++ b/webapp/src/Controller/Jury/ShadowDifferencesController.php @@ -2,6 +2,7 @@ namespace App\Controller\Jury; +use App\DataTransferObject\SubmissionRestriction; use App\Service\ConfigurationService; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; @@ -163,34 +164,33 @@ public function indexAction( } } - $restrictions = ['with_external_id' => true]; + $restrictions = new SubmissionRestriction(withExternalId: true); if ($viewTypes[$view] == 'unjudged local') { - $restrictions['judged'] = false; + $restrictions->judged = false; } if ($viewTypes[$view] == 'unjudged external') { - $restrictions['externally_judged'] = false; + $restrictions->externallyJudged = false; } if ($viewTypes[$view] == 'diff') { - $restrictions['external_diff'] = true; + $restrictions->externalDifference = true; } if ($verificationViewTypes[$verificationView] == 'unverified') { - $restrictions['externally_verified'] = false; + $restrictions->externallyVerified = false; } if ($verificationViewTypes[$verificationView] == 'verified') { - $restrictions['externally_verified'] = true; + $restrictions->externallyVerified = true; } if ($external !== 'all') { - $restrictions['external_result'] = $external; + $restrictions->externalResult = $external; } if ($local !== 'all') { - $restrictions['result'] = $local; + $restrictions->result = $local; } /** @var Submission[] $submissions */ [$submissions, $submissionCounts] = $this->submissions->getSubmissionList( $this->dj->getCurrentContests(honorCookie: true), $restrictions, - limit: 0, showShadowUnverified: true ); diff --git a/webapp/src/Controller/Jury/SubmissionController.php b/webapp/src/Controller/Jury/SubmissionController.php index c343b74d5a..60fd0d6aae 100644 --- a/webapp/src/Controller/Jury/SubmissionController.php +++ b/webapp/src/Controller/Jury/SubmissionController.php @@ -3,6 +3,7 @@ namespace App\Controller\Jury; use App\Controller\BaseController; +use App\DataTransferObject\SubmissionRestriction; use App\Doctrine\DBAL\Types\JudgeTaskType; use App\Entity\Contest; use App\Entity\DebugPackage; @@ -94,15 +95,15 @@ public function indexAction( 'ajax' => true, ]; - $restrictions = []; + $restrictions = new SubmissionRestriction(); if ($viewTypes[$view] == 'unverified') { - $restrictions['verified'] = false; + $restrictions->verified = false; } if ($viewTypes[$view] == 'unjudged') { - $restrictions['judged'] = false; + $restrictions->judged = false; } if ($viewTypes[$view] == 'judging') { - $restrictions['judging'] = true; + $restrictions->judging = true; } $contests = $this->dj->getCurrentContests(); diff --git a/webapp/src/Controller/Jury/TeamCategoryController.php b/webapp/src/Controller/Jury/TeamCategoryController.php index 3630d1e624..2cdba5b922 100644 --- a/webapp/src/Controller/Jury/TeamCategoryController.php +++ b/webapp/src/Controller/Jury/TeamCategoryController.php @@ -3,6 +3,7 @@ namespace App\Controller\Jury; use App\Controller\BaseController; +use App\DataTransferObject\SubmissionRestriction; use App\Entity\Judging; use App\Entity\Submission; use App\Entity\Team; @@ -128,11 +129,10 @@ public function viewAction(Request $request, SubmissionService $submissionServic throw new NotFoundHttpException(sprintf('Team category with ID %s not found', $categoryId)); } - $restrictions = ['categoryid' => $teamCategory->getCategoryid()]; /** @var Submission[] $submissions */ [$submissions, $submissionCounts] = $submissionService->getSubmissionList( $this->dj->getCurrentContests(honorCookie: true), - $restrictions + new SubmissionRestriction(categoryId: $teamCategory->getCategoryid()) ); $data = [ diff --git a/webapp/src/Controller/Jury/TeamController.php b/webapp/src/Controller/Jury/TeamController.php index 8e22f76896..60dba907c7 100644 --- a/webapp/src/Controller/Jury/TeamController.php +++ b/webapp/src/Controller/Jury/TeamController.php @@ -3,6 +3,7 @@ namespace App\Controller\Jury; use App\Controller\BaseController; +use App\DataTransferObject\SubmissionRestriction; use App\Entity\Contest; use App\Entity\Role; use App\Entity\Team; @@ -270,24 +271,24 @@ public function viewAction( $data['limitToTeams'] = [$team]; } - $restrictions = []; + $restrictions = new SubmissionRestriction(); $restrictionText = null; if ($request->query->has('restrict')) { - $restrictions = $request->query->all('restrict'); + $restrictionsFromQuery = $request->query->all('restrict'); $restrictionTexts = []; - foreach ($restrictions as $key => $value) { + foreach ($restrictionsFromQuery as $key => $value) { $restrictionKeyText = match ($key) { - 'probid' => 'problem', - 'langid' => 'language', + 'problemId' => 'problem', + 'languageId' => 'language', 'judgehost' => 'judgehost', default => throw new BadRequestHttpException(sprintf('Restriction on %s not allowed.', $key)), }; + $restrictions->$key = is_numeric($value) ? (int)$value : $value; $restrictionTexts[] = sprintf('%s %s', $restrictionKeyText, $value); } $restrictionText = implode(', ', $restrictionTexts); - /** @var array{probid?: int, langid?: string, judgehost?: string, ...} $restrictions */ } - $restrictions['teamid'] = $teamId; + $restrictions->teamId = $teamId; [$submissions, $submissionCounts] = $submissionService->getSubmissionList($this->dj->getCurrentContests(honorCookie: true), $restrictions); diff --git a/webapp/src/Controller/Jury/UserController.php b/webapp/src/Controller/Jury/UserController.php index c7add55406..7f0278dbaf 100644 --- a/webapp/src/Controller/Jury/UserController.php +++ b/webapp/src/Controller/Jury/UserController.php @@ -3,6 +3,7 @@ namespace App\Controller\Jury; use App\Controller\BaseController; +use App\DataTransferObject\SubmissionRestriction; use App\Entity\Role; use App\Entity\Submission; use App\Entity\Team; @@ -178,11 +179,10 @@ public function viewAction(int $userId, SubmissionService $submissionService): R throw new NotFoundHttpException(sprintf('User with ID %s not found', $userId)); } - $restrictions = ['userid' => $user->getUserid()]; /** @var Submission[] $submissions */ [$submissions, $submissionCounts] = $submissionService->getSubmissionList( $this->dj->getCurrentContests(honorCookie: true), - $restrictions + new SubmissionRestriction(userId: $user->getUserid()), ); return $this->render('jury/user.html.twig', [ diff --git a/webapp/src/Controller/Team/MiscController.php b/webapp/src/Controller/Team/MiscController.php index 46be78ec5c..59d1709155 100644 --- a/webapp/src/Controller/Team/MiscController.php +++ b/webapp/src/Controller/Team/MiscController.php @@ -3,6 +3,7 @@ namespace App\Controller\Team; use App\Controller\BaseController; +use App\DataTransferObject\SubmissionRestriction; use App\Entity\Clarification; use App\Entity\Language; use App\Form\Type\PrintType; @@ -82,8 +83,7 @@ public function homeAction(Request $request): Response $this->em->clear(); $data['submissions'] = $this->submissionService->getSubmissionList( [$contest->getCid() => $contest], - ['teamid' => $teamId], - 0 + new SubmissionRestriction(teamId: $teamId) )[0]; /** @var Clarification[] $clarifications */ diff --git a/webapp/src/DataTransferObject/Access.php b/webapp/src/DataTransferObject/Access.php new file mode 100644 index 0000000000..60a2d2f1e1 --- /dev/null +++ b/webapp/src/DataTransferObject/Access.php @@ -0,0 +1,19 @@ +')] + public array $capabilities, + #[Serializer\Type('array')] + public array $endpoints + ) {} +} diff --git a/webapp/src/DataTransferObject/AccessEndpoint.php b/webapp/src/DataTransferObject/AccessEndpoint.php new file mode 100644 index 0000000000..b7fa89b07e --- /dev/null +++ b/webapp/src/DataTransferObject/AccessEndpoint.php @@ -0,0 +1,17 @@ +')] + public readonly array $properties, + ) {} +} diff --git a/webapp/src/DataTransferObject/ApiInfo.php b/webapp/src/DataTransferObject/ApiInfo.php new file mode 100644 index 0000000000..bc517b7900 --- /dev/null +++ b/webapp/src/DataTransferObject/ApiInfo.php @@ -0,0 +1,16 @@ +')] + public readonly array $teamIds, + ) {} +} diff --git a/webapp/src/DataTransferObject/Balloon.php b/webapp/src/DataTransferObject/Balloon.php new file mode 100644 index 0000000000..f9d5648373 --- /dev/null +++ b/webapp/src/DataTransferObject/Balloon.php @@ -0,0 +1,30 @@ + $total + */ + public function __construct( + public readonly int $balloonid, + public readonly string $time, + public readonly string $problem, + public readonly ContestProblem $contestproblem, + public readonly string $team, + public readonly int $teamid, + public readonly ?string $location, + public readonly ?string $affiliation, + public readonly ?int $affiliationid, + public readonly ?string $category, + public readonly ?int $categoryid, + #[Serializer\Type('array')] + public readonly array $total, + public readonly string $awards, + public readonly bool $done, + ) {} +} diff --git a/webapp/src/DataTransferObject/BaseFile.php b/webapp/src/DataTransferObject/BaseFile.php new file mode 100644 index 0000000000..1725b34d40 --- /dev/null +++ b/webapp/src/DataTransferObject/BaseFile.php @@ -0,0 +1,11 @@ + $enumClass + * @param array|null $options + * @param array|null $keyOptions + * @param array|null $valueOptions + */ + public function __construct( + public readonly string $name, + public readonly string $type, + public readonly bool $public, + public readonly string $description, + public readonly string $category, + public readonly mixed $defaultValue, + public readonly ?string $regex = null, + public readonly ?string $keyPlaceholder = null, + public readonly ?string $valuePlaceholder = null, + public readonly ?string $errorMessage = null, + public readonly ?string $docdescription = null, + public readonly ?string $enumClass = null, + public ?array $options = null, + public ?array $keyOptions = null, + public ?array $valueOptions = null, + ) {} + + /** + * @param array{name: string, type: string, public: bool, + * description: string, category: string, default_value: mixed|mixed[], + * regex?: string, key_placeholder?: string, value_placeholder?: string, + * error_message?: string, docdescription?: string, enum_class?: string, + * options?: array, key_options?: array, + * value_options?: array} $array + */ + public static function fromArray(array $array): self + { + return new self( + $array['name'], + $array['type'], + $array['public'], + $array['description'], + $array['category'], + $array['default_value'], + $array['regex'] ?? null, + $array['key_placeholder'] ?? null, + $array['value_placeholder'] ?? null, + $array['error_message'] ?? null, + $array['docdescription'] ?? null, + $array['enum_class'] ?? null, + $array['options'] ?? null, + $array['key_options'] ?? null, + $array['value_options'] ?? null, + ); + } +} diff --git a/webapp/src/DataTransferObject/ContestProblemArray.php b/webapp/src/DataTransferObject/ContestProblemArray.php new file mode 100644 index 0000000000..23ac64af51 --- /dev/null +++ b/webapp/src/DataTransferObject/ContestProblemArray.php @@ -0,0 +1,33 @@ +items = []; + $ordinal = 0; + foreach ($items as $item) { + $this->items[] = new OrdinalContestProblemWrapper($ordinal, $item); + $ordinal++; + } + } + + /** + * @return OrdinalContestProblemWrapper[] + */ + public function getItems(): array + { + return $this->items; + } +} diff --git a/webapp/src/Helpers/ContestProblemWrapper.php b/webapp/src/DataTransferObject/ContestProblemWrapper.php similarity index 93% rename from webapp/src/Helpers/ContestProblemWrapper.php rename to webapp/src/DataTransferObject/ContestProblemWrapper.php index fdfa8634e3..177b5c06f9 100644 --- a/webapp/src/Helpers/ContestProblemWrapper.php +++ b/webapp/src/DataTransferObject/ContestProblemWrapper.php @@ -1,6 +1,6 @@ item; + } +} diff --git a/webapp/src/DataTransferObject/Scoreboard/Problem.php b/webapp/src/DataTransferObject/Scoreboard/Problem.php new file mode 100644 index 0000000000..4f0d363825 --- /dev/null +++ b/webapp/src/DataTransferObject/Scoreboard/Problem.php @@ -0,0 +1,29 @@ +")] + public readonly array $problems, + ) {} +} diff --git a/webapp/src/DataTransferObject/Scoreboard/Score.php b/webapp/src/DataTransferObject/Scoreboard/Score.php new file mode 100644 index 0000000000..aa5aadddd5 --- /dev/null +++ b/webapp/src/DataTransferObject/Scoreboard/Score.php @@ -0,0 +1,18 @@ +")] + public array $rows = [], + ) {} +} diff --git a/webapp/src/DataTransferObject/SourceCode.php b/webapp/src/DataTransferObject/SourceCode.php new file mode 100644 index 0000000000..2e712d08bc --- /dev/null +++ b/webapp/src/DataTransferObject/SourceCode.php @@ -0,0 +1,13 @@ +problems = new ArrayCollection(); @@ -1113,9 +1118,9 @@ public function getDataForJuryInterface(): array } /** - * @return array + * @return ContestState */ - public function getState(): array + public function getState(): ContestState { $time_or_null = function ($time, $extra_cond = true) { if (!$extra_cond || $time === null || Utils::now() < $time) { @@ -1123,23 +1128,29 @@ public function getState(): array } return Utils::absTime($time); }; - $result = []; - $result['started'] = $time_or_null($this->getStarttime()); - $result['ended'] = $time_or_null($this->getEndtime(), $result['started'] !== null); - $result['frozen'] = $time_or_null($this->getFreezetime(), $result['started'] !== null); - $result['thawed'] = $time_or_null($this->getUnfreezetime(), $result['frozen'] !== null); - $result['finalized'] = $time_or_null($this->getFinalizetime(), $result['ended'] !== null); - $result['end_of_updates'] = null; - if ($result['finalized'] !== null && - ($result['thawed'] !== null || $result['frozen'] === null)) { - if ($result['thawed'] !== null && + $started = $time_or_null($this->getStarttime()); + $ended = $time_or_null($this->getEndtime(), $started !== null); + $frozen = $time_or_null($this->getFreezetime(), $started !== null); + $thawed = $time_or_null($this->getUnfreezetime(), $frozen !== null); + $finalized = $time_or_null($this->getFinalizetime(), $ended !== null); + $endOfUpdates = null; + if ($finalized !== null && + ($thawed !== null || $frozen === null)) { + if ($thawed !== null && $this->getFreezetime() > $this->getFinalizetime()) { - $result['end_of_updates'] = $result['thawed']; + $endOfUpdates = $thawed; } else { - $result['end_of_updates'] = $result['finalized']; + $endOfUpdates = $finalized; } } - return $result; + return new ContestState( + started: $started, + ended: $ended, + frozen: $frozen, + thawed: $thawed, + finalized: $finalized, + endOfUpdates: $endOfUpdates + ); } public function getMinutesRemaining(): int @@ -1379,4 +1390,33 @@ public function isClearAsset(string $property): ?bool default => null, }; } + + public function setPenaltyTimeForApi(?int $penaltyTimeForApi): Contest + { + $this->penaltyTimeForApi = $penaltyTimeForApi; + return $this; + } + + public function getPenaltyTimeForApi(): ?int + { + return $this->penaltyTimeForApi; + } + + public function setBannerForApi(?ImageFile $bannerForApi = null): Contest + { + $this->bannerForApi = $bannerForApi; + return $this; + } + + /** + * @return ImageFile[] + */ + #[Serializer\VirtualProperty] + #[Serializer\SerializedName('banner')] + #[Serializer\Type('array')] + #[Serializer\Exclude(if: 'object.getBannerForApi() === []')] + public function getBannerForApi(): array + { + return array_filter([$this->bannerForApi]); + } } diff --git a/webapp/src/Entity/ContestProblem.php b/webapp/src/Entity/ContestProblem.php index bde9be2667..062e9c216e 100644 --- a/webapp/src/Entity/ContestProblem.php +++ b/webapp/src/Entity/ContestProblem.php @@ -7,6 +7,7 @@ use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; +use JMS\Serializer\Metadata\StaticPropertyMetadata; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Context\ExecutionContextInterface; @@ -170,6 +171,28 @@ public function getColor(): ?string return $this->color; } + #[Serializer\VirtualProperty] + #[Serializer\SerializedName('rgb')] + public function getApiRgb(): ?string + { + if (!$this->getColor()) { + return null; + } + + return Utils::convertToHex($this->getColor()); + } + + #[Serializer\VirtualProperty] + #[Serializer\SerializedName('color')] + public function getApiColor(): ?string + { + if (!$this->getColor()) { + return null; + } + + return Utils::convertToColor($this->getColor()); + } + public function setLazyEvalResults(?int $lazyEvalResults): ContestProblem { $this->lazyEvalResults = $lazyEvalResults === 0 ? null : $lazyEvalResults; diff --git a/webapp/src/Entity/Language.php b/webapp/src/Entity/Language.php index fa5c9c4b5e..b5fc9b2aa9 100644 --- a/webapp/src/Entity/Language.php +++ b/webapp/src/Entity/Language.php @@ -2,6 +2,7 @@ namespace App\Entity; use App\Controller\API\AbstractRestController as ARC; +use App\DataTransferObject\Command; use App\Validator\Constraints\Identifier; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -217,37 +218,31 @@ public function getCompileExecutableHash(): ?string return $this->compile_executable?->getImmutableExecutable()->getHash(); } - /** - * @return array - */ #[Serializer\VirtualProperty] #[Serializer\SerializedName('compiler')] #[Serializer\Exclude(if:'object.getCompilerVersionCommand() == ""')] - public function getCompilerData(): array + public function getCompilerData(): Command { - $ret = []; + $ret = new Command(); if (!empty($this->getCompilerVersionCommand())) { - $ret['version_command'] = $this->getCompilerVersionCommand(); + $ret->versionCommand = $this->getCompilerVersionCommand(); if (!empty($this->getCompilerVersion())) { - $ret['version'] = $this->getCompilerVersion(); + $ret->version = $this->getCompilerVersion(); } } return $ret; } - /** - * @return array - */ #[Serializer\VirtualProperty] #[Serializer\SerializedName('runner')] #[Serializer\Exclude(if:'object.getRunnerVersionCommand() == ""')] - public function getRunnerData(): array + public function getRunnerData(): Command { - $ret = []; + $ret = new Command(); if (!empty($this->getRunnerVersionCommand())) { - $ret['version_command'] = $this->getRunnerVersionCommand(); + $ret->versionCommand = $this->getRunnerVersionCommand(); if (!empty($this->getRunnerVersion())) { - $ret['version'] = $this->getRunnerVersion(); + $ret->version = $this->getRunnerVersion(); } } return $ret; diff --git a/webapp/src/Entity/Problem.php b/webapp/src/Entity/Problem.php index 81ccb59566..e6ce64d540 100644 --- a/webapp/src/Entity/Problem.php +++ b/webapp/src/Entity/Problem.php @@ -3,6 +3,7 @@ namespace App\Entity; use App\Controller\API\AbstractRestController as ARC; +use App\DataTransferObject\FileWithName; use App\Utils\Utils; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -168,6 +169,11 @@ class Problem extends BaseApiEntity #[Serializer\Exclude] private Collection $attachments; + // This field gets filled by the contest problem visitor with a data transfer + // object that represents the problem statement. + #[Serializer\Exclude] + private ?FileWithName $statementForApi = null; + public function setProbid(int $probid): Problem { $this->probid = $probid; @@ -516,4 +522,20 @@ public function getProblemTextStreamedResponse(): StreamedResponse return $response; } + + public function setStatementForApi(?FileWithName $statementForApi = null): void + { + $this->statementForApi = $statementForApi; + } + + /** + * @return FileWithName[] + */ + #[Serializer\VirtualProperty] + #[Serializer\SerializedName('statement')] + #[Serializer\Type('array')] + public function getStatementForApi(): array + { + return array_filter([$this->statementForApi]); + } } diff --git a/webapp/src/Entity/Submission.php b/webapp/src/Entity/Submission.php index 440802689e..46f12f6f7d 100644 --- a/webapp/src/Entity/Submission.php +++ b/webapp/src/Entity/Submission.php @@ -3,6 +3,7 @@ namespace App\Entity; use App\Controller\API\AbstractRestController as ARC; +use App\DataTransferObject\FileWithName; use App\Utils\Utils; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -184,6 +185,11 @@ class Submission extends BaseApiEntity implements ExternalRelationshipEntityInte #[Serializer\Exclude] private ?string $old_result = null; + // This field gets filled by the submission visitor with a data transfer + // object that represents the submission file + #[Serializer\Exclude] + private ?FileWithName $fileForApi = null; + public function getResult(): ?string { foreach ($this->judgings as $j) { @@ -538,4 +544,21 @@ public function getExternalJudgements(): Collection { return $this->external_judgements; } + + public function setFileForApi(?FileWithName $fileForApi = null): Submission + { + $this->fileForApi = $fileForApi; + return $this; + } + + /** + * @return FileWithName[] + */ + #[Serializer\VirtualProperty] + #[Serializer\SerializedName('files')] + #[Serializer\Type('array')] + public function getFileForApi(): array + { + return array_filter([$this->fileForApi]); + } } diff --git a/webapp/src/Entity/Team.php b/webapp/src/Entity/Team.php index d3a2e7daa1..d789052f7a 100644 --- a/webapp/src/Entity/Team.php +++ b/webapp/src/Entity/Team.php @@ -3,7 +3,9 @@ namespace App\Entity; use App\Controller\API\AbstractRestController as ARC; -use App\Helpers\TeamLocation; +use App\DataTransferObject\FileWithName; +use App\DataTransferObject\ImageFile; +use App\DataTransferObject\TeamLocation; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -188,6 +190,11 @@ class Team extends BaseApiEntity implements ExternalRelationshipEntityInterface, #[Serializer\Exclude] private Collection $unread_clarifications; + // This field gets filled by the team visitor with a data transfer + // object that represents the team photo + #[Serializer\Exclude] + private ?ImageFile $photoForApi = null; + public function setTeamid(int $teamid): Team { $this->teamid = $teamid; @@ -646,4 +653,21 @@ public function isClearAsset(string $property): ?bool default => null, }; } + + public function setPhotoForApi(?ImageFile $photoForApi = null): void + { + $this->photoForApi = $photoForApi; + } + + /** + * @return ImageFile[] + */ + #[Serializer\VirtualProperty] + #[Serializer\SerializedName('photo')] + #[Serializer\Type('array')] + #[Serializer\Exclude(if: 'object.getPhotoForApi() === []')] + public function getPhotoForApi(): array + { + return array_filter([$this->photoForApi]); + } } diff --git a/webapp/src/Entity/TeamAffiliation.php b/webapp/src/Entity/TeamAffiliation.php index 5b9f229e31..f1f63a0298 100644 --- a/webapp/src/Entity/TeamAffiliation.php +++ b/webapp/src/Entity/TeamAffiliation.php @@ -1,6 +1,7 @@ ')] + #[Serializer\Exclude(if: 'object.getCountryFlagForApi() === []')] + private array $countryFlagsForApi = []; + + // This field gets filled by the team affiliation visitor with a data transfer + // object that represents the logo + #[Serializer\Exclude] + private ?ImageFile $logoForApi = null; + public function __construct() { $this->teams = new ArrayCollection(); @@ -243,4 +260,41 @@ public function isClearAsset(string $property): ?bool default => null, }; } + + /** + * @param ImageFile[] $countryFlagsForApi + * + * @return $this + */ + public function setCountryFlagForApi(array $countryFlagsForApi = []): TeamAffiliation + { + $this->countryFlagsForApi = $countryFlagsForApi; + return $this; + } + + /** + * @return ImageFile[] + */ + public function getCountryFlagForApi(): array + { + return $this->countryFlagsForApi; + } + + public function setLogoForApi(?ImageFile $logoForApi = null): TeamAffiliation + { + $this->logoForApi = $logoForApi; + return $this; + } + + /** + * @return ImageFile[] + */ + #[Serializer\VirtualProperty] + #[Serializer\SerializedName('logo')] + #[Serializer\Type('array')] + #[Serializer\Exclude(if: 'object.getLogoForApi() === []')] + public function getLogoForApi(): array + { + return array_filter([$this->logoForApi]); + } } diff --git a/webapp/src/Helpers/OrdinalArray.php b/webapp/src/Helpers/OrdinalArray.php deleted file mode 100644 index 11b04f6eac..0000000000 --- a/webapp/src/Helpers/OrdinalArray.php +++ /dev/null @@ -1,32 +0,0 @@ -items = []; - $ordinal = 0; - foreach ($items as $item) { - $this->items[] = new OrdinalItem($ordinal, $item); - $ordinal++; - } - } - - /** - * @return OrdinalItem[] - */ - public function getItems(): array - { - return $this->items; - } -} diff --git a/webapp/src/Helpers/OrdinalItem.php b/webapp/src/Helpers/OrdinalItem.php deleted file mode 100644 index 75284f8f67..0000000000 --- a/webapp/src/Helpers/OrdinalItem.php +++ /dev/null @@ -1,23 +0,0 @@ -item; - } -} diff --git a/webapp/src/Serializer/ContestProblemVisitor.php b/webapp/src/Serializer/ContestProblemVisitor.php index e4318be3db..4951a29dcd 100644 --- a/webapp/src/Serializer/ContestProblemVisitor.php +++ b/webapp/src/Serializer/ContestProblemVisitor.php @@ -2,6 +2,7 @@ namespace App\Serializer; +use App\DataTransferObject\FileWithName; use App\Entity\ContestProblem; use App\Service\DOMJudgeService; use App\Service\EventLogService; @@ -26,36 +27,20 @@ public static function getSubscribedEvents(): array { return [ [ - 'event' => Events::POST_SERIALIZE, + 'event' => Events::PRE_SERIALIZE, 'class' => ContestProblem::class, 'format' => 'json', - 'method' => 'onPostSerialize' + 'method' => 'onPreSerialize' ], ]; } - public function onPostSerialize(ObjectEvent $event): void + public function onPreSerialize(ObjectEvent $event): void { /** @var JsonSerializationVisitor $visitor */ $visitor = $event->getVisitor(); /** @var ContestProblem $contestProblem */ $contestProblem = $event->getObject(); - if ($contestProblem->getColor() && ($hex = Utils::convertToHex($contestProblem->getColor()))) { - $property = new StaticPropertyMetadata( - ContestProblem::class, - 'rgb', - null - ); - $visitor->visitProperty($property, $hex); - } - if ($contestProblem->getColor() && ($color = Utils::convertToColor($contestProblem->getColor()))) { - $property = new StaticPropertyMetadata( - ContestProblem::class, - 'color', - null - ); - $visitor->visitProperty($property, $color); - } // Problem statement if ($contestProblem->getProblem()->getProblemtextType() === 'pdf') { @@ -66,18 +51,13 @@ public function onPostSerialize(ObjectEvent $event): void 'id' => $contestProblem->getApiId($this->eventLogService), ] ); - $property = new StaticPropertyMetadata( - ContestProblem::class, - 'statement', - null - ); - $visitor->visitProperty($property, [ - [ - 'href' => $route, - 'mime' => 'application/pdf', - 'filename' => $contestProblem->getShortname() . '.pdf', - ] - ]); + $contestProblem->getProblem()->setStatementForApi(new FileWithName( + $route, + 'application/pdf', + $contestProblem->getShortname() . '.pdf' + )); + } else { + $contestProblem->getProblem()->setStatementForApi(); } } } diff --git a/webapp/src/Serializer/ContestVisitor.php b/webapp/src/Serializer/ContestVisitor.php index 8447eefdce..6614b1c724 100644 --- a/webapp/src/Serializer/ContestVisitor.php +++ b/webapp/src/Serializer/ContestVisitor.php @@ -2,6 +2,7 @@ namespace App\Serializer; +use App\DataTransferObject\ImageFile; use App\Entity\Contest; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; @@ -10,7 +11,6 @@ use JMS\Serializer\EventDispatcher\Events; use JMS\Serializer\EventDispatcher\EventSubscriberInterface; use JMS\Serializer\EventDispatcher\ObjectEvent; -use JMS\Serializer\JsonSerializationVisitor; use JMS\Serializer\Metadata\StaticPropertyMetadata; class ContestVisitor implements EventSubscriberInterface @@ -28,18 +28,16 @@ public static function getSubscribedEvents(): array { return [ [ - 'event' => Events::POST_SERIALIZE, - 'class' => Contest::class, + 'event' => Events::PRE_SERIALIZE, + 'class' => Contest::class, 'format' => 'json', - 'method' => 'onPostSerialize' + 'method' => 'onPreSerialize', ], ]; } - public function onPostSerialize(ObjectEvent $event): void + public function onPreSerialize(ObjectEvent $event): void { - /** @var JsonSerializationVisitor $visitor */ - $visitor = $event->getVisitor(); /** @var Contest $contest */ $contest = $event->getObject(); @@ -48,33 +46,28 @@ public function onPostSerialize(ObjectEvent $event): void 'penalty_time', null ); - $visitor->visitProperty($property, (int)$this->config->get('penalty_time')); + $contest->setPenaltyTimeForApi((int)$this->config->get('penalty_time')); $id = $contest->getApiId($this->eventLogService); // Banner if ($banner = $this->dj->assetPath($id, 'contest', true)) { $imageSize = Utils::getImageSize($banner); - $parts = explode('.', $banner); + $parts = explode('.', $banner); $extension = $parts[count($parts) - 1]; $route = $this->dj->apiRelativeUrl( 'v4_contest_banner', ['cid' => $id] ); - $property = new StaticPropertyMetadata( - Contest::class, - 'banner', - null - ); - $visitor->visitProperty($property, [ - [ - 'href' => $route, - 'mime' => mime_content_type($banner), - 'width' => $imageSize[0], - 'height' => $imageSize[1], - 'filename' => 'banner.' . $extension, - ] - ]); + $contest->setBannerForApi(new ImageFile( + href: $route, + mime: mime_content_type($banner), + filename: 'banner.' . $extension, + width: $imageSize[0], + height: $imageSize[1], + )); + } else { + $contest->setBannerForApi(); } } } diff --git a/webapp/src/Serializer/SubmissionVisitor.php b/webapp/src/Serializer/SubmissionVisitor.php index 4e1776b89f..45d45b8ea9 100644 --- a/webapp/src/Serializer/SubmissionVisitor.php +++ b/webapp/src/Serializer/SubmissionVisitor.php @@ -2,6 +2,8 @@ namespace App\Serializer; +use App\DataTransferObject\BaseFile; +use App\DataTransferObject\FileWithName; use App\Entity\Submission; use App\Service\DOMJudgeService; use App\Service\EventLogService; @@ -27,21 +29,19 @@ public static function getSubscribedEvents(): array { return [ [ - 'event' => Events::POST_SERIALIZE, + 'event' => Events::PRE_SERIALIZE, 'class' => Submission::class, 'format' => 'json', - 'method' => 'onPostSerialize' + 'method' => 'onPreSerialize', ], ]; } - public function onPostSerialize(ObjectEvent $event): void + public function onPreSerialize(ObjectEvent $event): void { + /** @var Submission $submission */ + $submission = $event->getObject(); if ($this->dj->checkrole('api_source_reader')) { - /** @var JsonSerializationVisitor $visitor */ - $visitor = $event->getVisitor(); - /** @var Submission $submission */ - $submission = $event->getObject(); $route = $this->dj->apiRelativeUrl( 'v4_submission_files', [ @@ -54,13 +54,13 @@ public function onPostSerialize(ObjectEvent $event): void 'files', null ); - $visitor->visitProperty($property, [ - [ - 'href' => $route, - 'mime' => 'application/zip', - 'filename' => 'submission.zip', - ] - ]); + $submission->setFileForApi(new FileWithName( + href: $route, + mime: 'application/zip', + filename: 'submission.zip', + )); + } else { + $submission->setFileForApi(); } } } diff --git a/webapp/src/Serializer/TeamAffiliationVisitor.php b/webapp/src/Serializer/TeamAffiliationVisitor.php index 2de036c6d4..98fe4ab536 100644 --- a/webapp/src/Serializer/TeamAffiliationVisitor.php +++ b/webapp/src/Serializer/TeamAffiliationVisitor.php @@ -2,6 +2,7 @@ namespace App\Serializer; +use App\DataTransferObject\ImageFile; use App\Entity\TeamAffiliation; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; @@ -10,10 +11,7 @@ use JMS\Serializer\EventDispatcher\Events; use JMS\Serializer\EventDispatcher\EventSubscriberInterface; use JMS\Serializer\EventDispatcher\ObjectEvent; -use JMS\Serializer\JsonSerializationVisitor; -use JMS\Serializer\Metadata\StaticPropertyMetadata; use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\Intl\Countries; class TeamAffiliationVisitor implements EventSubscriberInterface { @@ -31,18 +29,16 @@ public static function getSubscribedEvents(): array { return [ [ - 'event' => Events::POST_SERIALIZE, - 'class' => TeamAffiliation::class, + 'event' => Events::PRE_SERIALIZE, + 'class' => TeamAffiliation::class, 'format' => 'json', - 'method' => 'onPostSerialize' + 'method' => 'onPreSerialize', ], ]; } - public function onPostSerialize(ObjectEvent $event): void + public function onPreSerialize(ObjectEvent $event): void { - /** @var JsonSerializationVisitor $visitor */ - $visitor = $event->getVisitor(); /** @var TeamAffiliation $affiliation */ $affiliation = $event->getObject(); @@ -58,30 +54,30 @@ public function onPostSerialize(ObjectEvent $event): void ]; foreach ($countryFlagSizes as $size => $viewBoxSize) { - $route = $this->dj->apiRelativeUrl( - 'v4_app_api_generalinfo_countryflag', ['countryCode' => $affiliation->getCountry(), 'size' => $size] + $route = $this->dj->apiRelativeUrl( + 'v4_app_api_generalinfo_countryflag', [ + 'countryCode' => $affiliation->getCountry(), + 'size' => $size, + ] + ); + $countryFlags[] = new ImageFile( + href: $route, + mime: 'image/svg+xml', + filename: 'country-flag-' . $size . '.svg', + width: $viewBoxSize[0], + height: $viewBoxSize[1], ); - $countryFlags[] = [ - 'href' => $route, - 'mime' => 'image/svg+xml', - 'width' => $viewBoxSize[0], - 'height' => $viewBoxSize[1], - 'filename' => 'country-flag-' . $size . '.svg', - ]; } - $property = new StaticPropertyMetadata( - TeamAffiliation::class, - 'country_flag', - null - ); - $visitor->visitProperty($property, $countryFlags); + $affiliation->setCountryFlagForApi($countryFlags); + } else { + $affiliation->setCountryFlagForApi(); } // Affiliation logo if ($affiliationLogo = $this->dj->assetPath((string)$id, 'affiliation', true)) { $imageSize = Utils::getImageSize($affiliationLogo); - $parts = explode('.', $affiliationLogo); + $parts = explode('.', $affiliationLogo); $extension = $parts[count($parts) - 1]; if ($cid = $this->requestStack->getCurrentRequest()->attributes->get('cid')) { @@ -89,26 +85,21 @@ public function onPostSerialize(ObjectEvent $event): void 'v4_organization_logo', [ 'cid' => $cid, - 'id' => $id, + 'id' => $id, ] ); } else { $route = $this->dj->apiRelativeUrl('v4_no_contest_organization_logo', ['id' => $id]); } - $property = new StaticPropertyMetadata( - TeamAffiliation::class, - 'logo', - null - ); - $visitor->visitProperty($property, [ - [ - 'href' => $route, - 'mime' => mime_content_type($affiliationLogo), - 'width' => $imageSize[0], - 'height' => $imageSize[1], - 'filename' => 'logo.' . $extension, - ] - ]); + $affiliation->setLogoForApi(new ImageFile( + href: $route, + mime: mime_content_type($affiliationLogo), + filename: 'logo.' . $extension, + width: $imageSize[0], + height: $imageSize[1], + )); + } else { + $affiliation->setLogoForApi(); } } } diff --git a/webapp/src/Serializer/TeamVisitor.php b/webapp/src/Serializer/TeamVisitor.php index e7ecb3e5a0..2b8edf0a5f 100644 --- a/webapp/src/Serializer/TeamVisitor.php +++ b/webapp/src/Serializer/TeamVisitor.php @@ -2,6 +2,7 @@ namespace App\Serializer; +use App\DataTransferObject\ImageFile; use App\Entity\Team; use App\Service\DOMJudgeService; use App\Service\EventLogService; @@ -28,10 +29,16 @@ public static function getSubscribedEvents(): array { return [ [ - 'event' => Events::POST_SERIALIZE, - 'class' => Team::class, + 'event' => Events::POST_SERIALIZE, + 'class' => Team::class, 'format' => 'json', - 'method' => 'onPostSerialize' + 'method' => 'onPostSerialize', + ], + [ + 'event' => Events::PRE_SERIALIZE, + 'class' => Team::class, + 'format' => 'json', + 'method' => 'onPreSerialize', ], ]; } @@ -52,15 +59,22 @@ public function onPostSerialize(ObjectEvent $event): void ); $visitor->visitProperty($property, $team->getApiId($this->eventLogService)); } + } + + public function onPreSerialize(ObjectEvent $event): void + { + /** @var Team $team */ + $team = $event->getObject(); $id = $team->getApiId($this->eventLogService); // Check if the asset actually exists if (!($teamPhoto = $this->dj->assetPath($id, 'team', true))) { + $team->setPhotoForApi(); return; } - $parts = explode('.', $teamPhoto); + $parts = explode('.', $teamPhoto); $extension = $parts[count($parts) - 1]; $imageSize = Utils::getImageSize($teamPhoto); @@ -70,25 +84,18 @@ public function onPostSerialize(ObjectEvent $event): void 'v4_team_photo', [ 'cid' => $cid, - 'id' => $id, + 'id' => $id, ] ); } else { - $route = $this->dj->apiRelativeUrl('v4_no_contest_team_photo', ['id' => $id,]); + $route = $this->dj->apiRelativeUrl('v4_no_contest_team_photo', ['id' => $id]); } - $property = new StaticPropertyMetadata( - Team::class, - 'photo', - null - ); - $visitor->visitProperty($property, [ - [ - 'href' => $route, - 'mime' => mime_content_type($teamPhoto), - 'width' => $imageSize[0], - 'height' => $imageSize[1], - 'filename' => 'photo.' . $extension - ] - ]); + $team->setPhotoForApi(new ImageFile( + href: $route, + mime: mime_content_type($teamPhoto), + filename: 'photo.' . $extension, + width: $imageSize[0], + height: $imageSize[1] + )); } } diff --git a/webapp/src/Service/AwardService.php b/webapp/src/Service/AwardService.php index 4b956de8c2..c01acdabe1 100644 --- a/webapp/src/Service/AwardService.php +++ b/webapp/src/Service/AwardService.php @@ -2,13 +2,14 @@ namespace App\Service; +use App\DataTransferObject\Award; use App\Entity\Contest; use App\Entity\Team; use App\Utils\Scoreboard\Scoreboard; class AwardService { - /** @var array $awardCache */ + /** @var array */ protected array $awardCache = []; public function __construct(protected readonly EventLogService $eventLogService) @@ -38,21 +39,19 @@ protected function loadAwards(Contest $contest, Scoreboard $scoreboard): void $results = []; foreach ($group_winners as $id => $team_ids) { $type = 'group-winner-' . $id; - $result = [ - 'id' => $type, - 'citation' => 'Winner(s) of group ' . $groups[$id], - 'team_ids' => $team_ids - ]; - $results[] = $result; + $results[] = new Award( + id: $type, + citation: 'Winner(s) of group ' . $groups[$id], + teamIds: $team_ids + ); } foreach ($problem_winners as $id => $team_ids) { $type = 'first-to-solve-' . $id; - $result = [ - 'id' => $type, - 'citation' => 'First to solve problem ' . $problem_shortname[$id], - 'team_ids' => $team_ids - ]; - $results[] = $result; + $results[] = new Award( + id: $type, + citation: 'First to solve problem ' . $problem_shortname[$id], + teamIds: $team_ids + ); } $overall_winners = $medal_winners = []; @@ -95,41 +94,44 @@ protected function loadAwards(Contest $contest, Scoreboard $scoreboard): void } if (count($overall_winners) > 0) { $type = 'winner'; - $result = [ - 'id' => $type, - 'citation' => 'Contest winner', - 'team_ids' => $overall_winners - ]; - $results[] = $result; + $results[] = new Award( + id: $type, + citation: 'Contest winner', + teamIds: $overall_winners + ); } foreach ($medal_winners as $metal => $team_ids) { $type = $metal . '-medal'; - $result = [ - 'id' => $type, - 'citation' => ucfirst($metal) . ' medal winner', - 'team_ids' => $team_ids - ]; - $results[] = $result; + $results[] = new Award( + id: $type, + citation: ucfirst($metal) . ' medal winner', + teamIds: $team_ids + ); } $this->awardCache[$contest->getCid()] = $results; } /** - * @return array|array{id: string, citation: string, team_ids: string[]}|null + * @return Award[] */ - public function getAwards(Contest $contest, Scoreboard $scoreboard, string $requestedType = null): ?array + public function getAwards(Contest $contest, Scoreboard $scoreboard): array { if (!isset($this->awardCache[$contest->getCid()])) { $this->loadAwards($contest, $scoreboard); } - if ($requestedType === null) { - return $this->awardCache[$contest->getCid()]; + return $this->awardCache[$contest->getCid()]; + } + + public function getAward(Contest $contest, Scoreboard $scoreboard, string $requestedType): ?Award + { + if (!isset($this->awardCache[$contest->getCid()])) { + $this->loadAwards($contest, $scoreboard); } foreach ($this->awardCache[$contest->getCid()] as $award) { - if ($award['id'] == $requestedType) { + if ($award->id == $requestedType) { return $award; } } @@ -146,11 +148,11 @@ public function medalType(Team $team, Contest $contest, Scoreboard $scoreboard): $awards = $this->awardCache[$contest->getCid()]; $awardsById = []; foreach ($awards as $award) { - $awardsById[$award['id']] = $award; + $awardsById[$award->id] = $award; } $medalNames = ['gold-medal', 'silver-medal', 'bronze-medal']; foreach ($medalNames as $medalName) { - if (in_array($teamid, $awardsById[$medalName]['team_ids'] ?? [])) { + if (in_array($teamid, $awardsById[$medalName]->teamIds ?? [])) { return $medalName; } } diff --git a/webapp/src/Service/BalloonService.php b/webapp/src/Service/BalloonService.php index 383fbabe9e..38bfcbca8b 100644 --- a/webapp/src/Service/BalloonService.php +++ b/webapp/src/Service/BalloonService.php @@ -80,7 +80,7 @@ public function updateBalloons( /** * @return array, * awards: string, done: bool}}> */ diff --git a/webapp/src/Service/CheckConfigService.php b/webapp/src/Service/CheckConfigService.php index 1c978170e9..db70509ac0 100644 --- a/webapp/src/Service/CheckConfigService.php +++ b/webapp/src/Service/CheckConfigService.php @@ -2,6 +2,7 @@ namespace App\Service; +use App\DataTransferObject\ConfigCheckItem; use App\Entity\ContestProblem; use App\Entity\Executable; use App\Entity\Language; @@ -40,7 +41,7 @@ public function __construct( } /** - * @return array> + * @return array> */ public function runAll(): array { @@ -107,25 +108,21 @@ public function runAll(): array return $results; } - /** - * @return array{caption: string, result: string, desc: string} - */ - public function checkPhpVersion(): array + public function checkPhpVersion(): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); $my = PHP_VERSION; $req = '8.1.0'; $result = version_compare($my, $req, '>='); $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'PHP version', - 'result' => ($result ? 'O' : 'E'), - 'desc' => sprintf('You have PHP version %s. The minimum required is %s', $my, $req)]; + return new ConfigCheckItem( + caption: 'PHP version', + result: ($result ? 'O' : 'E'), + desc: sprintf('You have PHP version %s. The minimum required is %s', $my, $req) + ); } - /** - * @return array{caption: string, result: string, desc: string} - */ - public function checkPhpExtensions(): array + public function checkPhpExtensions(): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); $required = ['json', 'mbstring', 'mysqli', 'zip', 'gd', 'intl']; @@ -140,15 +137,14 @@ public function checkPhpExtensions(): array $remark = ($remark ?: 'All required and recommended extensions present.'); $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'PHP extensions', - 'result' => $state, - 'desc' => $remark]; + return new ConfigCheckItem( + caption: 'PHP extensions', + result: $state, + desc: $remark + ); } - /** - * @return array{caption: string, result: string, desc: string} - */ - public function checkPhpSettings(): array + public function checkPhpSettings(): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); $sourcefiles_limit = $this->config->get('sourcefiles_limit'); @@ -189,15 +185,14 @@ public function checkPhpSettings(): array } $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'PHP settings', - 'result' => $result, - 'desc' => $desc]; + return new ConfigCheckItem( + caption: 'PHP settings', + result: $result, + desc: $desc + ); } - /** - * @return array{caption: string, result: string, desc: string} - */ - public function checkMysqlSettings(): array + public function checkMysqlSettings(): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); $r = $this->em->getConnection()->fetchAllAssociative( @@ -251,15 +246,14 @@ public function checkMysqlSettings(): array } $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'MySQL settings', - 'result' => $result, - 'desc' => $desc ?: 'MySQL settings are all ok']; + return new ConfigCheckItem( + caption: 'MySQL settings', + result: $result, + desc: $desc ?: 'MySQL settings are all ok' + ); } - /** - * @return array{caption: string, result: string, desc: string} - */ - public function checkAdminPass(): array + public function checkAdminPass(): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); $res = 'O'; @@ -272,15 +266,14 @@ public function checkAdminPass(): array } $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'Non-default admin password', - 'result' => $res, - 'desc' => $desc]; + return new ConfigCheckItem( + caption: 'Non-default admin password', + result: $res, + desc: $desc + ); } - /** - * @return array{caption: string, result: string, desc: string} - */ - public function checkDefaultCompareRunExist(): array + public function checkDefaultCompareRunExist(): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); $res = 'O'; @@ -298,15 +291,14 @@ public function checkDefaultCompareRunExist(): array } $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'Default compare and run scripts exist', - 'result' => $res, - 'desc' => $desc]; + return new ConfigCheckItem( + caption: 'Default compare and run scripts exist', + result: $res, + desc: $desc + ); } - /** - * @return array{caption: string, result: string, desc: string} - */ - public function checkScriptFilesizevsMemoryLimit(): array + public function checkScriptFilesizevsMemoryLimit(): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); if ($this->config->get('script_filesize_limit') <= @@ -316,55 +308,58 @@ public function checkScriptFilesizevsMemoryLimit(): array $result = 'O'; } $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'Compile file size vs. memory limit', - 'result' => $result, - 'desc' => 'If the script filesize limit is lower than the memory limit, then ' . - 'compilation of sources that statically allocate memory may fail. We ' . - 'recommend to include a margin to be on the safe side. The current ' . - '"script_filesize_limit" = ' . $this->config->get('script_filesize_limit') . ' ' . - 'while "memory_limit" = ' . $this->config->get('memory_limit') . '.' - ]; + return new ConfigCheckItem( + caption: 'Compile file size vs. memory limit', + result: $result, + desc: 'If the script filesize limit is lower than the memory limit, then ' . + 'compilation of sources that statically allocate memory may fail. We ' . + 'recommend to include a margin to be on the safe side. The current ' . + '"script_filesize_limit" = ' . $this->config->get('script_filesize_limit') . ' ' . + 'while "memory_limit" = ' . $this->config->get('memory_limit') . '.' + ); } - /** - * @return array{caption: string, result: string, desc: string} - */ - public function checkDebugDisabled(): array + public function checkDebugDisabled(): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); if ($this->debug) { $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'Debugging', - 'result' => 'W', - 'desc' => "Debugging enabled.\nShould not be enabled on live systems."]; + return new ConfigCheckItem( + caption: 'Debugging', + result: 'W', + desc: "Debugging enabled.\nShould not be enabled on live systems." + ); } $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'Debugging', - 'result' => 'O', - 'desc' => 'Debugging disabled.']; + return new ConfigCheckItem( + caption: 'Debugging', + result: 'O', + desc: 'Debugging disabled.' + ); } - /** - * @return array{caption: string, result: string, desc: string} - */ - public function checkTmpdirWritable(): array + public function checkTmpdirWritable(): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); $tmpdir = $this->dj->getDomjudgeTmpDir(); if (is_writable($tmpdir)) { $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'TMPDIR writable', - 'result' => 'O', - 'desc' => sprintf('TMPDIR (%s) can be used to store temporary ' . - 'files for submission diffs and edits.', - $tmpdir)]; + return new ConfigCheckItem( + caption: 'TMPDIR writable', + result: 'O', + desc: sprintf('TMPDIR (%s) can be used to store temporary ' . + 'files for submission diffs and edits.', + $tmpdir) + ); } $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'TMPDIR writable', - 'result' => 'W', - 'desc' => sprintf('TMPDIR (%s) is not writable by the webserver; ' . - 'Showing diffs and editing of submissions may not work.', - $tmpdir)]; + return new ConfigCheckItem( + caption: 'TMPDIR writable', + result: 'W', + desc: sprintf('TMPDIR (%s) is not writable by the webserver; ' . + 'Showing diffs and editing of submissions may not work.', + $tmpdir) + ); } private function randomString(int $length): string @@ -378,10 +373,7 @@ private function randomString(int $length): string return $randomString; } - /** - * @return array{caption: string, result: string, desc: string} - */ - public function checkHashTime(): array + public function checkHashTime(): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); $tmp_user = new User(); @@ -397,49 +389,53 @@ public function checkHashTime(): array if ($counter>300) { $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'User password hashing', - 'result' => 'W', - 'desc' => sprintf('Hashing is too simple for small sized contests (Did %d hashes).', $counter)]; + return new ConfigCheckItem( + caption: 'User password hashing', + result: 'W', + desc: sprintf('Hashing is too simple for small sized contests (Did %d hashes).', $counter) + ); } if ($counter<100) { $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'User password hashing', - 'result' => 'W', - 'desc' => sprintf('Hashing is too expensive for medium sized contests (%d done).', $counter)]; + return new ConfigCheckItem( + caption: 'User password hashing', + result: 'W', + desc: sprintf('Hashing is too expensive for medium sized contests (%d done).', $counter) + ); } $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'User password hashing', - 'result' => 'O', - 'desc' => sprintf('Hashing cost is reasonable (Did %d hashes).', $counter)]; + return new ConfigCheckItem( + caption: 'User password hashing', + result: 'O', + desc: sprintf('Hashing cost is reasonable (Did %d hashes).', $counter) + ); } - /** - * @return array{caption: string, result: string, desc: string} - */ - public function checkContestActive(): array + public function checkContestActive(): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); $contests = $this->dj->getCurrentContests(); if (empty($contests)) { $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'Active contests', - 'result' => 'E', - 'desc' => 'No currently active contests found. System will not function.']; + return new ConfigCheckItem( + caption: 'Active contests', + result: 'E', + desc: 'No currently active contests found. System will not function.' + ); } $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'Active contests', - 'result' => 'O', - 'desc' => 'Currently active contests: ' . - implode(', ', array_map( - fn($contest) => 'c' . $contest->getCid() . ' (' . $contest->getShortname() . ')', - $contests - ))]; + return new ConfigCheckItem( + caption: 'Active contests', + result: 'O', + desc: 'Currently active contests: ' . + implode(', ', array_map( + fn($contest) => 'c' . $contest->getCid() . ' (' . $contest->getShortname() . ')', + $contests + )) + ); } - /** - * @return array{caption: string, result: string, desc: string} - */ - public function checkContestsValidate(): array + public function checkContestsValidate(): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); // Fetch all active and future contests. @@ -472,16 +468,15 @@ public function checkContestsValidate(): array } $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'Contests validation', - 'result' => $result, - 'desc' => "Validated all active and future contests:\n\n" . - ($desc ?: 'No problems found.')]; + return new ConfigCheckItem( + caption: 'Contests validation', + result: $result, + desc: "Validated all active and future contests:\n\n" . + ($desc ?: 'No problems found.') + ); } - /** - * @return array{caption: string, result: string, desc: string} - */ - public function checkContestBanners(): array + public function checkContestBanners(): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); // Fetch all active and future contests. @@ -514,15 +509,14 @@ public function checkContestBanners(): array $desc = $desc ?: 'Everything OK'; $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'Contest banners', - 'result' => $result, - 'desc' => $desc]; + return new ConfigCheckItem( + caption: 'Contest banners', + result: $result, + desc: $desc + ); } - /** - * @return array{caption: string, result: string, desc: string} - */ - public function checkProblemsValidate(): array + public function checkProblemsValidate(): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); $problems = $this->em->getRepository(Problem::class)->findAll(); @@ -606,16 +600,15 @@ public function checkProblemsValidate(): array } $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'Problems validation', - 'result' => $result, - 'desc' => "Validated all problems:\n\n" . - ($desc ?: 'No problems with problems found.')]; + return new ConfigCheckItem( + caption: 'Problems validation', + result: $result, + desc: "Validated all problems:\n\n" . + ($desc ?: 'No problems with problems found.') + ); } - /** - * @return array{caption: string, result: string, desc: string} - */ - public function checkLanguagesValidate(): array + public function checkLanguagesValidate(): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); $languages = $this->em->getRepository(Language::class)->findAll(); @@ -660,16 +653,15 @@ public function checkLanguagesValidate(): array } $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'Languages validation', - 'result' => $result, - 'desc' => "Validated all languages:\n\n" . - ($desc ?: 'No languages with problems found.')]; + return new ConfigCheckItem( + caption: 'Languages validation', + result: $result, + desc: "Validated all languages:\n\n" . + ($desc ?: 'No languages with problems found.') + ); } - /** - * @return array{caption: string, result: string, desc: string} - */ - public function checkTeamPhotos(): array + public function checkTeamPhotos(): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); /** @var Team[] $teams */ @@ -690,24 +682,25 @@ public function checkTeamPhotos(): array $desc = $desc ?: 'Everything OK'; $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'Team photos', - 'result' => $result, - 'desc' => $desc]; + return new ConfigCheckItem( + caption: 'Team photos', + result: $result, + desc: $desc + ); } - /** - * @return array{caption: string, result: string, desc: string} - */ - public function checkAffiliations(): array + public function checkAffiliations(): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); $show_logos = $this->config->get('show_affiliation_logos'); if (!$show_logos) { $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'Team affiliations', - 'result' => 'O', - 'desc' => 'Affiliations display disabled, skipping checks']; + return new ConfigCheckItem( + caption: 'Team affiliations', + result: 'O', + desc: 'Affiliations display disabled, skipping checks' + ); } /** @var TeamAffiliation[] $affils */ @@ -750,15 +743,14 @@ public function checkAffiliations(): array $desc = $desc ?: 'Everything OK'; $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'Team affiliations', - 'result' => $result, - 'desc' => $desc]; + return new ConfigCheckItem( + caption: 'Team affiliations', + result: $result, + desc: $desc + ); } - /** - * @return array{caption: string, result: string, desc: string} - */ - public function checkTeamDuplicateNames(): array + public function checkTeamDuplicateNames(): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); $teams = $this->em->getRepository(Team::class)->findAll(); @@ -779,15 +771,14 @@ public function checkTeamDuplicateNames(): array $desc = $desc ?: 'Every team name is unique'; $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'Team name uniqueness', - 'result' => $result, - 'desc' => $desc]; + return new ConfigCheckItem( + caption: 'Team name uniqueness', + result: $result, + desc: $desc + ); } - /** - * @return array{caption: string, result: string, desc: string} - */ - public function checkSelfRegistration(): array + public function checkSelfRegistration(): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); $result = 'O'; @@ -814,13 +805,15 @@ public function checkSelfRegistration(): array } $this->stopwatch->stop(__FUNCTION__); - return ['caption' => 'Self-registration', - 'result' => $result, - 'desc' => $desc]; + return new ConfigCheckItem( + caption: 'Self-registration', + result: $result, + desc: $desc + ); } /** - * @return array + * @return ConfigCheckItem[] */ public function checkAllExternalIdentifiers(): array { @@ -853,9 +846,8 @@ public function checkAllExternalIdentifiers(): array /** * @param class-string $class - * @return array{caption: string, result: string, desc: string, escape: bool} */ - protected function checkExternalIdentifiers(string $class, string $externalIdField): array + protected function checkExternalIdentifiers(string $class, string $externalIdField): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); $parts = explode('\\', $class); @@ -903,12 +895,12 @@ protected function checkExternalIdentifiers(string $class, string $externalIdFie } $this->stopwatch->stop(__FUNCTION__); - return [ - 'caption' => ucfirst($inflector->pluralize(str_replace('_', ' ', $inflector->tableize($entityType)))), - 'result' => $result, - 'desc' => $description, - 'escape' => false, - ]; + return new ConfigCheckItem( + caption: ucfirst($inflector->pluralize(str_replace('_', ' ', $inflector->tableize($entityType)))), + result: $result, + desc: $description, + escape: false + ); } public function getStopwatch(): Stopwatch diff --git a/webapp/src/Service/ConfigurationService.php b/webapp/src/Service/ConfigurationService.php index 7d46b3cfbb..65457f168a 100644 --- a/webapp/src/Service/ConfigurationService.php +++ b/webapp/src/Service/ConfigurationService.php @@ -3,9 +3,11 @@ namespace App\Service; use App\Config\Loader\YamlConfigLoader; +use App\DataTransferObject\ConfigurationSpecification; use App\Entity\Configuration; use App\Entity\Executable; use App\Entity\Judging; +use BackedEnum; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; use InvalidArgumentException; @@ -51,18 +53,18 @@ public function get(string $name, bool $onlyIfPublic = false) { $spec = $this->getConfigSpecification()[$name] ?? null; - if (!isset($spec) || ($onlyIfPublic && !$spec['public'])) { + if (!isset($spec) || ($onlyIfPublic && !$spec->public)) { throw new InvalidArgumentException("Configuration variable '$name' not found."); } - $value = $this->getDbValues()[$name] ?? $spec['default_value']; + $value = $this->getDbValues()[$name] ?? $spec->defaultValue; - if (isset($spec['enum_class'])) { - if (!class_exists($spec['enum_class'])) { - throw new InvalidArgumentException("Enum class '$spec[enum_class]' not found."); + if (isset($spec->enumClass)) { + if (!class_exists($spec->enumClass)) { + throw new InvalidArgumentException("Enum class '$spec->enumClass' not found."); } - return call_user_func($spec['enum_class'] . '::from', $value); + return call_user_func($spec->enumClass . '::from', $value); } return $value; @@ -79,8 +81,8 @@ public function all(bool $onlyIfPublic = false): array $specs = $this->getConfigSpecification(); $result = []; foreach ($specs as $name => $spec) { - if ($spec['public'] || !$onlyIfPublic) { - $result[$name] = $spec['default_value']; + if ($spec->public || !$onlyIfPublic) { + $result[$name] = $spec->defaultValue; } } @@ -103,12 +105,7 @@ public function all(bool $onlyIfPublic = false): array /** * Get all configuration specifications. * - * @return array, key_options?: array, - * value_options?: array}> + * @return ConfigurationSpecification[] */ public function getConfigSpecification(): array { @@ -146,7 +143,10 @@ function (ConfigCacheInterface $cache) { // @codeCoverageIgnoreEnd }); - return require $cacheFile; + return array_map( + fn(array $item) => ConfigurationSpecification::fromArray($item), + require $cacheFile + ); } /** @@ -179,7 +179,7 @@ public function saveChanges( $errors = []; $logUnverifiedJudgings = false; foreach ($specs as $specName => $spec) { - $oldValue = $spec['default_value']; + $oldValue = $spec->defaultValue; if (isset($options[$specName])) { $optionToSet = $options[$specName]; $oldValue = $optionToSet->getValue(); @@ -191,11 +191,11 @@ public function saveChanges( $options[$specName] = $optionToSet; } if (!array_key_exists($specName, $dataToSet)) { - if ($spec['type'] == 'bool') { + if ($spec->type == 'bool') { // Special-case bool, since checkboxes don't return a // value when unset. $val = false; - } elseif ($spec['type'] == 'array_val' && isset($spec['options'])) { + } elseif ($spec->type == 'array_val' && isset($spec->options)) { // Special-case array_val with options, since multiselects // don't return a value when unset. $val = []; @@ -206,9 +206,9 @@ public function saveChanges( $val = $dataToSet[$specName]; } - if (isset($spec['regex'])) { - if (preg_match($spec['regex'], (string)$val) === 0) { - $errors[$specName] = $spec['error_message'] ?? 'This is not a valid value'; + if (isset($spec->regex)) { + if (preg_match($spec->regex, (string)$val) === 0) { + $errors[$specName] = $spec->errorMessage ?? 'This is not a valid value'; } } @@ -221,7 +221,7 @@ public function saveChanges( // since it will invalidate Doctrine entities. $logUnverifiedJudgings = true; } - switch ($spec['type']) { + switch ($spec->type) { case 'bool': $optionToSet->setValue((bool)$val); break; @@ -258,7 +258,7 @@ public function saveChanges( default: $this->logger->warning( "configuration option '%s' has unknown type '%s'", - [ $specName, $spec['type'] ] + [ $specName, $spec->type ] ); } if (!isset($errors[$specName])) { @@ -349,53 +349,41 @@ private function findExecutableOptions(string $type): array * * This method is used to add predefined options that need to be loaded * from the database to certain items. - * - * @param array{name: string, type: string, default_value: bool, public: bool, - * description: string, category: string, default_value: bool|int, - * regex?: string, key_placeholder: string, value_placeholder: string, - * error_message?: string, docdescription?: string, enum_class?: string, - * options?: array} $item - * @return array{name: string, type: string, default_value: bool, public: bool, - * description: string, category: string, default_value: bool|int|bool, - * regex?: string, key_placeholder: string, value_placeholder: string, - * error_message?: string, docdescription?: string, enum_class?: string, - * options?: array, key_options?: array, - * value_options?: array} */ - public function addOptions(array $item): array + public function addOptions(ConfigurationSpecification $item): ConfigurationSpecification { - switch ($item['name']) { + switch ($item->name) { case 'default_compare': - $item['options'] = $this->findExecutableOptions('compare'); + $item->options = $this->findExecutableOptions('compare'); break; case 'default_run': - $item['options'] = $this->findExecutableOptions('run'); + $item->options = $this->findExecutableOptions('run'); break; case 'default_full_debug': - $item['options'] = $this->findExecutableOptions('debug'); + $item->options = $this->findExecutableOptions('debug'); break; case 'results_prio': case 'results_remap': $verdictsConfig = $this->etcDir . '/verdicts.php'; $verdicts = include $verdictsConfig; - $item['key_options'] = ['' => '']; + $item->keyOptions = ['' => '']; foreach (array_keys($verdicts) as $verdict) { - $item['key_options'][$verdict] = $verdict; + $item->keyOptions[$verdict] = $verdict; } - if ($item['name'] === 'results_remap') { - $item['value_options'] = $item['key_options']; + if ($item->name === 'results_remap') { + $item->valueOptions = $item->keyOptions; } } - if ($item['type'] === 'enum') { - $enumClass = $item['enum_class']; - /** @var \BackedEnum[] $cases */ + if ($item->type === 'enum') { + $enumClass = $item->enumClass; + /** @var BackedEnum[] $cases */ $cases = call_user_func($enumClass . '::cases'); foreach ($cases as $case) { if (method_exists($case, 'getConfigDescription')) { - $item['options'][$case->value] = $case->getConfigDescription(); + $item->options[$case->value] = $case->getConfigDescription(); } else { - $item['options'][$case->value] = $case->name; + $item->options[$case->value] = $case->name; } } } diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index 0cdf4fcd37..ff18d37463 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -2,6 +2,7 @@ namespace App\Service; +use App\DataTransferObject\ContestStatus; use App\Doctrine\DBAL\Types\JudgeTaskType; use App\Entity\AssetEntityInterface; use App\Entity\AuditLog; @@ -930,19 +931,17 @@ public function getAttachmentStreamedResponse(ContestProblem $contestProblem, in /** * @throws NoResultException * @throws NonUniqueResultException - * @return array */ - public function getContestStats(Contest $contest): array + public function getContestStats(Contest $contest): ContestStatus { - $stats = []; - $stats['num_submissions'] = (int)$this->em + $numSubmissions = (int)$this->em ->createQuery( 'SELECT COUNT(s) FROM App\Entity\Submission s WHERE s.contest = :cid') ->setParameter('cid', $contest->getCid()) ->getSingleScalarResult(); - $stats['num_queued'] = (int)$this->em + $numQueued = (int)$this->em ->createQuery( 'SELECT COUNT(s) FROM App\Entity\Submission s @@ -952,7 +951,7 @@ public function getContestStats(Contest $contest): array AND s.valid = 1') ->setParameter('cid', $contest->getCid()) ->getSingleScalarResult(); - $stats['num_judging'] = (int)$this->em + $numJudging = (int)$this->em ->createQuery( 'SELECT COUNT(s) FROM App\Entity\Submission s @@ -963,7 +962,7 @@ public function getContestStats(Contest $contest): array AND s.valid = 1') ->setParameter('cid', $contest->getCid()) ->getSingleScalarResult(); - return $stats; + return new ContestStatus($numSubmissions, $numQueued, $numJudging); } /** diff --git a/webapp/src/Service/SubmissionService.php b/webapp/src/Service/SubmissionService.php index 7a82aca82e..bc9e7a02ba 100644 --- a/webapp/src/Service/SubmissionService.php +++ b/webapp/src/Service/SubmissionService.php @@ -2,6 +2,7 @@ namespace App\Service; +use App\DataTransferObject\SubmissionRestriction; use App\Entity\Contest; use App\Entity\ContestProblem; use App\Entity\JudgeTask; @@ -55,45 +56,7 @@ public function __construct( * Get a list of submissions that can be displayed in the interface using * the submission_list partial. * - * Restrictions can contain the following keys; - * - rejudgingid: ID of a rejudging to filter on - * - verified: If true, only return verified submissions. - * If false, only return unverified or unjudged submissions. - * - judged: If true, only return judged submissions. - * If false, only return unjudged submissions. - * - judging: If true, only return submissions currently being judged - * If false, only return submssions which are already judged or still - * need to be judged - * - rejudgingdiff: If true, only return judgings that differ from their - * original result in final verdict. Vice versa if false. - * - teamid: ID of a team to filter on - * - categoryid: ID of a team category to filter on - * - probid: ID of a problem to filter on - * - langid: ID of a language to filter on - * - judgehost: hostname of a judgehost to filter on - * - old_result: result of old judging to filter on - * - result: result of current judging to filter on - * - userid: filter on specific user - * - visible: If true, only return submissions from visible teams - * When shadowing another system these keys can also be used: - * - external_diff: If true, only return results with a difference with an - * external system. - * If false, only return results without a difference with an - * external system. - * - external_result: result in the external system - * - externally_judged: If true, only return externally judged submissions. - * If false, only return externally unjudged submissions. - * - externally_verified: If true, only return verified submissions. - * If false, only return unverified or unjudged submissions. - * - with_external_id: If true, only return submissions with an external ID. - * * @param Contest[] $contests - * @param array{rejudgingid?: int, verified?: bool, judged?: bool, judging?: bool, - * rejudgingdiff?: bool, teamid?: int, categoryid?: int, - * probid?: string|int|null, langid?: string, judgehost?: string, old_result?: string, - * result?: string, userid?: int, visible?: bool, external_diff?: bool, - * external_result?: string, externally_judged?: bool, - * externally_verified?: bool, with_external_id?: true} $restrictions * * @return array{Submission[], array} array An array with * two elements: the first one is the list of submissions @@ -103,7 +66,7 @@ public function __construct( */ public function getSubmissionList( array $contests, - array $restrictions, + SubmissionRestriction $restrictions, int $limit = 0, bool $showShadowUnverified = false ): array { @@ -125,11 +88,11 @@ public function getSubmissionList( $queryBuilder->setMaxResults($limit); } - if ($restrictions['with_external_id'] ?? false) { + if ($restrictions->withExternalId ?? false) { $queryBuilder->andWhere('s.externalid IS NOT NULL'); } - if (isset($restrictions['rejudgingid'])) { + if (isset($restrictions->rejudgingId)) { $queryBuilder ->leftJoin('s.judgings', 'j', Join::WITH, 'j.rejudging = :rejudgingid') ->leftJoin(Judging::class, 'jold', Join::WITH, @@ -138,20 +101,20 @@ public function getSubmissionList( 'j.original_judging = jold2.judgingid') ->addSelect('COALESCE(jold.result, jold2.result) AS oldresult') ->andWhere('s.rejudging = :rejudgingid OR j.rejudging = :rejudgingid') - ->setParameter('rejudgingid', $restrictions['rejudgingid']); + ->setParameter('rejudgingid', $restrictions->rejudgingId); - if (isset($restrictions['rejudgingdiff'])) { - if ($restrictions['rejudgingdiff']) { + if (isset($restrictions->rejudgingDifference)) { + if ($restrictions->rejudgingDifference) { $queryBuilder->andWhere('j.result != COALESCE(jold.result, jold2.result)'); } else { $queryBuilder->andWhere('j.result = COALESCE(jold.result, jold2.result)'); } } - if (isset($restrictions['old_result'])) { + if (isset($restrictions->oldResult)) { $queryBuilder ->andWhere('COALESCE(jold.result, jold2.result) = :oldresult') - ->setParameter('oldresult', $restrictions['old_result']); + ->setParameter('oldresult', $restrictions->oldResult); } } else { $queryBuilder->leftJoin('s.judgings', 'j', Join::WITH, 'j.valid = 1'); @@ -159,55 +122,55 @@ public function getSubmissionList( $queryBuilder->leftJoin('j.rejudging', 'r'); - if (isset($restrictions['verified'])) { - if ($restrictions['verified']) { + if (isset($restrictions->verified)) { + if ($restrictions->verified) { $queryBuilder->andWhere('j.verified = 1'); } else { $queryBuilder->andWhere('j.verified = 0 OR j.verified IS NULL'); } } - if (isset($restrictions['judged'])) { - if ($restrictions['judged']) { + if (isset($restrictions->judged)) { + if ($restrictions->judged) { $queryBuilder->andWhere('j.result IS NOT NULL'); } else { $queryBuilder->andWhere('j.result IS NULL OR j.endtime IS NULL'); } } - if (isset($restrictions['judging'])) { - if ($restrictions['judging']) { + if (isset($restrictions->judging)) { + if ($restrictions->judging) { $queryBuilder->andWhere('j.starttime IS NOT NULL AND j.result IS NULL'); } else { $queryBuilder->andWhere('j.starttime IS NULL OR j.result IS NOT NULL'); } } - if (isset($restrictions['externally_judged'])) { - if ($restrictions['externally_judged']) { + if (isset($restrictions->externallyJudged)) { + if ($restrictions->externallyJudged) { $queryBuilder->andWhere('ej.result IS NOT NULL'); } else { $queryBuilder->andWhere('ej.result IS NULL OR ej.endtime IS NULL'); } } - if (isset($restrictions['externally_verified'])) { - if ($restrictions['externally_verified']) { + if (isset($restrictions->externallyVerified)) { + if ($restrictions->externallyVerified) { $queryBuilder->andWhere('ej.verified = true'); } else { $queryBuilder->andWhere('ej.verified = false'); } } - if (isset($restrictions['external_diff'])) { - if ($restrictions['external_diff']) { + if (isset($restrictions->externalDifference)) { + if ($restrictions->externalDifference) { $queryBuilder->andWhere('j.result != ej.result'); } else { $queryBuilder->andWhere('j.result = ej.result'); } } - if (isset($restrictions['external_result'])) { - if ($restrictions['external_result'] === 'judging') { + if (isset($restrictions->externalResult)) { + if ($restrictions->externalResult === 'judging') { $queryBuilder->andWhere('ej.result IS NULL or ej.endtime IS NULL'); } else { $queryBuilder @@ -216,59 +179,59 @@ public function getSubmissionList( } } - if (isset($restrictions['teamid'])) { + if (isset($restrictions->teamId)) { $queryBuilder ->andWhere('s.team = :teamid') - ->setParameter('teamid', $restrictions['teamid']); + ->setParameter('teamid', $restrictions->teamId); } - if (isset($restrictions['userid'])) { + if (isset($restrictions->userId)) { $queryBuilder ->andWhere('s.user = :userid') - ->setParameter('userid', $restrictions['userid']); + ->setParameter('userid', $restrictions->userId); } - if (isset($restrictions['categoryid'])) { + if (isset($restrictions->categoryId)) { $queryBuilder ->andWhere('t.category = :categoryid') - ->setParameter('categoryid', $restrictions['categoryid']); + ->setParameter('categoryid', $restrictions->categoryId); } - if (isset($restrictions['visible'])) { + if (isset($restrictions->visible)) { $queryBuilder ->innerJoin('t.category', 'cat') ->andWhere('cat.visible = true'); } - if (isset($restrictions['probid'])) { + if (isset($restrictions->problemId)) { $queryBuilder ->andWhere('s.problem = :probid') - ->setParameter('probid', $restrictions['probid']); + ->setParameter('probid', $restrictions->problemId); } - if (isset($restrictions['langid'])) { + if (isset($restrictions->languageId)) { $queryBuilder ->andWhere('s.language = :langid') - ->setParameter('langid', $restrictions['langid']); + ->setParameter('langid', $restrictions->languageId); } - if (isset($restrictions['judgehost'])) { + if (isset($restrictions->judgehost)) { $queryBuilder ->andWhere('s.judgehost = :judgehost') - ->setParameter('judgehost', $restrictions['judgehost']); + ->setParameter('judgehost', $restrictions->judgehost); } - if (isset($restrictions['result'])) { - if ($restrictions['result'] === 'judging') { + if (isset($restrictions->result)) { + if ($restrictions->result === 'judging') { $queryBuilder ->andWhere('s.importError IS NULL') ->andWhere('j.result IS NULL OR j.endtime IS NULL'); - } elseif ($restrictions['result'] === 'import-error') { + } elseif ($restrictions->result === 'import-error') { $queryBuilder->andWhere('s.importError IS NOT NULL'); } else { $queryBuilder ->andWhere('j.result = :result') - ->setParameter('result', $restrictions['result']); + ->setParameter('result', $restrictions->result); } } @@ -281,7 +244,7 @@ public function getSubmissionList( } $submissions = $queryBuilder->getQuery()->getResult(); - if (isset($restrictions['rejudgingid'])) { + if (isset($restrictions->rejudgingId)) { // Doctrine will return an array for each item. At index '0' will // be the submission and at index 'oldresult' will be the old // result. Remap this. diff --git a/webapp/templates/partials/scoreboard_table.html.twig b/webapp/templates/partials/scoreboard_table.html.twig index 7e2ab04023..3bd18b0d1b 100644 --- a/webapp/templates/partials/scoreboard_table.html.twig +++ b/webapp/templates/partials/scoreboard_table.html.twig @@ -270,7 +270,7 @@ {% set link = null %} {% if jury %} - {% set restrict = {probid: problem.probid} %} + {% set restrict = {problemId: problem.probid} %} {% set link = path('jury_team', {teamId: score.team.teamid, restrict: restrict}) %} {% endif %} diff --git a/webapp/tests/Unit/Controller/API/ContestControllerAdminTest.php b/webapp/tests/Unit/Controller/API/ContestControllerAdminTest.php index 1cdb9bb461..64d3eafadc 100644 --- a/webapp/tests/Unit/Controller/API/ContestControllerAdminTest.php +++ b/webapp/tests/Unit/Controller/API/ContestControllerAdminTest.php @@ -144,9 +144,9 @@ public function testBannerManagement(): void [ 'href' => "contests/$id/banner", 'mime' => 'image/svg+xml', + 'filename' => 'banner.svg', 'width' => 510, 'height' => 1122, - 'filename' => 'banner.svg', ], ]; self::assertSame($bannerConfig, $object['banner']); diff --git a/webapp/tests/Unit/Controller/API/OrganizationControllerTest.php b/webapp/tests/Unit/Controller/API/OrganizationControllerTest.php index 25cc8309c7..8c9676b373 100644 --- a/webapp/tests/Unit/Controller/API/OrganizationControllerTest.php +++ b/webapp/tests/Unit/Controller/API/OrganizationControllerTest.php @@ -145,9 +145,9 @@ public function testLogoManagement(): void [ 'href' => "contests/1/organizations/$id/logo", 'mime' => 'image/png', + 'filename' => 'logo.png', 'width' => 181, 'height' => 101, - 'filename' => 'logo.png', ] ]; self::assertSame($logoConfig, $object['logo']); diff --git a/webapp/tests/Unit/Controller/API/TeamControllerTest.php b/webapp/tests/Unit/Controller/API/TeamControllerTest.php index c84171f6cb..9101e9e324 100644 --- a/webapp/tests/Unit/Controller/API/TeamControllerTest.php +++ b/webapp/tests/Unit/Controller/API/TeamControllerTest.php @@ -54,9 +54,9 @@ public function testLogoManagement(): void [ 'href' => "contests/1/teams/$id/photo", 'mime' => 'image/jpeg', + 'filename' => 'photo.jpg', 'width' => 320, 'height' => 200, - 'filename' => 'photo.jpg', ] ]; self::assertSame($logoConfig, $object['photo']); diff --git a/webapp/tests/Unit/Service/AwardServiceTest.php b/webapp/tests/Unit/Service/AwardServiceTest.php index 8ca908e562..f0bb1dc432 100644 --- a/webapp/tests/Unit/Service/AwardServiceTest.php +++ b/webapp/tests/Unit/Service/AwardServiceTest.php @@ -2,6 +2,7 @@ namespace App\Tests\Unit\Service; +use App\DataTransferObject\Award; use App\Entity\Contest; use App\Entity\ContestProblem; use App\Entity\Problem; @@ -181,17 +182,17 @@ protected function getAwardService(): AwardService return new AwardService($eventLogService); } - protected function getAward(string $label): ?array + protected function getAward(string $label): ?Award { - return $this->getAwardService()->getAwards($this->contest, $this->scoreboard, $label); + return $this->getAwardService()->getAward($this->contest, $this->scoreboard, $label); } public function testWinner(): void { $winner = $this->getAward('winner'); static::assertNotNull($winner); - static::assertEquals('Contest winner', $winner['citation']); - static::assertEquals(['team_A'], $winner['team_ids']); + static::assertEquals('Contest winner', $winner->citation); + static::assertEquals(['team_A'], $winner->teamIds); } public function testMedals(): void @@ -207,8 +208,8 @@ public function testMedals(): void static::assertNull($medalAward); } else { static::assertNotNull($medalAward); - static::assertEquals(ucfirst($medal) . ' medal winner', $medalAward['citation']); - static::assertEquals($teams, $medalAward['team_ids']); + static::assertEquals(ucfirst($medal) . ' medal winner', $medalAward->citation); + static::assertEquals($teams, $medalAward->teamIds); } } } @@ -217,20 +218,20 @@ public function testGroupWinners(): void { $groupAWinner = $this->getAward('group-winner-cat_A'); static::assertNotNull($groupAWinner); - static::assertEquals('Winner(s) of group Category A', $groupAWinner['citation']); - static::assertEquals(['team_A'], $groupAWinner['team_ids']); + static::assertEquals('Winner(s) of group Category A', $groupAWinner->citation); + static::assertEquals(['team_A'], $groupAWinner->teamIds); $a = $this->getAwardService()->getAwards($this->contest, $this->scoreboard); $groupBWinner = $this->getAward('group-winner-cat_B'); static::assertNotNull($groupBWinner); - static::assertEquals('Winner(s) of group Category B', $groupBWinner['citation']); - static::assertEquals(['team_D'], $groupBWinner['team_ids']); + static::assertEquals('Winner(s) of group Category B', $groupBWinner->citation); + static::assertEquals(['team_D'], $groupBWinner->teamIds); $a = $this->getAwardService()->getAwards($this->contest, $this->scoreboard); $groupBWinner = $this->getAward('group-winner-cat_C'); static::assertNotNull($groupBWinner); - static::assertEquals('Winner(s) of group Category C', $groupBWinner['citation']); - static::assertEquals(['team_G'], $groupBWinner['team_ids']); + static::assertEquals('Winner(s) of group Category C', $groupBWinner->citation); + static::assertEquals(['team_G'], $groupBWinner->teamIds); } public function testFirstToSolve(): void @@ -244,9 +245,9 @@ public function testFirstToSolve(): void foreach ($fts as $problem => $teams) { $firstToSolve = $this->getAward('first-to-solve-problem_' . $problem); static::assertNotNull($firstToSolve); - static::assertEquals('First to solve problem ' . $problem, $firstToSolve['citation']); + static::assertEquals('First to solve problem ' . $problem, $firstToSolve->citation); $teamIds = array_map(static fn(string $team) => 'team_' . $team, $teams); - static::assertEquals($teamIds, $firstToSolve['team_ids']); + static::assertEquals($teamIds, $firstToSolve->teamIds); } } diff --git a/webapp/tests/Unit/Service/ConfigurationServiceTest.php b/webapp/tests/Unit/Service/ConfigurationServiceTest.php index 6ba67d587e..97f02ec6bb 100644 --- a/webapp/tests/Unit/Service/ConfigurationServiceTest.php +++ b/webapp/tests/Unit/Service/ConfigurationServiceTest.php @@ -308,10 +308,10 @@ public function testAddOptionsExecutables(string $item, array $expected): void ->willReturn($executables); $spec = $this->config->getConfigSpecification()[$item]; - self::assertArrayNotHasKey('options', $spec); + self::assertNull($spec->options); $spec = $this->config->addOptions($spec); - self::assertSame($expected, $spec['options']); + self::assertSame($expected, $spec->options); } public function provideAddOptionsExecutables(): Generator @@ -339,14 +339,14 @@ public function testAddOptionsResults(string $item): void } $spec = $this->config->getConfigSpecification()[$item]; - self::assertArrayNotHasKey('options', $spec); + self::assertNull($spec->options); $spec = $this->config->addOptions($spec); - self::assertSame($verdictOptions, $spec['key_options']); + self::assertSame($verdictOptions, $spec->keyOptions); if ($item === 'results_remap') { - self::assertSame($verdictOptions, $spec['value_options']); + self::assertSame($verdictOptions, $spec->valueOptions); } else { - self::assertArrayNotHasKey('value_options', $spec); + self::assertNull($spec->valueOptions); } }