Skip to content

Commit 7b4a669

Browse files
Add page with language stats
1 parent 90c3ab8 commit 7b4a669

File tree

4 files changed

+176
-0
lines changed

4 files changed

+176
-0
lines changed

webapp/src/Controller/Jury/AnalysisController.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,25 @@ public function problemAction(
120120
$this->stats->getProblemStats($contest, $problem, $view)
121121
);
122122
}
123+
124+
#[Route(path: '/languages', name: 'analysis_languages')]
125+
public function languagesAction(
126+
#[MapQueryParameter]
127+
?string $view = null
128+
): Response {
129+
$contest = $this->dj->getCurrentContest();
130+
131+
if ($contest === null) {
132+
return $this->render('jury/error.html.twig', [
133+
'error' => 'No contest selected',
134+
]);
135+
}
136+
137+
$filterKeys = array_keys(StatisticsService::FILTERS);
138+
$view = $view ?: reset($filterKeys);
139+
140+
return $this->render('jury/analysis/languages.html.twig',
141+
$this->stats->getLanguagesStats($contest, $view)
142+
);
143+
}
123144
}

webapp/src/Service/StatisticsService.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use App\Entity\Contest;
66
use App\Entity\ContestProblem;
77
use App\Entity\Judging;
8+
use App\Entity\Language;
89
use App\Entity\Problem;
910
use App\Entity\Submission;
1011
use App\Entity\Team;
@@ -58,6 +59,7 @@ public function getTeams(Contest $contest, string $filter): array
5859
->join('t.category', 'tc')
5960
->leftJoin('t.affiliation', 'a')
6061
->join('t.submissions', 'ts')
62+
->join('ts.language', 'l')
6163
->join('ts.judgings', 'j')
6264
->andWhere('j.valid = true')
6365
->join('ts.language', 'lang')
@@ -72,6 +74,7 @@ public function getTeams(Contest $contest, string $filter): array
7274
->join('t.category', 'tc')
7375
->leftJoin('tc.contests', 'cc')
7476
->join('t.submissions', 'ts')
77+
->join('ts.language', 'l')
7578
->join('ts.judgings', 'j')
7679
->andWhere('j.valid = true')
7780
->join('ts.language', 'lang')
@@ -514,6 +517,86 @@ public function getGroupedProblemsStats(
514517
return $stats;
515518
}
516519

520+
/**
521+
* @return array{
522+
* contest: Contest,
523+
* filters: array<string, string>,
524+
* view: string,
525+
* languages: array<string, array{
526+
* name: string,
527+
* teams: array<int, Team>,
528+
* team_count: int,
529+
* solved: int,
530+
* not_solved: int,
531+
* total: int,
532+
* }>
533+
* }
534+
*/
535+
public function getLanguagesStats(Contest $contest, string $view): array
536+
{
537+
/** @var Language[] $languages */
538+
$languages = $this->em->getRepository(Language::class)
539+
->createQueryBuilder('l')
540+
->andWhere('l.allowSubmit = 1')
541+
->orderBy('l.name')
542+
->getQuery()
543+
->getResult();
544+
545+
$languageStats = [];
546+
547+
foreach ($languages as $language) {
548+
$languageStats[$language->getLangid()] = [
549+
'name' => $language->getName(),
550+
'teams' => [],
551+
'team_count' => 0,
552+
'solved' => 0,
553+
'not_solved' => 0,
554+
'total' => 0,
555+
];
556+
}
557+
558+
$teams = $this->getTeams($contest, $view);
559+
foreach ($teams as $team) {
560+
foreach ($team->getSubmissions() as $s) {
561+
if ($s->getContest() != $contest) {
562+
continue;
563+
}
564+
if ($s->getSubmitTime() > $contest->getEndTime()) {
565+
continue;
566+
}
567+
if ($s->getSubmitTime() < $contest->getStartTime()) {
568+
continue;
569+
}
570+
if ($s->getSubmittime() > $contest->getFreezetime()) {
571+
continue;
572+
}
573+
574+
$language = $s->getLanguage();
575+
576+
$languageStats[$language->getLangid()]['teams'][$team->getTeamid()] = $team;
577+
$languageStats[$language->getLangid()]['total']++;
578+
if ($s->getResult() === 'correct') {
579+
$languageStats[$language->getLangid()]['solved']++;
580+
} else {
581+
$languageStats[$language->getLangid()]['not_solved']++;
582+
}
583+
}
584+
}
585+
586+
foreach ($languageStats as &$languageStat) {
587+
usort($languageStat['teams'], static fn(Team $a, Team $b) => ($a->getLabel() ?: $a->getExternalid()) <=> ($b->getLabel() ?: $b->getExternalid()));
588+
$languageStat['team_count'] = count($languageStat['teams']);
589+
}
590+
unset($languageStat);
591+
592+
return [
593+
'contest' => $contest,
594+
'filters' => StatisticsService::FILTERS,
595+
'view' => $view,
596+
'languages' => $languageStats,
597+
];
598+
}
599+
517600
/**
518601
* Apply the filter to the given query builder.
519602
*/

webapp/templates/jury/analysis/contest_overview.html.twig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ $(function() {
8181
<div class="card">
8282
<div class="card-header">
8383
Language Stats
84+
<a href="{{ path('analysis_languages', {'view': view}) }}" class="btn btn-sm btn-outline-primary">
85+
<i class="fas fa-list"></i>
86+
Details
87+
</a>
8488
</div>
8589
<div class="card-body">
8690
<svg style="height: 300px"></svg>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{% extends "jury/base.html.twig" %}
2+
3+
{% block title %}Analysis - Languages in {{ current_contest.shortname | default('') }} - {{ parent() }}{% endblock %}
4+
5+
{% block content %}
6+
<h1>Language stats</h1>
7+
{% include 'jury/partials/analysis_filter.html.twig' %}
8+
9+
<div class="row">
10+
{% for langid, language in languages %}
11+
<div class="col-12 mt-3">
12+
<div class="card">
13+
<div class="card-header">
14+
{{ language.name }}
15+
</div>
16+
<div class="card-body">
17+
<i class="fas fa-users fa-fw"></i> {{ language.team_count }} team{% if language.team_count != 1 %}s{% endif %}
18+
{% if language.team_count > 0 %}
19+
<div class="btn-group" role="group">
20+
<input type="checkbox"
21+
class="btn-check team-list-toggle"
22+
id="team-list-toggle-{{ langid }}"
23+
data-team-list-container="#team-list-{{ langid }}"
24+
autocomplete="off" />
25+
<label for="team-list-toggle-{{ langid }}" class="btn-sm btn btn-outline-secondary">
26+
<i class="fas fa-eye"></i> Show
27+
</label>
28+
</div>
29+
<div class="card mt-2 mb-2 d-none" id="team-list-{{ langid }}">
30+
<div class="card-body">
31+
<ul class="mb-0">
32+
{% for team in language.teams %}
33+
<li>
34+
<a href="{{ path('jury_team', {'teamId': team.teamid}) }}">
35+
{{ team.effectiveName }} {{ team | entityIdBadge('t') }}
36+
</a>
37+
</li>
38+
{% endfor %}
39+
</ul>
40+
</div>
41+
</div>
42+
{% endif %}
43+
<br/>
44+
<i class="fas fa-file-code fa-fw"></i> {{ language.total }} total submission{% if language.total != 1 %}s{% endif %}<br />
45+
<i class="fas fa-check fa-fw"></i> {{ language.solved }} submission{% if language.solved != 1 %}s{% endif %} solved problems<br />
46+
<i class="fas fa-xmark fa-fw"></i> {{ language.not_solved }} submission{% if language.not_solved != 1 %}s{% endif %} did not solve a problem<br />
47+
</div>
48+
</div>
49+
</div>
50+
{% endfor %}
51+
</div>
52+
{% endblock %}
53+
54+
{% block extrafooter %}
55+
<script>
56+
$('.team-list-toggle').on('change', function () {
57+
const $container = $($(this).data('team-list-container'));
58+
const $label = $(this).parent().find('label');
59+
if ($(this).is(':checked')) {
60+
$container.removeClass('d-none');
61+
$label.html('<i class="fas fa-eye-slash"></i> Hide');
62+
} else {
63+
$container.addClass('d-none');
64+
$label.html('<i class="fas fa-eye"></i> Show');
65+
}
66+
});
67+
</script>
68+
{% endblock %}

0 commit comments

Comments
 (0)