diff --git a/mittab/apps/tab/admin.py b/mittab/apps/tab/admin.py index c73313016..81a2d71b3 100644 --- a/mittab/apps/tab/admin.py +++ b/mittab/apps/tab/admin.py @@ -50,3 +50,5 @@ class TeamAdmin(admin.ModelAdmin): admin.site.register(models.NoShow) admin.site.register(models.BreakingTeam) admin.site.register(models.Outround, OutroundAdmin) +admin.site.register(models.JudgeJudgeScratch) +admin.site.register(models.TeamTeamScratch) diff --git a/mittab/apps/tab/forms.py b/mittab/apps/tab/forms.py index e59dd64a0..d03ad452f 100644 --- a/mittab/apps/tab/forms.py +++ b/mittab/apps/tab/forms.py @@ -231,6 +231,46 @@ class Meta: exclude = ["team", "judge"] +class JudgeJudgeScratchForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + judge_queryset = kwargs.pop("judge_queryset", Judge.objects.all()) + super().__init__(*args, **kwargs) + self.fields["judge_one"].queryset = judge_queryset + self.fields["judge_two"].queryset = judge_queryset + + def clean(self): + cleaned_data = super().clean() + judge_one = cleaned_data.get("judge_one") + judge_two = cleaned_data.get("judge_two") + if judge_one and judge_two and judge_one == judge_two: + raise forms.ValidationError("Pick two different judges") + return cleaned_data + + class Meta: + model = JudgeJudgeScratch + fields = "__all__" + + +class TeamTeamScratchForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + team_queryset = kwargs.pop("team_queryset", Team.objects.all()) + super().__init__(*args, **kwargs) + self.fields["team_one"].queryset = team_queryset + self.fields["team_two"].queryset = team_queryset + + def clean(self): + cleaned_data = super().clean() + team_one = cleaned_data.get("team_one") + team_two = cleaned_data.get("team_two") + if team_one and team_two and team_one == team_two: + raise forms.ValidationError("Pick two different teams") + return cleaned_data + + class Meta: + model = TeamTeamScratch + fields = "__all__" + + class DebaterForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(DebaterForm, self).__init__(*args, **kwargs) diff --git a/mittab/apps/tab/judge_views.py b/mittab/apps/tab/judge_views.py index df8c594ec..04ba6340c 100644 --- a/mittab/apps/tab/judge_views.py +++ b/mittab/apps/tab/judge_views.py @@ -1,7 +1,7 @@ from django.http import HttpResponse from django.shortcuts import render -from mittab.apps.tab.forms import JudgeForm, ScratchForm +from mittab.apps.tab.forms import JudgeForm from mittab.apps.tab.helpers import redirect_and_flash_error, redirect_and_flash_success from mittab.apps.tab.models import * from mittab.libs.errors import * @@ -103,8 +103,7 @@ def view_judge(request, judge_id): form = JudgeForm(instance=judge) judging_rounds = list(Round.objects.filter(judges=judge).select_related( "gov_team", "opp_team", "room")) - base_url = f"/judge/{judge_id}/" - scratch_url = f"{base_url}scratches/view/" + scratch_url = f"/scratches/judge/{judge_id}/" links = [(scratch_url, f"Scratches for {judge.name}")] return render( request, "tab/judge_detail.html", { @@ -137,105 +136,6 @@ def enter_judge(request): }) -def add_scratches(request, judge_id, number_scratches): - try: - judge_id, number_scratches = int(judge_id), int(number_scratches) - except ValueError: - return redirect_and_flash_error(request, "Got invalid data") - try: - judge = Judge.objects.get(pk=judge_id) - except Judge.DoesNotExist: - return redirect_and_flash_error(request, "No such judge") - - if request.method == "POST": - forms = [ - ScratchForm(request.POST, prefix=str(i)) - for i in range(1, number_scratches + 1) - ] - all_good = True - for form in forms: - all_good = all_good and form.is_valid() - if all_good: - for form in forms: - form.save() - return redirect_and_flash_success( - request, "Scratches created successfully") - else: - forms = [ - ScratchForm( - prefix=str(i), - initial={ - "judge": judge_id, - "scratch_type": 0 - } - ) - for i in range(1, number_scratches + 1) - ] - return render( - request, "common/data_entry_multiple.html", { - "forms": list(zip(forms, [None] * len(forms))), - "data_type": "Scratch", - "title": f"Adding Scratch(es) for {judge.name}" - }) - - -def view_scratches(request, judge_id): - try: - judge_id = int(judge_id) - except ValueError: - return redirect_and_flash_error(request, "Received invalid data") - - judge = Judge.objects.prefetch_related( - "scratches", "scratches__judge", "scratches__team" - ).get(pk=judge_id) - scratches = judge.scratches.all() - - all_teams = Team.objects.all() - all_judges = Judge.objects.all() - - if request.method == "POST": - forms = [ - ScratchForm( - request.POST, - prefix=str(i + 1), - instance=scratches[i], - team_queryset=all_teams, - judge_queryset=all_judges - ) - for i in range(len(scratches)) - ] - all_good = True - for form in forms: - all_good = all_good and form.is_valid() - if all_good: - for form in forms: - form.save() - return redirect_and_flash_success( - request, "Scratches created successfully") - else: - forms = [ - ScratchForm( - prefix=str(i + 1), - instance=scratches[i], - team_queryset=all_teams, - judge_queryset=all_judges - ) - for i in range(len(scratches)) - ] - delete_links = [ - f"/judge/{judge_id}/scratches/delete/{scratches[i].id}" - for i in range(len(scratches)) - ] - links = [(f"/judge/{judge_id}/scratches/add/1/", "Add Scratch")] - - return render( - request, "common/data_entry_multiple.html", { - "forms": list(zip(forms, delete_links)), - "data_type": "Scratch", - "links": links, - "title": f"Viewing Scratch Information for {judge.name}" - }) - def download_judge_codes(request): codes = [ f"{getattr(judge, 'name', 'Unknown')}: {getattr(judge, 'ballot_code', 'N/A')}" diff --git a/mittab/apps/tab/migrations/0031_judgejudgescratch_teamteamscratch.py b/mittab/apps/tab/migrations/0031_judgejudgescratch_teamteamscratch.py new file mode 100644 index 000000000..1d65cda08 --- /dev/null +++ b/mittab/apps/tab/migrations/0031_judgejudgescratch_teamteamscratch.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.25 on 2025-10-29 16:17 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tab', '0030_auto_20251024_1855'), + ] + + operations = [ + migrations.CreateModel( + name='TeamTeamScratch', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('team_one', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_scratch_primary', to='tab.team')), + ('team_two', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_scratch_secondary', to='tab.team')), + ], + options={ + 'verbose_name_plural': 'team scratches', + 'unique_together': {('team_one', 'team_two')}, + }, + ), + migrations.CreateModel( + name='JudgeJudgeScratch', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('judge_one', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='judge_scratch_primary', to='tab.judge')), + ('judge_two', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='judge_scratch_secondary', to='tab.judge')), + ], + options={ + 'verbose_name_plural': 'judge scratches', + 'unique_together': {('judge_one', 'judge_two')}, + }, + ), + ] diff --git a/mittab/apps/tab/models.py b/mittab/apps/tab/models.py index e6ec8c754..f49b8b7a0 100644 --- a/mittab/apps/tab/models.py +++ b/mittab/apps/tab/models.py @@ -398,6 +398,70 @@ def __str__(self): s_type = ("Team", "Tab")[self.scratch_type] return f"{self.team} <={s_type}=> {self.judge}" +class JudgeJudgeScratch(models.Model): + judge_one = models.ForeignKey( + "Judge", + related_name="judge_scratch_primary", + on_delete=models.CASCADE, + ) + judge_two = models.ForeignKey( + "Judge", + related_name="judge_scratch_secondary", + on_delete=models.CASCADE, + ) + + class Meta: + unique_together = ("judge_one", "judge_two") + verbose_name_plural = "judge scratches" + + def __str__(self): + return f"{self.judge_one} <=> {self.judge_two}" + + def clean(self): + if self.judge_one and self.judge_two and self.judge_one == self.judge_two: + raise ValidationError("Judge scratches must involve two distinct judges") + + def save(self, *args, **kwargs): + if self.judge_one and self.judge_two: + if self.judge_one == self.judge_two: + raise ValidationError( + "Judge scratches must involve two distinct judges") + if self.judge_one.id > self.judge_two.id: + self.judge_one, self.judge_two = self.judge_two, self.judge_one + super().save(*args, **kwargs) + + +class TeamTeamScratch(models.Model): + team_one = models.ForeignKey( + "Team", + related_name="team_scratch_primary", + on_delete=models.CASCADE, + ) + team_two = models.ForeignKey( + "Team", + related_name="team_scratch_secondary", + on_delete=models.CASCADE, + ) + + class Meta: + unique_together = ("team_one", "team_two") + verbose_name_plural = "team scratches" + + def __str__(self): + return f"{self.team_one} <=> {self.team_two}" + + def clean(self): + if self.team_one and self.team_two and self.team_one == self.team_two: + raise ValidationError("Team scratches must involve two distinct teams") + + def save(self, *args, **kwargs): + if self.team_one and self.team_two: + if self.team_one == self.team_two: + raise ValidationError("Team scratches must involve two distinct teams") + if self.team_one.id > self.team_two.id: + self.team_one, self.team_two = self.team_two, self.team_one + super().save(*args, **kwargs) + class Room(models.Model): name = models.CharField(max_length=30, unique=True) diff --git a/mittab/apps/tab/scratch_views.py b/mittab/apps/tab/scratch_views.py new file mode 100644 index 000000000..190c36df9 --- /dev/null +++ b/mittab/apps/tab/scratch_views.py @@ -0,0 +1,315 @@ +from django.contrib.auth.decorators import permission_required +from django.db import IntegrityError, transaction +from django.shortcuts import get_object_or_404, render, reverse + +from mittab.apps.tab.forms import ( + ScratchForm, + JudgeJudgeScratchForm, + TeamTeamScratchForm, +) +from mittab.apps.tab.helpers import ( + redirect_and_flash_error, + redirect_and_flash_success, +) +from mittab.apps.tab.models import ( + Judge, + Team, + Scratch, + JudgeJudgeScratch, + TeamTeamScratch, +) + +SCRATCH_OBJECTS = { + "judge-team": Scratch, + "judge-judge": JudgeJudgeScratch, + "team-team": TeamTeamScratch, +} + +SCRATCH_FORMS = { + "judge-team": ScratchForm, + "judge-judge": JudgeJudgeScratchForm, + "team-team": TeamTeamScratchForm, +} + + +def add_scratch(request): + judges = Judge.objects.order_by("name") + teams = Team.objects.order_by("name") + + judge_id, team_id = request.GET.get("judge_id"), request.GET.get("team_id") + active_tab = request.POST.get("form_type") or request.GET.get("tab") or "judge_team" + if active_tab not in {"judge_team", "judge_judge", "team_team"}: + active_tab = "judge_team" + + is_post = request.method == "POST" + + # shared initial data + scratch_initial = { + "scratch_type": Scratch.TEAM_SCRATCH, + "judge": judge_id, + "team": team_id, + } + judge_pair_initial = {"judge_one": judge_id} if judge_id else {} + team_pair_initial = {"team_one": team_id} if team_id else {} + + def make_form(form_cls, prefix, queryset_args, initial): + data = ( + request.POST if (is_post and active_tab == prefix.split("_")[0]) else None + ) + return form_cls( + data, + prefix=f"{prefix}_0", + **queryset_args, + initial=None if data else initial, + ) + + forms_by_type = { + "judge_team": [ + make_form( + ScratchForm, + "judge_team", + {"judge_queryset": judges, "team_queryset": teams}, + scratch_initial, + ) + ], + "judge_judge": [ + make_form( + JudgeJudgeScratchForm, + "judge_judge", + {"judge_queryset": judges}, + judge_pair_initial, + ) + ], + "team_team": [ + make_form( + TeamTeamScratchForm, + "team_team", + {"team_queryset": teams}, + team_pair_initial, + ) + ], + } + + if is_post: + forms = forms_by_type[active_tab] + if all(f.is_valid() for f in forms): + try: + with transaction.atomic(): + for f in forms: + f.save() + except IntegrityError: + for f in forms: + f.add_error(None, "This scratch already exists.") + else: + return redirect_and_flash_success( + request, + "Scratches created successfully", + path=request.get_full_path(), + ) + + tab_labels = { + "judge_team": "Judge ↔ Team", + "judge_judge": "Judge ↔ Judge", + "team_team": "Team ↔ Team", + } + + return render( + request, + "scratches/add_scratches.html", + { + "forms_by_type": forms_by_type, + "tabs": list(tab_labels.items()), + "forms_context": [ + {"key": k, "label": v, "forms": forms_by_type[k]} + for k, v in tab_labels.items() + ], + "active_tab": active_tab, + }, + ) + + +SCRATCH_FILTER_DEFS = { + "judge-team": {"bit": 1, "label": "Judge ↔ Team"}, + "judge-judge": {"bit": 2, "label": "Judge ↔ Judge"}, + "team-team": {"bit": 4, "label": "Team ↔ Team"}, +} + + +def view_scratches(request): + def build_items(qs, type_key, labels): + """Build (id, name, bitmask, symbols) tuples for each scratch type.""" + bit = SCRATCH_FILTER_DEFS[type_key]["bit"] + items = [] + for s in qs: + left_obj = getattr(s, labels[0]) + right_obj = getattr(s, labels[1]) + left_name = ( + left_obj.display_backend + if isinstance(left_obj, Team) or "team" in labels[0] + else left_obj.name + ) + right_name = ( + right_obj.display_backend + if isinstance(right_obj, Team) or "team" in labels[1] + else right_obj.name + ) + item_id = f"{type_key}/{s.id}" + item_label = left_name + " ↔ " + right_name + items.append((item_id, item_label, bit, "")) + return items + + configs = [ + ( + "judge-team", + Scratch.objects.select_related("team", "judge").order_by( + "team__name", "judge__name" + ), + ("team", "judge"), + ), + ( + "judge-judge", + JudgeJudgeScratch.objects.select_related("judge_one", "judge_two").order_by( + "judge_one__name", "judge_two__name" + ), + ("judge_one", "judge_two"), + ), + ( + "team-team", + TeamTeamScratch.objects.select_related("team_one", "team_two").order_by( + "team_one__name", "team_two__name" + ), + ("team_one", "team_two"), + ), + ] + + # Flatten all items + item_list = [ + item for key, qs, labels in configs for item in build_items(qs, key, labels) + ] + + # Each filter group entry should be (bit, label) + filters_group = [(v["bit"], v["label"]) for v in SCRATCH_FILTER_DEFS.values()] + + return render( + request, + "common/list_data.html", + { + "title": "Scratches", + "item_type": "scratch", + "item_list": item_list, + "filters": [filters_group], + }, + ) + + +def view_scratches_for_object(request, object_type, object_id): + try: + object_id = int(object_id) + obj = get_object_or_404(Judge if object_type == "judge" else Team, pk=object_id) + except ValueError: + return redirect_and_flash_error(request, "Received invalid data") + if object_type == "judge": + forms = get_scratch_forms_for_judge(object_id) + elif object_type == "team": + forms = get_scratch_forms_for_team(object_id) + else: + return redirect_and_flash_error(request, "Unknown object type") + + return render( + request, + "common/data_entry_multiple.html", + { + "title": f"Viewing Scratch Information for {obj}", + "data_type": "Scratch", + "forms": [(form, None) for form in forms], + }, + ) + + +def get_scratch_forms_for_judge(judge_id): + forms = [] + for scratch in Scratch.objects.filter(judge_id=judge_id).select_related("team"): + form = ScratchForm( + instance=scratch, + prefix=str(len(forms) + 1), + team_queryset=Team.objects.order_by("name"), + judge_queryset=Judge.objects.order_by("name"), + ) + forms.append(form) + for scratch in JudgeJudgeScratch.objects.filter( + judge_one_id=judge_id + ).select_related("judge_two") | JudgeJudgeScratch.objects.filter( + judge_two_id=judge_id + ).select_related( + "judge_one" + ): + form = JudgeJudgeScratchForm( + instance=scratch, + prefix=str(len(forms) + 1), + judge_queryset=Judge.objects.order_by("name"), + ) + forms.append(form) + return forms + + +def get_scratch_forms_for_team(team_id): + forms = [] + for scratch in Scratch.objects.filter(team_id=team_id).select_related("judge"): + form = ScratchForm( + instance=scratch, + prefix=str(len(forms) + 1), + team_queryset=Team.objects.order_by("name"), + judge_queryset=Judge.objects.order_by("name"), + ) + forms.append(form) + for scratch in TeamTeamScratch.objects.filter(team_one_id=team_id).select_related( + "team_two" + ) | TeamTeamScratch.objects.filter(team_two_id=team_id).select_related("team_one"): + form = TeamTeamScratchForm( + instance=scratch, + prefix=str(len(forms) + 1), + team_queryset=Team.objects.order_by("name"), + ) + forms.append(form) + return forms + +# Backwards-compatible aliases for callers that use the old camelCase names +getScratchFormsForJudge = get_scratch_forms_for_judge +getScratchFormsForTeam = get_scratch_forms_for_team + + +def scratch_detail(request, scratch_type, scratch_id): + model = SCRATCH_OBJECTS.get(scratch_type) + form_class = SCRATCH_FORMS.get(scratch_type) + scratch_obj = model.objects.get(pk=scratch_id) + form_obj = form_class(instance=scratch_obj) + return render( + request, + "common/data_entry.html", + { + "title": f"Viewing Scratch: {scratch_obj}", + "data_type": "Scratch", + "form": form_obj, + "delete_link": reverse("scratch_delete", args=(scratch_type, scratch_id)), + }, + ) + + +@permission_required("tab.scratch.can_delete", login_url="/403/") +def scratch_delete(request, scratch_type, scratch_id): + model = SCRATCH_OBJECTS.get(scratch_type) + if not model: + return redirect_and_flash_error( + request, "Unknown scratch type", path=reverse("view_scratches") + ) + try: + scratch = model.objects.get(pk=scratch_id) + except model.DoesNotExist: + return redirect_and_flash_error( + request, "Scratch not found", path=reverse("view_scratches") + ) + + scratch.delete() + return redirect_and_flash_success( + request, "Scratch deleted successfully", path=reverse("view_scratches") + ) diff --git a/mittab/apps/tab/team_views.py b/mittab/apps/tab/team_views.py index ab93e21e3..76a4179c5 100644 --- a/mittab/apps/tab/team_views.py +++ b/mittab/apps/tab/team_views.py @@ -1,8 +1,11 @@ +from urllib.parse import urlencode + from django.http import HttpResponseRedirect from django.contrib.auth.decorators import permission_required from django.shortcuts import render +from django.urls import reverse -from mittab.apps.tab.forms import TeamForm, TeamEntryForm, ScratchForm +from mittab.apps.tab.forms import TeamForm, TeamEntryForm from mittab.libs.errors import * from mittab.apps.tab.helpers import redirect_and_flash_error, \ redirect_and_flash_success @@ -113,9 +116,12 @@ def enter_team(request): ) num_forms = form.cleaned_data["number_scratches"] if num_forms > 0: - return HttpResponseRedirect( - f"/team/{team.pk}/scratches/add/{num_forms}" - ) + query = urlencode({ + "team_id": team.pk, + "tab": "judge_team", + "count": num_forms, + }) + return HttpResponseRedirect(f"{reverse('add_scratch')}?{query}") else: team_name = team.display_backend return redirect_and_flash_success( @@ -130,109 +136,6 @@ def enter_team(request): }) -def add_scratches(request, team_id, number_scratches): - try: - team_id, number_scratches = int(team_id), int(number_scratches) - except ValueError: - return redirect_and_flash_error(request, "Received invalid data") - try: - team = Team.objects.get(pk=team_id) - except Team.DoesNotExist: - return redirect_and_flash_error(request, - "The selected team does not exist") - all_teams = Team.objects.all() - all_judges = Judge.objects.all() - if request.method == "POST": - forms = [ - ScratchForm( - request.POST, - prefix=str( - i + 1), - team_queryset=all_teams, - judge_queryset=all_judges) - for i in range(number_scratches) - ] - all_good = True - for form in forms: - all_good = all_good and form.is_valid() - if all_good: - for form in forms: - form.save() - return redirect_and_flash_success( - request, "Scratches created successfully") - else: - forms = [ - ScratchForm( - prefix=str(i), - initial={ - "team": team_id, - "scratch_type": 0 - }, - team_queryset=all_teams, - judge_queryset=all_judges - ) for i in range(1, number_scratches + 1) - ] - return render( - request, "common/data_entry_multiple.html", { - "forms": list(zip(forms, [None] * len(forms))), - "data_type": "Scratch", - "title": f"Adding Scratch(es) for {team.display_backend}" - }) - - -def view_scratches(request, team_id): - try: - team_id = int(team_id) - except ValueError: - return redirect_and_flash_error(request, "Received invalid data") - scratches = Scratch.objects.filter(team=team_id) - number_scratches = len(scratches) - team = Team.objects.get(pk=team_id) - all_teams = Team.objects.all() - all_judges = Judge.objects.all() - if request.method == "POST": - forms = [ - ScratchForm( - request.POST, - prefix=str(i + 1), - instance=scratches[i], - team_queryset=all_teams, - judge_queryset=all_judges - ) - for i in range(number_scratches) - ] - all_good = True - for form in forms: - all_good = all_good and form.is_valid() - if all_good: - for form in forms: - form.save() - return redirect_and_flash_success( - request, "Scratches successfully modified") - else: - forms = [ - ScratchForm( - prefix=str(i + 1), - instance=scratches[i], - team_queryset=all_teams, - judge_queryset=all_judges - ) - for i in range(len(scratches)) - ] - delete_links = [ - f"/team/{team_id}/scratches/delete/{scratches[i].id}" - for i in range(len(scratches)) - ] - links = [(f"/team/{team_id}/scratches/add/1/", "Add Scratch")] - return render( - request, "common/data_entry_multiple.html", { - "forms": list(zip(forms, delete_links)), - "data_type": "Scratch", - "links": links, - "title": f"Viewing Scratch Information for {team.display_backend}" - }) - - @permission_required("tab.tab_settings.can_change", login_url="/403/") def all_tab_cards(request): all_teams = Team.objects.all() diff --git a/mittab/apps/tab/views.py b/mittab/apps/tab/views.py index 43718033b..95d97f858 100644 --- a/mittab/apps/tab/views.py +++ b/mittab/apps/tab/views.py @@ -1,5 +1,4 @@ import os -from django.db import IntegrityError from django.contrib.auth.decorators import permission_required from django.contrib.auth import logout from django.conf import settings @@ -11,7 +10,7 @@ from mittab.apps.tab.archive import ArchiveExporter from mittab.apps.tab.debater_views import get_speaker_rankings from mittab.apps.tab.forms import MiniRoomTagForm, RoomTagForm, SchoolForm, RoomForm, \ - UploadDataForm, ScratchForm, SettingsForm + UploadDataForm, SettingsForm from mittab.apps.tab.helpers import redirect_and_flash_error, \ redirect_and_flash_success from mittab.apps.tab.models import * @@ -86,26 +85,6 @@ def render_500(request, *args, **kwargs): return response -# View for manually adding scratches -def add_scratch(request): - if request.method == "POST": - form = ScratchForm(request.POST) - if form.is_valid(): - try: - form.save() - except IntegrityError: - return redirect_and_flash_error(request, - "This scratch already exists.") - return redirect_and_flash_success(request, - "Scratch created successfully") - else: - form = ScratchForm(initial={"scratch_type": 0}) - return render(request, "common/data_entry.html", { - "title": "Adding Scratch", - "form": form - }) - - #### BEGIN SCHOOL ### # Three views for entering, viewing, and editing schools def view_schools(request): @@ -333,31 +312,6 @@ def bulk_check_in(request): return JsonResponse({"success": True}) -@permission_required("tab.scratch.can_delete", login_url="/403/") -def delete_scratch(request, _item_id, scratch_id): - try: - scratch_id = int(scratch_id) - scratch = Scratch.objects.get(pk=scratch_id) - scratch.delete() - except Scratch.DoesNotExist: - return redirect_and_flash_error( - request, - "This scratch does not exist, please try again with a valid id.") - return redirect_and_flash_success(request, - "Scratch deleted successfully", - path="/") - - -def view_scratches(request): - # Get a list of (id,school_name) tuples - c_scratches = [(s.team.pk, str(s), 0, "") for s in Scratch.objects.all()] - return render( - request, "common/list_data.html", { - "item_type": "team", - "title": "Viewing All Scratches for Teams", - "item_list": c_scratches - }) - def get_settings_from_yaml(): settings_dir = os.path.join(settings.BASE_DIR, "settings") diff --git a/mittab/libs/assign_judges.py b/mittab/libs/assign_judges.py index 0f2e35281..6137f6f56 100644 --- a/mittab/libs/assign_judges.py +++ b/mittab/libs/assign_judges.py @@ -90,6 +90,15 @@ def add_judges(): ) ) + judge_pair_blocks = {} + for judge_one_id, judge_two_id in JudgeJudgeScratch.objects.values_list( + "judge_one_id", "judge_two_id" + ): + judge_pair_blocks.setdefault(judge_one_id, set()).add(judge_two_id) + judge_pair_blocks.setdefault(judge_two_id, set()).add(judge_one_id) + for judge in all_judges: + judge.panel_scratch_ids = judge_pair_blocks.get(judge.id, set()) + # Sort all_judges once before creating filtered subsets random.seed(1337) random.shuffle(all_judges) @@ -128,6 +137,7 @@ def pairing_sort_key(pairing): if settings.allow_rejudges: rejudge_counts = judge_team_rejudge_counts(chairs, all_teams) + panel_members_by_pair = [set() for _ in range(num_rounds)] graph_edges = [] for chair_i, chair in enumerate(chairs): chair_score = chair_scores[chair_i] @@ -200,6 +210,7 @@ def pairing_sort_key(pairing): round_obj.chair = chair chair_by_pairing[pairing_i] = chair_i assigned_judge_objects.add(chair.id) # Track by judge ID + panel_members_by_pair[pairing_i].add(chair.id) judge_round_joins.append( Round.judges.through(judge=chair, round=round_obj) ) @@ -226,6 +237,11 @@ def pairing_sort_key(pairing): judge_score = wing_judge_scores[wing_judge_i] for pairing_i in pairing_indices: pairing = pairings[pairing_i] + if judge.panel_scratch_ids and any( + blocked_id in panel_members_by_pair[pairing_i] + for blocked_id in judge.panel_scratch_ids + ): + continue has_conflict = judge_conflict( judge, pairing.gov_team, @@ -269,6 +285,11 @@ def pairing_sort_key(pairing): judge = wing_judges[wing_judge_i] if judge.id in assigned_judge_objects: continue + if judge.panel_scratch_ids and any( + blocked_id in panel_members_by_pair[pairing_i] + for blocked_id in judge.panel_scratch_ids + ): + continue judge_round_joins.append( Round.judges.through( judge=judge, @@ -276,6 +297,7 @@ def pairing_sort_key(pairing): ) ) assigned_judge_objects.add(judge.id) + panel_members_by_pair[pairing_i].add(judge.id) Round.judges.through.objects.bulk_create(judge_round_joins) @@ -300,6 +322,14 @@ def add_outround_judges(round_type=Outround.VARSITY): "scratches", ) ) + judge_pair_blocks = {} + for judge_one_id, judge_two_id in JudgeJudgeScratch.objects.values_list( + "judge_one_id", "judge_two_id" + ): + judge_pair_blocks.setdefault(judge_one_id, set()).add(judge_two_id) + judge_pair_blocks.setdefault(judge_two_id, set()).add(judge_one_id) + for judge in judges: + judge.panel_scratch_ids = judge_pair_blocks.get(judge.id, set()) pairings = tab_logic.sorted_pairings(num_teams, outround=True) pairings = [p for p in pairings if p.type_of_round == round_type] # Try to have consistent ordering with the round display @@ -326,6 +356,7 @@ def add_outround_judges(round_type=Outround.VARSITY): num_rounds = len(pairings) judge_round_joins, available_indices = [], list(range(len(judges))) snake_draft_mode = settings.draft_mode == OutroundJudgePairingMode.SNAKE_DRAFT + panel_members_by_pair = [set() for _ in range(num_rounds)] # Iterate once for each member of the panel for panel_member in range(settings.panel_size): @@ -333,6 +364,11 @@ def add_outround_judges(round_type=Outround.VARSITY): for judge_i in available_indices: judge = judges[judge_i] for pairing_i, pairing in enumerate(pairings): + if judge.panel_scratch_ids and any( + blocked_id in panel_members_by_pair[pairing_i] + for blocked_id in judge.panel_scratch_ids + ): + continue has_conflict = judge_conflict( judge, pairing.gov_team, @@ -380,6 +416,7 @@ def add_outround_judges(round_type=Outround.VARSITY): if panel_member == 0: round_obj.chair = judge + panel_members_by_pair[pairing_i].add(judge.id) judge_round_joins.append(link_outround(judge=judge, outround=round_obj)) available_indices.remove(judge_i) diff --git a/mittab/libs/tab_logic/__init__.py b/mittab/libs/tab_logic/__init__.py index b29af1750..44eabdbcc 100644 --- a/mittab/libs/tab_logic/__init__.py +++ b/mittab/libs/tab_logic/__init__.py @@ -511,9 +511,19 @@ def perfect_pairing(list_of_teams): """Uses the mwmatching library to assign teams in a pairing""" graph_edges = [] weights = get_weights() + scratched_pairs = { + (team_one, team_two) + for team_one, team_two in TeamTeamScratch.objects.values_list( + "team_one_id", "team_two_id" + ) + } for i, team1 in enumerate(list_of_teams): for j, team2 in enumerate(list_of_teams): if i > j: + if team1.id and team2.id: + pair_key = (min(team1.id, team2.id), max(team1.id, team2.id)) + if pair_key in scratched_pairs: + continue weight = calc_weight( team1, team2, diff --git a/mittab/templates/common/_form.html b/mittab/templates/common/_form.html index 1d3e91d4f..b40218c3c 100644 --- a/mittab/templates/common/_form.html +++ b/mittab/templates/common/_form.html @@ -14,5 +14,9 @@ {% csrf_token %} {% buttons %} + {% if delete_link %} + Delete + {% endif %} {% endbuttons %} diff --git a/mittab/templates/scratches/add_scratches.html b/mittab/templates/scratches/add_scratches.html new file mode 100644 index 000000000..3c496a8af --- /dev/null +++ b/mittab/templates/scratches/add_scratches.html @@ -0,0 +1,59 @@ +{% extends "base/__normal.html" %} +{% load bootstrap4 %} + +{% block title %}Adding Scratches{% endblock %} +{% block banner %}Adding Scratches{% endblock %} + +{% block content %} +
+ + +
+ {% for tab in forms_context %} + {% with key=tab.key %} +
+
+ {% csrf_token %} + + + {% for form in tab.forms %} +
+
+
+ {{ tab.label }} #{{ forloop.counter }} +
+
+
+ {% bootstrap_form form %} +
+
+ {% endfor %} + +
+
+ {% endwith %} + {% endfor %} +
+
+{% endblock %} diff --git a/mittab/urls.py b/mittab/urls.py index 177604c94..9bd205a4e 100644 --- a/mittab/urls.py +++ b/mittab/urls.py @@ -9,6 +9,7 @@ import mittab.apps.tab.views as views import mittab.apps.tab.api_views as api_views import mittab.apps.tab.judge_views as judge_views +import mittab.apps.tab.scratch_views as scratch_views import mittab.apps.tab.team_views as team_views import mittab.apps.tab.debater_views as debater_views import mittab.apps.tab.pairing_views as pairing_views @@ -38,12 +39,6 @@ # Judge related re_path(r"^judges/", judge_views.public_view_judges, name="public_judges"), re_path(r"^judge/(\d+)/$", judge_views.view_judge, name="view_judge"), - re_path(r"^judge/(\d+)/scratches/add/(\d+)/", - judge_views.add_scratches, - name="add_scratches"), - re_path(r"^judge/(\d+)/scratches/view/", - judge_views.view_scratches, - name="view_scratches"), path("view_judges/", judge_views.view_judges, name="view_judges"), path("enter_judge/", judge_views.enter_judge, name="enter_judge"), path("download_judge_codes/", @@ -68,23 +63,24 @@ # Scratch related - re_path(r"^judge/(\d+)/scratches/delete/(\d+)/", - views.delete_scratch, - name="delete_scratch_judge"), - re_path(r"^team/(\d+)/scratches/delete/(\d+)/", - views.delete_scratch, - name="delete_scratch_team"), - re_path(r"^scratches/view/", views.view_scratches, name="view_scratches"), - re_path(r"^enter_scratch/", views.add_scratch, name="add_scratch"), + path( + "scratch///", + scratch_views.scratch_detail, + name="scratch_detail", + ), + path( + "scratch///delete/", + scratch_views.scratch_delete, + name="scratch_delete", + ), + re_path(r"^scratches/view/", scratch_views.view_scratches, name="view_scratches"), + re_path(r"^enter_scratch/", scratch_views.add_scratch, name="add_scratch"), # Team related re_path(r"^teams/", team_views.public_view_teams, name="public_teams"), re_path(r"^team/(\d+)/$", team_views.view_team, name="view_team"), - re_path(r"^team/(\d+)/scratches/add/(\d+)/", - team_views.add_scratches, - name="add_scratches"), - re_path(r"^team/(\d+)/scratches/view/", - team_views.view_scratches, + path(r"scratches///", + scratch_views.view_scratches_for_object, name="view_scratches_team"), path("view_teams/", team_views.view_teams, name="view_teams"), path("enter_team/", team_views.enter_team, name="enter_team"),