Skip to content

Commit 94c8b4e

Browse files
authored
Ranking groups (#485)
* create ranking groups feature
1 parent 45be0d5 commit 94c8b4e

19 files changed

Lines changed: 461 additions & 69 deletions

mittab/apps/tab/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def motion_text_truncated(self, obj):
5757
admin.site.register(models.TabSettings)
5858
admin.site.register(models.Room)
5959
admin.site.register(models.RoomTag)
60+
admin.site.register(models.RankingGroup)
6061
admin.site.register(models.Bye)
6162
admin.site.register(models.NoShow)
6263
admin.site.register(models.BreakingTeam)

mittab/apps/tab/forms.py

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,10 @@ class Media:
144144

145145

146146
class TeamForm(forms.ModelForm):
147-
debaters = forms.ModelMultipleChoiceField(queryset=Debater.objects.all(),
148-
required=False)
147+
debaters = forms.ModelMultipleChoiceField(
148+
queryset=Debater.objects.all(),
149+
required=False
150+
)
149151

150152
def clean_debaters(self):
151153
data = self.cleaned_data["debaters"]
@@ -233,9 +235,6 @@ class Meta:
233235

234236

235237
class DebaterForm(forms.ModelForm):
236-
def __init__(self, *args, **kwargs):
237-
super(DebaterForm, self).__init__(*args, **kwargs)
238-
239238
class Meta:
240239
model = Debater
241240
exclude = ["tiebreaker"]
@@ -763,6 +762,49 @@ def __init__(self, *args, **kwargs):
763762
self.fields.pop("judges")
764763
self.fields.pop("rooms")
765764

765+
766+
class RankingGroupForm(forms.ModelForm):
767+
teams = forms.ModelMultipleChoiceField(
768+
queryset=Team.objects.all(),
769+
required=False,
770+
)
771+
debaters = forms.ModelMultipleChoiceField(
772+
queryset=Debater.objects.all(),
773+
required=False,
774+
)
775+
776+
def __init__(self, *args, **kwargs):
777+
super().__init__(*args, **kwargs)
778+
if self.instance.pk:
779+
self.fields["teams"].initial = self.instance.teams.all()
780+
self.fields["debaters"].initial = self.instance.debaters.all()
781+
782+
def save(self, commit=True):
783+
ranking_group = super().save(commit=False)
784+
785+
def save_m2m():
786+
ranking_group.teams.set(self.cleaned_data.get("teams", []))
787+
ranking_group.debaters.set(self.cleaned_data.get("debaters", []))
788+
789+
if commit:
790+
ranking_group.save()
791+
save_m2m()
792+
else:
793+
self._save_m2m = save_m2m
794+
795+
return ranking_group
796+
797+
class Meta:
798+
model = RankingGroup
799+
fields = ("name", "teams", "debaters")
800+
801+
802+
class MiniRankingGroupForm(RankingGroupForm):
803+
def __init__(self, *args, **kwargs):
804+
super().__init__(*args, **kwargs)
805+
self.fields.pop("teams")
806+
self.fields.pop("debaters")
807+
766808
class BackupForm(forms.Form):
767809
backup_name = forms.CharField(
768810
max_length=255,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated by Django 3.2.25 on 2025-11-10 02:30
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('tab', '0031_remove_noshow_lenient_late'),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name='RankingGroup',
15+
fields=[
16+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17+
('name', models.CharField(max_length=255, unique=True)),
18+
('debaters', models.ManyToManyField(blank=True, related_name='ranking_groups', to='tab.Debater')),
19+
('teams', models.ManyToManyField(blank=True, related_name='ranking_groups', to='tab.Team')),
20+
],
21+
),
22+
]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Generated by Django 3.2.25 on 2026-02-20 13:22
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('tab', '0032_rankinggroup'),
10+
('tab', '0034_add_motion_model'),
11+
]
12+
13+
operations = [
14+
]

mittab/apps/tab/models.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ def with_preloaded_relations_for_tab_card(cls):
236236
"debaters__roundstats_set__round",
237237
"debaters__team_set",
238238
"debaters__team_set__no_shows",
239+
"ranking_groups",
239240
)
240241

241242
@classmethod
@@ -266,6 +267,7 @@ def with_preloaded_relations_for_tabbing(cls):
266267
"debaters__roundstats_set__round",
267268
"debaters__team_set",
268269
"debaters__team_set__no_shows",
270+
"ranking_groups",
269271
)
270272

271273
def set_unique_team_code(self):
@@ -705,6 +707,23 @@ def __str__(self):
705707
return self.tag
706708

707709

710+
class RankingGroup(models.Model):
711+
name = models.CharField(max_length=255, unique=True)
712+
teams = models.ManyToManyField(
713+
"Team",
714+
blank=True,
715+
related_name="ranking_groups",
716+
)
717+
debaters = models.ManyToManyField(
718+
"Debater",
719+
blank=True,
720+
related_name="ranking_groups",
721+
)
722+
723+
def __str__(self):
724+
return self.name
725+
726+
708727
class Motion(models.Model):
709728
"""
710729
Represents a debate motion (topic) for a specific round.

mittab/apps/tab/views/debater_views.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.shortcuts import render
2+
from django.utils.text import slugify
23

34
from mittab.apps.tab.forms import DebaterForm
45
from mittab.apps.tab.helpers import redirect_and_flash_error, \
@@ -12,8 +13,16 @@
1213

1314
def view_debaters(request):
1415
# Get a list of (id,debater_name) tuples
15-
c_debaters = [(debater.pk, debater.display, 0, "")
16-
for debater in Debater.objects.all()]
16+
c_debaters = [
17+
(
18+
debater.pk,
19+
debater.display,
20+
0,
21+
"",
22+
list(debater.ranking_groups.order_by("name").values_list("name", flat=True)),
23+
)
24+
for debater in Debater.objects.prefetch_related("ranking_groups").all()
25+
]
1726
return render(
1827
request, "common/list_data.html", {
1928
"item_type": "debater",
@@ -25,7 +34,7 @@ def view_debaters(request):
2534
def view_debater(request, debater_id):
2635
debater_id = int(debater_id)
2736
try:
28-
debater = Debater.objects.get(pk=debater_id)
37+
debater = Debater.objects.prefetch_related("ranking_groups").get(pk=debater_id)
2938
except Debater.DoesNotExist:
3039
return redirect_and_flash_error(request, "No such debater")
3140
if request.method == "POST":
@@ -132,9 +141,25 @@ def rank_debaters(request):
132141
request
133142
)
134143

144+
ranking_group_tables = []
145+
ranking_groups = RankingGroup.objects.prefetch_related("debaters").order_by("name")
146+
for ranking_group in ranking_groups:
147+
debater_ids = {debater.id for debater in ranking_group.debaters.all()}
148+
grouped_debaters = [
149+
debater_entry for debater_entry in debaters
150+
if debater_entry[0].id in debater_ids
151+
]
152+
if grouped_debaters:
153+
ranking_group_tables.append({
154+
"title": f"{ranking_group.name} Rankings",
155+
"anchor": f"debater-ranking-group-{slugify(ranking_group.name)}",
156+
"debaters": grouped_debaters,
157+
})
158+
135159
return render(
136160
request, "tab/rank_debaters_component.html", {
137161
"debaters": debaters,
138162
"nov_debaters": nov_debaters,
163+
"debater_ranking_groups": ranking_group_tables,
139164
"title": "Speaker Rankings"
140165
})

mittab/apps/tab/views/team_views.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.http import HttpResponseRedirect
22
from django.contrib.auth.decorators import permission_required
33
from django.shortcuts import render
4+
from django.utils.text import slugify
45

56
from mittab.apps.tab.forms import TeamForm, TeamEntryForm, ScratchForm
67
from mittab.libs.cacheing import cache_logic
@@ -25,9 +26,16 @@ def flags(team):
2526
result |= TabFlags.TEAM_NOT_CHECKED_IN
2627
return result
2728

28-
c_teams = [(team.id, team.display_backend, flags(team),
29-
TabFlags.flags_to_symbols(flags(team)))
30-
for team in Team.objects.all()]
29+
c_teams = [
30+
(
31+
team.id,
32+
team.display_backend,
33+
flags(team),
34+
TabFlags.flags_to_symbols(flags(team)),
35+
list(team.ranking_groups.order_by("name").values_list("name", flat=True)),
36+
)
37+
for team in Team.objects.prefetch_related("ranking_groups").all()
38+
]
3139
all_flags = [[TabFlags.TEAM_CHECKED_IN, TabFlags.TEAM_NOT_CHECKED_IN]]
3240
filters, symbol_text = TabFlags.get_filters_and_symbols(all_flags)
3341
return render(
@@ -384,9 +392,21 @@ def rank_teams(request):
384392
public=False
385393
)
386394

395+
ranking_group_tables = []
396+
ranking_groups = RankingGroup.objects.prefetch_related("teams").order_by("name")
397+
for ranking_group in ranking_groups:
398+
team_ids = {team.id for team in ranking_group.teams.all()}
399+
grouped_teams = [team for team in teams if team[0].id in team_ids]
400+
if grouped_teams:
401+
ranking_group_tables.append({
402+
"title": f"{ranking_group.name} Rankings",
403+
"anchor": f"team-ranking-group-{slugify(ranking_group.name)}",
404+
"teams": grouped_teams,
405+
})
406+
387407
return render(request, "tab/rank_teams_component.html", {
388408
"varsity": teams,
389409
"novice": nov_teams,
410+
"team_ranking_groups": ranking_group_tables,
390411
"title": "Team Rankings"
391412
})
392-

mittab/apps/tab/views/views.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,17 @@
1212

1313
from mittab.apps.tab.archive import ArchiveExporter
1414
from mittab.apps.tab.views.debater_views import get_speaker_rankings
15-
from mittab.apps.tab.forms import MiniRoomTagForm, RoomTagForm, SchoolForm, RoomForm, \
16-
UploadDataForm, ScratchForm, SettingsForm
15+
from mittab.apps.tab.forms import (
16+
MiniRankingGroupForm,
17+
MiniRoomTagForm,
18+
RankingGroupForm,
19+
RoomTagForm,
20+
SchoolForm,
21+
RoomForm,
22+
UploadDataForm,
23+
ScratchForm,
24+
SettingsForm,
25+
)
1726
from mittab.apps.tab.helpers import redirect_and_flash_error, \
1827
redirect_and_flash_success
1928
from mittab.apps.tab.models import *
@@ -596,6 +605,56 @@ def manage_room_tags(request):
596605
{"room_tags": room_tags,
597606
"form": form})
598607

608+
609+
def ranking_group(request, group_id=None):
610+
group = None
611+
if group_id is not None:
612+
group = RankingGroup.objects.filter(pk=group_id).first()
613+
614+
if request.method == "POST":
615+
if request.POST.get("_method") == "DELETE":
616+
if group is not None:
617+
group.delete()
618+
return redirect_and_flash_success(
619+
request, "Ranking group deleted successfully"
620+
)
621+
return redirect_and_flash_error(request, "Ranking group does not exist")
622+
623+
form = RankingGroupForm(request.POST, instance=group)
624+
if not form.is_valid():
625+
return redirect_and_flash_error(request, "Error saving ranking group.")
626+
627+
ranking_group_instance = form.save()
628+
path = reverse("manage_ranking_groups")
629+
message = (
630+
f"Ranking group {ranking_group_instance.name} "
631+
f"{'updated' if group else 'created'} successfully"
632+
)
633+
return redirect_and_flash_success(request, message, path=path)
634+
635+
form = RankingGroupForm(instance=group)
636+
return render(
637+
request,
638+
"common/data_entry.html",
639+
{
640+
"form": form,
641+
"links": [],
642+
"title": f"Viewing Ranking Group: {group.name}" if group else "Create Ranking Group",
643+
},
644+
)
645+
646+
647+
def manage_ranking_groups(request):
648+
if request.method == "POST":
649+
return ranking_group(request)
650+
form = MiniRankingGroupForm(request.POST or None)
651+
ranking_groups = RankingGroup.objects.all().order_by("name")
652+
return render(
653+
request,
654+
"pairing/manage_ranking_groups.html",
655+
{"ranking_groups": ranking_groups, "form": form},
656+
)
657+
599658
def batch_checkin(request):
600659
round_numbers = list([i + 1 for i in range(TabSettings.get("tot_rounds"))])
601660
all_round_numbers = [0] + round_numbers

0 commit comments

Comments
 (0)