diff --git a/bullet/bullet_admin/access.py b/bullet/bullet_admin/access.py index 90b0e2e4..bf2f4b79 100644 --- a/bullet/bullet_admin/access.py +++ b/bullet/bullet_admin/access.py @@ -1,279 +1,193 @@ -from typing import TYPE_CHECKING +from inspect import signature +from typing import Callable -from competitions.branches import Branch +from competitions.models.competitions import Competition +from competitions.models.venues import Venue +from django.contrib.auth.views import redirect_to_login from django.core.exceptions import ImproperlyConfigured -from django_countries.fields import Country +from django.http import HttpResponse +from django.shortcuts import render +from django.urls import reverse +from users.models.organizers import User -from bullet_admin.mixins import AccessMixin +from bullet_admin.mixins import MixinProtocol from bullet_admin.utils import get_active_competition -if TYPE_CHECKING: - from competitions.models import Competition, Venue - from users.models import User +def is_competition_unlocked(user: User, competition: Competition) -> bool: + """The competition must be unlocked.""" + return not competition.results_public -def can_access_venue( - user: "User", venue: "Venue", allow_operator: bool = False -) -> bool: - """ - Checks whether the given user has access to the given venue. - This check does not pass for operators unless allow_operator is True. - """ - if not user.is_authenticated: - return False - # Superuser has all permissions +def is_branch_admin(user: User, competition: Competition) -> bool: + """You must be a branch administrator of the competition.""" if user.is_superuser: return True - competition = venue.category.competition - branch = competition.branch + brole = user.get_branch_role(competition.branch) + return brole.is_admin - # Branch admin can access any venue - if user.get_branch_role(branch).is_admin: + +def is_country_admin(user: User, competition: Competition) -> bool: + """You must be a country administrator or higher in the competition.""" + if is_branch_admin(user, competition): return True crole = user.get_competition_role(competition) - - if crole.is_operator and not allow_operator: - return False - - # Country admin can access veunes in his country - if crole.countries: - return venue.country in crole.countries - - # Venue admin can access his venues - if crole.venues: - return venue in crole.venues - - return False + return not crole.is_operator and bool(crole.countries) -def is_any_admin( - user: "User", competition: "Competition", allow_operator: bool = False -) -> bool: - """ - Checks whether the user is any admin in the competition. - """ - if not user.is_authenticated: - return False - - # Superuser has all permissions - if user.is_superuser: - return True - - # Branch admin is, obviously an admin - if user.get_branch_role(competition.branch).is_admin: +def is_admin(user: User, competition: Competition) -> bool: + """You must be a venue administrator or higher in the competition.""" + if is_country_admin(user, competition): return True crole = user.get_competition_role(competition) - if crole.is_operator and not allow_operator: - return False - - # Country admin and venue admins are admins, too - return bool(crole.venues) or bool(crole.countries) + return not crole.is_operator and bool(crole.venues) -def is_country_admin( - user: "User", competition: "Competition", allow_operator: bool = False -) -> bool: - """ - Checks whether the user is country admin (or better) in the competition. - """ - if not user.is_authenticated: - return False - - # Superuser has all permissions - if user.is_superuser: - return True - - # Branch admin is, obviously an admin - if user.get_branch_role(competition.branch).is_admin: +def is_operator(user: User, competition: Competition) -> bool: + """You must be a operator or higher in the competition.""" + if is_admin(user, competition): return True crole = user.get_competition_role(competition) - if crole.is_operator and not allow_operator: - return False + return crole.is_operator - # Country admin and venue admins are admins, too - return bool(crole.countries) +def is_branch_admin_in(user: User, venue: Venue) -> bool: + """You must be a branch administrator of the venue.""" + return is_branch_admin(user, venue.category.competition) -def is_country_admin_in( - user: "User", - competition: "Competition", - country: "Country", - allow_operator: bool = False, -) -> bool: - """ - Checks whether the user is country admin (or better) in the competition in a given - country. - """ - if not user.is_authenticated: - return False - - # Superuser has all permissions - if user.is_superuser: - return True - # Branch admin is, obviously an admin - if user.get_branch_role(competition.branch).is_admin: +def is_country_admin_in(user: User, venue: Venue) -> bool: + """You must be a contry administrator or higher of the venue.""" + if is_branch_admin_in(user, venue): return True - crole = user.get_competition_role(competition) - if crole.is_operator and not allow_operator: + crole = user.get_competition_role(venue.category.competition) + if crole.is_operator: return False - # Country admin and venue admins are admins, too - return country in crole.countries - - -def is_branch_admin(user: "User", branch: "Branch") -> bool: - """ - Checks whether the user is country admin (or better) in the competition. - """ - if not user.is_authenticated: + if not crole.countries: return False - # Superuser has all permissions - if user.is_superuser: - return True - - # Branch admin is, obviously an admin - return user.get_branch_role(branch).is_admin - + return venue.country in crole.countries -class VenueAccess(AccessMixin): - """ - Permission check mixin, uses `get_permission_venue` to check users' access to the - venue. - `require_unlocked_competition` - whether to require the competition to be unlocked - to allow access (the competition cannot have results_public) - `allow_operator` - whether to allow operators to access this view - """ +def is_admin_in(user: User, venue: Venue) -> bool: + """You must be a venue administrator or higher of the venue.""" + if is_country_admin_in(user, venue): + return True - require_unlocked_competition = True - allow_operator = False + crole = user.get_competition_role(venue.category.competition) + if crole.is_operator: + return False - def get_permission_venue(self) -> "Venue": - raise ImproperlyConfigured( - "Override get_permission_venue to use VenueAdminMixin." - ) + return venue in crole.venues - def can_access(self): - venue = self.get_permission_venue() - if ( - self.require_unlocked_competition - and venue.category.competition.results_public - ): - return False - return can_access_venue(self.request.user, venue, self.allow_operator) +def is_operator_in(user: User, venue: Venue) -> bool: + """You must be a operator or higher of the venue.""" + if is_admin_in(user, venue): + return True + crole = user.get_competition_role(venue.category.competition) + return venue in crole.venues -class AdminAccess(AccessMixin): - """ - Permission check mixin, uses `get_permission_competition` to check users' access - to the competition. Allows any admin (branch, venue, country) to acces the view. - `require_unlocked_competition` - whether to require the competition to be unlocked - to allow access (the competition cannot have results_public) - `allow_operator` - whether to allow operators to access this view - """ +type CompetitionPermissionCallable = Callable[[User, Competition], bool] +type VenuePermissionCallable = Callable[[User, Venue], bool] +type PermissionCallable = CompetitionPermissionCallable | VenuePermissionCallable - require_unlocked_competition = True - allow_operator = False - def get_permission_competition(self) -> "Competition": +def check_access( + user: User, + permissions: list[PermissionCallable], + *, + competition: Competition, + venue: Venue | None = None, + checked_permissions: list[tuple[str, bool]] | None = None, +) -> bool: + allowed = True + + for perm in permissions: + sig = signature(perm) + if "venue" in sig.parameters: + if not venue: + raise ImproperlyConfigured( + "Did not receive a venue for permission check, " + "but checking requires one." + ) + returned = perm(user, venue) # type:ignore + else: + returned = perm(user, competition) # type:ignore + + name = perm.__doc__ or perm.__name__ + if checked_permissions is not None: + checked_permissions.append((name, returned)) + allowed = allowed and returned + + return allowed + + +class PermissionCheckMixin(MixinProtocol): + required_permissions: PermissionCallable | list[PermissionCallable] = [] + _checked_permissions: list[tuple[str, bool]] + + def get_required_permissions(self) -> list[PermissionCallable]: + if isinstance(self.required_permissions, list): + return self.required_permissions + return [self.required_permissions] + + def get_permission_venue(self) -> Venue | None: + if hasattr(self, "venue"): + return getattr(self, "venue") + + def get_permission_competition(self) -> Competition: return get_active_competition(self.request) - def can_access(self): - competition = self.get_permission_competition() - if self.require_unlocked_competition and competition.results_public: - return False - - return is_any_admin(self.request.user, competition, self.allow_operator) - - -class CountryAdminAccess(AdminAccess): - """ - Permission check mixin, uses `get_permission_competition` to check users' - access to the competition. Allows country admin (branch, country) to acces the view. - - `require_unlocked_competition` - whether to require the competition to be unlocked - to allow access (the competition cannot have results_public) - `allow_operator` - whether to allow operators to access this view - """ - - def can_access(self): - competition = self.get_permission_competition() - if self.require_unlocked_competition and competition.results_public: - return False - - return is_country_admin(self.request.user, competition, self.allow_operator) - - -class CountryAdminInAccess(AdminAccess): - """ - Permission check mixin, uses `get_permission_competition` to check users' - access to the competition. Allows country admin of a given country to acces the - view. - - `require_unlocked_competition` - whether to require the competition to be unlocked - to allow access (the competition cannot have results_public) - `allow_operator` - whether to allow operators to access this view - """ - - def get_permission_country(self) -> "Country": - raise ImproperlyConfigured( - "Override get_permission_country to use CountryAdminInAccess." + def check_access(self, user: User) -> bool: + self._checked_permissions = [] + return check_access( + user, + self.get_required_permissions(), + competition=self.get_permission_competition(), + venue=self.get_permission_venue(), + checked_permissions=self._checked_permissions, ) - def can_access(self): - competition = self.get_permission_competition() - if self.require_unlocked_competition and competition.results_public: - return False - - return is_country_admin_in( - self.request.user, - competition, - self.get_permission_country(), - self.allow_operator, + def check_custom_permission(self, user: User) -> bool | None: + return None + + def permission_denied_response(self) -> HttpResponse: + return render( + self.request, + "bullet_admin/generic/denied.html", + { + "checked_permissions": self._checked_permissions, + "perm_competition": self.get_permission_competition(), + "perm_venue": self.get_permission_venue(), + }, ) - -class BranchAdminAccess(AdminAccess): - """ - Permission check mixin, uses `get_permission_competition` to check users' - access to the competition. Allows branch admin to access the view. - """ - - def can_access(self): - return is_branch_admin(self.request.user, self.request.BRANCH) - - -class UnlockedCompetitionMixin: - def can_access(self): - can = super().can_access() - if not can: - return False - - competition = get_active_competition(self.request) - return not competition.results_public - - -class PhotoUploadAccess(AccessMixin): - """ - Allows access only to Album & Gallery editing - """ - - def can_access(self): - if not self.request.user.is_authenticated: - return False - - if self.request.user.is_superuser: - return True - - brole = self.request.user.get_branch_role(self.request.BRANCH) - return brole.is_photographer or brole.is_admin + def dispatch(self, request, *args, **kwargs) -> HttpResponse: + if not request.user.is_authenticated: + return redirect_to_login( + self.request.get_full_path(), reverse("badmin:login"), "next" + ) + else: + access = self.check_access(request.user) + custom = self.check_custom_permission(request.user) + if custom is not None: + custom_name = ( + self.check_custom_permission.__doc__ or "check_custom_permission" + ) + self._checked_permissions.append((custom_name, custom)) + else: + custom = True + + if not access or not custom: + return self.permission_denied_response() + + return super().dispatch(request, *args, **kwargs) diff --git a/bullet/bullet_admin/admin.py b/bullet/bullet_admin/admin.py index e19ccdc1..9e37e643 100644 --- a/bullet/bullet_admin/admin.py +++ b/bullet/bullet_admin/admin.py @@ -5,7 +5,7 @@ @admin.register(BranchRole) class BranchRoleAdmin(admin.ModelAdmin): - list_display = ("branch", "user", "is_translator", "is_admin") + list_display = ("branch", "user", "is_admin") list_filter = ("branch",) autocomplete_fields = ("user",) diff --git a/bullet/bullet_admin/forms/category.py b/bullet/bullet_admin/forms/category.py index fac8c635..e0ea32c3 100644 --- a/bullet/bullet_admin/forms/category.py +++ b/bullet/bullet_admin/forms/category.py @@ -1,5 +1,5 @@ from competitions.models import Category -from django.forms import ModelForm +from django.forms import CheckboxSelectMultiple, ModelForm class CategoryForm(ModelForm): @@ -14,6 +14,7 @@ class Meta: "max_members_per_team", "max_teams_per_school", "max_teams_second_round", + "educations", ] labels = { @@ -21,6 +22,7 @@ class Meta: "max_members_per_team": "Max. number of team members", "max_teams_per_school": "Max. number of teams per school", "max_teams_second_round": "Max. number of teams per school (2nd round)", + "educations": "Allowed school grades", } help_texts = { @@ -35,5 +37,9 @@ class Meta: "round of registration.", } + widgets = { + "educations": CheckboxSelectMultiple, + } + def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/bullet/bullet_admin/forms/education.py b/bullet/bullet_admin/forms/education.py index a6286714..8a8a9eab 100644 --- a/bullet/bullet_admin/forms/education.py +++ b/bullet/bullet_admin/forms/education.py @@ -1,6 +1,8 @@ from django import forms from education.models import School +from bullet_admin.forms.utils import get_country_choices + class SchoolForm(forms.ModelForm): class Meta: @@ -16,3 +18,7 @@ class Meta: "search": "You can add any extra words or phrases that won't be shown, but " "will get used by the search engine.", } + + def __init__(self, competition, user, **kwargs): + super().__init__(**kwargs) + self.fields["country"].choices = get_country_choices(competition, user) diff --git a/bullet/bullet_admin/forms/users.py b/bullet/bullet_admin/forms/users.py index 85a61f19..a4b601c0 100644 --- a/bullet/bullet_admin/forms/users.py +++ b/bullet/bullet_admin/forms/users.py @@ -17,13 +17,13 @@ class Meta: class BranchRoleForm(forms.ModelForm): class Meta: model = BranchRole - fields = ("is_translator", "is_photographer", "is_admin") + fields = ("is_admin",) class CompetitionRoleForm(forms.ModelForm): class Meta: model = CompetitionRole - fields = ("venue_objects", "countries", "can_delegate", "is_operator") + fields = ("venue_objects", "countries", "is_operator") widgets = { "countries": forms.CheckboxSelectMultiple(), "venue_objects": forms.CheckboxSelectMultiple(), @@ -72,5 +72,7 @@ def clean(self): "The user cannot be both a venue and a country administrator." ) - if self.cleaned_data["can_delegate"] and self.cleaned_data["is_operator"]: - raise ValidationError("Operator cannot have delegate permission.") + if countries and self.cleaned_data["is_operator"]: + raise ValidationError( + "Operator with countries is unsupported permission configuration." + ) diff --git a/bullet/bullet_admin/forms/utils.py b/bullet/bullet_admin/forms/utils.py index 7da683e1..4c248cc1 100644 --- a/bullet/bullet_admin/forms/utils.py +++ b/bullet/bullet_admin/forms/utils.py @@ -6,7 +6,9 @@ from users.models import User -def get_country_choices(competition: Competition, user: User = None, allow_empty=False): +def get_country_choices( + competition: Competition, user: User | None = None, allow_empty=False +): countries = [ Country(c) for c in BranchCountry.objects.filter(branch=competition.branch).values_list( @@ -17,9 +19,11 @@ def get_country_choices(competition: Competition, user: User = None, allow_empty choices = [(c.code, c.name) for c in countries] choices.sort(key=lambda x: x[1]) - if not user.get_branch_role(competition.branch).is_admin: - crole = user.get_competition_role(competition) - choices = list(filter(lambda x: x[0] in crole.countries, choices)) + if user: + if not user.get_branch_role(competition.branch).is_admin: + crole = user.get_competition_role(competition) + countries = set(crole.countries or []) + choices = list(filter(lambda x: x[0] in countries, choices)) if allow_empty: choices.insert(0, ("", "--------")) diff --git a/bullet/bullet_admin/migrations/0009_remove_branchrole_is_photographer_and_more.py b/bullet/bullet_admin/migrations/0009_remove_branchrole_is_photographer_and_more.py new file mode 100644 index 00000000..7c69ffee --- /dev/null +++ b/bullet/bullet_admin/migrations/0009_remove_branchrole_is_photographer_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.7 on 2025-10-13 08:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("bullet_admin", "0008_remove_branchrole_is_school_editor"), + ] + + operations = [ + migrations.RemoveField( + model_name="branchrole", + name="is_photographer", + ), + migrations.RemoveField( + model_name="branchrole", + name="is_translator", + ), + migrations.RemoveField( + model_name="competitionrole", + name="can_delegate", + ), + ] diff --git a/bullet/bullet_admin/mixins.py b/bullet/bullet_admin/mixins.py index 5dea55af..73e33ad8 100644 --- a/bullet/bullet_admin/mixins.py +++ b/bullet/bullet_admin/mixins.py @@ -1,19 +1,18 @@ -from typing import Callable, Protocol +from typing import Any, Callable, Protocol from competitions.models import Venue -from django.contrib.auth.views import redirect_to_login -from django.core.exceptions import ImproperlyConfigured, PermissionDenied +from django.core.exceptions import ImproperlyConfigured from django.db.models import QuerySet from django.http import HttpRequest, HttpResponseNotFound -from django.urls import reverse from users.models.organizers import User -from bullet_admin.utils import get_active_competition, get_redirect_url, is_admin +from bullet_admin.utils import get_active_competition, get_redirect_url class MixinProtocol(Protocol): request: HttpRequest get_context_data: Callable[..., dict] + kwargs: dict[str, Any] get_object: Callable get_queryset: Callable get_model: Callable @@ -24,73 +23,10 @@ class AuthedHttpRequest(HttpRequest): user: User # type: ignore -class AccessMixin(MixinProtocol): - def can_access(self): - raise NotImplementedError() - - def handle_fail(self): - if self.request.user.is_authenticated: - raise PermissionDenied("You don't have access to this page.") - - return redirect_to_login( - self.request.get_full_path(), reverse("badmin:login"), "next" - ) - - def dispatch(self, request, *args, **kwargs): - if self.request.user.is_anonymous or not self.can_access(): - return self.handle_fail() - return super().dispatch(request, *args, **kwargs) - - -class AdminRequiredMixin(AccessMixin): - def can_access(self): - competition = get_active_competition(self.request) - if not competition: - return False - - return is_admin(self.request.user, competition) - - -class OperatorRequiredMixin(AccessMixin): - def can_access(self): - competition = get_active_competition(self.request) - if not competition: - return False - - brole = self.request.user.get_branch_role(self.request.BRANCH) - if brole.is_admin: - return True - - crole = self.request.user.get_competition_role(competition) - return crole.venues or crole.countries - - -class TranslatorRequiredMixin(AccessMixin): - def can_access(self): - role = self.request.user.get_branch_role(self.request.BRANCH) - return role.is_translator - - -class DelegateRequiredMixin(AccessMixin): - def can_access(self): - role = self.request.user.get_branch_role(self.request.BRANCH) - if role.is_admin: - return True - - competition = get_active_competition(self.request) - if not competition: - return False - - crole = self.request.user.get_competition_role(competition) - return crole.can_delegate and not crole.is_operator - - -class VenueMixin: +class VenueMixin(MixinProtocol): """ Sets self.venue to venue admin's venue or ?venue parameter if accessed by higher admin. - - Should be used after `AnyAdminRequiredMixin`. """ def get_available_venues(self, request) -> QuerySet[Venue]: @@ -128,7 +64,7 @@ def get_context_data(self, *args, **kwargs): return ctx -class IsOperatorContext: +class IsOperatorContext(MixinProtocol): def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) competition = get_active_competition(self.request) @@ -137,7 +73,7 @@ def get_context_data(self, **kwargs): return ctx -class RedirectBackMixin: +class RedirectBackMixin(MixinProtocol): default_success_url = None def get_default_success_url(self) -> str: diff --git a/bullet/bullet_admin/models.py b/bullet/bullet_admin/models.py index 4631bf32..349c8d85 100644 --- a/bullet/bullet_admin/models.py +++ b/bullet/bullet_admin/models.py @@ -1,13 +1,18 @@ +from typing import TYPE_CHECKING, Collection + from django.db import models from django_countries.fields import CountryField from web.fields import BranchField, ChoiceArrayField +if TYPE_CHECKING: + from competitions.models.venues import Venue + class BranchRole(models.Model): + id: int user = models.ForeignKey("users.User", on_delete=models.CASCADE) + user_id: int branch = BranchField() - is_translator = models.BooleanField(default=False) - is_photographer = models.BooleanField(default=False) is_admin = models.BooleanField(default=False) class Meta: @@ -17,21 +22,23 @@ class Meta: class CompetitionRole(models.Model): + id: int user = models.ForeignKey("users.User", on_delete=models.CASCADE) + user_id: int competition = models.ForeignKey( "competitions.Competition", on_delete=models.CASCADE ) + competition_id: int countries = ChoiceArrayField(CountryField(), blank=True, null=True) venue_objects = models.ManyToManyField( "competitions.Venue", related_name="+", blank=True, ) - can_delegate = models.BooleanField(default=False) is_operator = models.BooleanField(default=False) @property - def venues(self): + def venues(self) -> "Collection[Venue]": if not self.id: return [] return self.venue_objects.all() diff --git a/bullet/bullet_admin/sidebar.py b/bullet/bullet_admin/sidebar.py new file mode 100644 index 00000000..75a61e4f --- /dev/null +++ b/bullet/bullet_admin/sidebar.py @@ -0,0 +1,223 @@ +from dataclasses import dataclass + +from competitions.models.competitions import Competition +from django.urls import reverse_lazy +from users.models.organizers import User + +from bullet_admin.access import PermissionCallable, check_access +from bullet_admin.views.album import AlbumListView +from bullet_admin.views.archive import ProblemImportView, ProblemPDFUploadView +from bullet_admin.views.category import CategoryListView +from bullet_admin.views.competition import ( + CompetitionAutomoveView, + CompetitionTearoffUploadView, + CompetitionUpdateView, +) +from bullet_admin.views.content import ( + ContentBlockListView, + MenuItemListView, + PageListView, +) +from bullet_admin.views.education import SchoolListView +from bullet_admin.views.emails import CampaignListView +from bullet_admin.views.files import FileTreeView +from bullet_admin.views.results import ResultsHomeView +from bullet_admin.views.scanning import ProblemScanView, VenueReviewView +from bullet_admin.views.teams import RecentlyDeletedTeamsView, TeamListView +from bullet_admin.views.tex import TemplateListView +from bullet_admin.views.users import UserListView +from bullet_admin.views.venues import VenueListView +from bullet_admin.views.wildcards import WildcardListView + + +@dataclass +class Item: + icon: str + name: str + link: str + permissions: list[PermissionCallable] | PermissionCallable + + +@dataclass +class Group: + name: str + items: list[Item] + + +SIDEBAR: list[Group] = [ + Group( + "Competition", + [ + Item( + "mdi:account-group", + "Teams", + reverse_lazy("badmin:team_list"), + TeamListView.required_permissions, + ), + Item( + "mdi:email", + "Email campaigns", + reverse_lazy("badmin:email_list"), + CampaignListView.required_permissions, + ), + Item( + "mdi:delete-clock", + "Deleted teams", + reverse_lazy("badmin:recently_deleted"), + RecentlyDeletedTeamsView.required_permissions, + ), + Item( + "mdi:barcode-scan", + "Problem scanning", + reverse_lazy("badmin:scanning_problems"), + ProblemScanView.required_permissions, + ), + Item( + "mdi:magnify", + "Review", + reverse_lazy("badmin:scanning_review"), + VenueReviewView.required_permissions, + ), + Item( + "mdi:trophy", + "Results", + reverse_lazy("badmin:results"), + ResultsHomeView.required_permissions, + ), + Item( + "mdi:star", + "Wildcards", + reverse_lazy("badmin:wildcard_list"), + WildcardListView.required_permissions, + ), + ], + ), + Group( + "Content", + [ + Item( + "mdi:file-document", + "Pages", + reverse_lazy("badmin:page_list"), + PageListView.required_permissions, + ), + Item( + "mdi:menu", + "Menu items", + reverse_lazy("badmin:menu_list"), + MenuItemListView.required_permissions, + ), + Item( + "mdi:folder-open", + "File browser", + reverse_lazy("badmin:file_tree"), + FileTreeView.required_permissions, + ), + Item( + "mdi:image", + "Photos", + reverse_lazy("badmin:album_list"), + AlbumListView.required_permissions, + ), + Item( + "mdi:cube-outline", + "Content blocks", + reverse_lazy("badmin:contentblock_list"), + ContentBlockListView.required_permissions, + ), + ], + ), + Group( + "Settings", + [ + Item( + "mdi:account-cowboy-hat", + "Users", + reverse_lazy("badmin:user_list"), + UserListView.required_permissions, + ), + Item( + "mdi:school", + "Schools", + reverse_lazy("badmin:school_list"), + SchoolListView.required_permissions, + ), + Item( + "mdi:map-marker", + "Venues", + reverse_lazy("badmin:venue_list"), + VenueListView.required_permissions, + ), + Item( + "mdi:select-compare", + "Categories", + reverse_lazy("badmin:category_list"), + CategoryListView.required_permissions, + ), + Item( + "mdi:cog", + "Competition", + reverse_lazy("badmin:competition_edit"), + CompetitionUpdateView.required_permissions, + ), + Item( + "mdi:format-paint", + "TeX templates", + reverse_lazy("badmin:tex_template_list"), + TemplateListView.required_permissions, + ), + Item( + "mdi:fast-forward", + "Move waiting lists", + reverse_lazy("badmin:competition_automove"), + CompetitionAutomoveView.required_permissions, + ), + ], + ), + Group( + "Problems", + [ + Item( + "mdi:format-page-break", + "Upload tearoffs", + reverse_lazy("badmin:competition_upload_tearoffs"), + CompetitionTearoffUploadView.required_permissions, + ), + Item( + "mdi:book-open-blank-variant", + "Upload booklets", + reverse_lazy("badmin:archive_problem_upload"), + ProblemPDFUploadView.required_permissions, + ), + Item( + "mdi:archive", + "Upload archive data", + reverse_lazy("badmin:archive_import"), + ProblemImportView.required_permissions, + ), + ], + ), +] + + +def get_sidebar(user: User, competition: Competition) -> list[Group]: + real_sidebar = [] + + for group in SIDEBAR: + items = list( + filter( + lambda i: check_access( + user, + i.permissions + if isinstance(i.permissions, list) + else [i.permissions], + competition=competition, + ), + group.items, + ) + ) + + if items: + real_sidebar.append(Group(group.name, items)) + + return real_sidebar diff --git a/bullet/bullet_admin/templates/bullet_admin/competition/automove.html b/bullet/bullet_admin/templates/bullet_admin/competition/automove.html index 6aabb598..06781158 100644 --- a/bullet/bullet_admin/templates/bullet_admin/competition/automove.html +++ b/bullet/bullet_admin/templates/bullet_admin/competition/automove.html @@ -1,7 +1,7 @@ {% extends "bullet_admin/generic/form.html" %} {% block before_form %} -

- This action will move all eligible teams from the waiting list based on current registration rules for all venues. -

+

+ This action will move all eligible teams from the waiting list based on current registration rules for all venues. +

{% endblock before_form %} diff --git a/bullet/bullet_admin/templates/bullet_admin/competition/confirm.html b/bullet/bullet_admin/templates/bullet_admin/competition/confirm.html deleted file mode 100644 index ce1d03ac..00000000 --- a/bullet/bullet_admin/templates/bullet_admin/competition/confirm.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "bullet_admin/base.html" %} -{% load badmin %} -{% block title %} - {{ form_title }} -{% endblock title %} - -{% block content %} -
- {% aheader show_subtitle=True %} - {% slot title %} - Are you sure? - {% endslot %} - Finalizing the competition will recalucalte the results and prevent any additional modifications from being made. You cannot undo this action. - {% endaheader %} -
- {% csrf_token %} - {% #abtn button label="Finalize" icon="mdi:check" color="red" %} -
-
-{% endblock content %} diff --git a/bullet/bullet_admin/templates/bullet_admin/competition/finalize.html b/bullet/bullet_admin/templates/bullet_admin/competition/finalize.html new file mode 100644 index 00000000..13a06319 --- /dev/null +++ b/bullet/bullet_admin/templates/bullet_admin/competition/finalize.html @@ -0,0 +1,7 @@ +{% extends "bullet_admin/generic/form.html" %} + +{% block before_form %} +

+ Finalizing the competition will recalucalte the results and prevent any additional modifications from being made. You cannot undo this action. +

+{% endblock before_form %} diff --git a/bullet/bullet_admin/templates/bullet_admin/competition/form.html b/bullet/bullet_admin/templates/bullet_admin/competition/form.html index e4b2e131..9089ac45 100644 --- a/bullet/bullet_admin/templates/bullet_admin/competition/form.html +++ b/bullet/bullet_admin/templates/bullet_admin/competition/form.html @@ -1,26 +1,8 @@ -{% extends "bullet_admin/base.html" %} -{% load badmin %} -{% block title %} - {{ form_title }} -{% endblock title %} +{% extends "bullet_admin/generic/form.html" %} -{% block content %} -
- {% aheader %} - {% slot title %} - {{ form_title }} - {% endslot %} - {% endaheader %} -
- {% csrf_token %} - {% admin_form2 form %} -
- {% #abtn button label="Save" icon="mdi:content-save" color="blue" %} - {% if not object.results_public %} - {% url "badmin:competition_finalize" as finalize_url %} - {% #abtn label="Finalize results" icon="mdi:check" url=finalize_url %} - {% endif %} -
-
-
-{% endblock content %} +{% block after_submit %} + {% if not object.results_public %} + {% url "badmin:competition_finalize" as finalize_url %} + {% #abtn label="Finalize results" icon="mdi:check" url=finalize_url %} + {% endif %} +{% endblock after_submit %} diff --git a/bullet/bullet_admin/templates/bullet_admin/competition/tearoff.html b/bullet/bullet_admin/templates/bullet_admin/competition/tearoff.html index 73889ae6..67cbbe63 100644 --- a/bullet/bullet_admin/templates/bullet_admin/competition/tearoff.html +++ b/bullet/bullet_admin/templates/bullet_admin/competition/tearoff.html @@ -1,5 +1,5 @@ {% extends "bullet_admin/generic/form.html" %} {% block before_form %} -

Current uploaded tearoff files: {{ available_langs|join:", " }}

+

Current uploaded tearoff files: {{ available_langs|join:", " }}

{% endblock before_form %} diff --git a/bullet/bullet_admin/templates/bullet_admin/components/button.html b/bullet/bullet_admin/templates/bullet_admin/components/button.html index 76480f52..20f5d153 100644 --- a/bullet/bullet_admin/templates/bullet_admin/components/button.html +++ b/bullet/bullet_admin/templates/bullet_admin/components/button.html @@ -1,6 +1,6 @@ {% if "button" in attributes %}