diff --git a/webapp/src/Controller/Jury/AnalysisController.php b/webapp/src/Controller/Jury/AnalysisController.php index 891bfe7453..932f66757d 100644 --- a/webapp/src/Controller/Jury/AnalysisController.php +++ b/webapp/src/Controller/Jury/AnalysisController.php @@ -120,4 +120,25 @@ public function problemAction( $this->stats->getProblemStats($contest, $problem, $view) ); } + + #[Route(path: '/languages', name: 'analysis_languages')] + public function languagesAction( + #[MapQueryParameter] + ?string $view = null + ): Response { + $contest = $this->dj->getCurrentContest(); + + if ($contest === null) { + return $this->render('jury/error.html.twig', [ + 'error' => 'No contest selected', + ]); + } + + $filterKeys = array_keys(StatisticsService::FILTERS); + $view = $view ?: reset($filterKeys); + + return $this->render('jury/analysis/languages.html.twig', + $this->stats->getLanguagesStats($contest, $view) + ); + } } diff --git a/webapp/src/Service/StatisticsService.php b/webapp/src/Service/StatisticsService.php index 76a52a5457..15ff41ce32 100644 --- a/webapp/src/Service/StatisticsService.php +++ b/webapp/src/Service/StatisticsService.php @@ -5,6 +5,7 @@ use App\Entity\Contest; use App\Entity\ContestProblem; use App\Entity\Judging; +use App\Entity\Language; use App\Entity\Problem; use App\Entity\Submission; use App\Entity\Team; @@ -58,6 +59,7 @@ public function getTeams(Contest $contest, string $filter): array ->join('t.category', 'tc') ->leftJoin('t.affiliation', 'a') ->join('t.submissions', 'ts') + ->join('ts.language', 'l') ->join('ts.judgings', 'j') ->andWhere('j.valid = true') ->join('ts.language', 'lang') @@ -72,6 +74,7 @@ public function getTeams(Contest $contest, string $filter): array ->join('t.category', 'tc') ->leftJoin('tc.contests', 'cc') ->join('t.submissions', 'ts') + ->join('ts.language', 'l') ->join('ts.judgings', 'j') ->andWhere('j.valid = true') ->join('ts.language', 'lang') @@ -514,6 +517,118 @@ public function getGroupedProblemsStats( return $stats; } + /** + * @return array{ + * contest: Contest, + * problems: ContestProblem[], + * filters: array, + * view: string, + * languages: array, + * team_count: int, + * solved: int, + * not_solved: int, + * total: int, + * problems_solved: array, + * problems_solved_count: int, + * problems_attempted: array, + * problems_attempted_count: int, + * }> + * } + */ + public function getLanguagesStats(Contest $contest, string $view): array + { + /** @var Language[] $languages */ + $languages = $this->em->getRepository(Language::class) + ->createQueryBuilder('l') + ->andWhere('l.allowSubmit = 1') + ->orderBy('l.name') + ->getQuery() + ->getResult(); + + $languageStats = []; + + foreach ($languages as $language) { + $languageStats[$language->getLangid()] = [ + 'name' => $language->getName(), + 'teams' => [], + 'team_count' => 0, + 'solved' => 0, + 'not_solved' => 0, + 'total' => 0, + 'problems_solved' => [], + 'problems_solved_count' => 0, + 'problems_attempted' => [], + 'problems_attempted_count' => 0, + ]; + } + + $teams = $this->getTeams($contest, $view); + foreach ($teams as $team) { + foreach ($team->getSubmissions() as $s) { + if ($s->getContest() != $contest) { + continue; + } + if ($s->getSubmitTime() > $contest->getEndTime()) { + continue; + } + if ($s->getSubmitTime() < $contest->getStartTime()) { + continue; + } + if ($s->getSubmittime() > $contest->getFreezetime()) { + continue; + } + + $language = $s->getLanguage(); + + if (!isset($languageStats[$language->getLangid()]['teams'][$team->getTeamid()])) { + $languageStats[$language->getLangid()]['teams'][$team->getTeamid()] = [ + 'team' => $team, + 'solved' => 0, + 'total' => 0, + ]; + } + $languageStats[$language->getLangid()]['teams'][$team->getTeamid()]['total']++; + $languageStats[$language->getLangid()]['total']++; + if ($s->getResult() === 'correct') { + $languageStats[$language->getLangid()]['solved']++; + $languageStats[$language->getLangid()]['teams'][$team->getTeamid()]['solved']++; + $languageStats[$language->getLangid()]['problems_solved'][$s->getProblem()->getProbId()] = $s->getContestProblem(); + } else { + $languageStats[$language->getLangid()]['not_solved']++; + } + $languageStats[$language->getLangid()]['problems_attempted'][$s->getProblem()->getProbId()] = $s->getContestProblem(); + } + } + + foreach ($languageStats as &$languageStat) { + usort($languageStat['teams'], static function (array $a, array $b): int { + if ($a['solved'] === $b['solved']) { + return $b['total'] <=> $a['total']; + } + + return $b['solved'] <=> $a['solved']; + }); + $languageStat['team_count'] = count($languageStat['teams']); + $languageStat['problems_solved_count'] = count($languageStat['problems_solved']); + $languageStat['problems_attempted_count'] = count($languageStat['problems_attempted']); + } + unset($languageStat); + + return [ + 'contest' => $contest, + 'problems' => $this->getContestProblems($contest), + 'filters' => StatisticsService::FILTERS, + 'view' => $view, + 'languages' => $languageStats, + ]; + } + /** * Apply the filter to the given query builder. */ diff --git a/webapp/src/Twig/TwigExtension.php b/webapp/src/Twig/TwigExtension.php index c81026a0ad..e4f30664b4 100644 --- a/webapp/src/Twig/TwigExtension.php +++ b/webapp/src/Twig/TwigExtension.php @@ -1061,9 +1061,12 @@ public function fileTypeIcon(string $type): string return 'fas fa-file-' . $iconName; } - public function problemBadge(ContestProblem $problem): string + public function problemBadge(ContestProblem $problem, bool $grayedOut = false): string { $rgb = Utils::convertToHex($problem->getColor() ?? '#ffffff'); + if ($grayedOut) { + $rgb = 'whitesmoke'; + } $background = Utils::parseHexColor($rgb); // Pick a border that's a bit darker. @@ -1075,6 +1078,10 @@ public function problemBadge(ContestProblem $problem): string // Pick the foreground text color based on the background color. $foreground = ($background[0] + $background[1] + $background[2] > 450) ? '#000000' : '#ffffff'; + if ($grayedOut) { + $foreground = 'silver'; + $border = 'linen'; + } return sprintf( '%s', $rgb, diff --git a/webapp/templates/jury/analysis/contest_overview.html.twig b/webapp/templates/jury/analysis/contest_overview.html.twig index f153178a8f..9a860e4ef1 100644 --- a/webapp/templates/jury/analysis/contest_overview.html.twig +++ b/webapp/templates/jury/analysis/contest_overview.html.twig @@ -81,6 +81,10 @@ $(function() {
Language Stats + + + Details +
diff --git a/webapp/templates/jury/analysis/languages.html.twig b/webapp/templates/jury/analysis/languages.html.twig new file mode 100644 index 0000000000..7cb29236af --- /dev/null +++ b/webapp/templates/jury/analysis/languages.html.twig @@ -0,0 +1,100 @@ +{% extends "jury/base.html.twig" %} + +{% block title %}Analysis - Languages in {{ current_contest.shortname | default('') }} - {{ parent() }}{% endblock %} + +{% block content %} +

Language stats

+ {% include 'jury/partials/analysis_filter.html.twig' %} + +
+ {% for langid, language in languages %} +
+
+
+ {{ language.name }} +
+
+ {{ language.team_count }} team{% if language.team_count != 1 %}s{% endif %} + {% if language.team_count > 0 %} +
+ + +
+
+
+ + + + + + + + + + {% for team in language.teams %} + + + + + + + {% endfor %} + +
TeamNumber of solved problems in {{ language.name }}Total attempts in {{ language.name }}
+ + {{ team.team | entityIdBadge('t') }} + + + + {{ team.team.effectiveName }} + + {{ team.solved }}{{ team.total }}
+
+
+ {% endif %} +
+ {{ language.total }} total submission{% if language.total != 1 %}s{% endif %} + for {{ language.problems_attempted_count }} problem{% if language.problems_attempted_count != 1 %}s{% endif %}:
+ {% for problem in problems %} + + {{ problem | problemBadge(language.problems_attempted[problem.probid] is not defined) }} + + {% endfor %} +
+ {{ language.solved }} submission{% if language.solved != 1 %}s{% endif %} solved problems + for {{ language.problems_solved_count }} problem{% if language.problems_solved_count != 1 %}s{% endif %}:
+ {% for problem in problems %} + + {{ problem | problemBadge(language.problems_solved[problem.probid] is not defined) }} + + {% endfor %} +
+ {{ language.not_solved }} submission{% if language.not_solved != 1 %}s{% endif %} did not solve a problem
+
+
+
+ {% endfor %} +
+{% endblock %} + +{% block extrafooter %} + +{% endblock %}