diff --git a/mittab/apps/tab/admin.py b/mittab/apps/tab/admin.py
index c73313016..96ad60041 100644
--- a/mittab/apps/tab/admin.py
+++ b/mittab/apps/tab/admin.py
@@ -46,6 +46,7 @@ class TeamAdmin(admin.ModelAdmin):
admin.site.register(models.TabSettings)
admin.site.register(models.Room)
admin.site.register(models.RoomTag)
+admin.site.register(models.RankingGroup)
admin.site.register(models.Bye)
admin.site.register(models.NoShow)
admin.site.register(models.BreakingTeam)
diff --git a/mittab/apps/tab/forms.py b/mittab/apps/tab/forms.py
index 06d54d38a..c5dacce9b 100644
--- a/mittab/apps/tab/forms.py
+++ b/mittab/apps/tab/forms.py
@@ -144,8 +144,10 @@ class Media:
class TeamForm(forms.ModelForm):
- debaters = forms.ModelMultipleChoiceField(queryset=Debater.objects.all(),
- required=False)
+ debaters = forms.ModelMultipleChoiceField(
+ queryset=Debater.objects.all(),
+ required=False
+ )
def clean_debaters(self):
data = self.cleaned_data["debaters"]
@@ -233,9 +235,6 @@ class Meta:
class DebaterForm(forms.ModelForm):
- def __init__(self, *args, **kwargs):
- super(DebaterForm, self).__init__(*args, **kwargs)
-
class Meta:
model = Debater
exclude = ["tiebreaker"]
@@ -763,6 +762,40 @@ def __init__(self, *args, **kwargs):
self.fields.pop("judges")
self.fields.pop("rooms")
+
+class RankingGroupForm(forms.ModelForm):
+ teams = forms.ModelMultipleChoiceField(
+ queryset=Team.objects.all(),
+ required=False,
+ )
+ debaters = forms.ModelMultipleChoiceField(
+ queryset=Debater.objects.all(),
+ required=False,
+ )
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ if self.instance.pk:
+ self.fields["teams"].initial = self.instance.teams.all()
+ self.fields["debaters"].initial = self.instance.debaters.all()
+
+ def save(self, commit=True):
+ ranking_group = super().save(commit=commit)
+ ranking_group.teams.set(self.cleaned_data.get("teams", []))
+ ranking_group.debaters.set(self.cleaned_data.get("debaters", []))
+ return ranking_group
+
+ class Meta:
+ model = RankingGroup
+ fields = ("name", "teams", "debaters")
+
+
+class MiniRankingGroupForm(RankingGroupForm):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields.pop("teams")
+ self.fields.pop("debaters")
+
class BackupForm(forms.Form):
backup_name = forms.CharField(
max_length=255,
diff --git a/mittab/apps/tab/migrations/0032_rankinggroup.py b/mittab/apps/tab/migrations/0032_rankinggroup.py
new file mode 100644
index 000000000..5b2672dd7
--- /dev/null
+++ b/mittab/apps/tab/migrations/0032_rankinggroup.py
@@ -0,0 +1,22 @@
+# Generated by Django 3.2.25 on 2025-11-10 02:30
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('tab', '0031_remove_noshow_lenient_late'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='RankingGroup',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=255, unique=True)),
+ ('debaters', models.ManyToManyField(blank=True, related_name='ranking_groups', to='tab.Debater')),
+ ('teams', models.ManyToManyField(blank=True, related_name='ranking_groups', to='tab.Team')),
+ ],
+ ),
+ ]
diff --git a/mittab/apps/tab/models.py b/mittab/apps/tab/models.py
index da71674f7..9e971b981 100644
--- a/mittab/apps/tab/models.py
+++ b/mittab/apps/tab/models.py
@@ -202,6 +202,7 @@ def with_preloaded_relations_for_tab_card(cls):
"debaters__roundstats_set__round",
"debaters__team_set",
"debaters__team_set__no_shows",
+ "ranking_groups",
)
@classmethod
@@ -232,6 +233,7 @@ def with_preloaded_relations_for_tabbing(cls):
"debaters__roundstats_set__round",
"debaters__team_set",
"debaters__team_set__no_shows",
+ "ranking_groups",
)
def set_unique_team_code(self):
@@ -648,3 +650,20 @@ class RoomTag(models.Model):
def __str__(self):
return self.tag
+
+
+class RankingGroup(models.Model):
+ name = models.CharField(max_length=255, unique=True)
+ teams = models.ManyToManyField(
+ "Team",
+ blank=True,
+ related_name="ranking_groups",
+ )
+ debaters = models.ManyToManyField(
+ "Debater",
+ blank=True,
+ related_name="ranking_groups",
+ )
+
+ def __str__(self):
+ return self.name
diff --git a/mittab/apps/tab/views/debater_views.py b/mittab/apps/tab/views/debater_views.py
index 9e06ca6f1..dc5572d7a 100644
--- a/mittab/apps/tab/views/debater_views.py
+++ b/mittab/apps/tab/views/debater_views.py
@@ -1,4 +1,5 @@
from django.shortcuts import render
+from django.utils.text import slugify
from mittab.apps.tab.forms import DebaterForm
from mittab.apps.tab.helpers import redirect_and_flash_error, \
@@ -12,8 +13,16 @@
def view_debaters(request):
# Get a list of (id,debater_name) tuples
- c_debaters = [(debater.pk, debater.display, 0, "")
- for debater in Debater.objects.all()]
+ c_debaters = [
+ (
+ debater.pk,
+ debater.display,
+ 0,
+ "",
+ list(debater.ranking_groups.order_by("name").values_list("name", flat=True)),
+ )
+ for debater in Debater.objects.prefetch_related("ranking_groups").all()
+ ]
return render(
request, "common/list_data.html", {
"item_type": "debater",
@@ -25,7 +34,7 @@ def view_debaters(request):
def view_debater(request, debater_id):
debater_id = int(debater_id)
try:
- debater = Debater.objects.get(pk=debater_id)
+ debater = Debater.objects.prefetch_related("ranking_groups").get(pk=debater_id)
except Debater.DoesNotExist:
return redirect_and_flash_error(request, "No such debater")
if request.method == "POST":
@@ -132,9 +141,25 @@ def rank_debaters(request):
request
)
+ ranking_group_tables = []
+ ranking_groups = RankingGroup.objects.prefetch_related("debaters").order_by("name")
+ for ranking_group in ranking_groups:
+ debater_ids = {debater.id for debater in ranking_group.debaters.all()}
+ grouped_debaters = [
+ debater_entry for debater_entry in debaters
+ if debater_entry[0].id in debater_ids
+ ]
+ if grouped_debaters:
+ ranking_group_tables.append({
+ "title": f"{ranking_group.name} Rankings",
+ "anchor": f"debater-ranking-group-{slugify(ranking_group.name)}",
+ "debaters": grouped_debaters,
+ })
+
return render(
request, "tab/rank_debaters_component.html", {
"debaters": debaters,
"nov_debaters": nov_debaters,
+ "debater_ranking_groups": ranking_group_tables,
"title": "Speaker Rankings"
})
diff --git a/mittab/apps/tab/views/team_views.py b/mittab/apps/tab/views/team_views.py
index 2981f329f..869c0cf3a 100644
--- a/mittab/apps/tab/views/team_views.py
+++ b/mittab/apps/tab/views/team_views.py
@@ -1,6 +1,7 @@
from django.http import HttpResponseRedirect
from django.contrib.auth.decorators import permission_required
from django.shortcuts import render
+from django.utils.text import slugify
from mittab.apps.tab.forms import TeamForm, TeamEntryForm, ScratchForm
from mittab.libs.cacheing import cache_logic
@@ -25,9 +26,16 @@ def flags(team):
result |= TabFlags.TEAM_NOT_CHECKED_IN
return result
- c_teams = [(team.id, team.display_backend, flags(team),
- TabFlags.flags_to_symbols(flags(team)))
- for team in Team.objects.all()]
+ c_teams = [
+ (
+ team.id,
+ team.display_backend,
+ flags(team),
+ TabFlags.flags_to_symbols(flags(team)),
+ list(team.ranking_groups.order_by("name").values_list("name", flat=True)),
+ )
+ for team in Team.objects.prefetch_related("ranking_groups").all()
+ ]
all_flags = [[TabFlags.TEAM_CHECKED_IN, TabFlags.TEAM_NOT_CHECKED_IN]]
filters, symbol_text = TabFlags.get_filters_and_symbols(all_flags)
return render(
@@ -384,9 +392,21 @@ def rank_teams(request):
public=False
)
+ ranking_group_tables = []
+ ranking_groups = RankingGroup.objects.prefetch_related("teams").order_by("name")
+ for ranking_group in ranking_groups:
+ team_ids = {team.id for team in ranking_group.teams.all()}
+ grouped_teams = [team for team in teams if team[0].id in team_ids]
+ if grouped_teams:
+ ranking_group_tables.append({
+ "title": f"{ranking_group.name} Rankings",
+ "anchor": f"team-ranking-group-{slugify(ranking_group.name)}",
+ "teams": grouped_teams,
+ })
+
return render(request, "tab/rank_teams_component.html", {
"varsity": teams,
"novice": nov_teams,
+ "team_ranking_groups": ranking_group_tables,
"title": "Team Rankings"
})
-
diff --git a/mittab/apps/tab/views/views.py b/mittab/apps/tab/views/views.py
index fa3b3b906..eac0b3c0c 100644
--- a/mittab/apps/tab/views/views.py
+++ b/mittab/apps/tab/views/views.py
@@ -10,8 +10,17 @@
from mittab.apps.tab.archive import ArchiveExporter
from mittab.apps.tab.views.debater_views import get_speaker_rankings
-from mittab.apps.tab.forms import MiniRoomTagForm, RoomTagForm, SchoolForm, RoomForm, \
- UploadDataForm, ScratchForm, SettingsForm
+from mittab.apps.tab.forms import (
+ MiniRankingGroupForm,
+ MiniRoomTagForm,
+ RankingGroupForm,
+ RoomTagForm,
+ SchoolForm,
+ RoomForm,
+ UploadDataForm,
+ ScratchForm,
+ SettingsForm,
+)
from mittab.apps.tab.helpers import redirect_and_flash_error, \
redirect_and_flash_success
from mittab.apps.tab.models import *
@@ -571,6 +580,56 @@ def manage_room_tags(request):
{"room_tags": room_tags,
"form": form})
+
+def ranking_group(request, group_id=None):
+ group = None
+ if group_id is not None:
+ group = RankingGroup.objects.filter(pk=group_id).first()
+
+ if request.method == "POST":
+ if request.POST.get("_method") == "DELETE":
+ if group is not None:
+ group.delete()
+ return redirect_and_flash_success(
+ request, "Ranking group deleted successfully"
+ )
+ return redirect_and_flash_error(request, "Ranking group does not exist")
+
+ form = RankingGroupForm(request.POST, instance=group)
+ if not form.is_valid():
+ return redirect_and_flash_error(request, "Error saving ranking group.")
+
+ ranking_group_instance = form.save()
+ path = reverse("manage_ranking_groups")
+ message = (
+ f"Ranking group {ranking_group_instance.name} "
+ f"{'updated' if group else 'created'} successfully"
+ )
+ return redirect_and_flash_success(request, message, path=path)
+
+ form = RankingGroupForm(instance=group)
+ return render(
+ request,
+ "common/data_entry.html",
+ {
+ "form": form,
+ "links": [],
+ "title": f"Viewing Ranking Group: {group.name}" if group else "Create Ranking Group",
+ },
+ )
+
+
+def manage_ranking_groups(request):
+ if request.method == "POST":
+ return ranking_group(request)
+ form = MiniRankingGroupForm(request.POST or None)
+ ranking_groups = RankingGroup.objects.all().order_by("name")
+ return render(
+ request,
+ "pairing/manage_ranking_groups.html",
+ {"ranking_groups": ranking_groups, "form": form},
+ )
+
def batch_checkin(request):
round_numbers = list([i + 1 for i in range(TabSettings.get("tot_rounds"))])
all_round_numbers = [0] + round_numbers
diff --git a/mittab/templates/common/_index_list.html b/mittab/templates/common/_index_list.html
index 765087d62..85f066eea 100644
--- a/mittab/templates/common/_index_list.html
+++ b/mittab/templates/common/_index_list.html
@@ -23,11 +23,17 @@
+ {% if model_name == "Teams" %}
+
+ {% endif %}
{% if model_name == "Rooms" %}
- {% elif view_path_prefix == "judge" %}
+ {% endif %}
+ {% if view_path_prefix == "judge" %}
Download Judge Codes
diff --git a/mittab/templates/common/list_data.html b/mittab/templates/common/list_data.html
index f8eb32e4a..9137f838c 100644
--- a/mittab/templates/common/list_data.html
+++ b/mittab/templates/common/list_data.html
@@ -17,10 +17,21 @@
{% endfor %}
{% else %}
- {% for id, name, flags, symbols in item_list %}
-
- {{name}} {{symbols}}
-
+ {% for item in item_list %}
+ {% with id=item.0 name=item.1 flags=item.2 symbols=item.3 %}
+
+
+ {{name}} {{symbols}}
+ {% if item|length > 4 and item.4 %}
+
+ {% for badge in item.4 %}
+ {{ badge }}
+ {% endfor %}
+
+ {% endif %}
+
+
+ {% endwith %}
{% endfor %}
{% endif %}
diff --git a/mittab/templates/pairing/manage_ranking_groups.html b/mittab/templates/pairing/manage_ranking_groups.html
new file mode 100644
index 000000000..5dccf58a0
--- /dev/null
+++ b/mittab/templates/pairing/manage_ranking_groups.html
@@ -0,0 +1,45 @@
+{% extends "base/__normal.html" %}
+
+{% block title %} Manage Ranking Groups {% endblock title %}
+{% block banner %} Manage Ranking Groups {% endblock banner %}
+
+
+{% block content %}
+
+
+
Current Ranking Groups
+
+ | Name | Teams | Debaters | Delete |
+
+ {% for group in ranking_groups %}
+
+ |
+
+ |
+ {{ group.teams.count }} |
+ {{ group.debaters.count }} |
+
+
+ |
+
+ {% endfor %}
+
+
+
+
+
Add New Ranking Group
+ {% include "common/_form.html" %}
+
+
+
+{% endblock content %}
diff --git a/mittab/templates/tab/_rank_debaters_table.html b/mittab/templates/tab/_rank_debaters_table.html
new file mode 100644
index 000000000..8ff9f3dcb
--- /dev/null
+++ b/mittab/templates/tab/_rank_debaters_table.html
@@ -0,0 +1,23 @@
+
+
{{ table_title }}
+
+
+ | # |
+ Name |
+ Speaks |
+ Ranks |
+ Team |
+ Tiebreaker |
+
+ {% for debater, speaks, ranks, team, tiebreaker in debaters %}
+
+ | {{ forloop.counter }} |
+ {{ debater.name }} |
+ {{ speaks|floatformat:2 }} |
+ {{ ranks|floatformat:2 }} |
+ {{ team.name }} |
+ {{ tiebreaker }} |
+
+ {% endfor %}
+
+
diff --git a/mittab/templates/tab/_rank_teams_table.html b/mittab/templates/tab/_rank_teams_table.html
index a5bed462b..cb089ecfa 100644
--- a/mittab/templates/tab/_rank_teams_table.html
+++ b/mittab/templates/tab/_rank_teams_table.html
@@ -1,4 +1,4 @@
-