Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 5 additions & 2 deletions doc/manual/running.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,12 @@ show for submissions after the freeze. It is possible that new
entries appear for some times after the freeze, if the result of
a submission before the freeze is only known after (this can also
happen in case of a :ref:`rejudging`).
The global configuration option ``show_balloons_postfreeze`` will
The global configuration option ``minimum_number_of_balloons`` will
ignore a contest freeze for purposes of balloons and new correct
submissions will trigger a balloon entry in the table.
submissions will trigger a balloon entry in the table. This only
happens when the team problem has not received the amount of balloons
set by the configuration option and the newly solved problem must have
been solved before the freeze. This is to prevent an information leak.

Static scoreboard
-----------------
Expand Down
8 changes: 4 additions & 4 deletions etc/db-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -255,11 +255,11 @@
default_value: false
public: true
description: Show results of TOO-LATE submissions in team interface?
- name: show_balloons_postfreeze
type: bool
default_value: false
- name: minimum_number_of_balloons
type: int
default_value: 0
public: true
description: Give out balloon notifications after the scoreboard has been frozen?
description: How many balloons to hand out ignoring freeze time. Only hands out balloons for problems solved pre-freeze.
- name: show_relative_time
type: bool
default_value: false
Expand Down
118 changes: 70 additions & 48 deletions webapp/src/Service/BalloonService.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,13 @@ public function updateBalloons(
public function collectBalloonTable(Contest $contest, bool $todo = false): array
{
$em = $this->em;
$showPostFreeze = (bool)$this->config->get('show_balloons_postfreeze');
if (!$showPostFreeze) {
$freezetime = $contest->getFreezeTime();
}

// Retrieve all relevant balloons in 'submit order'. This allows accurate
// counts when deciding whether to hand out post-freeze balloons.
$query = $em->createQueryBuilder()
->select('b', 's.submittime', 'p.probid',
't.teamid', 's', 't', 't.location',
'c.categoryid AS categoryid', 'c.name AS catname',
'c.categoryid AS categoryid', 'c.sortorder', 'c.name AS catname',
'co.cid', 'co.shortname',
'cp.shortname AS probshortname', 'cp.color',
'a.affilid AS affilid', 'a.shortname AS affilshort')
Expand All @@ -114,28 +112,21 @@ public function collectBalloonTable(Contest $contest, bool $todo = false): array
->leftJoin('t.affiliation', 'a')
->andWhere('co.cid = :cid')
->setParameter('cid', $contest->getCid())
->orderBy('b.done', 'ASC')
->addOrderBy('s.submittime', 'DESC');
->orderBy('b.done', 'DESC')
->addOrderBy('s.submittime', 'ASC');

$balloons = $query->getQuery()->getResult();
// Loop once over the results to get totals.
$TOTAL_BALLOONS = [];
foreach ($balloons as $balloonsData) {
if ($balloonsData['color'] === null) {
continue;
}

$stime = $balloonsData['submittime'];
$minumumNumberOfBalloons = (int)$this->config->get('minimum_number_of_balloons');
$freezetime = $contest->getFreezeTime();

if (isset($freezetime) && $stime >= $freezetime) {
continue;
}
$balloonsTable = [];

$TOTAL_BALLOONS[$balloonsData['teamid']][$balloonsData['probshortname']] = $balloonsData[0]->getSubmission()->getContestProblem();
}
// Total balloons keeps track of the total balloons for a team, will be used to fill the rhs for every row in $balloonsTable.
// The same summary is used for every row for a team. References to elements in this array ensure easy updates.
/** @var mixed[] $balloonSummaryPerTeam */
$balloonSummaryPerTeam = [];

// Loop again to construct table.
$balloons_table = [];
foreach ($balloons as $balloonsData) {
if ($balloonsData['color'] === null) {
continue;
Expand All @@ -144,41 +135,72 @@ public function collectBalloonTable(Contest $contest, bool $todo = false): array
$balloon = $balloonsData[0];
$done = $balloon->getDone();

if ($todo && $done) {
continue;
// Ensure a summary-row exists for this sortorder and take a reference to these summaries. References are needed to ensure array reuse.
// Summaries are used to determine whether a balloon has been handed out so they need to be separated between sortorders.
$balloonSummaryPerTeam[$balloonsData['sortorder']] ??= [];
$relevantBalloonSummaries = &$balloonSummaryPerTeam[$balloonsData['sortorder']];

// Commonly no balloons are handed out post freeze.
// Underperforming teams' moral can be boosted by handing out balloons post-freeze.
// Handing out balloons for problems that have not been solved pre-freeze poses a potential information leak, so these are always excluded.
// So to decide whether to skip showing a balloon:
// 1. Check whether the scoreboard has been frozen.
// 2. Check whether the team has exceeded minimum number of balloons.
// 3. Check whether the problem been solved pre-freeze.
$stime = $balloonsData['submittime'];
if (isset($freezetime) && $stime >= $freezetime) {
if (key_exists($balloonsData['teamid'], $relevantBalloonSummaries) &&
count($relevantBalloonSummaries[$balloonsData['teamid']]) >= $minumumNumberOfBalloons) {
continue;
}

// Check if problem has been solved before the freeze by someone in the same sortorder to prevent information leak.
// The DOMjudge team (that commonly runs jury submissions) has so must be ignored.
// If a balloon for this problem should've been handed out it is safe to hand out again since balloons are handled in 'submit order'.
if (!array_reduce($relevantBalloonSummaries, fn($c, $i) => $c ||
array_key_exists($balloonsData['probshortname'], $i), false)) {
continue;
}
}

$balloonId = $balloon->getBalloonId();
// Register the balloon that is handed out in the team summary.
$relevantBalloonSummaries[$balloonsData['teamid']][$balloonsData['probshortname']] = $balloon->getSubmission()->getContestProblem();

$stime = $balloonsData['submittime'];

if (isset($freezetime) && $stime >= $freezetime) {
// This balloon might not need to be listed, entire order is needed for counts though.
if ($todo && $done) {
continue;
}

$balloondata = [];
$balloondata['balloonid'] = $balloonId;
$balloondata['time'] = $stime;
$balloondata['problem'] = $balloonsData['probshortname'];
$balloondata['contestproblem'] = $balloon->getSubmission()->getContestProblem();
$balloondata['team'] = $balloon->getSubmission()->getTeam();
$balloondata['teamid'] = $balloonsData['teamid'];
$balloondata['location'] = $balloonsData['location'];
$balloondata['affiliation'] = $balloonsData['affilshort'];
$balloondata['affiliationid'] = $balloonsData['affilid'];
$balloondata['category'] = $balloonsData['catname'];
$balloondata['categoryid'] = $balloonsData['categoryid'];

ksort($TOTAL_BALLOONS[$balloonsData['teamid']]);
$balloondata['total'] = $TOTAL_BALLOONS[$balloonsData['teamid']];

$balloondata['done'] = $done;

$balloons_table[] = [
'data' => $balloondata,
$balloonsTable[] = [
'data' => [
'balloonid' => $balloon->getBalloonId(),
'time' => $stime,
'problem' => $balloonsData['probshortname'],
'contestproblem' => $balloon->getSubmission()->getContestProblem(),
'team' => $balloon->getSubmission()->getTeam(),
'teamid' => $balloonsData['teamid'],
'location' => $balloonsData['location'],
'affiliation' => $balloonsData['affilshort'],
'affiliationid' => $balloonsData['affilid'],
'category' => $balloonsData['catname'],
'categoryid' => $balloonsData['categoryid'],
'done' => $done,

// Reuse the same total summary table by taking a reference, makes updates easier.
'total' => &$relevantBalloonSummaries[$balloonsData['teamid']],
]
];
}
return $balloons_table;

// Sort the balloons, since these are handled by reference each summary item only need to be sorted once.
foreach ($balloonSummaryPerTeam as $relevantBalloonSummaries) {
foreach ($relevantBalloonSummaries as &$balloons) {
ksort($balloons);
}
}

// Reverse the order so the newest appear first
return array_reverse($balloonsTable);
}

public function setDone(int $balloonId): void
Expand Down
29 changes: 9 additions & 20 deletions webapp/src/Service/DOMJudgeService.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ class DOMJudgeService

public function __construct(
protected readonly EntityManagerInterface $em,
protected readonly BalloonService $balloonService,
protected readonly LoggerInterface $logger,
protected readonly RequestStack $requestStack,
protected readonly ParameterBagInterface $params,
Expand Down Expand Up @@ -437,26 +438,14 @@ public function getUpdates(): array
}

if ($this->checkrole('balloon') && $contest) {
$balloonsQuery = $this->em->createQueryBuilder()
->select('b.balloonid', 't.name', 't.location', 'p.name AS pname')
->from(Balloon::class, 'b')
->leftJoin('b.submission', 's')
->leftJoin('s.problem', 'p')
->leftJoin('s.contest', 'co')
->leftJoin('p.contest_problems', 'cp', Join::WITH, 'co.cid = cp.contest AND p.probid = cp.problem')
->leftJoin('s.team', 't')
->andWhere('co.cid = :cid')
->andWhere('b.done = 0')
->setParameter('cid', $contest->getCid());

$freezetime = $contest->getFreezeTime();
if ($freezetime !== null && !(bool)$this->config->get('show_balloons_postfreeze')) {
$balloonsQuery
->andWhere('s.submittime < :freeze')
->setParameter('freeze', $freezetime);
}

$balloons = $balloonsQuery->getQuery()->getResult();
$balloons = array_map(function ($balloon) {
return [
'balloonid' => $balloon['data']['balloonid'],
'name' => $balloon['data']['team']->getName(),
'location' => $balloon['data']['location'],
'pname' => $balloon['data']['contestproblem']->getProblem()->getName(),
];
}, $this->balloonService->collectBalloonTable($contest, true));
}

return [
Expand Down
Loading