Skip to content

Commit c1fcd84

Browse files
Use separate JSON to load submission modal data
1 parent 23f7a37 commit c1fcd84

File tree

10 files changed

+272
-84
lines changed

10 files changed

+272
-84
lines changed

webapp/public/js/domjudge.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1235,3 +1235,60 @@ $(function() {
12351235
});
12361236
});
12371237
});
1238+
1239+
function loadSubmissions(dataElement, $displayElement) {
1240+
const url = dataElement.dataset.submissionsUrl
1241+
fetch(url)
1242+
.then(data => data.json())
1243+
.then(data => {
1244+
const teamId = dataElement.dataset.teamId;
1245+
const problemId = dataElement.dataset.problemId;
1246+
const teamKey = `team-${teamId}`;
1247+
const problemKey = `problem-${problemId}`;
1248+
if (!data.submissions || !data.submissions[teamKey] || !data.submissions[teamKey][problemKey]) {
1249+
return;
1250+
}
1251+
1252+
const submissions = data.submissions[teamKey][problemKey];
1253+
if (submissions.length === 0) {
1254+
$displayElement.html(document.querySelector('#empty-submission-list').innerHTML);
1255+
} else {
1256+
let templateData = document.querySelector('#submission-list').innerHTML;
1257+
const $table = $(templateData);
1258+
const itemTemplateData = document.querySelector('#submission-list-item').innerHTML;
1259+
const $itemTemplate = $(itemTemplateData);
1260+
const $submissionList = $table.find('[data-submission-list]');
1261+
for (const submission of submissions) {
1262+
const $item = $itemTemplate.clone();
1263+
$item.find('[data-time]').html(submission.time);
1264+
$item.find('[data-language-id]').html(submission.language);
1265+
$item.find('[data-verdict]').html(submission.verdict);
1266+
$submissionList.append($item);
1267+
}
1268+
$displayElement.find('.spinner-border').remove();
1269+
$displayElement.append($table);
1270+
}
1271+
});
1272+
}
1273+
1274+
function initScoreboardSubmissions() {
1275+
$('[data-submissions-url]').on('click', function (e) {
1276+
const linkEl = e.currentTarget;
1277+
e.preventDefault();
1278+
const $modal = $('[data-submissions-modal] .modal').clone();
1279+
const $teamEl = $(`[data-team-external-id="${linkEl.dataset.teamId}"]`);
1280+
const $problemEl = $(`[data-problem-external-id="${linkEl.dataset.problemId}"]`);
1281+
$modal.find('[data-team]').html($teamEl.data('teamName'));
1282+
$modal.find('[data-problem-badge]').html($problemEl.data('problemBadge'));
1283+
$modal.find('[data-problem-name]').html($problemEl.data('problemName'));
1284+
$modal.modal();
1285+
$modal.modal('show');
1286+
$modal.on('hidden.bs.modal', function (e) {
1287+
$(e.currentTarget).remove();
1288+
});
1289+
$modal.on('shown.bs.modal', function (e) {
1290+
const $modalBody = $(e.currentTarget).find('.modal-body');
1291+
loadSubmissions(linkEl, $modalBody);
1292+
});
1293+
});
1294+
}

webapp/src/Controller/PublicController.php

Lines changed: 98 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use App\DataTransferObject\SubmissionRestriction;
66
use App\Entity\Contest;
77
use App\Entity\ContestProblem;
8+
use App\Entity\Submission;
89
use App\Entity\Team;
910
use App\Entity\TeamCategory;
1011
use App\Service\ConfigurationService;
@@ -13,8 +14,10 @@
1314
use App\Service\ScoreboardService;
1415
use App\Service\StatisticsService;
1516
use App\Service\SubmissionService;
17+
use App\Twig\TwigExtension;
1618
use Doctrine\ORM\EntityManagerInterface;
1719
use Doctrine\ORM\NonUniqueResultException;
20+
use Symfony\Component\HttpFoundation\JsonResponse;
1821
use Symfony\Component\HttpFoundation\RedirectResponse;
1922
use Symfony\Component\HttpFoundation\Request;
2023
use Symfony\Component\HttpFoundation\RequestStack;
@@ -36,6 +39,7 @@ public function __construct(
3639
protected readonly ScoreboardService $scoreboardService,
3740
protected readonly StatisticsService $stats,
3841
protected readonly SubmissionService $submissionService,
42+
protected readonly TwigExtension $twigExtension,
3943
EntityManagerInterface $em,
4044
EventLogService $eventLog,
4145
KernelInterface $kernel,
@@ -283,8 +287,8 @@ protected function getBinaryFile(int $probId, callable $response): StreamedRespo
283287
return $response($probId, $contest, $contestProblem);
284288
}
285289

286-
#[Route(path: '/submissions/team/{teamId<\d+>}/problem/{problemId<\d+>}', name: 'public_submissions')]
287-
public function submissionsAction(Request $request, int $teamId, int $problemId): Response
290+
#[Route(path: '/submissions/team/{teamId}/problem/{problemId}', name: 'public_submissions')]
291+
public function submissionsAction(Request $request, string $teamId, string $problemId): Response
288292
{
289293
$contest = $this->dj->getCurrentContest(onlyPublic: true);
290294

@@ -293,7 +297,7 @@ public function submissionsAction(Request $request, int $teamId, int $problemId)
293297
}
294298

295299
/** @var Team|null $team */
296-
$team = $this->em->getRepository(Team::class)->find($teamId);
300+
$team = $this->em->getRepository(Team::class)->findOneBy(['externalid' => $teamId]);
297301
if ($team && $team->getCategory() && !$team->getCategory()->getVisible()) {
298302
$team = null;
299303
}
@@ -303,32 +307,108 @@ public function submissionsAction(Request $request, int $teamId, int $problemId)
303307
}
304308

305309
/** @var ContestProblem|null $problem */
306-
$problem = $this->em->getRepository(ContestProblem::class)->find([
307-
'problem' => $problemId,
308-
'contest' => $contest,
309-
]);
310+
$problem = $this->em->createQueryBuilder()
311+
->from(ContestProblem::class, 'cp')
312+
->select('cp')
313+
->innerJoin('cp.problem', 'p')
314+
->andWhere('cp.contest = :contest')
315+
->andWhere('p.externalid = :problem')
316+
->setParameter('contest', $contest)
317+
->setParameter('problem', $problemId)
318+
->getQuery()
319+
->getOneOrNullResult();
310320

311321
if (!$problem) {
312322
throw $this->createNotFoundException('Problem not found');
313323
}
314324

315-
$submissions = $this->submissionService->getSubmissionList(
316-
[$contest->getCid() => $contest],
317-
new SubmissionRestriction(teamId: $teamId, problemId: $problemId, valid: true),
318-
paginated: false
319-
)[0];
320-
321325
$data = [
322326
'contest' => $contest,
323327
'problem' => $problem,
324328
'team' => $team,
325-
'submissions' => $submissions,
326-
'verificationRequired' => $this->config->get('verification_required'),
327329
];
328330

329-
if ($request->isXmlHttpRequest()) {
330-
return $this->render('public/team_submissions_modal.html.twig', $data);
331-
}
332331
return $this->render('public/team_submissions.html.twig', $data);
333332
}
333+
334+
#[Route(path: '/submissions-data.json', name: 'public_submissions_data')]
335+
#[Route(path: '/submissions-data/team/{teamId}/problem/{problemId}.json', name: 'public_submissions_data_cell')]
336+
public function submissionsDataAction(Request $request, ?string $teamId, ?string $problemId): JsonResponse
337+
{
338+
$contest = $this->dj->getCurrentContest(onlyPublic: true);
339+
340+
if (!$contest) {
341+
throw $this->createNotFoundException('No active contest found');
342+
}
343+
344+
$scoreboard = $this->scoreboardService->getScoreboard($contest);
345+
346+
/** @var Submission[] $submissions */
347+
$submissions = $this->submissionService->getSubmissionList(
348+
[$contest->getCid() => $contest],
349+
restrictions: new SubmissionRestriction(valid: true),
350+
paginated: false
351+
)[0];
352+
353+
$submissionData = [];
354+
355+
// We prepend IDs with team- and problem- to make sure they are not
356+
// consecutive integers
357+
foreach ($scoreboard->getTeamsInDescendingOrder() as $team) {
358+
if ($teamId && $teamId !== $team->getExternalid()) {
359+
continue;
360+
}
361+
$teamKey = 'team-' . $team->getExternalid();
362+
$submissionData[$teamKey] = [];
363+
foreach ($scoreboard->getProblems() as $problem) {
364+
if ($problemId && $problemId !== $problem->getExternalId()) {
365+
continue;
366+
}
367+
$problemKey = 'problem-' . $problem->getExternalId();
368+
$submissionData[$teamKey][$problemKey] = [];
369+
}
370+
}
371+
372+
$verificationRequired = $this->config->get('verification_required');
373+
374+
foreach ($submissions as $submission) {
375+
$teamKey = 'team-' . $submission->getTeam()->getExternalid();
376+
$problemKey = 'problem-' . $submission->getProblem()->getExternalid();
377+
if ($teamId && $teamId !== $submission->getTeam()->getExternalid()) {
378+
continue;
379+
}
380+
if ($problemId && $problemId !== $submission->getProblem()->getExternalid()) {
381+
continue;
382+
}
383+
$submissionData[$teamKey][$problemKey][] = [
384+
'time' => $this->twigExtension->printtime($submission->getSubmittime(), contest: $contest),
385+
'language' => $submission->getLanguageId(),
386+
'verdict' => $this->submissionVerdict($submission, $contest, $verificationRequired),
387+
];
388+
}
389+
390+
return new JsonResponse([
391+
'submissions' => $submissionData,
392+
]);
393+
}
394+
395+
protected function submissionVerdict(
396+
Submission $submission,
397+
Contest $contest,
398+
bool $verificationRequired
399+
): string {
400+
if ($submission->getSubmittime() >= $contest->getEndtime()) {
401+
return $this->twigExtension->printResult('too-late');
402+
}
403+
if ($contest->getFreezetime() && $submission->getSubmittime() >= $contest->getFreezetime() && !$contest->getFreezeData()->showFinal()) {
404+
return $this->twigExtension->printResult('');
405+
}
406+
if (!$submission->getJudgings()->first() || !$submission->getJudgings()->first()->getResult()) {
407+
return $this->twigExtension->printResult('');
408+
}
409+
if ($verificationRequired && !$submission->getJudgings()->first()->getVerified()) {
410+
return $this->twigExtension->printResult('');
411+
}
412+
return $this->twigExtension->printResult($submission->getJudgings()->first()->getResult(), onlyRejectedForIncorrect: true);
413+
}
334414
}

webapp/src/Service/DOMJudgeService.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
use Symfony\Component\HttpFoundation\Cookie;
4747
use Symfony\Component\HttpFoundation\File\UploadedFile;
4848
use Symfony\Component\HttpFoundation\InputBag;
49+
use Symfony\Component\HttpFoundation\JsonResponse;
4950
use Symfony\Component\HttpFoundation\Request;
5051
use Symfony\Component\HttpFoundation\RequestStack;
5152
use Symfony\Component\HttpFoundation\Response;
@@ -1507,6 +1508,7 @@ public function getScoreboardZip(
15071508
$assetRegex = '|/CHANGE_ME/([/a-z0-9_\-\.]*)(\??[/a-z0-9_\-\.=]*)|i';
15081509
preg_match_all($assetRegex, $contestPage, $assetMatches);
15091510
$contestPage = preg_replace($assetRegex, '$1$2', $contestPage);
1511+
$contestPage = str_replace('/public/submissions-data.json', 'submissions-data.json', $contestPage);
15101512

15111513
$zip = new ZipArchive();
15121514
if (!($tempFilename = tempnam($this->getDomjudgeTmpDir(), "contest-"))) {
@@ -1519,6 +1521,13 @@ public function getScoreboardZip(
15191521
}
15201522
$zip->addFromString('index.html', $contestPage);
15211523

1524+
$submissionsDataRequest = Request::create('/public/submissions-data.json', Request::METHOD_GET);
1525+
$submissionsDataRequest->setSession($this->requestStack->getSession());
1526+
/** @var JsonResponse $response */
1527+
$response = $this->httpKernel->handle($submissionsDataRequest, HttpKernelInterface::SUB_REQUEST);
1528+
$submissionsData = $response->getContent();
1529+
$zip->addFromString('submissions-data.json', $submissionsData);
1530+
15221531
$publicPath = realpath(sprintf('%s/public/', $this->projectDir));
15231532
foreach ($assetMatches[1] as $file) {
15241533
$filepath = realpath($publicPath . '/' . $file);

webapp/src/Twig/TwigExtension.php

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@
2323
use App\Service\EventLogService;
2424
use App\Service\SubmissionService;
2525
use App\Utils\Scoreboard\ScoreboardMatrixItem;
26+
use App\Utils\Scoreboard\TeamScore;
2627
use App\Utils\Utils;
2728
use Doctrine\Common\Collections\Collection;
2829
use Doctrine\ORM\EntityManagerInterface;
2930
use Symfony\Component\DependencyInjection\Attribute\Autowire;
3031
use Symfony\Component\Intl\Countries;
3132
use Symfony\Component\Intl\Exception\MissingResourceException;
3233
use Symfony\Component\PropertyAccess\PropertyAccess;
34+
use Symfony\Component\Routing\RouterInterface;
3335
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
3436
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
3537
use Twig\Environment;
@@ -51,6 +53,7 @@ public function __construct(
5153
protected readonly AwardService $awards,
5254
protected readonly TokenStorageInterface $tokenStorage,
5355
protected readonly AuthorizationCheckerInterface $authorizationChecker,
56+
protected readonly RouterInterface $router,
5457
#[Autowire('%kernel.project_dir%')]
5558
protected readonly string $projectDir
5659
) {}
@@ -1224,7 +1227,12 @@ public function problemBadge(ContestProblem $problem, bool $grayedOut = false):
12241227
);
12251228
}
12261229

1227-
public function problemBadgeMaybe(ContestProblem $problem, ScoreboardMatrixItem $matrixItem): string
1230+
public function problemBadgeMaybe(
1231+
ContestProblem $problem,
1232+
ScoreboardMatrixItem $matrixItem,
1233+
TeamScore $score,
1234+
bool $static = false,
1235+
): string
12281236
{
12291237
$rgb = Utils::convertToHex($problem->getColor() ?? '#ffffff');
12301238
if (!$matrixItem->isCorrect || empty($rgb)) {
@@ -1238,10 +1246,27 @@ public function problemBadgeMaybe(ContestProblem $problem, ScoreboardMatrixItem
12381246
$border = 'linen';
12391247
}
12401248

1241-
$ret = sprintf(
1242-
'<span class="badge problem-badge" style="font-size: x-small; background-color: %s; min-width: 18px; border: 1px solid %s;"><span style="color: %s;">%s</span></span>',
1249+
$submissionsUrl = $static
1250+
? $this->router->generate('public_submissions_data')
1251+
: $this->router->generate('public_submissions_data_cell', [
1252+
'teamId' => $score->team->getExternalid(),
1253+
'problemId' => $problem->getExternalId(),
1254+
]);
1255+
1256+
$ret = sprintf(<<<HTML
1257+
<span class="badge problem-badge"
1258+
style="font-size: x-small; background-color: %s; min-width: 18px; border: 1px solid %s;"
1259+
data-submissions-url="%s"
1260+
data-team-id="%s"
1261+
data-problem-id="%s">
1262+
<span style="color: %s;">%s</span>
1263+
</span>
1264+
HTML,
12431265
$rgb,
12441266
$border,
1267+
$submissionsUrl,
1268+
$score->team->getExternalid(),
1269+
$problem->getExternalId(),
12451270
$foreground,
12461271
$problem->getShortname()
12471272
);

0 commit comments

Comments
 (0)