Skip to content

Commit 820b4dd

Browse files
Use RelTime for relative time fields in new API format, allow parsing it
Fixes #2946
1 parent 47efe69 commit 820b4dd

23 files changed

+364
-162
lines changed

etc/db-config.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -349,12 +349,12 @@
349349
public: true
350350
description: If set, enable teams and jury to send source code to this command. See admin manual for allowed arguments.
351351
docdescription: See :ref:`printing` for more information.
352-
- name: event_feed_format
352+
- name: ccs_api_version
353353
type: enum
354-
default_value: 2022-07
355-
enum_class: App\Utils\EventFeedFormat
354+
default_value: 2025-draft
355+
enum_class: App\Utils\CcsApiVersion
356356
public: false
357-
description: Format of the event feed to use. See [current draft](https://ccs-specs.icpc.io/draft/contest_api#event-feed) and [versions available](https://ccs-specs.icpc.io/).
357+
description: Version of the CCS API to use for the API and event feed. See [current draft](https://ccs-specs.icpc.io/draft/contest_api#event-feed) and [versions available](https://ccs-specs.icpc.io/).
358358
- name: shadow_mode
359359
type: bool
360360
default_value: false
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
/**
11+
* Auto-generated Migration: Please modify to your needs!
12+
*/
13+
final class Version20250706121413 extends AbstractMigration
14+
{
15+
public function getDescription(): string
16+
{
17+
return 'Update `event_feed_format` config to `ccs_api_version`';
18+
}
19+
20+
public function up(Schema $schema): void
21+
{
22+
// Change key and values to the new format
23+
$this->addSql('UPDATE configuration SET name = \'ccs_api_version\' WHERE name = \'event_feed_format\' AND value = \'"2020-03"\'');
24+
$this->addSql('UPDATE configuration SET name = \'ccs_api_version\', value = \'"2023-06"\' WHERE name = \'event_feed_format\' AND value = \'"2022-07"\'');
25+
}
26+
27+
public function down(Schema $schema): void
28+
{
29+
// Change key and values back to the old format
30+
$this->addSql('UPDATE configuration SET name = \'event_feed_format\' WHERE name = \'ccs_api_version\' AND value = \'"2020-03"\'');
31+
$this->addSql('UPDATE configuration SET name = \'event_feed_format\', value = \'"2022-07"\' WHERE name = \'ccs_api_version\' AND value = \'"2023-06"\'');
32+
// Delete it if we have a non-supported version
33+
$this->addSql('DELETE FROM configuration WHERE name = \'ccs_api_version\'');
34+
}
35+
36+
public function isTransactional(): bool
37+
{
38+
return false;
39+
}
40+
}

webapp/src/Command/ImportEventFeedCommand.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace App\Command;
44

5-
use App\Controller\API\GeneralInfoController as GI;
65
use App\Entity\Contest;
76
use App\Entity\ExternalContestSource;
87
use App\Entity\User;
@@ -53,7 +52,6 @@ protected function configure(): void
5352
$this
5453
->setHelp(
5554
'Import contest data from an event feed following the Contest API specification:' . PHP_EOL .
56-
GI::CCS_SPEC_API_URL . ' or any version starting from "2021-11"' . PHP_EOL . PHP_EOL .
5755
'Note the following assumptions and caveats:' . PHP_EOL .
5856
'- Configuration data will only be verified.' . PHP_EOL .
5957
'- Team members will not be imported.' . PHP_EOL .

webapp/src/Controller/API/ContestController.php

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
use App\Service\DOMJudgeService;
1414
use App\Service\EventLogService;
1515
use App\Service\ImportExportService;
16-
use App\Utils\EventFeedFormat;
16+
use App\Utils\CcsApiVersion;
1717
use App\Utils\Utils;
1818
use BadMethodCallException;
1919
use Doctrine\Inflector\InflectorFactory;
@@ -648,7 +648,8 @@ public function getEventFeedAction(
648648
$since_id = -1;
649649
}
650650

651-
$format = $this->config->get('event_feed_format');
651+
/** @var CcsApiVersion $format */
652+
$format = $this->config->get('ccs_api_version');
652653

653654
$response = new StreamedResponse();
654655
$response->headers->set('X-Accel-Buffering', 'no');
@@ -835,21 +836,25 @@ public function getEventFeedAction(
835836
unset($data[$property]);
836837
}
837838
}
839+
840+
$data = $this->eventLogService->applyCcsVersionChanges($event->getEndpointtype(), $data);
841+
838842
switch ($format) {
839-
case EventFeedFormat::Format_2020_03:
843+
case CcsApiVersion::Format_2020_03:
840844
$result = [
841845
'id' => (string)$event->getEventid(),
842-
'type' => (string)$event->getEndpointtype(),
843-
'op' => (string)$event->getAction(),
846+
'type' => $event->getEndpointtype(),
847+
'op' => $event->getAction(),
844848
'data' => $data,
845849
];
846850
break;
847-
case EventFeedFormat::Format_2022_07:
851+
case CcsApiVersion::Format_2023_06:
852+
case CcsApiVersion::Format_2025_DRAFT:
848853
if ($event->getAction() === EventLogService::ACTION_DELETE) {
849854
$data = null;
850855
}
851-
$id = (string)$event->getEndpointid() ?: null;
852-
$type = (string)$event->getEndpointtype();
856+
$id = $event->getEndpointid() ?: null;
857+
$type = $event->getEndpointtype();
853858
if ($type === 'contests') {
854859
// Special case: the type for a contest is singular and the ID must not be set
855860
$id = null;

webapp/src/Controller/API/GeneralInfoController.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use App\Service\DOMJudgeService;
1414
use App\Service\EventLogService;
1515
use App\Service\ImportProblemService;
16+
use App\Utils\CcsApiVersion;
1617
use Doctrine\ORM\EntityManagerInterface;
1718
use Doctrine\ORM\NonUniqueResultException;
1819
use Doctrine\ORM\NoResultException;
@@ -42,8 +43,6 @@ class GeneralInfoController extends AbstractFOSRestController
4243
{
4344
protected const API_VERSION = 4;
4445

45-
final public const CCS_SPEC_API_VERSION = '2023-06';
46-
final public const CCS_SPEC_API_URL = 'https://ccs-specs.icpc.io/2023-06/contest_api';
4746

4847
public function __construct(
4948
protected readonly EntityManagerInterface $em,
@@ -94,9 +93,12 @@ public function getInfoAction(
9493
);
9594
}
9695

96+
/** @var CcsApiVersion $ccsApiVersion */
97+
$ccsApiVersion = $this->config->get('ccs_api_version');
98+
9799
return new ApiInfo(
98-
version: self::CCS_SPEC_API_VERSION,
99-
versionUrl: self::CCS_SPEC_API_URL,
100+
version: $ccsApiVersion->value,
101+
versionUrl: $ccsApiVersion->getCcsSpecsApiUrl(),
100102
name: 'DOMjudge',
101103
//TODO: Add DOMjudge logo
102104
provider: new ApiInfoProvider(

webapp/src/Controller/API/ScoreboardController.php

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313
use App\Service\DOMJudgeService;
1414
use App\Service\EventLogService;
1515
use App\Service\ScoreboardService;
16+
use App\Utils\CcsApiVersion;
1617
use App\Utils\Scoreboard\Filter;
1718
use App\Utils\Utils;
1819
use Doctrine\ORM\EntityManagerInterface;
1920
use Doctrine\ORM\NonUniqueResultException;
2021
use FOS\RestBundle\Controller\Annotations as Rest;
2122
use Nelmio\ApiDocBundle\Attribute\Model;
2223
use OpenApi\Attributes as OA;
24+
use Symfony\Component\Form\FormFactoryInterface;
2325
use Symfony\Component\HttpFoundation\Request;
2426
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
2527
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
@@ -39,7 +41,8 @@ public function __construct(
3941
DOMJudgeService $DOMJudgeService,
4042
ConfigurationService $config,
4143
EventLogService $eventLogService,
42-
protected readonly ScoreboardService $scoreboardService
44+
protected readonly ScoreboardService $scoreboardService,
45+
private readonly FormFactoryInterface $form
4346
) {
4447
parent::__construct($entityManager, $DOMJudgeService, $config, $eventLogService);
4548
}
@@ -169,6 +172,8 @@ public function getScoreboardAction(
169172
}
170173

171174
$scoreIsInSeconds = (bool)$this->config->get('score_in_seconds');
175+
/** @var CcsApiVersion $ccsApiVersion */
176+
$ccsApiVersion = $this->config->get('ccs_api_version');
172177

173178
foreach ($scoreboard->getScores() as $teamScore) {
174179
if ($teamScore->team->getCategory()->getSortorder() !== $sortorder) {
@@ -183,10 +188,12 @@ public function getScoreboardAction(
183188
} else {
184189
$score = new Score(
185190
numSolved: $teamScore->numPoints,
186-
totalTime: $teamScore->totalTime,
191+
totalTime: $this->formatTime($teamScore->totalTime, $ccsApiVersion, $scoreIsInSeconds),
187192
);
188193
}
189194

195+
$lastProblemTime = null;
196+
190197
$problems = [];
191198
foreach ($scoreboard->getMatrix()[$teamScore->team->getTeamid()] as $problemId => $matrixItem) {
192199
$contestProblem = $scoreboard->getProblems()[$problemId];
@@ -206,13 +213,19 @@ public function getScoreboardAction(
206213
} else {
207214
$problem->firstToSolve = $matrixItem->isCorrect && $scoreboard->solvedFirst($teamScore->team, $contestProblem);
208215
if ($matrixItem->isCorrect) {
209-
$problem->time = Utils::scoretime($matrixItem->time, $scoreIsInSeconds);
216+
$problemTime = Utils::scoretime($matrixItem->time, $scoreIsInSeconds);
217+
$problem->time = $this->formatTime($problemTime, $ccsApiVersion, $scoreIsInSeconds);
218+
$lastProblemTime = max($lastProblemTime, $problemTime);
210219
}
211220
}
212221

213222
$problems[] = $problem;
214223
}
215224

225+
if ($lastProblemTime !== null) {
226+
$score->time = $this->formatTime($lastProblemTime, $ccsApiVersion, $scoreIsInSeconds);
227+
}
228+
216229
usort($problems, fn(Problem $a, Problem $b) => $a->label <=> $b->label);
217230

218231
$row = new Row(
@@ -227,4 +240,16 @@ public function getScoreboardAction(
227240

228241
return $results;
229242
}
243+
244+
protected function formatTime(
245+
int $time,
246+
CcsApiVersion $ccsApiVersion,
247+
bool $scoreIsInSeconds
248+
): int|string {
249+
if ($ccsApiVersion->useRelTimes()) {
250+
return $scoreIsInSeconds ? Utils::relTime($time) : Utils::relTime($time * 60);
251+
} else {
252+
return $time;
253+
}
254+
}
230255
}

webapp/src/Controller/Jury/JuryMiscController.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use App\Service\DOMJudgeService;
1616
use App\Service\EventLogService;
1717
use App\Service\ScoreboardService;
18+
use App\Utils\CcsApiVersion;
1819
use App\Utils\Utils;
1920
use Doctrine\ORM\EntityManagerInterface;
2021
use Doctrine\ORM\Query\Expr\Join;
@@ -39,6 +40,7 @@ class JuryMiscController extends BaseController
3940
public function __construct(
4041
EntityManagerInterface $em,
4142
DOMJudgeService $dj,
43+
protected readonly ConfigurationService $config,
4244
protected readonly EventLogService $eventLogService,
4345
protected readonly RequestStack $requestStack,
4446
KernelInterface $kernel,
@@ -57,9 +59,12 @@ public function indexAction(ConfigurationService $config): Response
5759
}
5860
}
5961

62+
/** @var CcsApiVersion $ccsApiVersion */
63+
$ccsApiVersion = $this->config->get('ccs_api_version');
64+
6065
return $this->render('jury/index.html.twig', [
6166
'adminer_enabled' => $config->get('adminer_enabled'),
62-
'CCS_SPEC_API_URL' => GI::CCS_SPEC_API_URL,
67+
'CCS_SPEC_API_URL' => $ccsApiVersion->getCcsSpecsApiUrl(),
6368
]);
6469
}
6570

webapp/src/DataTransferObject/Scoreboard/Problem.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public function __construct(
1515
public readonly int $numPending,
1616
public readonly bool $solved,
1717
#[Serializer\Exclude(if: 'object.time === null')]
18-
public ?int $time = null,
18+
public int|string|null $time = null,
1919
#[Serializer\Groups([ARC::GROUP_NONSTRICT])]
2020
#[Serializer\Exclude(if: 'object.firstToSolve === null')]
2121
public ?bool $firstToSolve = null,

webapp/src/DataTransferObject/Scoreboard/Score.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ class Score
1010
public function __construct(
1111
public readonly int $numSolved,
1212
#[Serializer\Exclude(if: 'object.totalTime === null')]
13-
public readonly ?int $totalTime = null,
13+
public readonly int|string|null $totalTime = null,
14+
#[Serializer\Exclude(if: 'object.time === null')]
15+
public int|string|null $time = null,
1416
#[Serializer\Exclude(if: 'object.totalRuntime === null')]
1517
#[Serializer\Groups([ARC::GROUP_NONSTRICT])]
1618
public readonly ?int $totalRuntime = null,

webapp/src/DataTransferObject/Shadowing/ContestData.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public function __construct(
99
public readonly string $name,
1010
public readonly string $duration,
1111
public readonly ?string $scoreboardFreezeDuration,
12-
public readonly int $penaltyTime,
12+
public readonly int|string $penaltyTime,
1313
public readonly ?string $startTime,
1414
// TODO: check for end time and scoreboard thaw time
1515
) {}

0 commit comments

Comments
 (0)