Skip to content

Commit 9c944be

Browse files
authored
seperate login level for APDA board (#504)
* add seperate login level for apda board to manage results
1 parent f2d5fe8 commit 9c944be

19 files changed

Lines changed: 757 additions & 15 deletions

Pipfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ six = "==1.17.0"
3636
exceptiongroup = "==1.3.0"
3737
wrapt = "==1.17.3"
3838
pyyaml = "==5.3.1"
39+
nplusone = "*"
3940

4041
[requires]
4142
python_version = "3.10.16"

Pipfile.lock

Lines changed: 17 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/conf.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,6 @@
179179
]
180180

181181
# Ignore bad random CI warning
182-
linkcheck_ignore = [r"https://www\.wikiwand\.com/en/articles/Blossom_algorithm.*"]
182+
linkcheck_ignore = [
183+
r"https://www\.wikiwand\.com/en/articles/Blossom_algorithm",
184+
]

mittab/apps/tab/auth_backends.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from django.contrib.auth.backends import ModelBackend
2+
3+
from mittab.apps.tab.auth_roles import is_apda_board_access_open, is_apda_board_user
4+
5+
6+
class TabAuthenticationBackend(ModelBackend):
7+
"""Enforce APDA board access timing while preserving default auth behavior."""
8+
9+
def authenticate(self, request, username=None, password=None, **kwargs):
10+
user = super().authenticate(
11+
request,
12+
username=username,
13+
password=password,
14+
**kwargs,
15+
)
16+
if user and is_apda_board_user(user) and not is_apda_board_access_open():
17+
return None
18+
return user

mittab/apps/tab/auth_roles.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from mittab.apps.tab.models import Round, TabSettings
2+
3+
APDA_BOARD_GROUP_NAME = "APDA Board"
4+
5+
6+
def is_apda_board_user(user):
7+
if not user or not user.is_authenticated:
8+
return False
9+
if user.is_superuser:
10+
return False
11+
return user.groups.filter(name=APDA_BOARD_GROUP_NAME).exists()
12+
13+
14+
def is_apda_board_access_open():
15+
"""APDA board access opens once the final inround has been paired."""
16+
try:
17+
total_inrounds = int(TabSettings.get("tot_rounds"))
18+
except (TypeError, ValueError):
19+
return False
20+
21+
if total_inrounds < 1:
22+
return False
23+
return Round.objects.filter(round_number=total_inrounds).exists()

mittab/apps/tab/forms.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ class Meta:
3333
fields = "__all__"
3434

3535

36+
class SchoolApdaIdForm(forms.ModelForm):
37+
class Meta:
38+
model = School
39+
fields = ["apda_id"]
40+
41+
3642
class RoomForm(forms.ModelForm):
3743
def __init__(self, *args, **kwargs):
3844
entry = "first_entry" in kwargs
@@ -240,6 +246,12 @@ class Meta:
240246
exclude = ["tiebreaker"]
241247

242248

249+
class DebaterApdaIdForm(forms.ModelForm):
250+
class Meta:
251+
model = Debater
252+
fields = ["apda_id"]
253+
254+
243255
def validate_speaks(value):
244256
if not (TabSettings.get("min_speak", 0) <= value <= TabSettings.get(
245257
"max_speak", 50)):

mittab/apps/tab/management/commands/initialize_tourney.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
from django.core.management import call_command
55
from django.contrib.auth import get_user_model
6+
from django.contrib.auth.models import Group
67
from django.core.management.base import BaseCommand
78

9+
from mittab.apps.tab.auth_roles import APDA_BOARD_GROUP_NAME
810
from mittab.apps.tab.models import TabSettings
911
from mittab.libs.backup import backup_round, BEFORE_NEW_TOURNAMENT, INITIAL
1012

@@ -26,6 +28,12 @@ def add_arguments(self, parser):
2628
help="Password for the entry user",
2729
nargs="?",
2830
default=USER_MODEL.objects.make_random_password(length=8))
31+
parser.add_argument(
32+
"--apda-board-password",
33+
dest="apda_board_password",
34+
help="Password for the APDA Board user",
35+
nargs="?",
36+
default=None)
2937
parser.add_argument(
3038
"--first-init",
3139
dest="first_init",
@@ -35,6 +43,12 @@ def add_arguments(self, parser):
3543
default=False)
3644

3745
def handle(self, *args, **options):
46+
apda_board_password = (
47+
options.get("apda_board_password")
48+
or os.environ.get("BOARD_PASSWORD")
49+
or USER_MODEL.objects.make_random_password(length=16)
50+
)
51+
3852
if not options["first_init"]:
3953
self.stdout.write("Backing up the previous tournament data")
4054
backup_round(btype=BEFORE_NEW_TOURNAMENT)
@@ -48,13 +62,16 @@ def handle(self, *args, **options):
4862
print(why)
4963
sys.exit(1)
5064

51-
self.stdout.write("Creating tab/entry users")
65+
self.stdout.write("Creating tab/entry/APDA Board users")
5266
tab = USER_MODEL.objects.create_user("tab", None, options["tab_password"])
5367
tab.is_staff = True
5468
tab.is_admin = True
5569
tab.is_superuser = True
5670
tab.save()
5771
USER_MODEL.objects.create_user("entry", None, options["entry_password"])
72+
apda_board = USER_MODEL.objects.create_user("board", None, apda_board_password)
73+
apda_board_group, _ = Group.objects.get_or_create(name=APDA_BOARD_GROUP_NAME)
74+
apda_board.groups.add(apda_board_group)
5875

5976
self.stdout.write("Setting default tab settings")
6077
TabSettings.set("tot_rounds", 5)
@@ -73,5 +90,9 @@ def handle(self, *args, **options):
7390
self.stdout.write(
7491
f"{'entry'.ljust(10, ' ')} | {options['entry_password'].ljust(10, ' ')}"
7592
)
93+
self.stdout.write(
94+
f"{'board'.ljust(10, ' ')} | "
95+
f"{apda_board_password.ljust(10, ' ')}"
96+
)
7697
if options["first_init"]:
7798
backup_round(name="initial-tournament", btype=INITIAL)

mittab/apps/tab/middleware.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import re
22
from urllib.parse import urlsplit
33

4+
from django.contrib.auth import logout
45
from django.contrib.auth.views import LoginView
56
from django.http import HttpResponse, JsonResponse
67
from django.utils.http import url_has_allowed_host_and_scheme
78

9+
from mittab.apps.tab.auth_roles import is_apda_board_access_open, is_apda_board_user
810
from mittab.apps.tab.helpers import redirect_and_flash_info
911
from mittab.apps.tab.public_rankings import get_standings_publication_setting
1012
from mittab.libs.backup import is_backup_active
@@ -40,6 +42,19 @@
4042
"team": "Team results not published",
4143
"shared": "Standings data not published",
4244
}
45+
APDA_BOARD_ALLOWED_PREFIXES = (
46+
"/apda-board/",
47+
"/public/",
48+
"/accounts/logout/",
49+
"/admin/logout/",
50+
"/403/",
51+
"/404/",
52+
"/500/",
53+
"/favicon.ico",
54+
"/static/",
55+
"/dynamic-media/",
56+
)
57+
APDA_BOARD_ALLOWED_EXACT_PATHS = ("/", "/archive/black_rod_bundle/")
4358

4459

4560
class Login:
@@ -50,6 +65,22 @@ def __init__(self, get_response):
5065

5166
def __call__(self, request):
5267
path = request.path
68+
if request.user.is_authenticated and is_apda_board_user(request.user):
69+
if not is_apda_board_access_open():
70+
logout(request)
71+
return redirect_and_flash_info(
72+
request,
73+
"APDA Board login activates after the final inround is paired.",
74+
path="/public/",
75+
)
76+
if path not in APDA_BOARD_ALLOWED_EXACT_PATHS and \
77+
not path.startswith(APDA_BOARD_ALLOWED_PREFIXES):
78+
return redirect_and_flash_info(
79+
request,
80+
"APDA Board access is limited to APDA tools and public pages.",
81+
path="/403/",
82+
)
83+
5384
should_store_return_to = (
5485
request.method == "GET"
5586
and not request.path.startswith("/api/")

mittab/apps/tab/templatetags/tags.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django import template
22
from django.forms.fields import FileField
33

4+
from mittab.apps.tab.auth_roles import is_apda_board_user
45
from mittab.apps.tab.helpers import get_redirect_target
56
from mittab.apps.tab.models import TabSettings
67
from mittab.apps.tab.public_rankings import get_public_display_flags
@@ -89,3 +90,8 @@ def public_display_flags():
8990
def motions_enabled():
9091
"""Returns True if motions feature is enabled."""
9192
return bool(TabSettings.get("motions_enabled", 0))
93+
94+
95+
@register.simple_tag
96+
def is_apda_board(user):
97+
return is_apda_board_user(user)

0 commit comments

Comments
 (0)