diff --git a/bullet/bullet_admin/forms/review.py b/bullet/bullet_admin/forms/review.py index 6e1ed735..3c332b68 100644 --- a/bullet/bullet_admin/forms/review.py +++ b/bullet/bullet_admin/forms/review.py @@ -17,5 +17,9 @@ def clean(self): def get_review_formset(team: Team): - problems = team.venue.category.problems.count() + problems = ( + team.venue.category.competition.problem_count + - team.venue.category.first_problem + + 1 + ) return formset_factory(ReviewForm, min_num=problems, max_num=problems) diff --git a/bullet/bullet_admin/urls/__init__.py b/bullet/bullet_admin/urls/__init__.py index cdad3925..8a3fa636 100644 --- a/bullet/bullet_admin/urls/__init__.py +++ b/bullet/bullet_admin/urls/__init__.py @@ -6,7 +6,6 @@ archive, auth, category, - categoryproblems, competition, content, education, @@ -277,11 +276,6 @@ education.SchoolCreateView.as_view(), name="school_create", ), - path( - "problems/generate/", - categoryproblems.ProblemsGenerateView.as_view(), - name="problems_generate", - ), path("gallery/albums/", albums.AlbumListView.as_view(), name="album_list"), path("gallery/albums/new/", albums.AlbumCreateView.as_view(), name="album_create"), path( diff --git a/bullet/bullet_admin/views/categoryproblems.py b/bullet/bullet_admin/views/categoryproblems.py deleted file mode 100644 index ad605bc4..00000000 --- a/bullet/bullet_admin/views/categoryproblems.py +++ /dev/null @@ -1,45 +0,0 @@ -from competitions.models import Category -from django.db import transaction -from django.shortcuts import redirect -from django.views.generic import FormView -from problems.models import CategoryProblem, Problem - -from bullet_admin.access import BranchAdminAccess -from bullet_admin.forms.problem import ProblemsGenerateForm -from bullet_admin.utils import get_active_competition -from bullet_admin.views import GenericForm - - -class ProblemsGenerateView(BranchAdminAccess, GenericForm, FormView): - form_class = ProblemsGenerateForm - form_title = "Problem generation" - - def get_form_kwargs(self): - kw = super().get_form_kwargs() - kw["competition"] = get_active_competition(self.request) - return kw - - @transaction.atomic - def form_valid(self, form): - problem_count = form.cleaned_data["problem_count"] - competition = get_active_competition(self.request) - for i in range(problem_count): - Problem(competition=competition, name=f"{i+1:02d}").save() - - for key in form.cleaned_data.keys(): - if not key.startswith("offset_"): - continue - - offset = form.cleaned_data[key] - category = Category.objects.get(competition=competition, identifier=key[7:]) - for i in range(problem_count - offset): - CategoryProblem( - problem=Problem.objects.get( - name=f"{i+1+offset:02d}", - competition=competition, - ), - category=category, - number=i + 1 + offset, - ).save() - - return redirect("badmin:home") diff --git a/bullet/bullet_admin/views/scanning.py b/bullet/bullet_admin/views/scanning.py index d42b4891..25502413 100644 --- a/bullet/bullet_admin/views/scanning.py +++ b/bullet/bullet_admin/views/scanning.py @@ -23,7 +23,7 @@ mark_problem_unsolved, ) from problems.logic.scanner import ScannedBarcode, parse_barcode, save_scan -from problems.models import CategoryProblem, Problem, ScannerLog, SolvedProblem +from problems.models import Problem, ScannerLog, SolvedProblem from users.models import Team from bullet_admin.forms.review import get_review_formset @@ -161,11 +161,9 @@ def scan(self, request): if scanned_barcode.team.is_reviewed: raise ValueError("The team was already reviewed.") - problem_count = CategoryProblem.objects.filter( - category=scanned_barcode.team.venue.category - ).count() + max_problem_number = self.venue.category.competition.problem_count last = get_last_problem_for_team(scanned_barcode.team) - if last < problem_count: + if last < max_problem_number: if scanned_barcode.problem_number != last + 1: raise ValueError( f"Expected problem number {last + 1}, got " @@ -291,22 +289,22 @@ def get_form_class(self): return get_review_formset(self.team) def get_initial(self): - category_problems = { - cp.number: cp.problem_id - for cp in CategoryProblem.objects.filter( - category=self.team.venue.category - ).order_by("number") - } + competition = get_active_competition(self.request) + problems = Problem.objects.filter(competition=competition) + solved_timestamps = { sp.problem_id: sp.competition_time for sp in self.team.solved_problems.all() } initial = [] - for num, id in category_problems.items(): - row = {"number": num} - if id in solved_timestamps: + for problem in problems: + if problem.number < self.team.venue.category.first_problem: + continue + + row = {"number": problem.number} + if problem.id in solved_timestamps: row["is_solved"] = True - row["competition_time"] = solved_timestamps[id] + row["competition_time"] = solved_timestamps[problem.id] initial.append(row) @@ -318,11 +316,9 @@ def get_context_data(self, **kwargs): return ctx def form_valid(self, form): - category_problems: dict[int, Problem] = { - cp.number: cp.problem - for cp in CategoryProblem.objects.filter(category=self.team.venue.category) - .order_by("number") - .select_related("problem") + competition = get_active_competition(self.request) + problems: dict[int, Problem] = { + p.number: p for p in Problem.objects.filter(competition=competition) } solved: dict[int, SolvedProblem] = { sp.problem_id: sp for sp in self.team.solved_problems.all() @@ -331,9 +327,10 @@ def form_valid(self, form): changed = False for row in form: num = row.cleaned_data.get("number") - if num not in category_problems: + if num not in problems or num < self.team.venue.category.first_problem: continue - problem: Problem = category_problems[num] + + problem: Problem = problems[num] is_solved: bool = row.cleaned_data.get("is_solved") competition_time: timedelta = row.cleaned_data.get("competition_time") @@ -383,8 +380,7 @@ def post(self, request, *args, **kwargs): continue problem = Problem.objects.filter( competition=team.venue.category.competition, - category_problems__category=team.venue.category, - category_problems__number=solve["problem"], + number=solve["problem"], ).first() if not problem: continue diff --git a/bullet/bullet_admin/views/venues.py b/bullet/bullet_admin/views/venues.py index fe8969fd..5e3160c1 100644 --- a/bullet/bullet_admin/views/venues.py +++ b/bullet/bullet_admin/views/venues.py @@ -20,7 +20,7 @@ from documents.generators.tearoff import TearoffGenerator, TearoffRequirementMissing from documents.models import TexJob from problems.logic.results import save_country_ranks, save_venue_ranks -from problems.models import CategoryProblem, Problem +from problems.models import Problem from users.logic import get_venue_waiting_list, move_eligible_teams from users.models import Team @@ -205,14 +205,10 @@ def get_form_kwargs(self): kw = super().get_form_kwargs() competition = get_active_competition(self.request) problem_count = Problem.objects.filter(competition=competition).count() - first_problem = ( - CategoryProblem.objects.filter(category=self.venue.category) - .order_by("number") - .first() - ) + kw["problems"] = problem_count kw["venue"] = self.venue - kw["first_problem"] = first_problem.number if first_problem else 1 + kw["first_problem"] = self.venue.category.first_problem return kw def form_valid(self, form): diff --git a/bullet/competitions/models/venues.py b/bullet/competitions/models/venues.py index c2d22704..f6a5a7ad 100644 --- a/bullet/competitions/models/venues.py +++ b/bullet/competitions/models/venues.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import TYPE_CHECKING from bullet_admin.utils import get_active_competition @@ -136,7 +137,7 @@ def contact_email(self): return self.email @property - def start_time(self): + def start_time(self) -> datetime: if self.local_start: return self.local_start return self.category.competition.competition_start diff --git a/bullet/problems/admin.py b/bullet/problems/admin.py index 03486537..e28ddff6 100644 --- a/bullet/problems/admin.py +++ b/bullet/problems/admin.py @@ -3,15 +3,15 @@ from problems import models -class CategoryProblemAdminInline(admin.TabularInline): - model = models.CategoryProblem +class ProblemStatementInline(admin.StackedInline): + model = models.ProblemStatement @admin.register(models.Problem) class ProblemAdmin(admin.ModelAdmin): list_filter = ("competition", "competition__branch") - list_display = ("name", "competition") - inlines = (CategoryProblemAdminInline,) + list_display = ("number", "competition") + inlines = (ProblemStatementInline,) @admin.register(models.ProblemStatement) diff --git a/bullet/problems/factories/generate.py b/bullet/problems/factories/generate.py index 56168847..3bedda34 100644 --- a/bullet/problems/factories/generate.py +++ b/bullet/problems/factories/generate.py @@ -1,10 +1,9 @@ from competitions.models import Competition -from problems.factories.problems import CategoryProblemFactory, ProblemFactory +from problems.factories.problems import ProblemFactory from problems.models import Problem def create_problems(competition: Competition) -> list[Problem]: problems = ProblemFactory.create_batch(42, competition=competition) - CategoryProblemFactory.create_batch(42) return problems diff --git a/bullet/problems/factories/problems.py b/bullet/problems/factories/problems.py index 9b2bd768..7c0a9169 100644 --- a/bullet/problems/factories/problems.py +++ b/bullet/problems/factories/problems.py @@ -1,23 +1,13 @@ import factory -from competitions.models import Category, Competition +from competitions.models import Competition from factory.django import DjangoModelFactory -from problems.models import CategoryProblem, Problem +from problems.models import Problem class ProblemFactory(DjangoModelFactory): class Meta: model = Problem - name = factory.Faker("sentence") - competition = factory.Faker("random_element", elements=Competition.objects.all()) - - -class CategoryProblemFactory(DjangoModelFactory): - class Meta: - model = CategoryProblem - django_get_or_create = ("problem", "category", "number") - - problem = factory.Faker("random_element", elements=Problem.objects.all()) - category = factory.Faker("random_element", elements=Category.objects.all()) number = factory.Faker("random_int") + competition = factory.Faker("random_element", elements=Competition.objects.all()) diff --git a/bullet/problems/logic/__init__.py b/bullet/problems/logic/__init__.py index 9256497e..603279e2 100644 --- a/bullet/problems/logic/__init__.py +++ b/bullet/problems/logic/__init__.py @@ -8,6 +8,9 @@ def get_last_problem_for_team(team: Team): + """ + Return the number of the highest problem the team should have on their table. + """ return ( SolvedProblem.objects.filter(team=team).count() + team.venue.category.problems_per_team diff --git a/bullet/problems/logic/results.py b/bullet/problems/logic/results.py index 5b95db22..9ac2961d 100644 --- a/bullet/problems/logic/results.py +++ b/bullet/problems/logic/results.py @@ -9,12 +9,12 @@ from django_rq import job from users.models import Team -from problems.models import CategoryProblem, ResultRow, SolvedProblem +from problems.models import ResultRow, SolvedProblem def get_results( team_filter: Q, - time: timedelta = None, + time: timedelta | None = None, ) -> QuerySet[ResultRow]: rows = ResultRow.objects.filter(team__is_disqualified=False) @@ -33,12 +33,14 @@ def get_results( ) -def get_venue_results(venue: Venue, time: timedelta = None) -> QuerySet[ResultRow]: +def get_venue_results( + venue: Venue, time: timedelta | None = None +) -> QuerySet[ResultRow]: return get_results(Q(team__venue=venue), time) def get_country_results( - country: str | Country, category: Category, time: timedelta = None + country: str | Country, category: Category, time: timedelta | None = None ) -> QuerySet[ResultRow]: return get_results( Q( @@ -51,7 +53,7 @@ def get_country_results( def get_category_results( - category: Category, time: timedelta = None + category: Category, time: timedelta | None = None ) -> QuerySet[ResultRow]: return get_results( Q(team__venue__category=category, team__venue__is_isolated=False), @@ -70,7 +72,7 @@ class ResultsTime: def results_time( competition: Competition, realtime: datetime, - start_time: datetime = None, + start_time: datetime | None = None, is_admin: bool = False, ) -> ResultsTime: if not start_time: @@ -97,18 +99,13 @@ def _set_solved_problems(rr: ResultRow): problems = ( SolvedProblem.objects.select_for_update() .filter(team=rr.team, competition_time__lte=rr.competition_time) - .values_list("problem") - ) - solved_problems = set( - CategoryProblem.objects.filter( - problem__in=problems, category=rr.team.venue.category - ).values_list("number", flat=True) + .values_list("problem__number", flat=True) ) - rr.solved_count = len(solved_problems) + rr.solved_count = len(problems) solved_bin = 0 - for p in solved_problems: + for p in problems: solved_bin |= 1 << (p - 1) rr.solved_problems = solved_bin.to_bytes(16, "big") @@ -139,35 +136,20 @@ def squash_results(competition: Competition | int): teams = Team.objects.filter(venue__category__competition=competition) for team in teams: - problems = ( - SolvedProblem.objects.filter(team=team) - .values_list("problem") - .order_by("-competition_time") - ) last_problem = ( SolvedProblem.objects.filter(team=team) .order_by("-competition_time") .first() ) - solved_problems = set( - CategoryProblem.objects.filter( - problem__in=problems, category=team.venue.category - ).values_list("number", flat=True) - ) if not last_problem: continue - result_row = ResultRow() - result_row.team = team - result_row.solved_count = len(solved_problems) - - solved_bin = 0 - for p in solved_problems: - solved_bin |= 1 << (p - 1) - result_row.solved_problems = solved_bin.to_bytes(16, "big") - - result_row.competition_time = last_problem.competition_time + result_row = ResultRow( + team=team, + competition_time=last_problem.competition_time, + ) + _set_solved_problems(result_row) result_row.save() diff --git a/bullet/problems/logic/scanner.py b/bullet/problems/logic/scanner.py index d70e0856..995e5240 100644 --- a/bullet/problems/logic/scanner.py +++ b/bullet/problems/logic/scanner.py @@ -63,16 +63,21 @@ def parse_barcode( f"Could not find team {match.group('team')} in {venue.shortcode}." ) - if allow_endmark and int(match.group("problem")) == 0: + problem_number = int(match.group("problem")) + if problem_number < venue.category.first_problem: + raise ValueError(f"Problem {problem_number} is not valid for this category.") + + if allow_endmark and problem_number == 0: problem = None else: problem = Problem.objects.filter( competition=competition, - category_problems__category=venue.category, - category_problems__number=match.group("problem"), + number=problem_number, ).first() if not problem: - raise ValueError(f"Could not find problem {match.group('problem')}.") + raise ValueError( + f"Problem {problem_number} is not valid for this category." + ) return ScannedBarcode(venue, team, int(match.group("problem")), problem) diff --git a/bullet/problems/logic/stats.py b/bullet/problems/logic/stats.py index eb471171..3133deca 100644 --- a/bullet/problems/logic/stats.py +++ b/bullet/problems/logic/stats.py @@ -5,7 +5,7 @@ from django_rq import job from users.models import Team -from problems.models import ProblemStat, SolvedProblem +from problems.models import Problem, ProblemStat, SolvedProblem @job @@ -19,12 +19,10 @@ def generate_stats(competition: Competition | int): def generate_stats_category(category: Category): - category_problems = category.problems.all() + problems = Problem.objects.filter(competition=category.competition).all() first_problem = category.first_problem - problem_number_map: dict[int, int] = { - p.problem.id: p.number for p in category_problems - } - problem_id_map: dict[int, int] = {p.number: p.problem.id for p in category_problems} + problem_number_map: dict[int, int] = {p.id: p.number for p in problems} + problem_id_map: dict[int, int] = {p.number: p.id for p in problems} solves = SolvedProblem.objects.filter(team__venue__category=category).order_by( "competition_time" @@ -54,7 +52,7 @@ def generate_stats_category(category: Category): stats = [] for team in teams: for number, received in receive_times[team.id].items(): - if number > len(category_problems) + first_problem - 1: + if number > len(problems) + first_problem - 1: continue solved = None if number in solve_times[team.id]: diff --git a/bullet/problems/migrations/0006_problem_number.py b/bullet/problems/migrations/0006_problem_number.py new file mode 100644 index 00000000..033b2e17 --- /dev/null +++ b/bullet/problems/migrations/0006_problem_number.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.5 on 2025-02-03 16:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("problems", "0005_problemstat"), + ] + + operations = [ + migrations.AddField( + model_name="problem", + name="number", + field=models.PositiveIntegerField(null=True), + ), + ] diff --git a/bullet/problems/migrations/0007_auto_20250203_1642.py b/bullet/problems/migrations/0007_auto_20250203_1642.py new file mode 100644 index 00000000..4c65ae2c --- /dev/null +++ b/bullet/problems/migrations/0007_auto_20250203_1642.py @@ -0,0 +1,21 @@ +# Generated by Django 5.1.5 on 2025-02-03 16:42 + +from django.db import migrations + + +def migrate_problem_numbers(apps, schema_editor): + Problem = apps.get_model("problems", "Problem") + + for p in Problem.objects.all(): + p.number = int(p.name) + p.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("problems", "0006_problem_number"), + ] + + operations = [ + migrations.RunPython(migrate_problem_numbers), + ] diff --git a/bullet/problems/migrations/0008_remove_problem_name_alter_problem_number.py b/bullet/problems/migrations/0008_remove_problem_name_alter_problem_number.py new file mode 100644 index 00000000..c9acd293 --- /dev/null +++ b/bullet/problems/migrations/0008_remove_problem_name_alter_problem_number.py @@ -0,0 +1,21 @@ +# Generated by Django 5.1.5 on 2025-02-03 16:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("problems", "0007_auto_20250203_1642"), + ] + + operations = [ + migrations.RemoveField( + model_name="problem", + name="name", + ), + migrations.AlterField( + model_name="problem", + name="number", + field=models.PositiveIntegerField(), + ), + ] diff --git a/bullet/problems/migrations/0009_solvedproblem_solvedproblem__team_problem.py b/bullet/problems/migrations/0009_solvedproblem_solvedproblem__team_problem.py new file mode 100644 index 00000000..e5da2ebe --- /dev/null +++ b/bullet/problems/migrations/0009_solvedproblem_solvedproblem__team_problem.py @@ -0,0 +1,31 @@ +# Generated by Django 5.1.5 on 2025-02-03 17:57 + +from django.db import migrations, models + + +def remove_duplicates(apps, schema_editor): + SolvedProblem = apps.get_model("problems", "SolvedProblem") + + distinct = SolvedProblem.objects.order_by("team_id", "problem_id").distinct( + "team_id", "problem_id" + ) + SolvedProblem.objects.exclude(id__in=distinct).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("problems", "0008_remove_problem_name_alter_problem_number"), + ("users", "0031_alter_spanishteamdata_agreement"), + ] + + operations = [ + migrations.RunPython(remove_duplicates), + migrations.AddConstraint( + model_name="solvedproblem", + constraint=models.UniqueConstraint( + models.F("team"), + models.F("problem"), + name="solvedproblem__team_problem", + ), + ), + ] diff --git a/bullet/problems/migrations/0010_delete_categoryproblem.py b/bullet/problems/migrations/0010_delete_categoryproblem.py new file mode 100644 index 00000000..5a062960 --- /dev/null +++ b/bullet/problems/migrations/0010_delete_categoryproblem.py @@ -0,0 +1,15 @@ +# Generated by Django 5.1.5 on 2025-02-03 18:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("problems", "0009_solvedproblem_solvedproblem__team_problem"), + ] + + operations = [ + migrations.DeleteModel( + name="CategoryProblem", + ), + ] diff --git a/bullet/problems/models.py b/bullet/problems/models.py index e13171f7..87e833b2 100644 --- a/bullet/problems/models.py +++ b/bullet/problems/models.py @@ -7,30 +7,8 @@ class Problem(models.Model): competition = models.ForeignKey( "competitions.Competition", on_delete=models.CASCADE, related_name="+" ) - name = models.CharField(max_length=128) - - -class CategoryProblem(models.Model): - problem = models.ForeignKey( - Problem, on_delete=models.CASCADE, related_name="category_problems" - ) - category = models.ForeignKey( - "competitions.Category", - on_delete=models.CASCADE, - related_name="problems", - ) number = models.PositiveIntegerField() - class Meta: - constraints = ( - UniqueConstraint( - "problem", - "category", - "number", - name="categoryproblem__problem_category_number", - ), - ) - class SolvedProblem(models.Model): team = models.ForeignKey( @@ -39,6 +17,11 @@ class SolvedProblem(models.Model): problem = models.ForeignKey(Problem, on_delete=models.RESTRICT, related_name="+") competition_time = models.DurationField() + class Meta: + constraints = [ + UniqueConstraint("team", "problem", name="solvedproblem__team_problem") + ] + class ProblemStat(models.Model): team = models.ForeignKey("users.Team", on_delete=models.CASCADE, related_name="+") diff --git a/bullet/problems/templates/archive/problems.html b/bullet/problems/templates/archive/problems.html index 055dbecd..e60bf3d8 100644 --- a/bullet/problems/templates/archive/problems.html +++ b/bullet/problems/templates/archive/problems.html @@ -24,7 +24,8 @@

{{ competition.name }}

{% for problem in object_list %}

- {% blocktrans with n=problem|problem_number %}Problem {{ n }}{% endblocktrans %} + {% problem_number problem categories as problem_number %} + {% blocktrans with n=problem_number %}Problem {{ n }}{% endblocktrans %}

{{ problem.statement|safe }} diff --git a/bullet/problems/templatetags/archive_statements.py b/bullet/problems/templatetags/archive_statements.py index f703ae46..7619ce1d 100644 --- a/bullet/problems/templatetags/archive_statements.py +++ b/bullet/problems/templatetags/archive_statements.py @@ -1,26 +1,29 @@ -from collections import defaultdict from datetime import timedelta from typing import Iterable +from competitions.models.competitions import Category from django import template -from problems.models import CategoryProblem, ProblemStatement +from problems.models import ProblemStatement register = template.Library() -@register.filter() -def problem_number(obj: ProblemStatement): - problems: Iterable[CategoryProblem] = obj.problem.category_problems.all() - numbers = set([cp.number for cp in problems]) - if len(numbers) == 1: - return numbers.pop() +@register.simple_tag() +def problem_number(problem: ProblemStatement, categories: Iterable[Category]): + numbers = [] + used_numbers = set() + + for cat in categories: + prefix = cat.identifier[0].upper() + number = problem.problem.number + 1 - cat.first_problem + numbers.append((prefix, number)) + used_numbers.add(number) - number_category: dict[int, set[str]] = defaultdict(set) - for cp in problems: - number_category[cp.number].add(cp.category.identifier[0].upper()) + if len(used_numbers) == 1: + return used_numbers.pop() - return " / ".join(f"{''.join(cat)}{n}" for n, cat in number_category.items()) + return " / ".join([f"{n[0]}{n[1]}" for n in numbers if n[1] >= 1]) @register.filter() diff --git a/bullet/problems/templatetags/results.py b/bullet/problems/templatetags/results.py index 01f607aa..14dbe054 100644 --- a/bullet/problems/templatetags/results.py +++ b/bullet/problems/templatetags/results.py @@ -1,6 +1,7 @@ from typing import Optional from competitions.models import Competition, Venue +from competitions.models.competitions import Category from django import template from problems.models import ResultRow @@ -15,12 +16,13 @@ def squares( team_problem_count: Optional[int] = None, first_problem: Optional[int] = None, ): + category: Category = obj.team.venue.category if not team_problem_count: - team_problem_count = obj.team.venue.category.problems_per_team + team_problem_count = category.problems_per_team if not first_problem: - first_problem = obj.team.venue.category.first_problem + first_problem = category.first_problem if not problem_count: - problem_count = obj.team.venue.category.problems.count() + problem_count = category.competition.problem_count - first_problem + 1 return { "squares": obj.get_squares(problem_count, team_problem_count, first_problem), diff --git a/bullet/problems/views/archive.py b/bullet/problems/views/archive.py index cd428372..8c956661 100644 --- a/bullet/problems/views/archive.py +++ b/bullet/problems/views/archive.py @@ -46,10 +46,12 @@ class ProblemStatementView(ArchiveCompetitionMixin, ListView): template_name = "archive/problems.html" def get_queryset(self): - return ProblemStatement.objects.filter( - problem__competition=self.competition, language=get_language() - ).prefetch_related( - "problem__category_problems", "problem__category_problems__category" + return ( + ProblemStatement.objects.filter( + problem__competition=self.competition, language=get_language() + ) + .order_by("problem__number") + .select_related("problem") ) def inject_stats(self, object_list): @@ -71,21 +73,13 @@ def inject_stats(self, object_list): return new_list - def order_problems(self, problem_list: list[ProblemStatement]): - reference_category = ( - Category.objects.filter(competition=self.competition) - .annotate(problem_count=Count("problems")) - .order_by("-problem_count") - .first() - ) - problems = reference_category.problems.all() - problem_order = {p.problem_id: p.number for p in problems} - problem_list.sort(key=lambda p: problem_order.get(p.problem_id, 99999)) + def get_categories(self): + return Category.objects.filter(competition=self.competition) def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx["object_list"] = self.inject_stats(ctx["object_list"]) - self.order_problems(ctx["object_list"]) + ctx["categories"] = self.get_categories() pdf_file = self.competition.secret_dir / f"problems-{get_language()}.pdf" if default_storage.exists(pdf_file): diff --git a/bullet/problems/views/results.py b/bullet/problems/views/results.py index 00f8b778..b476d424 100644 --- a/bullet/problems/views/results.py +++ b/bullet/problems/views/results.py @@ -1,6 +1,6 @@ from bullet_admin.access import is_any_admin from competitions.models import Category, Competition, Venue -from django.http import Http404 +from django.http import Http404, HttpRequest from django.shortcuts import get_object_or_404 from django.utils import timezone from django.utils.decorators import method_decorator @@ -10,6 +10,7 @@ from django_countries.fields import Country from problems.logic.results import ( + ResultsTime, get_category_results, get_country_results, get_venue_results, @@ -50,6 +51,38 @@ class ResultsViewMixin(CompetitionMixin): def get_results(self): raise NotImplementedError() + def get_category(self): + raise NotImplementedError() + + def get_venue_timer(self) -> Venue | None: + return None + + request: HttpRequest + + def get_results_time(self) -> ResultsTime: + admin = False + if self.request.GET.get("admin") == "1" and self.request.user.is_authenticated: + admin = is_any_admin( + self.request.user, self.competition, allow_operator=True + ) + + start_time = None + venue = self.get_venue_timer() + if venue: + start_time = venue.start_time + elif venue_code := self.request.GET.get("venue_timer"): + venue: Venue | None = ( + Venue.objects.for_competition(self.competition) + .filter(shortcode=venue_code) + .first() + ) + if venue: + start_time = venue.start_time + + return results_time( + self.competition, timezone.now(), is_admin=admin, start_time=start_time + ) + def get_queryset(self): qs = self.get_results() @@ -74,6 +107,14 @@ def get_context_data(self, *, object_list=None, **kwargs): ctx["start_index"] = ctx["page_obj"].start_index ctx["hide_squares"] = self.request.GET.get("hide_squares") == "1" ctx["hide_contestants"] = self.request.GET.get("hide_contestants") == "1" + + category = self.get_category() + ctx["team_problem_count"] = category.problems_per_team + ctx["first_problem"] = category.first_problem + ctx["problem_count"] = ( + category.competition.problem_count - category.first_problem + 1 + ) + ctx["results_time"] = self.get_results_time() return ctx @@ -81,6 +122,9 @@ def get_context_data(self, *, object_list=None, **kwargs): class CategoryResultsView(ResultsViewMixin, ListView): template_name = "problems/results.html" + def get_category(self): + return self.category + def dispatch(self, request, *args, **kwargs): self.country: str = kwargs.get("country") if self.country: @@ -95,42 +139,19 @@ def dispatch(self, request, *args, **kwargs): identifier=self.kwargs["category"], ) - admin = False - if request.GET.get("admin") == "1": - admin = is_any_admin( - self.request.user, self.competition, allow_operator=True - ) - - start_time = None - if "venue_timer" in request.GET: - start_time = ( - Venue.objects.for_competition(self.competition) - .filter(shortcode=request.GET["venue_timer"]) - .first() - ) - if start_time: - start_time = start_time.start_time - - self.results_time = results_time( - self.competition, timezone.now(), is_admin=admin, start_time=start_time - ) - return super().dispatch(request, *args, **kwargs) def get_results(self): if self.country: return get_country_results( - self.country.upper(), self.category, self.results_time.time + self.country.upper(), self.category, self.get_results_time().time ) - return get_category_results(self.category, self.results_time.time) + return get_category_results(self.category, self.get_results_time().time) def get_context_data(self, *, object_list=None, **kwargs): ctx = super().get_context_data(object_list=object_list, **kwargs) - ctx["team_problem_count"] = self.category.problems_per_team - ctx["problem_count"] = self.category.problems.count() - ctx["first_problem"] = self.category.first_problem + ctx["category"] = self.category - ctx["results_time"] = self.results_time ctx["country_name"] = ( Country(self.country).name if self.country else _("International") @@ -150,34 +171,24 @@ def get_context_data(self, *, object_list=None, **kwargs): class VenueResultsView(ResultsViewMixin, ListView): template_name = "problems/results/venue.html" + def get_category(self): + return self.venue.category + + def get_venue_timer(self) -> Venue | None: + return self.venue + def dispatch(self, request, *args, **kwargs): self.venue = get_object_or_404( Venue.objects.for_competition(self.competition), shortcode=self.kwargs["venue"].upper(), ) - admin = False - if request.GET.get("admin") == "1": - admin = is_any_admin( - self.request.user, self.competition, allow_operator=True - ) - self.results_time = results_time( - self.competition, - timezone.now(), - start_time=self.venue.local_start, - is_admin=admin, - ) - return super().dispatch(request, *args, **kwargs) def get_results(self): - return get_venue_results(self.venue, self.results_time.time) + return get_venue_results(self.venue, self.get_results_time().time) def get_context_data(self, *, object_list=None, **kwargs): ctx = super().get_context_data(object_list=object_list, **kwargs) - ctx["team_problem_count"] = self.venue.category.problems_per_team - ctx["problem_count"] = self.venue.category.problems.count() - ctx["first_problem"] = self.venue.category.first_problem ctx["venue"] = self.venue - ctx["results_time"] = self.results_time return ctx