diff --git a/judge/judgedaemon.main.php b/judge/judgedaemon.main.php index 262d3a1004..5d58d45dfe 100644 --- a/judge/judgedaemon.main.php +++ b/judge/judgedaemon.main.php @@ -60,6 +60,7 @@ function read_credentials(): void "waiting" => false, "errorred" => false, "last_attempt" => -1, + "retrying" => false, ]; } if (count($endpoints) <= 0) { @@ -164,9 +165,6 @@ function request(string $url, string $verb = 'GET', $data = '', bool $failonerro ": http status code: " . $status . ", request size = " . strlen(print_r($data, true)) . ", response: " . $response; - if ($status == 500) { - break; - } } else { $succeeded = true; break; @@ -751,9 +749,12 @@ function fetch_executable_internal( // Request open submissions to judge. Any errors will be treated as // non-fatal: we will just keep on retrying in this loop. + $row = []; $judging = request('judgehosts/fetch-work', 'POST', ['hostname' => $myhost], false); - // If $judging is null, an error occurred; don't try to decode. - if (!is_null($judging)) { + // If $judging is null, an error occurred; we marked the endpoint already as errorred above. + if (is_null($judging)) { + continue; + } else { $row = dj_json_decode($judging); } @@ -780,8 +781,19 @@ function fetch_executable_internal( // We have gotten a work packet. $endpoints[$endpointID]["waiting"] = false; + // All tasks are guaranteed to be of the same type. $type = $row[0]['type']; + + if ($type == 'try_again') { + if (!$endpoints[$endpointID]['retrying']) { + logmsg(LOG_INFO, "API indicated to retry fetching work (this might take a while to clean up)."); + } + $endpoints[$endpointID]['retrying'] = true; + continue; + } + $endpoints[$endpointID]['retrying'] = false; + logmsg(LOG_INFO, "⇝ Received " . sizeof($row) . " '" . $type . "' judge tasks (endpoint $endpointID)"); @@ -1410,6 +1422,15 @@ function judge(array $judgeTask): bool $passdir = $testcasedir . '/' . $passCnt; mkdir($passdir, 0755, true); + if ($passCnt > 1) { + $cpcmd = 'cp -R ' . $passdir . '/../' . ($passCnt - 1) . '/feedback ' . $passdir . '/'; + system($cpcmd); + logmsg(LOG_INFO, " Copying feedback dir from pass " . ($passCnt - 1) . ': ' . $cpcmd); + $rmcmd = 'rm ' . $passdir . '/feedback/nextpass.in'; + system($rmcmd); + logmsg(LOG_INFO, " Executing " . $rmcmd); + } + // Copy program with all possible additional files to testcase // dir. Use hardlinks to preserve space with big executables. $programdir = $passdir . '/execdir'; diff --git a/judge/testcase_run.sh b/judge/testcase_run.sh index 7cffaa7ec1..fa316ee76b 100755 --- a/judge/testcase_run.sh +++ b/judge/testcase_run.sh @@ -195,7 +195,7 @@ if [ $COMBINED_RUN_COMPARE -eq 1 ]; then # A combined run and compare script may now already need the # feedback directory, and perhaps access to the test answers (but # only the original that lives outside the chroot). - mkdir feedback + mkdir -p feedback RUNARGS="$RUNARGS $TESTOUT compare.meta feedback" fi @@ -232,8 +232,8 @@ if [ $COMBINED_RUN_COMPARE -eq 0 ]; then exitcode=0 # Create dir for feedback files and make it writable for $RUNUSER - mkdir feedback - chmod a+w feedback + mkdir -p feedback + chmod -R a+w feedback runcheck $GAINROOT "$RUNGUARD" ${DEBUG:+-v} $CPUSET_OPT -u "$RUNUSER" -g "$RUNGROUP" \ -m $SCRIPTMEMLIMIT -t $SCRIPTTIMELIMIT --no-core \ diff --git a/webapp/public/js/domjudge.js b/webapp/public/js/domjudge.js index 97dd01eb73..2e7e2097de 100644 --- a/webapp/public/js/domjudge.js +++ b/webapp/public/js/domjudge.js @@ -200,20 +200,36 @@ function getCookie(name) function getSelectedTeams() { - var cookieVal = getCookie("domjudge_teamselection"); + var cookieVal = localStorage.getItem("domjudge_teamselection"); if (cookieVal === null || cookieVal === "") { return new Array(); } return JSON.parse(cookieVal); } -function getScoreboard() +function getScoreboards(mobile) { - var scoreboard = document.getElementsByClassName("scoreboard"); - if (scoreboard === null || scoreboard[0] === null || scoreboard[0] === undefined) { + const scoreboards = document.getElementsByClassName("scoreboard"); + if (scoreboards === null || scoreboards[0] === null || scoreboards[0] === undefined) { return null; } - return scoreboard[0].rows; + let scoreboardRows = {}; + const mobileScoreboardClass = 'mobile-scoreboard'; + const desktopScoreboardClass = 'desktop-scoreboard'; + for (let i = 0; i < scoreboards.length; i++) { + if (scoreboards[i].classList.contains(mobileScoreboardClass)) { + scoreboardRows.mobile = scoreboards[i].rows; + } else if (scoreboards[i].classList.contains(desktopScoreboardClass)) { + scoreboardRows.desktop = scoreboards[i].rows; + } + } + if (mobile === undefined) { + return scoreboardRows; + } else if (mobile) { + return scoreboardRows.mobile; + } else { + return scoreboardRows.desktop; + } } function getRank(row) @@ -226,7 +242,7 @@ function getHeartCol(row) { var td = null; // search for td before the team name for (var i = 1; i < 4; i++) { - if (tds[i].className == "scoretn") { + if (tds[i].classList.contains("scoretn")) { td = tds[i - 1]; break; } @@ -246,21 +262,31 @@ function getHeartCol(row) { function getTeamname(row) { - var res = row.getAttribute("id"); - if ( res === null ) return res; - return res.replace(/^team:/, ''); + return row.getAttribute("data-team-id"); } -function toggle(id, show) +function toggle(id, show, mobile) { - var scoreboard = getScoreboard(); + var scoreboard = getScoreboards(mobile); if (scoreboard === null) return; + // Filter out all rows that do not have a data-team-id attribute or have + // the class `scoreheader`. + // The mobile scoreboard has them, and we need to ignore them. + scoreboard = Array.from(scoreboard) + .filter( + row => row.getAttribute("data-team-id") + || row.classList.contains("scoreheader") + ); + var favTeams = getSelectedTeams(); // count visible favourite teams (if filtered) var visCnt = 0; for (var i = 0; i < favTeams.length; i++) { for (var j = 0; j < scoreboard.length; j++) { + if (!scoreboard[j].getAttribute("data-team-id")) { + continue; + } var scoreTeamname = getTeamname(scoreboard[j]); if (scoreTeamname === null) { continue; @@ -286,10 +312,15 @@ function toggle(id, show) } var cookieVal = JSON.stringify(favTeams); - setCookie("domjudge_teamselection", cookieVal); + localStorage.setItem("domjudge_teamselection", cookieVal); $('.loading-indicator').addClass('ajax-loader'); + // If we are on a local file system, reload the window + if (window.location.protocol === 'file:') { + window.location.reload(); + return; + } $.ajax({ url: scoreboardUrl, cache: false @@ -299,69 +330,99 @@ function toggle(id, show) }); } -function addHeart(rank, row, id, isFav) +function addHeart(rank, row, id, isFav, mobile) { var heartCol = getHeartCol(row); var iconClass = isFav ? "fas fa-heart" : "far fa-heart"; - return heartCol.innerHTML + ""; + return heartCol.innerHTML + ""; } function initFavouriteTeams() { - var scoreboard = getScoreboard(); - if (scoreboard === null) { + const scoreboards = getScoreboards(); + if (scoreboards === null) { return; } var favTeams = getSelectedTeams(); - var toAdd = new Array(); - var cntFound = 0; - var lastRank = 0; - for (var j = 0; j < scoreboard.length; j++) { - var found = false; - var teamname = getTeamname(scoreboard[j]); - if (teamname === null) { - continue; - } - var firstCol = getRank(scoreboard[j]); - var heartCol = getHeartCol(scoreboard[j]); - var rank = firstCol.innerHTML; - for (var i = 0; i < favTeams.length; i++) { - if (teamname === favTeams[i]) { - found = true; - heartCol.innerHTML = addHeart(rank, scoreboard[j], j, found); - toAdd[cntFound] = scoreboard[j].cloneNode(true); - if (rank.trim().length === 0) { - // make rank explicit in case of tie - getRank(toAdd[cntFound]).innerHTML += lastRank; + Object.keys(scoreboards).forEach(function(key) { + var toAdd = new Array(); + var toAddMobile = new Array(); + var cntFound = 0; + var lastRank = 0; + const scoreboard = scoreboards[key]; + const mobile = key === 'mobile'; + let teamIndex = 1; + for (var j = 0; j < scoreboard.length; j++) { + var found = false; + var teamname = getTeamname(scoreboard[j]); + if (teamname === null) { + continue; + } + var firstCol = getRank(scoreboard[j]); + var heartCol = getHeartCol(scoreboard[j]); + var rank = firstCol.innerHTML; + for (var i = 0; i < favTeams.length; i++) { + if (teamname === favTeams[i]) { + found = true; + heartCol.innerHTML = addHeart(rank, scoreboard[j], teamIndex, found, mobile); + toAdd[cntFound] = scoreboard[j].cloneNode(true); + if (mobile) { + toAddMobile[cntFound] = scoreboard[j + 1].cloneNode(true); + } + if (rank.trim().length === 0) { + // make rank explicit in case of tie + getRank(toAdd[cntFound]).innerHTML += lastRank; + } + scoreboard[j].style.background = "lightyellow"; + const scoretn = scoreboard[j].querySelector('.scoretn'); + if (scoretn && scoretn.classList.contains('cl_FFFFFF')) { + scoretn.classList.remove('cl_FFFFFF'); + scoretn.classList.add('cl_FFFFE0'); + } + if (mobile) { + scoreboard[j + 1].style.background = "lightyellow"; + } + cntFound++; + break; } - scoreboard[j].style.background = "lightyellow"; - cntFound++; - break; } + if (!found) { + heartCol.innerHTML = addHeart(rank, scoreboard[j], teamIndex, found, mobile); + } + if (rank !== "") { + lastRank = rank; + } + + teamIndex++; } - if (!found) { - heartCol.innerHTML = addHeart(rank, scoreboard[j], j, found); - } - if (rank !== "") { - lastRank = rank; - } - } - // copy favourite teams to the top of the scoreboard - for (var i = 0; i < cntFound; i++) { - var copy = toAdd[i]; - var style = ""; - if (i === 0) { - style += "border-top: 2px solid black;"; + let addCounter = 1; + const copyRow = function (i, copy, addTopBorder, addBottomBorder, noMiddleBorder) { + let style = ""; + if (noMiddleBorder) { + style += "border-bottom-width: 0;"; + } + if (addTopBorder && i === 0) { + style += "border-top: 2px solid black;"; + } + if (addBottomBorder && i === cntFound - 1) { + style += "border-bottom: thick solid black;"; + } + copy.setAttribute("style", style); + const tbody = scoreboard[1].parentNode; + tbody.insertBefore(copy, scoreboard[addCounter]); + addCounter++; } - if (i === cntFound - 1) { - style += "border-bottom: thick solid black;"; + + // copy favourite teams to the top of the scoreboard + for (let i = 0; i < cntFound; i++) { + copyRow(i, toAdd[i], true, !mobile, mobile); + if (mobile) { + copyRow(i, toAddMobile[i], false, true, false); + } } - copy.setAttribute("style", style); - var tbody = scoreboard[1].parentNode; - tbody.insertBefore(copy, scoreboard[i + 1]); - } + }); } // This function is a specific addition for using DOMjudge within a @@ -944,3 +1005,54 @@ function initializeKeyboardShortcuts() { } }); } + +// Make sure the items in the desktop scoreboard fit +document.querySelectorAll(".desktop-scoreboard .forceWidth:not(.toolong)").forEach(el => { + if (el instanceof Element && el.scrollWidth > el.offsetWidth) { + el.classList.add("toolong"); + } +}); + +/** + * Helper method to resize mobile team names and problem badges + */ +function resizeMobileTeamNamesAndProblemBadges() { + // Make team names fit on the screen, but only when the mobile + // scoreboard is visible + const mobileScoreboard = document.querySelector('.mobile-scoreboard'); + if (mobileScoreboard.offsetWidth === 0) { + return; + } + const windowWidth = document.body.offsetWidth; + const teamNameMaxWidth = Math.max(10, windowWidth - 150); + const problemBadgesMaxWidth = Math.max(10, windowWidth - 78); + document.querySelectorAll(".mobile-scoreboard .forceWidth:not(.toolong)").forEach(el => { + el.classList.remove("toolong"); + el.style.maxWidth = teamNameMaxWidth + 'px'; + if (el instanceof Element && el.scrollWidth > el.offsetWidth) { + el.classList.add("toolong"); + } else { + el.classList.remove("toolong"); + } + }); + document.querySelectorAll(".mobile-scoreboard .mobile-problem-badges:not(.toolong)").forEach(el => { + el.classList.remove("toolong"); + el.style.maxWidth = problemBadgesMaxWidth + 'px'; + if (el instanceof Element && el.scrollWidth > el.offsetWidth) { + el.classList.add("toolong"); + const scale = el.offsetWidth / el.scrollWidth; + const offset = -1 * (el.scrollWidth - el.offsetWidth) / 2; + el.style.transform = `scale(${scale}) translateX(${offset}px)`; + } else { + el.classList.remove("toolong"); + el.style.transform = null; + } + }); +} + +$(function() { + if (document.querySelector('.mobile-scoreboard')) { + window.addEventListener('resize', resizeMobileTeamNamesAndProblemBadges); + resizeMobileTeamNamesAndProblemBadges(); + } +}); diff --git a/webapp/public/style_domjudge.css b/webapp/public/style_domjudge.css index e443f7271c..7b8db354a2 100644 --- a/webapp/public/style_domjudge.css +++ b/webapp/public/style_domjudge.css @@ -195,6 +195,9 @@ del { border-right: 1px solid silver; padding: 0; } +.scoreboard td.no-border, .scoreboard th.no-border { + border: none; +} .scoreboard td.score_cell { min-width: 4.2em; border-right: none; @@ -219,6 +222,13 @@ del { display: block; overflow: hidden; } + +.mobile-problem-badges { + position: relative; + display: block; + white-space: nowrap; +} + .toolong:after { content: ""; width: 30%; @@ -274,7 +284,7 @@ img.affiliation-logo { .silver-medal { background-color: #aaa } .bronze-medal { background-color: #c08e55 } -#scoresolv,#scoretotal { width: 2.5em; } +#scoresolv,#scoretotal,#scoresolvmobile,#scoretotalmobile { width: 2.5em; } .scorenc,.scorett,.scorepl { text-align: center; width: 2ex; } .scorenc { font-weight: bold; } td.scorenc { border-color: silver; border-right: 0; } @@ -699,3 +709,24 @@ blockquote { padding: 3px; border-radius: 5px; } + +.strike-diagonal { + position: relative; + text-align: center; +} + +.strike-diagonal:before { + position: absolute; + content: ""; + left: 0; + top: 50%; + right: 0; + border-top: 2px solid; + border-color: firebrick; + + -webkit-transform:rotate(-35deg); + -moz-transform:rotate(-35deg); + -ms-transform:rotate(-35deg); + -o-transform:rotate(-35deg); + transform:rotate(-35deg); +} diff --git a/webapp/src/Controller/API/JudgehostController.php b/webapp/src/Controller/API/JudgehostController.php index 8eb6239515..08bc208329 100644 --- a/webapp/src/Controller/API/JudgehostController.php +++ b/webapp/src/Controller/API/JudgehostController.php @@ -1032,6 +1032,12 @@ private function addSingleJudgingRun( // Only update if the current result is different from what we had before. // This should only happen when the old result was NULL. if ($oldResult !== $result) { + if ($oldResult === 'aborted') { + // This judging was cancelled while we worked on it, + // probably as part of a cancelled rejudging. + // Throw away our work, and return that we're done. + return false; + } if ($oldResult !== null) { throw new BadMethodCallException('internal bug: the evaluated result changed during judging'); } @@ -1583,32 +1589,42 @@ public function getJudgeTasksAction(Request $request): array // This is case 2.a) from above: start something new. // This runs transactional to prevent a queue task being picked up twice. $judgetasks = null; - $this->em->wrapInTransaction(function () use ($judgehost, $max_batchsize, &$judgetasks) { - $jobid = $this->em->createQueryBuilder() - ->from(QueueTask::class, 'qt') - ->innerJoin('qt.judging', 'j') - ->select('j.judgingid') + $jobid = $this->em->createQueryBuilder() + ->from(QueueTask::class, 'qt') + ->innerJoin('qt.judging', 'j') + ->select('j.judgingid') + ->andWhere('qt.startTime IS NULL') + ->addOrderBy('qt.priority') + ->addOrderBy('qt.teamPriority') + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR); + if ($jobid !== null) { + // Mark it as being worked on. + $result = $this->em->createQueryBuilder() + ->update(QueueTask::class, 'qt') + ->set('qt.startTime', Utils::now()) + ->andWhere('qt.judging = :jobid') ->andWhere('qt.startTime IS NULL') - ->addOrderBy('qt.priority') - ->addOrderBy('qt.teamPriority') - ->setMaxResults(1) + ->setParameter('jobid', $jobid) ->getQuery() - ->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR); - $judgetasks = $this->getJudgetasks($jobid, $max_batchsize, $judgehost); - if ($judgetasks !== null) { - // Mark it as being worked on. - $this->em->createQueryBuilder() - ->update(QueueTask::class, 'qt') - ->set('qt.startTime', Utils::now()) - ->andWhere('qt.judging = :jobid') - ->andWhere('qt.startTime IS NULL') - ->setParameter('jobid', $jobid) - ->getQuery() - ->execute(); + ->execute(); + + if ($result == 0) { + // Another judgehost beat us to it. + $judgetasks = [['type' => 'try_again']]; + } else { + $judgetasks = $this->getJudgetasks($jobid, $max_batchsize, $judgehost); + if (empty($judgetasks)) { + // Somehow we got ourselves in a situation that there was a queue task without remaining judge tasks. + // This should not happen, but if it does, we need to clean up. Each of the fetch-work calls will clean + // up one queue task. We need to signal to the judgehost that there might be more work to do. + $judgetasks = [['type' => 'try_again']]; + } + } + if (!empty($judgetasks)) { + return $judgetasks; } - }); - if (!empty($judgetasks)) { - return $judgetasks; } if ($this->config->get('enable_parallel_judging')) { diff --git a/webapp/src/Twig/TwigExtension.php b/webapp/src/Twig/TwigExtension.php index 2a73e324e4..3a44fccb52 100644 --- a/webapp/src/Twig/TwigExtension.php +++ b/webapp/src/Twig/TwigExtension.php @@ -23,6 +23,7 @@ use App\Service\DOMJudgeService; use App\Service\EventLogService; use App\Service\SubmissionService; +use App\Utils\Scoreboard\ScoreboardMatrixItem; use App\Utils\Utils; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\EntityManagerInterface; @@ -110,6 +111,7 @@ public function getFilters(): array new TwigFilter('fileTypeIcon', $this->fileTypeIcon(...)), new TwigFilter('problemBadge', $this->problemBadge(...), ['is_safe' => ['html']]), new TwigFilter('problemBadgeForContest', $this->problemBadgeForContest(...), ['is_safe' => ['html']]), + new TwigFilter('problemBadgeMaybe', $this->problemBadgeMaybe(...), ['is_safe' => ['html']]), new TwigFilter('printMetadata', $this->printMetadata(...), ['is_safe' => ['html']]), new TwigFilter('printWarningContent', $this->printWarningContent(...), ['is_safe' => ['html']]), new TwigFilter('entityIdBadge', $this->entityIdBadge(...), ['is_safe' => ['html']]), @@ -1063,9 +1065,9 @@ public function fileTypeIcon(string $type): string public function problemBadge(ContestProblem $problem, bool $grayedOut = false): string { - $rgb = Utils::convertToHex($problem->getColor() ?? '#ffffff'); + $rgb = Utils::convertToHex($problem->getColor() ?? '#ffffff'); if ($grayedOut) { - $rgb = 'whitesmoke'; + $rgb = Utils::convertToHex('whitesmoke'); } $background = Utils::parseHexColor($rgb); @@ -1091,6 +1093,45 @@ public function problemBadge(ContestProblem $problem, bool $grayedOut = false): ); } + public function problemBadgeMaybe(ContestProblem $problem, ScoreboardMatrixItem $matrixItem): string + { + $rgb = Utils::convertToHex($problem->getColor() ?? '#ffffff'); + if (!$matrixItem->isCorrect) { + $rgb = Utils::convertToHex('whitesmoke'); + } + $background = Utils::parseHexColor($rgb); + + // Pick a border that's a bit darker. + $darker = $background; + $darker[0] = max($darker[0] - 64, 0); + $darker[1] = max($darker[1] - 64, 0); + $darker[2] = max($darker[2] - 64, 0); + $border = Utils::rgbToHex($darker); + + // Pick the foreground text color based on the background color. + $foreground = ($background[0] + $background[1] + $background[2] > 450) ? '#000000' : '#ffffff'; + if (!$matrixItem->isCorrect) { + $foreground = 'silver'; + $border = 'linen'; + } + + $ret = sprintf( + '%s', + $rgb, + $border, + $foreground, + $problem->getShortname() + ); + if (!$matrixItem->isCorrect) { + if ($matrixItem->numSubmissionsPending > 0) { + $ret = '' . $ret . ''; + } elseif ($matrixItem->numSubmissions > 0) { + $ret = '' . $ret . ''; + } + } + return $ret; + } + public function problemBadgeForContest(Problem $problem, ?Contest $contest = null): string { $contest ??= $this->dj->getCurrentContest(); diff --git a/webapp/templates/partials/scoreboard.html.twig b/webapp/templates/partials/scoreboard.html.twig index 3c4f58e4e0..4fb8a18bfb 100644 --- a/webapp/templates/partials/scoreboard.html.twig +++ b/webapp/templates/partials/scoreboard.html.twig @@ -18,31 +18,37 @@ {% endif %}
-
- {{ current_contest.name }} - - {% if scoreboard is null %} - {{ current_contest | printContestStart }} - {% elseif scoreboard.freezeData.showFinal(jury) %} - {% if current_contest.finalizetime is empty %} - preliminary results - not final - {% else %} - final standings - {% endif %} - {% elseif scoreboard.freezeData.stopped %} - contest over, waiting for results - {% elseif static %} - {% set now = 'now'|date('U') %} - {{ current_contest.starttime | printelapsedminutes(now) }} - {% else %} - {% if current_contest.freezeData.started %} - started: +
+
+
+ {{ current_contest.name }} +
+
+ + {% if scoreboard is null %} + {{ current_contest | printContestStart }} + {% elseif scoreboard.freezeData.showFinal(jury) %} + {% if current_contest.finalizetime is empty %} + preliminary results - not final + {% else %} + final standings + {% endif %} + {% elseif scoreboard.freezeData.stopped %} + contest over, waiting for results + {% elseif static %} + {% set now = 'now'|date('U') %} + {{ current_contest.starttime | printelapsedminutes(now) }} {% else %} - starts: + {% if current_contest.freezeData.started %} + started: + {% else %} + starts: + {% endif %} + {{ current_contest.starttime | printtime }} - ends: {{ current_contest.endtime | printtime }} {% endif %} - {{ current_contest.starttime | printtime }} - ends: {{ current_contest.endtime | printtime }} - {% endif %} - + +
+
{% if static %} diff --git a/webapp/templates/partials/scoreboard_table.html.twig b/webapp/templates/partials/scoreboard_table.html.twig index b0f64f2eab..1d2591fc82 100644 --- a/webapp/templates/partials/scoreboard_table.html.twig +++ b/webapp/templates/partials/scoreboard_table.html.twig @@ -28,7 +28,12 @@ {% endif %} - +
+ + {% set teamColspan = 2 %} + {% if showAffiliationLogos %} + {% set teamColspan = teamColspan + 1 %} + {% endif %} {# output table column groups (for the styles) #} @@ -58,12 +63,6 @@ {% endfor %} {% endif %} - - {% set teamColspan = 2 %} - {% if showAffiliationLogos %} - {% set teamColspan = teamColspan + 1 %} - {% endif %} - {% if enable_ranking %} @@ -107,7 +106,7 @@ {% set previousSortOrder = -1 %} {% set previousTeam = null %} - {% set backgroundColors = {"#FFFFFF": 1} %} + {% set backgroundColors = {"#FFFFFF": 1, '#FFFFE0': 1} %} {% set medalCount = 0 %} {% for score in scores %} {% set classes = [] %} @@ -134,7 +133,7 @@ {% else %} {% set color = score.team.category.color %} {% endif %} - + {% if enable_ranking %}
{# Only print rank when score is different from the previous team #} @@ -316,6 +315,183 @@
+ + {# output table column groups (for the styles) #} + + {% if enable_ranking %} + + {% endif %} + {% if showFlags %} + + {% else %} + + {% endif %} + {% if showAffiliationLogos %} + + {% endif %} + + + {% if enable_ranking %} + + + + {% endif %} + + + {% set teamColspan = 2 %} + {% if showAffiliationLogos %} + {% set teamColspan = teamColspan + 1 %} + {% endif %} + + + {% if enable_ranking %} + + {% endif %} + + {% if enable_ranking %} + + {% endif %} + + + + {% set previousSortOrder = -1 %} + {% set previousTeam = null %} + {% set medalCount = 0 %} + {% for score in scores %} + {% set classes = [] %} + {% if score.team.category.sortorder != previousSortOrder %} + {% 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({"#": "_"}) %} + {% endif %} + + {% if enable_ranking %} + {% set totalTime = score.totalTime %} + {% if scoreInSeconds %} + {% set totalTime = totalTime | printTimeRelative %} + {% endif %} + {% set totalPoints = score.numPoints %} + + {% endif %} + + + {% if showAffiliationLogos %} + {% set problemSpan = 3 %} + {% else %} + {% set problemSpan = 2 %} + {% endif %} + + + {% endfor %} + +
rankteamscore
+ {# 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 %} + {{ score.team.affiliation.name }} + {% 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 false and 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 }}
{{ totalTime }}
+ {% for problem in problems %} + {% set matrixItem = scoreboard.matrix[score.team.teamid][problem.probid] %} + {{ problem | problemBadgeMaybe(matrixItem) }} + {% endfor %} +
+ {% if static %} {% for score in scores %} {% embed 'partials/modal.html.twig' with {'modalId': 'team-modal-' ~ score.team.teamid} %} @@ -366,7 +542,7 @@ {% else %} {% set cellColors = {first: 'Solved first', correct: 'Solved', incorrect: 'Tried, incorrect', pending: 'Tried, pending', neutral: 'Untried'} %} {% endif %} - +
@@ -385,7 +561,7 @@ {% endif %} {% if medalsEnabled %} -
Cell colours
+
diff --git a/webapp/templates/public/scoreboard.html.twig b/webapp/templates/public/scoreboard.html.twig index 6ab323c152..e1f0dbb559 100644 --- a/webapp/templates/public/scoreboard.html.twig +++ b/webapp/templates/public/scoreboard.html.twig @@ -12,7 +12,7 @@ {% set bannerImage = globalBannerAssetPath() %} {% endif %} {% if bannerImage %} - + {% endif %}
@@ -53,6 +53,7 @@ {% if static and refresh is defined %} disableRefreshOnModal(); {% endif %} + resizeMobileTeamNamesAndProblemBadges(); }; {% if static and refresh is defined %} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy

Medals {% if not scoreboard.freezeData.showFinal %}(tentative){% endif %}