Skip to content

Commit

Permalink
Introduce way to generated both honors and ranked results
Browse files Browse the repository at this point in the history
  • Loading branch information
nickygerritsen committed Jul 26, 2024
1 parent 937e71a commit d44c9b3
Show file tree
Hide file tree
Showing 12 changed files with 576 additions and 200 deletions.
142 changes: 84 additions & 58 deletions webapp/src/Controller/Jury/ImportExportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use App\Entity\TeamCategory;
use App\Form\Type\ContestExportType;
use App\Form\Type\ContestImportType;
use App\Form\Type\ExportResultsType;
use App\Form\Type\ICPCCmsType;
use App\Form\Type\JsonImportType;
use App\Form\Type\ProblemsImportType;
Expand Down Expand Up @@ -46,6 +47,7 @@
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Twig\Environment;

#[Route(path: '/jury/import-export')]
#[IsGranted('ROLE_JURY')]
Expand All @@ -63,6 +65,7 @@ public function __construct(
KernelInterface $kernel,
#[Autowire('%domjudge.version%')]
protected readonly string $domjudgeVersion,
protected readonly Environment $twig,
) {
parent::__construct($em, $eventLogService, $dj, $kernel);
}
Expand Down Expand Up @@ -257,21 +260,73 @@ public function indexAction(Request $request): Response
return $this->redirectToRoute('jury_import_export');
}

/** @var TeamCategory[] $teamCategories */
$teamCategories = $this->em->createQueryBuilder()
->from(TeamCategory::class, 'c', 'c.categoryid')
->select('c.sortorder, c.name')
->where('c.visible = 1')
->orderBy('c.sortorder')
->getQuery()
->getResult();
$sortOrders = [];
foreach ($teamCategories as $teamCategory) {
$sortOrder = $teamCategory['sortorder'];
if (!array_key_exists($sortOrder, $sortOrders)) {
$sortOrders[$sortOrder] = [];
$exportResultsForm = $this->createForm(ExportResultsType::class);

$exportResultsForm->handleRequest($request);

if ($exportResultsForm->isSubmitted() && $exportResultsForm->isValid()) {
$contest = $this->dj->getCurrentContest();
if ($contest === null) {
throw new BadRequestHttpException('No current contest');
}
$sortOrders[$sortOrder][] = $teamCategory['name'];

$data = $exportResultsForm->getData();
$format = $data['format'];
$sortOrder = $data['sortorder'];
$individuallyRanked = $data['individually_ranked'];
$honors = $data['honors'];

$extension = match ($format) {
'html_inline', 'html_download' => 'html',
'tsv' => 'tsv',
default => throw new BadRequestHttpException('Invalid format'),
};
$contentType = match ($format) {
'html_inline' => 'text/html',
'html_download' => 'text/html',
'tsv' => 'text/csv',
default => throw new BadRequestHttpException('Invalid format'),
};
$contentDisposition = match ($format) {
'html_inline' => 'inline',
'html_download', 'tsv' => 'attachment',
default => throw new BadRequestHttpException('Invalid format'),
};
$filename = 'results.' . $extension;

$response = new StreamedResponse();
$response->setCallback(function () use (
$format,
$sortOrder,
$individuallyRanked,
$honors
) {
if ($format === 'tsv') {
$data = $this->importExportService->getResultsData(
$sortOrder->sort_order,
$individuallyRanked,
$honors,
);

echo "results\t1\n";
foreach ($data as $row) {
echo implode("\t", array_map(fn($field) => Utils::toTsvField((string)$field), $row->toArray())) . "\n";
}
} else {
echo $this->getResultsHtml(
$sortOrder->sort_order,
$individuallyRanked,
$honors,
);
}
});
$response->headers->set('Content-Type', $contentType);
$response->headers->set('Content-Disposition', "$contentDisposition; filename=\"$filename\"");
$response->headers->set('Content-Transfer-Encoding', 'binary');
$response->headers->set('Connection', 'Keep-Alive');
$response->headers->set('Accept-Ranges', 'bytes');

return $response;
}

return $this->render('jury/import_export.html.twig', [
Expand All @@ -282,16 +337,13 @@ public function indexAction(Request $request): Response
'contest_export_form' => $contestExportForm,
'contest_import_form' => $contestImportForm,
'problems_import_form' => $problemsImportForm,
'sort_orders' => $sortOrders,
'export_results_form' => $exportResultsForm,
]);
}

#[Route(path: '/export/{type<groups|teams|wf_results|full_results>}.tsv', name: 'jury_tsv_export')]
public function exportTsvAction(
string $type,
#[MapQueryParameter(name: 'sort_order')]
?int $sortOrder,
): Response {
public function exportTsvAction(string $type): Response
{
$data = [];
$tsvType = $type;
try {
Expand All @@ -302,14 +354,6 @@ public function exportTsvAction(
case 'teams':
$data = $this->importExportService->getTeamData();
break;
case 'wf_results':
$data = $this->importExportService->getResultsData($sortOrder);
$tsvType = 'results';
break;
case 'full_results':
$data = $this->importExportService->getResultsData($sortOrder, full: true);
$tsvType = 'results';
break;
}
} catch (BadRequestHttpException $e) {
$this->addFlash('danger', $e->getMessage());
Expand All @@ -322,9 +366,6 @@ public function exportTsvAction(
echo sprintf("%s\t%s\n", $tsvType, $version);
foreach ($data as $row) {
// Utils::toTsvFields handles escaping of reserved characters.
if ($row instanceof ResultRow) {
$row = $row->toArray();
}
echo implode("\t", array_map(fn($field) => Utils::toTsvField((string)$field), $row)) . "\n";
}
});
Expand All @@ -335,29 +376,22 @@ public function exportTsvAction(
return $response;
}

#[Route(path: '/export/{type<wf_results|full_results|clarifications>}.html', name: 'jury_html_export')]
public function exportHtmlAction(Request $request, string $type): Response
#[Route(path: '/export/clarifications.html', name: 'jury_html_export_clarifications')]
public function exportClarificationsHtmlAction(): Response
{
try {
switch ($type) {
case 'wf_results':
return $this->getResultsHtml($request);
case 'full_results':
return $this->getResultsHtml($request, full: true);
case 'clarifications':
return $this->getClarificationsHtml();
default:
$this->addFlash('danger', "Unknown export type '" . $type . "' requested.");
return $this->redirectToRoute('jury_import_export');
}
return $this->getClarificationsHtml();
} catch (BadRequestHttpException $e) {
$this->addFlash('danger', $e->getMessage());
return $this->redirectToRoute('jury_import_export');
}
}

protected function getResultsHtml(Request $request, bool $full = false): Response
{
protected function getResultsHtml(
int $sortOrder,
bool $individuallyRanked,
bool $honors
): string {
/** @var TeamCategory[] $categories */
$categories = $this->em->createQueryBuilder()
->from(TeamCategory::class, 'c', 'c.categoryid')
Expand Down Expand Up @@ -392,9 +426,7 @@ protected function getResultsHtml(Request $request, bool $full = false): Respons
$regionWinners = [];
$rankPerTeam = [];

$sortOrder = $request->query->getInt('sort_order');

foreach ($this->importExportService->getResultsData($sortOrder, full: $full) as $row) {
foreach ($this->importExportService->getResultsData($sortOrder, $individuallyRanked, $honors) as $row) {
$team = $teamNames[$row->teamId];
$rankPerTeam[$row->teamId] = $row->rank;

Expand All @@ -421,7 +453,7 @@ protected function getResultsHtml(Request $request, bool $full = false): Respons
}
if ($row['rank'] === null) {
$honorable[] = $row['team'];
} elseif (in_array($row['award'], ['Highest Honors', 'High Honors', 'Honors'], true)) {
} elseif (in_array($row['award'], ['Ranked', 'Highest Honors', 'High Honors', 'Honors'], true)) {
$ranked[$row['award']][] = $row;
} else {
$awarded[] = $row;
Expand All @@ -432,7 +464,7 @@ protected function getResultsHtml(Request $request, bool $full = false): Respons

$collator = new Collator('en_US');
$collator->sort($honorable);
foreach ($ranked as $award => &$rankedTeams) {
foreach ($ranked as &$rankedTeams) {
usort($rankedTeams, function (array $a, array $b) use ($collator): int {
if ($a['rank'] !== $b['rank']) {
return $a['rank'] <=> $b['rank'];
Expand Down Expand Up @@ -494,16 +526,10 @@ protected function getResultsHtml(Request $request, bool $full = false): Respons
'firstToSolve' => $firstToSolve,
'domjudgeVersion' => $this->domjudgeVersion,
'title' => sprintf('Results for %s', $contest->getName()),
'download' => $request->query->getBoolean('download'),
'sortOrder' => $sortOrder,
];
$response = $this->render('jury/export/results.html.twig', $data);

if ($request->query->getBoolean('download')) {
$response->headers->set('Content-disposition', 'attachment; filename=results.html');
}

return $response;
return $this->twig->render('jury/export/results.html.twig', $data);
}

protected function getClarificationsHtml(): Response
Expand Down
78 changes: 78 additions & 0 deletions webapp/src/Form/Type/ExportResultsType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php declare(strict_types=1);

namespace App\Form\Type;

use App\Entity\TeamCategory;
use Doctrine\ORM\EntityManagerInterface;
use stdClass;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;

class ExportResultsType extends AbstractType
{
public function __construct(protected readonly EntityManagerInterface $em) {}

public function buildForm(FormBuilderInterface $builder, array $options): void
{
/** @var TeamCategory[] $teamCategories */
$teamCategories = $this->em->createQueryBuilder()
->from(TeamCategory::class, 'c', 'c.categoryid')
->select('c.sortorder, c.name')
->where('c.visible = 1')
->orderBy('c.sortorder')
->getQuery()
->getResult();
$sortOrders = [];
foreach ($teamCategories as $teamCategory) {
$sortOrder = $teamCategory['sortorder'];
if (!array_key_exists($sortOrder, $sortOrders)) {
$sortOrders[$sortOrder] = new stdClass();
$sortOrders[$sortOrder]->sort_order = $sortOrder;
$sortOrders[$sortOrder]->categories = [];
}
$sortOrders[$sortOrder]->categories[] = $teamCategory['name'];
}

$builder->add('sortorder', ChoiceType::class, [
'choices' => $sortOrders,
'group_by' => null,
'choice_label' => fn(stdClass $sortOrder) => sprintf(
'%d with %d categor%s',

Check failure on line 42 in webapp/src/Form/Type/ExportResultsType.php

View workflow job for this annotation

GitHub Actions / codespell

categor ==> category
$sortOrder->sort_order,
count($sortOrder->categories),
count($sortOrder->categories) === 1 ? 'y' : 'ies',
),
'choice_value' => 'sort_order',
'choice_attr' => fn(stdClass $sortOrder) => [
'data-categories' => json_encode($sortOrder->categories),
],
'label' => 'Sort order',
'help' => '[will be replaced by categories]',
]);
$builder->add('individually_ranked', ChoiceType::class, [
'choices' => [
'Yes' => true,
'No' => false,
],
'label' => 'Individually ranked?',
]);
$builder->add('honors', ChoiceType::class, [
'choices' => [
'Yes' => true,
'No' => false,
],
'label' => 'Honors?',
]);
$builder->add('format', ChoiceType::class, [
'choices' => [
'HTML (display inline)' => 'html_inline',
'HTML (download)' => 'html_download',
'TSV' => 'tsv',
],
'label' => 'Format',
]);
$builder->add('export', SubmitType::class, ['icon' => 'fa-download']);
}
}
23 changes: 15 additions & 8 deletions webapp/src/Service/ImportExportService.php
Original file line number Diff line number Diff line change
Expand Up @@ -457,8 +457,11 @@ public function getTeamData(): array
/**
* @return ResultRow[]
*/
public function getResultsData(int $sortOrder, bool $full = false): array
{
public function getResultsData(
int $sortOrder,
bool $individuallyRanked = false,
bool $honors = true,
): array {
$contest = $this->dj->getCurrentContest();
if ($contest === null) {
throw new BadRequestHttpException('No current contest');
Expand Down Expand Up @@ -530,18 +533,22 @@ public function getResultsData(int $sortOrder, bool $full = false): array
$lowestMedalPoints = $teamScore->numPoints;
} elseif ($numPoints >= $median) {
// Teams with equally solved number of problems get the same rank unless $full is true.
if (!$full) {
if (!$individuallyRanked) {
if (!isset($ranks[$numPoints])) {
$ranks[$numPoints] = $rank;
}
$rank = $ranks[$numPoints];
}
if ($numPoints === $lowestMedalPoints) {
$awardString = 'Highest Honors';
} elseif ($numPoints === $lowestMedalPoints - 1) {
$awardString = 'High Honors';
if ($honors) {
if ($numPoints === $lowestMedalPoints) {
$awardString = 'Highest Honors';
} elseif ($numPoints === $lowestMedalPoints - 1) {
$awardString = 'High Honors';
} else {
$awardString = 'Honors';
}
} else {
$awardString = 'Honors';
$awardString = 'Ranked';
}
} else {
$awardString = 'Honorable';
Expand Down
2 changes: 1 addition & 1 deletion webapp/templates/jury/clarifications.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
{%- else %}

<div class="float-end">
<a href="{{ path('jury_html_export', {'type': 'clarifications'}) }}" target="_blank" class="btn btn-secondary btn-sm">
<a href="{{ path('jury_html_export_clarifications') }}" target="_blank" class="btn btn-secondary btn-sm">
<i class="fas fa-print"></i> Print clarifications
</a>
<a href="{{ path('jury_clarification_new') }}" class="btn btn-primary btn-sm">
Expand Down
10 changes: 8 additions & 2 deletions webapp/templates/jury/export/results.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,15 @@
</tbody>
</table>

{% for award in ['Highest Honors', 'High Honors', 'Honors'] %}
{% for award in ['Ranked', 'Highest Honors', 'High Honors', 'Honors'] %}
{% if ranked[award] is defined %}
<h2>{{ award }}</h2>
<h2>
{% if award == 'Ranked' %}
Other ranked teams
{% else %}
{{ award }}
{% endif %}
</h2>
<table class="table">
<thead>
<tr>
Expand Down
Loading

0 comments on commit d44c9b3

Please sign in to comment.