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
    + + + + {% for group in ranking_groups %} + + + + + + + {% endfor %} +
    NameTeamsDebatersDelete
    +
    + + {{ group.name }} + +
    +
    {{ group.teams.count }}{{ group.debaters.count }} +
    + {% csrf_token %} + + +
    +
    +
    + +
    +
    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 }}
    + + + + + + + + + + {% for debater, speaks, ranks, team, tiebreaker in debaters %} + + + + + + + + + {% endfor %} +
    #NameSpeaksRanksTeamTiebreaker
    {{ forloop.counter }}{{ debater.name }}{{ speaks|floatformat:2 }}{{ ranks|floatformat:2 }}{{ team.name }}{{ tiebreaker }}
    +
    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 @@ -
    +
    {{ table_title }}
    diff --git a/mittab/templates/tab/debater_detail.html b/mittab/templates/tab/debater_detail.html index 348bd94ee..0e6061b13 100644 --- a/mittab/templates/tab/debater_detail.html +++ b/mittab/templates/tab/debater_detail.html @@ -50,6 +50,16 @@
    Team Information
    {% endif %} + {% if debater_obj.ranking_groups.count %} +
    +
    Ranking Groups
    + +
    + {% endif %}
    {% endif %} {% endblock %} diff --git a/mittab/templates/tab/rank_debaters_component.html b/mittab/templates/tab/rank_debaters_component.html index a62b9cb59..c5a265636 100644 --- a/mittab/templates/tab/rank_debaters_component.html +++ b/mittab/templates/tab/rank_debaters_component.html @@ -1,51 +1,19 @@ -
    - - Force Refresh + -
    -
    Varsity Ranking
    - - - - - - - - - - {% for debater, speaks, ranks, team, tiebreaker in debaters %} - - - - - - - - +{% if debater_ranking_groups %} +
    + Ranking Groups: + {% for ranking_group in debater_ranking_groups %} + {{ ranking_group.title }} {% endfor %} -
    #NameSpeaksRanksTeamTiebreaker
    {{ forloop.counter }} {{ debater.name }} {{ speaks|floatformat:2 }} {{ ranks|floatformat:2 }} {{ team.name }}{{ tiebreaker }}
    -
    -
    -
    Novice Ranking
    - - - - - - - - - - {% for debater, speaks, ranks, team, tiebreaker in nov_debaters %} - - - - - - - - - {% endfor %} -
    #NameSpeaksRanksTeamTiebreaker
    {{ forloop.counter }}{{ debater.name }}{{ speaks|floatformat:2 }}{{ ranks|floatformat:2 }} {{ team.name }}{{ tiebreaker }}
    +{% endif %} +{% include "tab/_rank_debaters_table.html" with debaters=debaters table_title="Varsity Ranking" %} +{% include "tab/_rank_debaters_table.html" with debaters=nov_debaters table_title="Novice Ranking" %} +{% for ranking_group in debater_ranking_groups %} + {% include "tab/_rank_debaters_table.html" with debaters=ranking_group.debaters table_title=ranking_group.title section_id=ranking_group.anchor %} +{% endfor %} diff --git a/mittab/templates/tab/rank_teams_component.html b/mittab/templates/tab/rank_teams_component.html index c193249a1..99f9d6f2d 100644 --- a/mittab/templates/tab/rank_teams_component.html +++ b/mittab/templates/tab/rank_teams_component.html @@ -1,8 +1,20 @@ -
    - + +{% if team_ranking_groups %} +
    + Ranking Groups: + {% for ranking_group in team_ranking_groups %} + {{ ranking_group.title }} + {% endfor %} +
    +{% endif %} {% include "tab/_rank_teams_table.html" with teams=varsity table_title="Varsity Rankings" %} {% include "tab/_rank_teams_table.html" with teams=novice table_title="Novice Rankings" %} +{% for ranking_group in team_ranking_groups %} + {% include "tab/_rank_teams_table.html" with teams=ranking_group.teams table_title=ranking_group.title section_id=ranking_group.anchor %} +{% endfor %}
    diff --git a/mittab/templates/tab/team_detail.html b/mittab/templates/tab/team_detail.html index d76495d39..702e84e32 100644 --- a/mittab/templates/tab/team_detail.html +++ b/mittab/templates/tab/team_detail.html @@ -26,6 +26,16 @@
    Team Details
    {% endfor %} + {% if team_obj.ranking_groups.count %} + + Ranking Groups + + {% for ranking_group in team_obj.ranking_groups.all %} + {{ ranking_group.name }}{% if not forloop.last %}, {% endif %} + {% endfor %} + + + {% endif %} School {{ team_obj.school.name }} diff --git a/mittab/urls.py b/mittab/urls.py index f0eec1c7e..f43e16b98 100644 --- a/mittab/urls.py +++ b/mittab/urls.py @@ -61,6 +61,11 @@ path("room-tag//", views.room_tag, name="room_tag"), path("room-tag/", views.room_tag, name="room_tag"), path("manage-room-tags", views.manage_room_tags, name="manage_room_tags"), + path("ranking-group//", views.ranking_group, name="ranking_group"), + path("ranking-group/", views.ranking_group, name="ranking_group"), + path("manage-ranking-groups", + views.manage_ranking_groups, + name="manage_ranking_groups"), # Scratch related