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
14 changes: 13 additions & 1 deletion webapp/src/Controller/Jury/BalloonController.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
Expand Down Expand Up @@ -58,6 +59,7 @@ public function indexAction(BalloonService $balloonService): Response
}

$balloons_table = $balloonService->collectBalloonTable($contest);
$teamSummary = $balloonService->collectTeamBalloonSummary($balloons_table);

// Add CSS class and actions.
foreach ($balloons_table as $element) {
Expand Down Expand Up @@ -164,7 +166,8 @@ public function indexAction(BalloonService $balloonService): Response
'filteredCategories' => $filteredCategories,
'availableCategories' => $availableCategories,
'defaultCategories' => $defaultCategories,
'balloons' => $balloons_table
'balloons' => $balloons_table,
'teamSummary' => $teamSummary,
]);
}

Expand All @@ -175,4 +178,13 @@ public function setDoneAction(int $balloonId, BalloonService $balloonService): R

return $this->redirectToRoute("jury_balloons");
}

#[Route(path: '/done', name: 'jury_balloons_setdone_multiple', methods: ['POST'])]
public function setMultipleDoneAction(Request $request, BalloonService $balloonService): RedirectResponse
{
$balloonIds = $request->request->all('balloonIds');
$balloonService->setDone($balloonIds);

return $this->redirectToRoute("jury_balloons");
}
}
60 changes: 55 additions & 5 deletions webapp/src/Service/BalloonService.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,14 +181,64 @@ public function collectBalloonTable(Contest $contest, bool $todo = false): array
return $balloons_table;
}

public function setDone(int $balloonId): void
/**
* Collect a summary of balloons per team, sorted by location (or external ID if no location).
*
* @param array<array{data: array{balloonid: int, time: string, problem: string, contestproblem: ContestProblem,
* team: Team, teamid: int, location: string|null, affiliation: string|null,
* affiliationid: int, category: string, categoryid: int, total: array<string, ContestProblem>,
* done: bool}}> $balloons
* @return array<int, array{team: Team, affiliation: string|null, affiliationid: int|null, location: string|null,
* category: string, categoryid: int, total: array<string, ContestProblem>}>
*/
public function collectTeamBalloonSummary(array $balloons): array
{
$teamSummary = [];
foreach ($balloons as $balloon) {
$data = $balloon['data'];
$teamId = $data['teamid'];

if (!isset($teamSummary[$teamId])) {
$teamSummary[$teamId] = [
'team' => $data['team'],
'affiliation' => $data['affiliation'],
'affiliationid' => $data['affiliationid'],
'location' => $data['location'],
'category' => $data['category'],
'categoryid' => $data['categoryid'],
'total' => $data['total'],
];
}
}

uasort($teamSummary, function ($a, $b) {
$aKey = $a['location'] ?? $a['team']->getExternalId();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For us location can be a string right? So those can be the same, maybe also append the externalId to it to make sure you can always sort?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that idea

$bKey = $b['location'] ?? $b['team']->getExternalId();
return strnatcasecmp((string)$aKey, (string)$bKey);
});

return $teamSummary;
}

/**
* @param int|list<int> $balloonId
*/
public function setDone(int|array $balloonId): void
{
$em = $this->em;
$balloon = $em->getRepository(Balloon::class)->find($balloonId);
if (!$balloon) {
throw new NotFoundHttpException('balloon not found');
$balloons = $em->createQueryBuilder()
->from(Balloon::class, 'b')
->select('b')
->andWhere('b.balloonid IN (:balloonIds)')
->setParameter('balloonIds', $balloonId)
->getQuery()
->getResult();
if (empty($balloons)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (empty($balloons)) {
if (count($balloons) !== count((array)$balloonId)) {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we drop the message if any of the balloonIds is invalid (for whatever reason)...

throw new NotFoundHttpException('balloon(s) not found');
}
foreach ($balloons as $balloon) {
$balloon->setDone(true);
}
$balloon->setDone(true);
$em->flush();
}
}
40 changes: 39 additions & 1 deletion webapp/templates/jury/balloons.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@
{% block extrafooter %}
{% if current_contest is not null %}
<script>
const selectedBalloons = new Set();
function updateMarkMultipleDone() {
const anyChecked = $('.balloons-table input[type="checkbox"]:checked').length > 0;
if (anyChecked) {
$('.mark-multiple-done-button').attr('disabled', false);
} else {
$('.mark-multiple-done-button').attr('disabled', true);
}
}

$(function () {
$('#filter-toggle').on('change', function () {
if ($(this).is(':checked')) {
Expand Down Expand Up @@ -119,7 +129,7 @@
});

window.process_balloons_filter = function () {
var $trs = $('table.balloons-table > tbody tr');
var $trs = $('table.balloons-table > tbody tr, table.team-summary-table > tbody tr');

var filters = [];

Expand Down Expand Up @@ -159,6 +169,34 @@
})
.show();
}

$('.balloons-table input[type="checkbox"]').on('change', function () {
updateMarkMultipleDone();
const balloonId = $(this).data('balloon-id');
if ($(this).is(':checked')) {
selectedBalloons.add(balloonId);
} else {
selectedBalloons.delete(balloonId);
}
});

// Restore any selected balloons and clear any selectedBalloons that don't exist anymore
const existingBalloonIds = new Set();
$('.balloons-table input[type="checkbox"]').each(function() {
existingBalloonIds.add($(this).data('balloon-id'));
});

for (const balloonId of selectedBalloons) {
if (existingBalloonIds.has(balloonId)) {
// Restore the selection
$('.balloons-table input[type="checkbox"][data-balloon-id="' + balloonId + '"]').prop('checked', true);
} else {
// Remove from selectedBalloons as it no longer exists
selectedBalloons.delete(balloonId);
}
}

updateMarkMultipleDone();
};

$('select[data-filter-field]').on('change', process_balloons_filter);
Expand Down
150 changes: 97 additions & 53 deletions webapp/templates/jury/partials/balloon_list.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -4,60 +4,104 @@
{% if balloons is empty %}
<div class="alert alert-warning">No balloons</div>
{% else %}
<table class="data-table table table-hover table-striped table-sm balloons-table" style="width:auto">
<thead class="thead-light">
<tr>
<th scope="col">&nbsp;</th>
<th scope="col">ID</th>
<th scope="col">time</th>
<th scope="col">solved</th>
<th scope="col">team</th>
<th scope="col">affiliation</th>
<th scope="col">location</th>
<th scope="col">category</th>
<th scope="col">total</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{%- for balloon in balloons %}
<tr class="{% if balloon.data.done %}disabled{% endif %}"
data-affiliation-id="{{ balloon.data.affiliationid }}"
data-location-str="{{ balloon.data.location }}"
data-category-id="{{ balloon.data.categoryid }}">
<td>
{%- if balloon.data.done -%}
<i class="far fa-check-circle"></i>
{%- else -%}
<i class="far fa-hourglass"></i>
{%- endif -%}
<td>{{ balloon.data.balloonid }}</td>
<td data-order="{{ balloon.data.time }}">{{ balloon.data.time | printtime }}</td>
<td>{{ balloon.data.contestproblem | problemBadge }}</td>
<td>
{{ balloon.data.team | entityIdBadge('t') }}
{{ balloon.data.team.effectiveName | u.truncate(teamname_max_length, '…') }}
</td>
<td>{{ balloon.data.affiliation }}</td>
<td>{{ balloon.data.location }}</td>
<td>{{ balloon.data.category }}</td>
<td>
{%- for totalballoon in balloon.data.total -%}
{{ totalballoon | problemBadge }}&nbsp;
{%- endfor -%}
</td>
<td>
{%- if not balloon.data.done -%}
{%- set link = path('jury_balloons_setdone', {balloonId: balloon.data.balloonid}) %}
<a href="{{ link }}" title="mark balloon as done">
<i class="fas fa-running"></i>
</a>
</td>
{%- endif -%}
<form action="{{ path('jury_balloons_setdone_multiple') }}" method="post">
<input type="submit" class="btn btn-primary mark-multiple-done-button" value="Mark selected as done" />

<table class="data-table table table-hover table-striped table-sm balloons-table" style="width:auto">
<thead class="thead-light">
<tr>
<th scope="col">&nbsp;</th>
<th scope="col">ID</th>
<th scope="col">time</th>
<th scope="col">solved</th>
<th scope="col">team</th>
<th scope="col">affiliation</th>
<th scope="col">location</th>
<th scope="col">category</th>
<th scope="col">total</th>
<th scope="col"></th>
</tr>
{%- endfor %}
</thead>
<tbody>
{%- for balloon in balloons %}
<tr class="{% if balloon.data.done %}disabled{% endif %}"
data-affiliation-id="{{ balloon.data.affiliationid }}"
data-location-str="{{ balloon.data.location }}"
data-category-id="{{ balloon.data.categoryid }}">
<td>
{%- if balloon.data.done -%}
<i class="far fa-check-circle"></i>
{%- else -%}
<i class="far fa-hourglass"></i>
{%- endif -%}
</td>
<td>{{ balloon.data.balloonid }}</td>
<td data-order="{{ balloon.data.time }}">{{ balloon.data.time | printtime }}</td>
<td>{{ balloon.data.contestproblem | problemBadge }}</td>
<td>
{{ balloon.data.team | entityIdBadge('t') }}
{{ balloon.data.team.effectiveName | u.truncate(teamname_max_length, '…') }}
</td>
<td>{{ balloon.data.affiliation }}</td>
<td>{{ balloon.data.location }}</td>
<td>{{ balloon.data.category }}</td>
<td>
{%- for totalballoon in balloon.data.total -%}
{{ totalballoon | problemBadge }}&nbsp;
{%- endfor -%}
</td>
<td>
{%- if not balloon.data.done -%}
{%- set link = path('jury_balloons_setdone', {balloonId: balloon.data.balloonid}) %}
<a href="{{ link }}" title="mark balloon as done">
<i class="fas fa-running"></i>
</a>
{%- endif -%}
</td>
<td>
{% if not balloon.data.done %}
<input type="checkbox" name="balloonIds[]" data-balloon-id="{{ balloon.data.balloonid }}" value="{{ balloon.data.balloonid }}" />
{% endif %}
</td>
</tr>
{%- endfor %}

</tbody>
</table>
</form>

</tbody>
</table>
{% if teamSummary is defined and teamSummary is not empty %}
<h3 class="mt-4">Team Summary</h3>
<table class="data-table table table-hover table-striped table-sm team-summary-table" style="width:auto">
<thead class="thead-light">
<tr>
<th scope="col">ID</th>
<th scope="col">team</th>
<th scope="col">affiliation</th>
<th scope="col">location</th>
<th scope="col">category</th>
<th scope="col">total</th>
</tr>
</thead>
<tbody>
{%- for teamId, summary in teamSummary %}
<tr data-affiliation-id="{{ summary.affiliationid }}"
data-location-str="{{ summary.location }}"
data-category-id="{{ summary.categoryid }}">
<td>{{ summary.team | entityIdBadge('t') }}</td>
<td>{{ summary.team.effectiveName | u.truncate(teamname_max_length, '…') }}</td>
<td>{{ summary.affiliation }}</td>
<td>{{ summary.location }}</td>
<td>{{ summary.category }}</td>
<td>
{%- for problem in summary.total -%}
{{ problem | problemBadge }}&nbsp;
{%- endfor -%}
</td>
</tr>
{%- endfor %}
</tbody>
</table>
{% endif %}

{% endif %}
Loading