Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion webapp/src/Entity/SubmissionSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,4 @@ enum SubmissionSource: string
case SHADOWING = 'shadowing';
case TEAM_PAGE = 'team page';
case UNKNOWN = 'unknown';

}
80 changes: 63 additions & 17 deletions webapp/src/Service/ImportExportService.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
use App\Utils\Scoreboard\Filter;
use App\Utils\Utils;
use Collator;
use DateInterval;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException;
Expand Down Expand Up @@ -83,10 +85,14 @@ public function getContestYamlData(Contest $contest, bool $includeProblems = tru
'formal_name' => $contest->getName(),
'name' => $contest->getShortname(),
'start_time' => Utils::absTime($contest->getStarttime(), true),
'end_time' => Utils::absTime($contest->getEndtime(), true),
'end_time' => Utils::isRelTime($contest->getEndtimeString())
? $contest->getEndtimeString()
: Utils::absTime($contest->getEndtime(), true),
'duration' => Utils::relTime($contest->getContestTime((float)$contest->getEndtime())),
'penalty_time' => $this->config->get('penalty_time'),
'activate_time' => Utils::absTime($contest->getActivatetime(), true),
'activate_time' => Utils::isRelTime($contest->getActivatetimeString())
? $contest->getActivatetimeString()
: Utils::absTime($contest->getActivatetime(), true),
];
if ($warnMsg = $contest->getWarningMessage()) {
$data['warning_message'] = $warnMsg;
Expand All @@ -100,21 +106,27 @@ public function getContestYamlData(Contest $contest, bool $includeProblems = tru
}

if ($contest->getFreezetime() !== null) {
$data['scoreboard_freeze_time'] = Utils::absTime($contest->getFreezetime(), true);
$data['scoreboard_freeze_time'] = Utils::isRelTime($contest->getFreezetimeString())
? $contest->getFreezetimeString()
: Utils::absTime($contest->getFreezetime(), true);
$data['scoreboard_freeze_duration'] = Utils::relTime(
$contest->getContestTime((float)$contest->getEndtime()) - $contest->getContestTime((float)$contest->getFreezetime()),
true,
);
if ($contest->getUnfreezetime() !== null) {
$data['scoreboard_thaw_time'] = Utils::absTime($contest->getUnfreezetime(), true);
$data['scoreboard_thaw_time'] = Utils::isRelTime($contest->getUnfreezetimeString())
? $contest->getUnfreezetimeString()
: Utils::absTime($contest->getUnfreezetime(), true);
}
}
if ($contest->getFinalizetime() !== null) {
$data['finalize_time'] = Utils::absTime($contest->getFinalizetime(), true);
}

if ($contest->getDeactivatetime() !== null) {
$data['deactivate_time'] = Utils::absTime($contest->getDeactivatetime(), true);
$data['deactivate_time'] = Utils::isRelTime($contest->getDeactivatetimeString())
? $contest->getDeactivatetimeString()
: Utils::absTime($contest->getDeactivatetime(), true);
}

if ($includeProblems) {
Expand Down Expand Up @@ -146,9 +158,15 @@ public function getContestYamlData(Contest $contest, bool $includeProblems = tru
*
* @param array<string> $fields
* @param array<string, string|DateTime|DateTimeImmutable> $data
* @return array{time: DateTimeImmutable|null, isRelative: bool|null, relativeTime: string|null}
*/
protected function convertImportedTime(array $fields, array $data, ?string &$errorMessage = null): ?DateTimeImmutable
{
protected function convertImportedTime(
array $fields,
array $data,
bool $allowRelative = true,
?DateTimeInterface $startTime = null,
?string &$errorMessage = null
): array {
$timeValue = null;
$usedField = null;
foreach ($fields as $field) {
Expand All @@ -160,22 +178,42 @@ protected function convertImportedTime(array $fields, array $data, ?string &$err
}
}

$isRelative = false;

if (is_string($timeValue)) {
$time = date_create_from_format(DateTime::ISO8601, $timeValue) ?:
// Make sure ISO 8601 but with the T replaced with a space also works.
date_create_from_format('Y-m-d H:i:sO', $timeValue);
if ($allowRelative && ($isRelative = Utils::isRelTime($timeValue)) && $startTime) {
$time = new DateTimeImmutable($startTime->format('Y-m-d H:i:s'), $startTime->getTimezone());
$seconds = Utils::relTimeToSeconds($timeValue);
if ($seconds < 0) {
$time = $time->sub(new DateInterval(sprintf('PT%sS', abs($seconds))));
} else {
$time = $time->add(new DateInterval(sprintf('PT%sS', $seconds)));
}
} else {
$time = date_create_from_format(DateTime::ISO8601, $timeValue) ?:
// Make sure ISO 8601 but with the T replaced with a space also works.
date_create_from_format('Y-m-d H:i:sO', $timeValue);
}
} else {
/** @var DateTime|DateTimeImmutable $time */
$time = $timeValue;
}
// If/When parsing fails we get a false instead of a null
if ($time === false) {
$errorMessage = 'Can not parse '.$usedField;
return null;
return [
'time' => null,
'isRelative' => null,
'relativeTime' => null,
];
} elseif ($time) {
$time = $time->setTimezone(new DateTimeZone(date_default_timezone_get()));
}
return $time instanceof DateTime ? DateTimeImmutable::createFromMutable($time) : $time;
return [
'time' => $time instanceof DateTime ? DateTimeImmutable::createFromMutable($time) : $time,
'isRelative' => $isRelative,
'relativeTime' => $isRelative ? $timeValue : null,
];
}

public function importContestData(mixed $data, ?string &$errorMessage = null, ?string &$cid = null): bool
Expand Down Expand Up @@ -215,13 +253,17 @@ public function importContestData(mixed $data, ?string &$errorMessage = null, ?s

$invalid_regex = str_replace(['/^[', '+$/'], ['/[^', '/'], DOMJudgeService::EXTERNAL_IDENTIFIER_REGEX);

$startTime = $this->convertImportedTime($startTimeFields, $data, $errorMessage);
['time' => $startTime] = $this->convertImportedTime($startTimeFields, $data, false, errorMessage: $errorMessage);
if ($errorMessage) {
return false;
}

// Activate time is special, it can return non empty message for parsing error or null if no field was provided
$activateTime = $this->convertImportedTime($activateTimeFields, $data, $errorMessage);
[
'time' => $activateTime,
'isRelative' => $activateTimeIsRelative,
'relativeTime' => $activateRelativeTime,
] = $this->convertImportedTime($activateTimeFields, $data, startTime: $startTime, errorMessage: $errorMessage);
if ($errorMessage) {
return false;
} elseif (!$activateTime) {
Expand All @@ -231,7 +273,11 @@ public function importContestData(mixed $data, ?string &$errorMessage = null, ?s
}
}

$deactivateTime = $this->convertImportedTime($deactivateTimeFields, $data, $errorMessage);
[
'time' => $deactivateTime,
'isRelative' => $deactivateTimeIsRelative,
'relativeTime' => $deactivateRelativeTime,
] = $this->convertImportedTime($deactivateTimeFields, $data, startTime: $startTime, errorMessage: $errorMessage);
if ($errorMessage) {
return false;
}
Expand All @@ -247,11 +293,11 @@ public function importContestData(mixed $data, ?string &$errorMessage = null, ?s
->setExternalid($contest->getShortname())
->setWarningMessage($data['warning_message'] ?? $data['warning-message'] ?? null)
->setStarttimeString(date_format($startTime, 'Y-m-d H:i:s e'))
->setActivatetimeString(date_format($activateTime, 'Y-m-d H:i:s e'))
->setActivatetimeString($activateTimeIsRelative ? $activateRelativeTime : date_format($activateTime, 'Y-m-d H:i:s e'))
->setEndtimeString(sprintf('+%s', $data['duration']))
->setPublic($data['public'] ?? true);
if ($deactivateTime) {
$contest->setDeactivatetimeString(date_format($deactivateTime, 'Y-m-d H:i:s e'));
$contest->setDeactivatetimeString($deactivateTimeIsRelative ? $deactivateRelativeTime : date_format($deactivateTime, 'Y-m-d H:i:s e'));
}

// Get all visible categories. For now, we assume these are the ones getting awards
Expand Down
9 changes: 7 additions & 2 deletions webapp/src/Utils/Utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ class Utils

final public const DAY_IN_SECONDS = 60*60*24;

final public const RELTIME_REGEX = '/^(-)?(\d+):(\d{2}):(\d{2})(?:\.(\d{3}))?$/';
final public const RELTIME_REGEX = '/^([+-])?(\d+):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?$/';

/**
* Returns the milliseconds part of a time stamp truncated at three digits.
Expand All @@ -192,6 +192,11 @@ public static function absTime(mixed $epoch, bool $floored = false): ?string
. date("P", (int) $epoch);
}

public static function isRelTime(string $time): bool
{
return preg_match(self::RELTIME_REGEX, $time) === 1;
}

/**
* Prints a time diff as relative time as ([-+])?(h)*h:mm:ss(.uuu)?
* (with millis if $floored is false and with + sign only if $includePlus is true).
Expand All @@ -216,7 +221,7 @@ public static function relTimeToSeconds(string $reltime): float
$seconds = $modifier * (
(int)$data[2] * 3600
+ (int)$data[3] * 60
+ (float)sprintf('%d.%03d', $data[4], $data[5] ?? 0));
+ (float)sprintf('%d.%03d', $data[4] ?? 0, $data[5] ?? 0));
return $seconds;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,14 @@ public function testAddYaml(): void
name: practice
activate_time: '2021-03-27T09:00:00+00:00'
start_time: '2021-03-27T09:00:00+00:00'
end_time: '2021-03-27T11:00:00+00:00'
end_time: '+2:00:00'
duration: 2:00:00.000
penalty_time: 20
medals:
gold: 4
silver: 4
bronze: 4
scoreboard_freeze_time: '2021-03-27T10:30:00+00:00'
scoreboard_freeze_time: '+01:30:00'
scoreboard_freeze_duration: 0:30:00
EOF;

Expand Down
32 changes: 10 additions & 22 deletions webapp/tests/Unit/Controller/Jury/ImportExportControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,15 @@ public function provideContestYamlContents(): Generator
formal_name: 'Demo contest'
name: demo
start_time: '{$year}-01-01T08:00:00+00:00'
end_time: '{$year}-01-01T13:00:00+00:00'
end_time: '+5:00:00'
duration: '5:00:00.000'
penalty_time: 20
activate_time: '{$pastYear}-01-01T08:00:00+00:00'
medals:
gold: 4
silver: 4
bronze: 4
scoreboard_freeze_time: '{$year}-01-01T12:00:00+00:00'
scoreboard_freeze_time: '+04:00:00'
scoreboard_freeze_duration: '1:00:00'
problems:
-
Expand Down Expand Up @@ -218,25 +218,13 @@ public function testResultsTsvExport(

public function provideResultsTsvExport(): Generator
{
yield [0, true, true, 'results 1
exteam Honorable 0 0 0
'];
yield [0, true, false, 'results 1
exteam Honorable 0 0 0
'];
yield [0, false, true, 'results 1
exteam Honorable 0 0 0
'];
yield [0, false, true, 'results 1
exteam Honorable 0 0 0
'];
yield [1, true, true, 'results 1
'];
yield [1, true, false, 'results 1
'];
yield [1, false, true, 'results 1
'];
yield [1, false, true, 'results 1
'];
yield [0, true, true, "results 1\nexteam Honorable 0 0 0 \n"];
yield [0, true, false, "results 1\nexteam Honorable 0 0 0 \n"];
yield [0, false, true, "results 1\nexteam Honorable 0 0 0 \n"];
yield [0, false, true, "results 1\nexteam Honorable 0 0 0 \n"];
yield [1, true, true, "results 1\n"];
yield [1, true, false, "results 1\n"];
yield [1, false, true, "results 1\n"];
yield [1, false, true, "results 1\n"];
}
}
49 changes: 47 additions & 2 deletions webapp/tests/Unit/Service/ImportExportServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,13 @@ public function provideImportContestDataErrors(): Generator
/**
* @dataProvider provideImportContestDataSuccess
*/
public function testImportContestDataSuccess(mixed $data, string $expectedShortName, array $expectedProblems = []): void
{
public function testImportContestDataSuccess(
mixed $data,
string $expectedShortName,
string $expectedActivateTimeString,
?string $expectedDeactivateTimeString,
array $expectedProblems = []
): void {
/** @var ImportExportService $importExportService */
$importExportService = static::getContainer()->get(ImportExportService::class);
self::assertTrue($importExportService->importContestData($data, $message, $cid), 'Importing failed: ' . $message);
Expand All @@ -125,6 +130,8 @@ public function testImportContestDataSuccess(mixed $data, string $expectedShortN
self::assertEquals($data['name'], $contest->getName());
self::assertEquals($data['public'] ?? true, $contest->getPublic());
self::assertEquals($expectedShortName, $contest->getShortname());
self::assertEquals($expectedActivateTimeString, $contest->getActivatetimeString());
self::assertEquals($expectedDeactivateTimeString, $contest->getDeactivatetimeString());

$problems = [];
/** @var ContestProblem $problem */
Expand All @@ -149,6 +156,38 @@ public function provideImportContestDataSuccess(): Generator
'scoreboard-freeze-length' => '1:00:00',
],
'test-contest',
'2020-01-01 10:34:56 UTC',
null,
];
// Adding absolute activate and deactivate time
yield [
[
'name' => 'Some test contest',
'short-name' => 'test-contest',
'duration' => '5:00:00',
'start-time' => '2020-01-01T12:34:56+02:00',
'activate_time' => '2020-01-01T06:34:56+02:00',
'deactivate_time' => '2020-01-01T18:34:56+02:00',
'scoreboard-freeze-length' => '1:00:00',
],
'test-contest',
'2020-01-01 04:34:56 UTC',
'2020-01-01 16:34:56 UTC',
];
// Adding relative activate and deactivate time
yield [
[
'name' => 'Some test contest',
'short-name' => 'test-contest',
'duration' => '5:00:00',
'start-time' => '2020-01-01T12:34:56+02:00',
'activate_time' => '-6:00',
'deactivate_time' => '+06:00:00',
'scoreboard-freeze-length' => '1:00:00',
],
'test-contest',
'-6:00',
'+06:00:00',
];
// - Freeze length without hours
// - Set a short name with invalid characters
Expand All @@ -162,6 +201,8 @@ public function provideImportContestDataSuccess(): Generator
'scoreboard-freeze-length' => '30:00',
],
'test-contest__-__test',
'2020-01-01 10:34:56 UTC',
null,
];
// Real life example from NWERC 2020 practice session, including problems.
yield [
Expand Down Expand Up @@ -195,6 +236,8 @@ public function provideImportContestDataSuccess(): Generator
],
],
'practice',
'2021-03-27 09:00:00 UTC',
null,
['A' => 'anothereruption', 'B' => 'brokengears', 'C' => 'cheating'],
];

Expand All @@ -209,6 +252,8 @@ public function provideImportContestDataSuccess(): Generator
'public' => false,
],
'test-contest',
'2020-01-01 10:34:56 UTC',
null,
];
}

Expand Down
Loading