From c786aa5d2b25cc964064b5f14c43fa9ae070610d Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Fri, 25 Oct 2024 15:43:46 +0200 Subject: [PATCH] Add support for caching the whole scoreboard. Still WIP. --- etc/db-config.yaml | 8 + webapp/composer.json | 1 + webapp/composer.lock | 67 ++- .../src/Controller/Jury/ConfigController.php | 3 + .../Controller/Jury/JuryMiscController.php | 26 +- webapp/src/Service/RejudgingService.php | 4 +- webapp/src/Service/ScoreboardCacheService.php | 47 ++ webapp/src/Service/ScoreboardService.php | 145 +++--- webapp/src/Twig/TwigExtension.php | 1 + webapp/src/Utils/Scoreboard/Filter.php | 11 + webapp/src/Utils/Scoreboard/Scoreboard.php | 9 +- .../Utils/Scoreboard/SingleTeamScoreboard.php | 11 +- webapp/templates/jury/jury_macros.twig | 2 +- webapp/templates/jury/refresh_cache.html.twig | 10 + .../partials/scoreboard_table.html.twig | 435 +----------------- .../partials/scoreboard_table_inner.html.twig | 429 +++++++++++++++++ 16 files changed, 716 insertions(+), 493 deletions(-) create mode 100644 webapp/src/Service/ScoreboardCacheService.php create mode 100644 webapp/templates/partials/scoreboard_table_inner.html.twig diff --git a/etc/db-config.yaml b/etc/db-config.yaml index 665a48a6e8..31865880d8 100644 --- a/etc/db-config.yaml +++ b/etc/db-config.yaml @@ -323,6 +323,14 @@ default_value: true public: true description: If disabled, no ranking information is shown to contestants. + - name: cache_full_scoreboard + type: bool + default_value: false + public: false + description: | + If enabled, will cache the whole scoreboard. + Make sure to clear the scoreboard cache after making changes to data + like teams, categories, affiliations or the contest when enabled. - category: Authentication description: Options related to authentication. items: diff --git a/webapp/composer.json b/webapp/composer.json index 124580fc0a..4f7975eae5 100644 --- a/webapp/composer.json +++ b/webapp/composer.json @@ -105,6 +105,7 @@ "symfony/web-profiler-bundle": "6.4.*", "symfony/yaml": "6.4.*", "twbs/bootstrap": "^5.2.0", + "twig/cache-extra": "^3.11", "twig/extra-bundle": "^3.5", "twig/markdown-extra": "^3.5", "twig/string-extra": "^3.5", diff --git a/webapp/composer.lock b/webapp/composer.lock index 84802abec0..d01aadde53 100644 --- a/webapp/composer.lock +++ b/webapp/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3ffb9ae6009d6e668f77b90a4c6d5cf5", + "content-hash": "f30f4561cc3ff9b5839aa3661ae2a193", "packages": [ { "name": "apalfrey/select2-bootstrap-5-theme", @@ -10466,6 +10466,71 @@ }, "time": "2024-02-20T15:14:29+00:00" }, + { + "name": "twig/cache-extra", + "version": "v3.11.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/cache-extra.git", + "reference": "678e765829fb4725bcd480ea7bdcf17b127d34b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/cache-extra/zipball/678e765829fb4725bcd480ea7bdcf17b127d34b4", + "reference": "678e765829fb4725bcd480ea7bdcf17b127d34b4", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/cache": "^5.4|^6.4|^7.0", + "twig/twig": "^3.11" + }, + "require-dev": { + "symfony/phpunit-bridge": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Twig\\Extra\\Cache\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Twig extension for Symfony Cache", + "homepage": "https://twig.symfony.com", + "keywords": [ + "cache", + "html", + "twig" + ], + "support": { + "source": "https://github.com/twigphp/cache-extra/tree/v3.11.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2024-08-07T17:34:09+00:00" + }, { "name": "twig/extra-bundle", "version": "v3.10.0", diff --git a/webapp/src/Controller/Jury/ConfigController.php b/webapp/src/Controller/Jury/ConfigController.php index 62000942df..36ed97e415 100644 --- a/webapp/src/Controller/Jury/ConfigController.php +++ b/webapp/src/Controller/Jury/ConfigController.php @@ -95,6 +95,9 @@ public function indexAction(EventLogService $eventLogService, Request $request): if ($category === 'Judging') { $needsRejudging = true; } + if ($key === 'cache_full_scoreboard') { + $needsRefresh = true; + } } if ($needsRefresh) { diff --git a/webapp/src/Controller/Jury/JuryMiscController.php b/webapp/src/Controller/Jury/JuryMiscController.php index aa076dbe74..3825f66a9e 100644 --- a/webapp/src/Controller/Jury/JuryMiscController.php +++ b/webapp/src/Controller/Jury/JuryMiscController.php @@ -14,6 +14,7 @@ use App\Service\ConfigurationService; use App\Service\DOMJudgeService; use App\Service\EventLogService; +use App\Service\ScoreboardCacheService; use App\Service\ScoreboardService; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query\Expr\Join; @@ -188,13 +189,16 @@ public function ajaxDataAction(Request $request, string $datatype): JsonResponse #[IsGranted('ROLE_ADMIN')] #[Route(path: '/refresh-cache', name: 'jury_refresh_cache')] - public function refreshCacheAction(Request $request, ScoreboardService $scoreboardService): Response - { + public function refreshCacheAction( + Request $request, + ScoreboardService $scoreboardService, + ScoreboardCacheService $cache, + ): Response { // Note: we use a XMLHttpRequest here as Symfony does not support // streaming Twig output. $contests = $this->dj->getCurrentContests(); - if ($cid = $request->request->get('cid')) { + if ($cid = $request->get('cid')) { if (!isset($contests[$cid])) { throw new BadRequestHttpException(sprintf('Contest %s not found', $cid)); } @@ -204,17 +208,29 @@ public function refreshCacheAction(Request $request, ScoreboardService $scoreboa $contests = [$contest->getCid() => $contest]; } + $onlyFullCache = $request->get('only_full_cache'); + if ($request->isXmlHttpRequest() && $request->isMethod('POST')) { $progressReporter = function (int $progress, string $log, ?string $message = null) { echo $this->dj->jsonEncode(['progress' => $progress, 'log' => htmlspecialchars($log), 'message' => $message]); ob_flush(); flush(); }; - return $this->streamResponse($this->requestStack, function () use ($contests, $progressReporter, $scoreboardService) { + return $this->streamResponse($this->requestStack, function () use ( + $contests, + $progressReporter, + $scoreboardService, + $onlyFullCache, + $cache + ) { $timeStart = microtime(true); foreach ($contests as $contest) { - $scoreboardService->refreshCache($contest, $progressReporter); + if ($onlyFullCache) { + $cache->invalidate($contest); + } else { + $scoreboardService->refreshCache($contest, $progressReporter); + } } $timeEnd = microtime(true); diff --git a/webapp/src/Service/RejudgingService.php b/webapp/src/Service/RejudgingService.php index 630fe65122..1f9e5b0a08 100644 --- a/webapp/src/Service/RejudgingService.php +++ b/webapp/src/Service/RejudgingService.php @@ -32,7 +32,8 @@ public function __construct( protected readonly DOMJudgeService $dj, protected readonly ScoreboardService $scoreboardService, protected readonly EventLogService $eventLogService, - protected readonly BalloonService $balloonService + protected readonly BalloonService $balloonService, + protected readonly ScoreboardCacheService $cache, ) {} /** @@ -381,6 +382,7 @@ public function finishRejudging(Rejudging $rejudging, string $action, ?callable } $this->scoreboardService->updateRankCache($contest, $team); } + $this->cache->invalidate($contest); } } diff --git a/webapp/src/Service/ScoreboardCacheService.php b/webapp/src/Service/ScoreboardCacheService.php new file mode 100644 index 0000000000..83d10ce7a7 --- /dev/null +++ b/webapp/src/Service/ScoreboardCacheService.php @@ -0,0 +1,47 @@ +config->get('cache_full_scoreboard')) { + return $callable(); + } + + return $this->twigCache->get($cacheKey, function (ItemInterface $item) use ( + $callable, + $contest + ) { + $item->tag('scoreboard_' . $contest->getCid()); + + return $callable(); + }); + } + + public function invalidate(Contest $contest): void + { + $this->twigCache->invalidateTags(['scoreboard_' . $contest->getCid()]); + } +} diff --git a/webapp/src/Service/ScoreboardService.php b/webapp/src/Service/ScoreboardService.php index e6f2ea208b..a7cc2cf055 100644 --- a/webapp/src/Service/ScoreboardService.php +++ b/webapp/src/Service/ScoreboardService.php @@ -29,6 +29,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Contracts\Cache\ItemInterface; class ScoreboardService { @@ -41,6 +42,7 @@ public function __construct( protected readonly DOMJudgeService $dj, protected readonly ConfigurationService $config, protected readonly LoggerInterface $logger, + protected readonly ScoreboardCacheService $cache ) {} /** @@ -67,17 +69,39 @@ public function getScoreboard( return null; } - $teams = $this->getTeams($contest, $jury && !$visibleOnly, $filter); - $problems = $this->getProblems($contest); - $categories = $this->getCategories($jury && !$visibleOnly); - $scoreCache = $this->getScorecache($contest); - - return new Scoreboard( - $contest, $teams, $categories, $problems, - $scoreCache, $freezeData, $jury || $forceUnfrozen, - (int)$this->config->get('penalty_time'), - (bool)$this->config->get('score_in_seconds'), + $scoreboardDataCachePostfix = sprintf( + '%d:%d:%d:%s:%d:%d:%d', + $forceUnfrozen, + $freezeData->showFinal($jury), + $freezeData->showFrozen(), + $filter?->getHash(), + $visibleOnly, + $jury, + $contest->getCid(), ); + + return $this->cache->cacheScoreboardData($contest, 'scoreboard_data:' . $scoreboardDataCachePostfix, function () use ( + $scoreboardDataCachePostfix, + $forceUnfrozen, + $freezeData, + $filter, + $visibleOnly, + $jury, + $contest + ) { + $teams = $this->getTeams($contest, $jury && !$visibleOnly, $filter); + $problems = $this->getProblems($contest); + $categories = $this->getCategories($jury && !$visibleOnly); + $scoreCache = $this->getScorecache($contest); + + return new Scoreboard( + $contest, $teams, $categories, $problems, + $scoreCache, $freezeData, $jury || $forceUnfrozen, + (int)$this->config->get('penalty_time'), + (bool)$this->config->get('score_in_seconds'), + $scoreboardDataCachePostfix, + ); + }); } /** @@ -499,6 +523,8 @@ public function calculateScoreRow( if ($updateRankCache && ($correctJury || $correctPubl)) { $this->updateRankCache($contest, $team); } + + $this->cache->invalidate($contest); } /** @@ -703,6 +729,8 @@ public function refreshCache(Contest $contest, ?callable $progressReporter = nul 'DELETE FROM rankcache WHERE cid = :cid AND teamid NOT IN (:teamIds)', $params, $types); + $this->cache->invalidate($contest); + $progressReporter(100, ''); } @@ -806,61 +834,64 @@ public function getGroupedAffiliations(Contest $contest): array */ public function getFilterValues(Contest $contest, bool $jury): array { - $filters = [ - 'affiliations' => [], - 'countries' => [], - 'categories' => [], - ]; - $showFlags = $this->config->get('show_flags'); - $showAffiliations = $this->config->get('show_affiliations'); - - $queryBuilder = $this->em->createQueryBuilder() - ->from(TeamCategory::class, 'c') - ->select('c'); - if (!$jury) { - $queryBuilder->andWhere('c.visible = 1'); - } - - /** @var TeamCategory[] $categories */ - $categories = $queryBuilder->getQuery()->getResult(); - foreach ($categories as $category) { - $filters['categories'][$category->getCategoryid()] = $category->getName(); - } + $cacheKey = sprintf('scoreboard_filter_values:%d', $jury); + return $this->cache->cacheScoreboardData($contest, $cacheKey, function() use ($contest, $jury) { + $filters = [ + 'affiliations' => [], + 'countries' => [], + 'categories' => [], + ]; + $showFlags = $this->config->get('show_flags'); + $showAffiliations = $this->config->get('show_affiliations'); - // Show only affiliations / countries with visible teams. - if (empty($categories) || !$showAffiliations) { - $filters['affiliations'] = []; - } else { $queryBuilder = $this->em->createQueryBuilder() - ->from(TeamAffiliation::class, 'a') - ->select('a') - ->join('a.teams', 't') - ->andWhere('t.category IN (:categories)') - ->setParameter('categories', $categories); - if (!$contest->isOpenToAllTeams()) { - $queryBuilder - ->leftJoin('t.contests', 'c') - ->join('t.category', 'cat') - ->leftJoin('cat.contests', 'cc') - ->andWhere('c = :contest OR cc = :contest') - ->setParameter('contest', $contest); + ->from(TeamCategory::class, 'c') + ->select('c'); + if (!$jury) { + $queryBuilder->andWhere('c.visible = 1'); + } + + /** @var TeamCategory[] $categories */ + $categories = $queryBuilder->getQuery()->getResult(); + foreach ($categories as $category) { + $filters['categories'][$category->getCategoryid()] = $category->getName(); } - /** @var TeamAffiliation[] $affiliations */ - $affiliations = $queryBuilder->getQuery()->getResult(); - foreach ($affiliations as $affiliation) { - $filters['affiliations'][$affiliation->getAffilid()] = $affiliation->getName(); - if ($showFlags && $affiliation->getCountry() !== null) { - $filters['countries'][] = $affiliation->getCountry(); + // Show only affiliations / countries with visible teams. + if (empty($categories) || !$showAffiliations) { + $filters['affiliations'] = []; + } else { + $queryBuilder = $this->em->createQueryBuilder() + ->from(TeamAffiliation::class, 'a') + ->select('a') + ->join('a.teams', 't') + ->andWhere('t.category IN (:categories)') + ->setParameter('categories', $categories); + if (!$contest->isOpenToAllTeams()) { + $queryBuilder + ->leftJoin('t.contests', 'c') + ->join('t.category', 'cat') + ->leftJoin('cat.contests', 'cc') + ->andWhere('c = :contest OR cc = :contest') + ->setParameter('contest', $contest); + } + + /** @var TeamAffiliation[] $affiliations */ + $affiliations = $queryBuilder->getQuery()->getResult(); + foreach ($affiliations as $affiliation) { + $filters['affiliations'][$affiliation->getAffilid()] = $affiliation->getName(); + if ($showFlags && $affiliation->getCountry() !== null) { + $filters['countries'][] = $affiliation->getCountry(); + } } } - } - $filters['countries'] = array_unique($filters['countries']); - sort($filters['countries']); - asort($filters['affiliations'], SORT_FLAG_CASE); + $filters['countries'] = array_unique($filters['countries']); + sort($filters['countries']); + asort($filters['affiliations'], SORT_FLAG_CASE); - return $filters; + return $filters; + }); } /** diff --git a/webapp/src/Twig/TwigExtension.php b/webapp/src/Twig/TwigExtension.php index 2a73e324e4..8e47542539 100644 --- a/webapp/src/Twig/TwigExtension.php +++ b/webapp/src/Twig/TwigExtension.php @@ -158,6 +158,7 @@ public function getGlobals(): array 'doc_links' => $this->dj->getDocLinks(), 'allow_registration' => $selfRegistrationCategoriesCount !== 0, 'enable_ranking' => $this->config->get('enable_ranking'), + 'cache_full_scoreboard' => $this->config->get('cache_full_scoreboard'), ]; } diff --git a/webapp/src/Utils/Scoreboard/Filter.php b/webapp/src/Utils/Scoreboard/Filter.php index 1dee9340b1..288d6d02a4 100644 --- a/webapp/src/Utils/Scoreboard/Filter.php +++ b/webapp/src/Utils/Scoreboard/Filter.php @@ -38,4 +38,15 @@ public function getFilteredOn(): string return implode(', ', $filteredOn); } + + public function getHash(): string + { + return sprintf( + '%s#%s#%s#%s', + implode('_', $this->affiliations), + implode('_', $this->countries), + implode('_', $this->categories), + implode('_', $this->teams), + ); + } } diff --git a/webapp/src/Utils/Scoreboard/Scoreboard.php b/webapp/src/Utils/Scoreboard/Scoreboard.php index 3fa0663095..ef6b87d343 100644 --- a/webapp/src/Utils/Scoreboard/Scoreboard.php +++ b/webapp/src/Utils/Scoreboard/Scoreboard.php @@ -40,7 +40,8 @@ public function __construct( protected readonly FreezeData $freezeData, bool $jury, protected readonly int $penaltyTime, - protected readonly bool $scoreIsInSeconds + protected readonly bool $scoreIsInSeconds, + protected readonly string $cacheKeyPostfix, ) { $this->restricted = $jury || $freezeData->showFinal($jury); @@ -111,9 +112,15 @@ public function getFreezeData(): FreezeData */ public function getProgress(): int { + dump('progress'); return $this->getFreezeData()->getProgress(); } + public function getCacheKeyPostfix(): string + { + return $this->cacheKeyPostfix; + } + /** * Initialize the scoreboard data */ diff --git a/webapp/src/Utils/Scoreboard/SingleTeamScoreboard.php b/webapp/src/Utils/Scoreboard/SingleTeamScoreboard.php index 3018141aef..caf9f44fd3 100644 --- a/webapp/src/Utils/Scoreboard/SingleTeamScoreboard.php +++ b/webapp/src/Utils/Scoreboard/SingleTeamScoreboard.php @@ -35,8 +35,17 @@ public function __construct( bool $scoreIsInSeconds ) { $this->showRestrictedFts = $showFtsInFreeze || $freezeData->showFinal(); + $cacheKeyPostFix = sprintf( + '%d:%d:%s:%d:%d:%d', + false, + $freezeData->showFinal(false), + $this->team->getTeamid(), + false, + false, + $contest->getCid(), + ); parent::__construct($contest, [$team->getTeamid() => $team], [], $problems, $scoreCache, $freezeData, true, - $penaltyTime, $scoreIsInSeconds); + $penaltyTime, $scoreIsInSeconds, $cacheKeyPostFix); } protected function calculateScoreboard(): void diff --git a/webapp/templates/jury/jury_macros.twig b/webapp/templates/jury/jury_macros.twig index 8ee16e64d6..811500df41 100644 --- a/webapp/templates/jury/jury_macros.twig +++ b/webapp/templates/jury/jury_macros.twig @@ -297,7 +297,7 @@ return consume(responseReader); }); }; - fetch('{{ url }}', { + fetch('{{ url | raw }}', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest', diff --git a/webapp/templates/jury/refresh_cache.html.twig b/webapp/templates/jury/refresh_cache.html.twig index c9216004f7..6df249a432 100644 --- a/webapp/templates/jury/refresh_cache.html.twig +++ b/webapp/templates/jury/refresh_cache.html.twig @@ -6,6 +6,7 @@ {% block extrahead %} {{ parent() }} {{ macros.table_extrahead() }} + {{ macros.toggle_extrahead() }} {% endblock %} {% block content %} @@ -34,6 +35,12 @@ {% if current_contest is not empty %} {% endif %} + {% if cache_full_scoreboard %} +
+ + Only clear full scoreboard cache +
+ {% endif %} @@ -48,6 +55,9 @@ {% if current_contest is not empty %} {% set progressUrl = progressUrl ~ '?cid=' ~ current_contest.cid %} {% endif %} + {% if app.request.request.has('only_full_cache') %} + {% set progressUrl = progressUrl ~ '&only_full_cache=1' %} + {% endif %} {{ macros.progress_loader(progressUrl) }} {% endif %} {% endblock %} diff --git a/webapp/templates/partials/scoreboard_table.html.twig b/webapp/templates/partials/scoreboard_table.html.twig index b0f64f2eab..a041079ee5 100644 --- a/webapp/templates/partials/scoreboard_table.html.twig +++ b/webapp/templates/partials/scoreboard_table.html.twig @@ -1,429 +1,12 @@ -{% if limitToTeams is not defined %} - {% set limitToTeams = null %} - {% set limitToTeamIds = null %} -{% else %} - {% set limitToTeamIds = [] %} - {% for team in limitToTeams %} - {% set limitToTeamIds = limitToTeamIds | merge([team.teamid]) %} - {% endfor %} -{% endif %} -{% if showLegends is not defined %} - {% set showLegends = false %} +{% set cacheKey = "scoreboard_twig:" ~ scoreboard.cacheKeyPostfix %} +{% if myTeamId is defined %} + {% set cacheKey = cacheKey ~ "myTeamId:" ~ myTeamId %} {% endif %} -{% if static is not defined %} - {% set static = false %} -{% endif %} -{% set showPoints = scoreboard.showPoints %} -{% set usedCategories = scoreboard.usedCategories(limitToTeamIds) %} -{% set hasDifferentCategoryColors = scoreboard.categoryColors(limitToTeamIds) %} -{% set scores = scoreboard.scores | filter(score => limitToTeams is null or score.team.teamid in limitToTeamIds) %} -{% set problems = scoreboard.problems %} -{% set medalsEnabled = contest.medalsEnabled %} - -{% if maxWidth > 0 %} - -{% endif %} - - - - {# output table column groups (for the styles) #} - - {% if enable_ranking %} - - {% endif %} - {% if showFlags %} - - {% else %} - - {% endif %} - {% if showAffiliationLogos %} - - {% endif %} - - - {% if enable_ranking %} - - - - - {% endif %} - - {% if showTeamSubmissions or jury %} - {% for problem in problems %} - - {% endfor %} - {% endif %} - - - {% set teamColspan = 2 %} - {% if showAffiliationLogos %} - {% set teamColspan = teamColspan + 1 %} - {% endif %} - - - - {% if enable_ranking %} - - {% endif %} - - {% if enable_ranking %} - - {% endif %} - {% if showTeamSubmissions or jury %} - {% for problem in problems %} - {% set link = null %} - {% set target = '_self' %} - {% if not static %} - {% if jury %} - {% set link = path('jury_problem', {'probId': problem.probid}) %} - {% elseif problem.problem.problemstatementType is not empty %} - {% if public %} - {% set link = path('public_problem_statement', {probId: problem.probid}) %} - {% set target = '_blank' %} - {% else %} - {% set link = path('team_problem_statement', {probId: problem.probid}) %} - {% set target = '_blank' %} - {% endif %} - {% endif %} - {% endif %} - - {% endfor %} - {% endif %} - - - - {% set previousSortOrder = -1 %} - {% set previousTeam = null %} - {% set backgroundColors = {"#FFFFFF": 1} %} - {% set medalCount = 0 %} - {% for score in scores %} - {% set classes = [] %} - {% if score.team.category.sortorder != previousSortOrder %} - {% if previousSortOrder != -1 %} - {# Output summary of previous sort order #} - {% include 'partials/scoreboard_summary.html.twig' with {sortOrder: previousSortOrder} %} - {% endif %} - {% set classes = classes | merge(['sortorderswitch']) %} - {% set previousSortOrder = score.team.category.sortorder %} - {% set previousTeam = null %} - {% endif %} - - {# process medal color #} - {% set medalColor = '' %} - {% if showLegends %} - {% set medalColor = score.team | medalType(contest, scoreboard) %} - {% endif %} - - {# check whether this is us, otherwise use category colour #} - {% if myTeamId is defined and myTeamId == score.team.teamid %} - {% set classes = classes | merge(['scorethisisme']) %} - {% set color = '#FFFF99' %} - {% else %} - {% set color = score.team.category.color %} - {% endif %} - - {% if enable_ranking %} - - {% endif %} - - {% if showAffiliationLogos %} - - {% endif %} - {% if color is null %} - {% set color = "#FFFFFF" %} - {% set colorClass = "_FFFFFF" %} - {% else %} - {% set colorClass = color | replace({"#": "_"}) %} - {% set backgroundColors = backgroundColors | merge({(color): 1}) %} - {% endif %} - - {% set totalTime = score.totalTime %} - {% if scoreInSeconds %} - {% set totalTime = totalTime | printTimeRelative %} - {% endif %} - {% if enable_ranking %} - {% set totalPoints = score.numPoints %} - - {% if scoreboard.getRuntimeAsScoreTiebreaker() %} - - {% else %} - - {% endif %} - {% endif %} - - {% if showTeamSubmissions or jury %} - {% for problem in problems %} - {# CSS class for correct/incorrect/neutral results #} - {% set scoreCssClass = 'score_neutral' %} - {% set matrixItem = scoreboard.matrix[score.team.teamid][problem.probid] %} - {% if matrixItem.isCorrect %} - {% set scoreCssClass = 'score_correct' %} - {% if enable_ranking %} - {% if not scoreboard.getRuntimeAsScoreTiebreaker() and scoreboard.solvedFirst(score.team, problem) %} - {% set scoreCssClass = scoreCssClass ~ ' score_first' %} - {% endif %} - {% if scoreboard.getRuntimeAsScoreTiebreaker() and scoreboard.isFastestSubmission(score.team, problem) %} - {% set scoreCssClass = scoreCssClass ~ ' score_first' %} - {% endif %} - {% endif %} - {% elseif showPending and matrixItem.numSubmissionsPending > 0 %} - {% set scoreCssClass = 'score_pending' %} - {% elseif matrixItem.numSubmissions > 0 %} - {% set scoreCssClass = 'score_incorrect' %} - {% endif %} - {% if jury and showPending and matrixItem.numSubmissionsInFreeze > 0 %} - {% if scoreCssClass != 'score_pending' %} - {% set scoreCssClass = scoreCssClass ~ ' score_pending' %} - {% endif %} - {% endif %} - - {% set numSubmissions = matrixItem.numSubmissions %} - {% if showPending and matrixItem.numSubmissionsPending > 0 %} - {% set numSubmissions = numSubmissions ~ ' + ' ~ matrixItem.numSubmissionsPending %} - {% endif %} - - {# If correct, print time scored. The format will vary depending on the scoreboard resolution setting #} - {% set time = '' %} - {% if matrixItem.isCorrect %} - {% set time = matrixItem.time %} - {% if scoreboard.getRuntimeAsScoreTiebreaker() %} - {% set time = "%0.3f s" | format(matrixItem.runtime / 1000.0) %} - {% elseif scoreInSeconds %} - {% set time = time | scoreTime | printTimeRelative %} - {% if matrixItem.numSubmissions > 1 %} - {% set time = time ~ ' + ' ~ (calculatePenaltyTime(true, matrixItem.numSubmissions) | printTimeRelative) %} - {% endif %} - {% else %} - {% set time = time | scoreTime %} - {% endif %} - {% endif %} - {% set link = null %} - {% if jury %} - {% set restrict = {problemId: problem.probid} %} - {% set link = path('jury_team', {teamId: score.team.teamid, restrict: restrict}) %} - {% endif %} - - - {% endfor %} - {% endif %} - - {% endfor %} - - {# Output summary of last sort order #} - {% include 'partials/scoreboard_summary.html.twig' with {sortOrder: previousSortOrder} %} - -
rankteamscore - - {{ problem | problemBadge }} - {% if showPoints %} - - [{% if problem.points == 1 %}1 point{% else %}{{ problem.points }} points{% endif %}] - - {% endif %} - -
- {# Only print rank when score is different from the previous team #} - {% if not displayRank %} - ? - {% elseif previousTeam is null or scoreboard.scores[previousTeam.teamid].rank != score.rank %} - {{ score.rank }} - {% else %} - {% endif %} - {% set previousTeam = score.team %} - - {% if showFlags %} - {% if score.team.affiliation %} - {% set link = null %} - {% if jury %} - {% set link = path('jury_team_affiliation', {'affilId': score.team.affiliation.affilid}) %} - {% endif %} - - {{ score.team.affiliation.country|countryFlag }} - - {% endif %} - {% endif %} - - {% if score.team.affiliation %} - {% set link = null %} - {% if jury %} - {% set link = path('jury_team_affiliation', {'affilId': score.team.affiliation.affilid}) %} - {% endif %} - - {% set affiliationId = score.team.affiliation.externalid %} - {% set affiliationImage = affiliationId | assetPath('affiliation') %} - {% if affiliationImage %} - - {% else %} - {{ affiliationId }} - {% endif %} - - {% endif %} - - {% set link = null %} - {% set extra = null %} - {% if static %} - {% set link = '#' %} - {% set extra = 'data-bs-toggle="modal" data-bs-target="#team-modal-' ~ score.team.teamid ~ '"' %} - {% else %} - {% if jury %} - {% set link = path('jury_team', {teamId: score.team.teamid}) %} - {% elseif public %} - {% set link = path('public_team', {teamId: score.team.teamid}) %} - {% set extra = 'data-ajax-modal' %} - {% else %} - {% set link = path('team_team', {teamId: score.team.teamid}) %} - {% set extra = 'data-ajax-modal' %} - {% endif %} - {% endif %} - - - {% if usedCategories | length > 1 and scoreboard.bestInCategory(score.team, limitToTeamIds) %} - - {{ score.team.category.name }} - - {% endif %} - {{ score.team.effectiveName }} - - {% if showAffiliations %} - - {% if score.team.affiliation %} - {{ score.team.affiliation.name }} - {% endif %} - - {% endif %} - - {{ totalPoints }}{{ "%0.3f s" | format(score.totalRuntime/1000.0) }}{{ totalTime }} - {% if numSubmissions != '0' %} - -
- {% if matrixItem.isCorrect %}{{ time }}{% else %} {% endif %} - - {% if numSubmissions is same as(1) %} - 1 try - {% else %} - {{ numSubmissions }} tries - {% endif %} - -
-
- {% endif %} -
- -{% if static %} - {% for score in scores %} - {% embed 'partials/modal.html.twig' with {'modalId': 'team-modal-' ~ score.team.teamid} %} - {% block title %}{{ score.team.effectiveName }}{% endblock %} - {% block content %} - {% include 'partials/team.html.twig' with {size: 6, team: score.team} %} - {% endblock %} - {% endembed %} - {% endfor %} -{% endif %} - -{% if showLegends %} -



- - {# only print legend when there's more than one category #} - {% if limitToTeamIds is null and usedCategories | length > 1 and hasDifferentCategoryColors %} - - - - - - - - {% for category in scoreboard.categories | filter(category => usedCategories[category.categoryid] is defined) %} - - - - {% endfor %} - -
- {% set link = null %} - {% if jury %} - {% set link = path('jury_team_categories') %} - {% endif %} - Categories -
- {% set link = null %} - {% if jury %} - {% set link = path('jury_team_category', {'categoryId': category.categoryid}) %} - {% endif %} - {{ category.name }} -
- {% endif %} - - {% if showTeamSubmissions or jury %} - {% if scoreboard.getRuntimeAsScoreTiebreaker() %} - {% set cellColors = {first: 'Solved, fastest', correct: 'Solved', incorrect: 'Tried, incorrect', pending: 'Tried, pending', neutral: 'Untried'} %} - {% else %} - {% set cellColors = {first: 'Solved first', correct: 'Solved', incorrect: 'Tried, incorrect', pending: 'Tried, pending', neutral: 'Untried'} %} - {% endif %} - - - - - - - - {% for color, description in cellColors %} - {% if color != 'pending' or showPending %} - - - - {% endif %} - {% endfor %} - -
Cell colours
{{ description }}
- {% endif %} - - {% if medalsEnabled %} - - - - - - - - {% for medalType in ['Gold', 'Silver', 'Bronze'] %} - - - - {% endfor %} - -
Medals {% if not scoreboard.freezeData.showFinal %}(tentative){% endif %}
{{ medalType }} Medal
- {% endif %} +{% if cache_full_scoreboard %} + {% cache cacheKey tags('scoreboard_' ~ contest.cid) %} + {% include 'partials/scoreboard_table_inner.html.twig' %} + {% endcache %} +{% else %} + {% include 'partials/scoreboard_table_inner.html.twig' %} {% endif %} - - - diff --git a/webapp/templates/partials/scoreboard_table_inner.html.twig b/webapp/templates/partials/scoreboard_table_inner.html.twig new file mode 100644 index 0000000000..b0f64f2eab --- /dev/null +++ b/webapp/templates/partials/scoreboard_table_inner.html.twig @@ -0,0 +1,429 @@ +{% if limitToTeams is not defined %} + {% set limitToTeams = null %} + {% set limitToTeamIds = null %} +{% else %} + {% set limitToTeamIds = [] %} + {% for team in limitToTeams %} + {% set limitToTeamIds = limitToTeamIds | merge([team.teamid]) %} + {% endfor %} +{% endif %} +{% if showLegends is not defined %} + {% set showLegends = false %} +{% endif %} +{% if static is not defined %} + {% set static = false %} +{% endif %} +{% set showPoints = scoreboard.showPoints %} +{% set usedCategories = scoreboard.usedCategories(limitToTeamIds) %} +{% set hasDifferentCategoryColors = scoreboard.categoryColors(limitToTeamIds) %} +{% set scores = scoreboard.scores | filter(score => limitToTeams is null or score.team.teamid in limitToTeamIds) %} +{% set problems = scoreboard.problems %} +{% set medalsEnabled = contest.medalsEnabled %} + +{% if maxWidth > 0 %} + +{% endif %} + + + + {# output table column groups (for the styles) #} + + {% if enable_ranking %} + + {% endif %} + {% if showFlags %} + + {% else %} + + {% endif %} + {% if showAffiliationLogos %} + + {% endif %} + + + {% if enable_ranking %} + + + + + {% endif %} + + {% if showTeamSubmissions or jury %} + {% for problem in problems %} + + {% endfor %} + {% endif %} + + + {% set teamColspan = 2 %} + {% if showAffiliationLogos %} + {% set teamColspan = teamColspan + 1 %} + {% endif %} + + + + {% if enable_ranking %} + + {% endif %} + + {% if enable_ranking %} + + {% endif %} + {% if showTeamSubmissions or jury %} + {% for problem in problems %} + {% set link = null %} + {% set target = '_self' %} + {% if not static %} + {% if jury %} + {% set link = path('jury_problem', {'probId': problem.probid}) %} + {% elseif problem.problem.problemstatementType is not empty %} + {% if public %} + {% set link = path('public_problem_statement', {probId: problem.probid}) %} + {% set target = '_blank' %} + {% else %} + {% set link = path('team_problem_statement', {probId: problem.probid}) %} + {% set target = '_blank' %} + {% endif %} + {% endif %} + {% endif %} + + {% endfor %} + {% endif %} + + + + {% set previousSortOrder = -1 %} + {% set previousTeam = null %} + {% set backgroundColors = {"#FFFFFF": 1} %} + {% set medalCount = 0 %} + {% for score in scores %} + {% set classes = [] %} + {% if score.team.category.sortorder != previousSortOrder %} + {% if previousSortOrder != -1 %} + {# Output summary of previous sort order #} + {% include 'partials/scoreboard_summary.html.twig' with {sortOrder: previousSortOrder} %} + {% endif %} + {% set classes = classes | merge(['sortorderswitch']) %} + {% set previousSortOrder = score.team.category.sortorder %} + {% set previousTeam = null %} + {% endif %} + + {# process medal color #} + {% set medalColor = '' %} + {% if showLegends %} + {% set medalColor = score.team | medalType(contest, scoreboard) %} + {% endif %} + + {# check whether this is us, otherwise use category colour #} + {% if myTeamId is defined and myTeamId == score.team.teamid %} + {% set classes = classes | merge(['scorethisisme']) %} + {% set color = '#FFFF99' %} + {% else %} + {% set color = score.team.category.color %} + {% endif %} + + {% if enable_ranking %} + + {% endif %} + + {% if showAffiliationLogos %} + + {% endif %} + {% if color is null %} + {% set color = "#FFFFFF" %} + {% set colorClass = "_FFFFFF" %} + {% else %} + {% set colorClass = color | replace({"#": "_"}) %} + {% set backgroundColors = backgroundColors | merge({(color): 1}) %} + {% endif %} + + {% set totalTime = score.totalTime %} + {% if scoreInSeconds %} + {% set totalTime = totalTime | printTimeRelative %} + {% endif %} + {% if enable_ranking %} + {% set totalPoints = score.numPoints %} + + {% if scoreboard.getRuntimeAsScoreTiebreaker() %} + + {% else %} + + {% endif %} + {% endif %} + + {% if showTeamSubmissions or jury %} + {% for problem in problems %} + {# CSS class for correct/incorrect/neutral results #} + {% set scoreCssClass = 'score_neutral' %} + {% set matrixItem = scoreboard.matrix[score.team.teamid][problem.probid] %} + {% if matrixItem.isCorrect %} + {% set scoreCssClass = 'score_correct' %} + {% if enable_ranking %} + {% if not scoreboard.getRuntimeAsScoreTiebreaker() and scoreboard.solvedFirst(score.team, problem) %} + {% set scoreCssClass = scoreCssClass ~ ' score_first' %} + {% endif %} + {% if scoreboard.getRuntimeAsScoreTiebreaker() and scoreboard.isFastestSubmission(score.team, problem) %} + {% set scoreCssClass = scoreCssClass ~ ' score_first' %} + {% endif %} + {% endif %} + {% elseif showPending and matrixItem.numSubmissionsPending > 0 %} + {% set scoreCssClass = 'score_pending' %} + {% elseif matrixItem.numSubmissions > 0 %} + {% set scoreCssClass = 'score_incorrect' %} + {% endif %} + {% if jury and showPending and matrixItem.numSubmissionsInFreeze > 0 %} + {% if scoreCssClass != 'score_pending' %} + {% set scoreCssClass = scoreCssClass ~ ' score_pending' %} + {% endif %} + {% endif %} + + {% set numSubmissions = matrixItem.numSubmissions %} + {% if showPending and matrixItem.numSubmissionsPending > 0 %} + {% set numSubmissions = numSubmissions ~ ' + ' ~ matrixItem.numSubmissionsPending %} + {% endif %} + + {# If correct, print time scored. The format will vary depending on the scoreboard resolution setting #} + {% set time = '' %} + {% if matrixItem.isCorrect %} + {% set time = matrixItem.time %} + {% if scoreboard.getRuntimeAsScoreTiebreaker() %} + {% set time = "%0.3f s" | format(matrixItem.runtime / 1000.0) %} + {% elseif scoreInSeconds %} + {% set time = time | scoreTime | printTimeRelative %} + {% if matrixItem.numSubmissions > 1 %} + {% set time = time ~ ' + ' ~ (calculatePenaltyTime(true, matrixItem.numSubmissions) | printTimeRelative) %} + {% endif %} + {% else %} + {% set time = time | scoreTime %} + {% endif %} + {% endif %} + + {% set link = null %} + {% if jury %} + {% set restrict = {problemId: problem.probid} %} + {% set link = path('jury_team', {teamId: score.team.teamid, restrict: restrict}) %} + {% endif %} + + + {% endfor %} + {% endif %} + + {% endfor %} + + {# Output summary of last sort order #} + {% include 'partials/scoreboard_summary.html.twig' with {sortOrder: previousSortOrder} %} + +
rankteamscore + + {{ problem | problemBadge }} + {% if showPoints %} + + [{% if problem.points == 1 %}1 point{% else %}{{ problem.points }} points{% endif %}] + + {% endif %} + +
+ {# Only print rank when score is different from the previous team #} + {% if not displayRank %} + ? + {% elseif previousTeam is null or scoreboard.scores[previousTeam.teamid].rank != score.rank %} + {{ score.rank }} + {% else %} + {% endif %} + {% set previousTeam = score.team %} + + {% if showFlags %} + {% if score.team.affiliation %} + {% set link = null %} + {% if jury %} + {% set link = path('jury_team_affiliation', {'affilId': score.team.affiliation.affilid}) %} + {% endif %} + + {{ score.team.affiliation.country|countryFlag }} + + {% endif %} + {% endif %} + + {% if score.team.affiliation %} + {% set link = null %} + {% if jury %} + {% set link = path('jury_team_affiliation', {'affilId': score.team.affiliation.affilid}) %} + {% endif %} + + {% set affiliationId = score.team.affiliation.externalid %} + {% set affiliationImage = affiliationId | assetPath('affiliation') %} + {% if affiliationImage %} + + {% else %} + {{ affiliationId }} + {% endif %} + + {% endif %} + + {% set link = null %} + {% set extra = null %} + {% if static %} + {% set link = '#' %} + {% set extra = 'data-bs-toggle="modal" data-bs-target="#team-modal-' ~ score.team.teamid ~ '"' %} + {% else %} + {% if jury %} + {% set link = path('jury_team', {teamId: score.team.teamid}) %} + {% elseif public %} + {% set link = path('public_team', {teamId: score.team.teamid}) %} + {% set extra = 'data-ajax-modal' %} + {% else %} + {% set link = path('team_team', {teamId: score.team.teamid}) %} + {% set extra = 'data-ajax-modal' %} + {% endif %} + {% endif %} + + + {% if usedCategories | length > 1 and scoreboard.bestInCategory(score.team, limitToTeamIds) %} + + {{ score.team.category.name }} + + {% endif %} + {{ score.team.effectiveName }} + + {% if showAffiliations %} + + {% if score.team.affiliation %} + {{ score.team.affiliation.name }} + {% endif %} + + {% endif %} + + {{ totalPoints }}{{ "%0.3f s" | format(score.totalRuntime/1000.0) }}{{ totalTime }} + {% if numSubmissions != '0' %} + +
+ {% if matrixItem.isCorrect %}{{ time }}{% else %} {% endif %} + + {% if numSubmissions is same as(1) %} + 1 try + {% else %} + {{ numSubmissions }} tries + {% endif %} + +
+
+ {% endif %} +
+ +{% if static %} + {% for score in scores %} + {% embed 'partials/modal.html.twig' with {'modalId': 'team-modal-' ~ score.team.teamid} %} + {% block title %}{{ score.team.effectiveName }}{% endblock %} + {% block content %} + {% include 'partials/team.html.twig' with {size: 6, team: score.team} %} + {% endblock %} + {% endembed %} + {% endfor %} +{% endif %} + +{% if showLegends %} +



+ + {# only print legend when there's more than one category #} + {% if limitToTeamIds is null and usedCategories | length > 1 and hasDifferentCategoryColors %} + + + + + + + + {% for category in scoreboard.categories | filter(category => usedCategories[category.categoryid] is defined) %} + + + + {% endfor %} + +
+ {% set link = null %} + {% if jury %} + {% set link = path('jury_team_categories') %} + {% endif %} + Categories +
+ {% set link = null %} + {% if jury %} + {% set link = path('jury_team_category', {'categoryId': category.categoryid}) %} + {% endif %} + {{ category.name }} +
+ {% endif %} + + {% if showTeamSubmissions or jury %} + {% if scoreboard.getRuntimeAsScoreTiebreaker() %} + {% set cellColors = {first: 'Solved, fastest', correct: 'Solved', incorrect: 'Tried, incorrect', pending: 'Tried, pending', neutral: 'Untried'} %} + {% else %} + {% set cellColors = {first: 'Solved first', correct: 'Solved', incorrect: 'Tried, incorrect', pending: 'Tried, pending', neutral: 'Untried'} %} + {% endif %} + + + + + + + + {% for color, description in cellColors %} + {% if color != 'pending' or showPending %} + + + + {% endif %} + {% endfor %} + +
Cell colours
{{ description }}
+ {% endif %} + + {% if medalsEnabled %} + + + + + + + + {% for medalType in ['Gold', 'Silver', 'Bronze'] %} + + + + {% endfor %} + +
Medals {% if not scoreboard.freezeData.showFinal %}(tentative){% endif %}
{{ medalType }} Medal
+ {% endif %} +{% endif %} + + +