Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions mittab/apps/tab/auth_backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django.contrib.auth.backends import ModelBackend

from mittab.apps.tab.auth_roles import is_apda_board_access_open, is_apda_board_user


class TabAuthenticationBackend(ModelBackend):
"""Enforce APDA board access timing while preserving default auth behavior."""

def authenticate(self, request, username=None, password=None, **kwargs):
user = super().authenticate(
request,
username=username,
password=password,
**kwargs,
)
if user and is_apda_board_user(user) and not is_apda_board_access_open():
return None
return user
23 changes: 23 additions & 0 deletions mittab/apps/tab/auth_roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from mittab.apps.tab.models import Round, TabSettings

APDA_BOARD_GROUP_NAME = "APDA Board"


def is_apda_board_user(user):
if not user or not user.is_authenticated:
return False
if user.is_superuser:
return False
return user.groups.filter(name=APDA_BOARD_GROUP_NAME).exists()


def is_apda_board_access_open():
"""APDA board access opens once the final inround has been paired."""
try:
total_inrounds = int(TabSettings.get("tot_rounds"))
except (TypeError, ValueError):
return False

if total_inrounds < 1:
return False
return Round.objects.filter(round_number=total_inrounds).exists()
12 changes: 12 additions & 0 deletions mittab/apps/tab/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ class Meta:
fields = "__all__"


class SchoolApdaIdForm(forms.ModelForm):
class Meta:
model = School
fields = ["apda_id"]


class RoomForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
entry = "first_entry" in kwargs
Expand Down Expand Up @@ -240,6 +246,12 @@ class Meta:
exclude = ["tiebreaker"]


class DebaterApdaIdForm(forms.ModelForm):
class Meta:
model = Debater
fields = ["apda_id"]


def validate_speaks(value):
if not (TabSettings.get("min_speak", 0) <= value <= TabSettings.get(
"max_speak", 50)):
Expand Down
23 changes: 22 additions & 1 deletion mittab/apps/tab/management/commands/initialize_tourney.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

from django.core.management import call_command
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.core.management.base import BaseCommand

from mittab.apps.tab.auth_roles import APDA_BOARD_GROUP_NAME
from mittab.apps.tab.models import TabSettings
from mittab.libs.backup import backup_round, BEFORE_NEW_TOURNAMENT, INITIAL

Expand All @@ -26,6 +28,12 @@ def add_arguments(self, parser):
help="Password for the entry user",
nargs="?",
default=USER_MODEL.objects.make_random_password(length=8))
parser.add_argument(
"--apda-board-password",
dest="apda_board_password",
help="Password for the APDA Board user",
nargs="?",
default=None)
parser.add_argument(
"--first-init",
dest="first_init",
Expand All @@ -35,6 +43,12 @@ def add_arguments(self, parser):
default=False)

def handle(self, *args, **options):
apda_board_password = (
options.get("apda_board_password")
or os.environ.get("BOARD_PASSWORD")
or USER_MODEL.objects.make_random_password(length=16)
)

if not options["first_init"]:
self.stdout.write("Backing up the previous tournament data")
backup_round(btype=BEFORE_NEW_TOURNAMENT)
Expand All @@ -48,13 +62,16 @@ def handle(self, *args, **options):
print(why)
sys.exit(1)

self.stdout.write("Creating tab/entry users")
self.stdout.write("Creating tab/entry/APDA Board users")
tab = USER_MODEL.objects.create_user("tab", None, options["tab_password"])
tab.is_staff = True
tab.is_admin = True
tab.is_superuser = True
tab.save()
USER_MODEL.objects.create_user("entry", None, options["entry_password"])
apda_board = USER_MODEL.objects.create_user("board", None, apda_board_password)
apda_board_group, _ = Group.objects.get_or_create(name=APDA_BOARD_GROUP_NAME)
apda_board.groups.add(apda_board_group)

self.stdout.write("Setting default tab settings")
TabSettings.set("tot_rounds", 5)
Expand All @@ -73,5 +90,9 @@ def handle(self, *args, **options):
self.stdout.write(
f"{'entry'.ljust(10, ' ')} | {options['entry_password'].ljust(10, ' ')}"
)
self.stdout.write(
f"{'board'.ljust(10, ' ')} | "
f"{apda_board_password.ljust(10, ' ')}"
)
if options["first_init"]:
backup_round(name="initial-tournament", btype=INITIAL)
31 changes: 31 additions & 0 deletions mittab/apps/tab/middleware.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import re

from django.contrib.auth import logout
from django.contrib.auth.views import LoginView
from django.http import HttpResponse, JsonResponse

from mittab.apps.tab.auth_roles import is_apda_board_access_open, is_apda_board_user
from mittab.apps.tab.helpers import redirect_and_flash_info
from mittab.apps.tab.public_rankings import get_standings_publication_setting
from mittab.libs.backup import is_backup_active
Expand Down Expand Up @@ -38,6 +40,19 @@
"team": "Team results not published",
"shared": "Standings data not published",
}
APDA_BOARD_ALLOWED_PREFIXES = (
"/apda-board/",
"/public/",
"/accounts/logout/",
"/admin/logout/",
"/403/",
"/404/",
"/500/",
"/favicon.ico",
"/static/",
"/dynamic-media/",
)
APDA_BOARD_ALLOWED_EXACT_PATHS = ("/",)


class Login:
Expand All @@ -48,6 +63,22 @@ def __init__(self, get_response):

def __call__(self, request):
path = request.path
if request.user.is_authenticated and is_apda_board_user(request.user):
if not is_apda_board_access_open():
logout(request)
return redirect_and_flash_info(
request,
"APDA Board login activates after the final inround is paired.",
path="/public/",
)
if path not in APDA_BOARD_ALLOWED_EXACT_PATHS and \
not path.startswith(APDA_BOARD_ALLOWED_PREFIXES):
return redirect_and_flash_info(
request,
"APDA Board access is limited to APDA tools and public pages.",
path="/403/",
)

whitelisted = (
path in LOGIN_WHITELIST
or path.startswith("/public/")
Expand Down
6 changes: 6 additions & 0 deletions mittab/apps/tab/templatetags/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django import template
from django.forms.fields import FileField

from mittab.apps.tab.auth_roles import is_apda_board_user
from mittab.apps.tab.helpers import get_redirect_target
from mittab.apps.tab.models import TabSettings
from mittab.apps.tab.public_rankings import get_public_display_flags
Expand Down Expand Up @@ -94,3 +95,8 @@ def public_display_flags():
def motions_enabled():
"""Returns True if motions feature is enabled."""
return bool(TabSettings.get("motions_enabled", 0))


@register.simple_tag
def is_apda_board(user):
return is_apda_board_user(user)
117 changes: 117 additions & 0 deletions mittab/apps/tab/views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
import yaml

from mittab.apps.tab.archive import ArchiveExporter
from mittab.apps.tab.auth_roles import is_apda_board_user
from mittab.apps.tab.views.debater_views import get_speaker_rankings
from mittab.apps.tab.forms import (
MiniRankingGroupForm,
MiniRoomTagForm,
DebaterApdaIdForm,
RankingGroupForm,
RoomTagForm,
SchoolApdaIdForm,
SchoolForm,
RoomForm,
UploadDataForm,
Expand Down Expand Up @@ -50,6 +53,8 @@
def index(request):
if not request.user.is_authenticated:
return redirect("public_home")
if is_apda_board_user(request.user):
return redirect("apda_board_home")

schools_missing_apda_qs = School.objects.filter(
Q(apda_id__isnull=True) | Q(apda_id=-1)
Expand Down Expand Up @@ -101,6 +106,118 @@ def index(request):
return render(request, "common/index.html", context)


def apda_board_home(request):
if not is_apda_board_user(request.user):
return redirect("index")

allowed_standing_slugs = ("speaker_results", "team_results")
if request.method == "POST":
for slug in allowed_standing_slugs:
published = bool(request.POST.get(f"standings_{slug}"))
set_standings_publication_setting(slug, published)
invalidate_public_rankings_cache()
return redirect_and_flash_success(
request,
"APDA standings publication settings saved.",
path=reverse("apda_board_home"),
)

standings_settings = [
setting for setting in get_all_standings_publication_settings()
if setting["slug"] in allowed_standing_slugs
]
schools_missing_apda_qs = School.objects.filter(
Q(apda_id__isnull=True) | Q(apda_id=-1)
)
debaters_missing_apda_qs = Debater.objects.filter(
Q(apda_id__isnull=True) | Q(apda_id=-1)
)

context = {
"school_list": [(school.pk, school.name) for school in School.objects.all()],
"debater_list": [
(debater.pk, debater.display) for debater in Debater.objects.all()
],
"standings_settings": standings_settings,
"schools_missing_apda_ids": [school.pk for school in schools_missing_apda_qs],
"debaters_missing_apda_ids": [
debater.pk for debater in debaters_missing_apda_qs
],
"schools_missing_apda_count": schools_missing_apda_qs.count(),
"debaters_missing_apda_count": debaters_missing_apda_qs.count(),
}
return render(request, "apda_board/home.html", context)


def apda_board_school_detail(request, school_id):
if not is_apda_board_user(request.user):
return redirect("index")

school = School.objects.filter(pk=int(school_id)).first()
if not school:
return redirect_and_flash_error(request, "School not found")

if request.method == "POST":
form = SchoolApdaIdForm(request.POST, instance=school)
if form.is_valid():
form.save()
return redirect_and_flash_success(
request,
f"APDA ID updated for {school.name}.",
path=reverse("apda_board_school_detail", args=[school.id]),
)
else:
form = SchoolApdaIdForm(instance=school)

teams = Team.objects.filter(school=school).prefetch_related("debaters")
hybrid_teams = Team.objects.filter(hybrid_school=school).prefetch_related("debaters")

return render(
request,
"apda_board/school_detail.html",
{
"form": form,
"school_obj": school,
"school_teams": teams,
"school_hybrid_teams": hybrid_teams,
"title": f"APDA Board: {school.name}",
},
)


def apda_board_debater_detail(request, debater_id):
if not is_apda_board_user(request.user):
return redirect("index")

debater = Debater.objects.filter(pk=int(debater_id)).first()
if not debater:
return redirect_and_flash_error(request, "Debater not found")

if request.method == "POST":
form = DebaterApdaIdForm(request.POST, instance=debater)
if form.is_valid():
form.save()
return redirect_and_flash_success(
request,
f"APDA ID updated for {debater.name}.",
path=reverse("apda_board_debater_detail", args=[debater.id]),
)
else:
form = DebaterApdaIdForm(instance=debater)

teams = Team.objects.filter(debaters=debater).select_related("school", "hybrid_school")
return render(
request,
"apda_board/debater_detail.html",
{
"form": form,
"debater_obj": debater,
"teams": teams,
"title": f"APDA Board: {debater.name}",
},
)


def tab_logout(request, *args):
logout(request)
return redirect_and_flash_success(request,
Expand Down
Loading