diff --git a/mittab/apps/tab/judge_views.py b/mittab/apps/tab/judge_views.py index d4fcb335d..36d15e62a 100644 --- a/mittab/apps/tab/judge_views.py +++ b/mittab/apps/tab/judge_views.py @@ -6,7 +6,10 @@ 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 * -from mittab.libs.tab_logic import TabFlags +from mittab.libs.tab_logic import ( + TabFlags, + add_scratches_for_single_judge_for_school_affiliation, +) def public_view_judges(request): @@ -19,14 +22,14 @@ def public_view_judges(request): rounds = [num for num in range(1, num_rounds + 1)] return render( - request, "public/judges.html", { - "judges": Judge.objects.order_by("name").all(), - "rounds": rounds - }) + request, + "public/judges.html", + {"judges": Judge.objects.order_by("name").all(), "rounds": rounds}, + ) def view_judges(request): - #Get a list of (id,school_name) tuples + # Get a list of (id,school_name) tuples current_round = TabSettings.objects.get(key="cur_round").value - 1 checkins = CheckIn.objects.filter(round_number=current_round) checkins_next = CheckIn.objects.filter(round_number=(current_round + 1)) @@ -52,25 +55,35 @@ def flags(judge): result |= TabFlags.HIGH_RANKED_JUDGE return result - c_judge = [(judge.pk, judge.name, flags(judge), "(%s)" % judge.ballot_code) - for judge in Judge.objects.all()] - - all_flags = [[ - TabFlags.JUDGE_CHECKED_IN_CUR, TabFlags.JUDGE_NOT_CHECKED_IN_CUR, - TabFlags.JUDGE_CHECKED_IN_NEXT, TabFlags.JUDGE_NOT_CHECKED_IN_NEXT - ], - [ - TabFlags.LOW_RANKED_JUDGE, TabFlags.MID_RANKED_JUDGE, - TabFlags.HIGH_RANKED_JUDGE - ]] + c_judge = [ + (judge.pk, judge.name, flags(judge), "(%s)" % judge.ballot_code) + for judge in Judge.objects.all() + ] + + all_flags = [ + [ + TabFlags.JUDGE_CHECKED_IN_CUR, + TabFlags.JUDGE_NOT_CHECKED_IN_CUR, + TabFlags.JUDGE_CHECKED_IN_NEXT, + TabFlags.JUDGE_NOT_CHECKED_IN_NEXT, + ], + [ + TabFlags.LOW_RANKED_JUDGE, + TabFlags.MID_RANKED_JUDGE, + TabFlags.HIGH_RANKED_JUDGE, + ], + ] filters, _symbol_text = TabFlags.get_filters_and_symbols(all_flags) return render( - request, "common/list_data.html", { + request, + "common/list_data.html", + { "item_type": "judge", "title": "Viewing All Judges", "item_list": c_judge, "filters": filters, - }) + }, + ) def view_judge(request, judge_id): @@ -86,21 +99,22 @@ def view_judge(request, judge_id): form.save() except ValueError: return redirect_and_flash_error( - request, "Judge information cannot be validated") + request, "Judge information cannot be validated" + ) return redirect_and_flash_success( - request, "Judge {} updated successfully".format( - form.cleaned_data["name"])) + request, + "Judge {} updated successfully".format(form.cleaned_data["name"]), + ) else: form = JudgeForm(instance=judge) base_url = "/judge/" + str(judge_id) + "/" scratch_url = base_url + "scratches/view/" links = [(scratch_url, "Scratches for {}".format(judge.name))] return render( - request, "common/data_entry.html", { - "form": form, - "links": links, - "title": "Viewing Judge: {}".format(judge.name) - }) + request, + "common/data_entry.html", + {"form": form, "links": links, "title": "Viewing Judge: {}".format(judge.name)}, + ) def enter_judge(request): @@ -108,21 +122,27 @@ def enter_judge(request): form = JudgeForm(request.POST) if form.is_valid(): try: - form.save() + judge = form.save() + + teams = Team.objects.all().prefetch_related("school", "hybrid_school") + + scratches = add_scratches_for_single_judge_for_school_affiliation( + judge, teams + ) + Scratch.objects.bulk_create(scratches) + except ValueError: - return redirect_and_flash_error(request, - "Judge cannot be validated") + return redirect_and_flash_error(request, "Judge cannot be validated") return redirect_and_flash_success( request, - "Judge {} created successfully".format( - form.cleaned_data["name"]), - path="/") + "Judge {} created successfully".format(form.cleaned_data["name"]), + path="/", + ) else: form = JudgeForm(first_entry=True) - return render(request, "common/data_entry.html", { - "form": form, - "title": "Create Judge" - }) + return render( + request, "common/data_entry.html", {"form": form, "title": "Create Judge"} + ) def add_scratches(request, judge_id, number_scratches): @@ -146,22 +166,21 @@ def add_scratches(request, judge_id, number_scratches): if all_good: for form in forms: form.save() - return redirect_and_flash_success( - request, "Scratches created successfully") + 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) + 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", { + request, + "common/data_entry_multiple.html", + { "forms": list(zip(forms, [None] * len(forms))), "data_type": "Scratch", - "title": "Adding Scratch(es) for %s" % (judge.name) - }) + "title": "Adding Scratch(es) for %s" % (judge.name), + }, + ) def view_scratches(request, judge_id): @@ -183,13 +202,11 @@ def view_scratches(request, judge_id): if all_good: for form in forms: form.save() - return redirect_and_flash_success( - request, "Scratches created successfully") + return redirect_and_flash_success(request, "Scratches created successfully") else: forms = [ ScratchForm(prefix=str(i), instance=scratches[i - 1]) - for i in range(1, - len(scratches) + 1) + for i in range(1, len(scratches) + 1) ] delete_links = [ "/judge/" + str(judge_id) + "/scratches/delete/" + str(scratches[i].id) @@ -198,12 +215,15 @@ def view_scratches(request, judge_id): links = [("/judge/" + str(judge_id) + "/scratches/add/1/", "Add Scratch")] return render( - request, "common/data_entry_multiple.html", { + request, + "common/data_entry_multiple.html", + { "forms": list(zip(forms, delete_links)), "data_type": "Scratch", "links": links, - "title": "Viewing Scratch Information for %s" % (judge.name) - }) + "title": "Viewing Scratch Information for %s" % (judge.name), + }, + ) def batch_checkin(request): @@ -212,14 +232,15 @@ def batch_checkin(request): round_numbers = list([i + 1 for i in range(TabSettings.get("tot_rounds"))]) for judge in Judge.objects.all(): checkins = [] - for round_number in [0] + round_numbers: # 0 is for outrounds + for round_number in [0] + round_numbers: # 0 is for outrounds checkins.append(judge.is_checked_in_for_round(round_number)) judges_and_checkins.append((judge, checkins)) - return render(request, "tab/batch_checkin.html", { - "judges_and_checkins": judges_and_checkins, - "round_numbers": round_numbers - }) + return render( + request, + "tab/batch_checkin.html", + {"judges_and_checkins": judges_and_checkins, "round_numbers": round_numbers}, + ) @permission_required("tab.tab_settings.can_change", login_url="/403") @@ -237,8 +258,7 @@ def judge_check_in(request, judge_id, round_number): check_in.save() elif request.method == "DELETE": if judge.is_checked_in_for_round(round_number): - check_ins = CheckIn.objects.filter(judge=judge, - round_number=round_number) + check_ins = CheckIn.objects.filter(judge=judge, round_number=round_number) check_ins.delete() else: raise Http404("Must be POST or DELETE") diff --git a/mittab/apps/tab/migrations/0013_auto_20191021_2136.py b/mittab/apps/tab/migrations/0013_auto_20191021_2136.py new file mode 100644 index 000000000..e40effe1c --- /dev/null +++ b/mittab/apps/tab/migrations/0013_auto_20191021_2136.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.5 on 2019-10-21 21:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tab', '0012_merge_20191017_0109'), + ] + + operations = [ + migrations.AlterField( + model_name='scratch', + name='scratch_type', + field=models.IntegerField(choices=[(0, 'Discretionary Scratch'), (1, 'Tab Scratch'), (2, 'School Scratch')]), + ), + ] diff --git a/mittab/apps/tab/models.py b/mittab/apps/tab/models.py index 3c917fe09..c18ad239a 100644 --- a/mittab/apps/tab/models.py +++ b/mittab/apps/tab/models.py @@ -45,19 +45,20 @@ def set(cls, key, value): obj = cls.objects.create(key=key, value=value) def delete(self, using=None, keep_parents=False): - cache_logic.invalidate_cache("tab_settings_%s" % self.key, - cache_logic.PERSISTENT) + cache_logic.invalidate_cache( + "tab_settings_%s" % self.key, cache_logic.PERSISTENT + ) super(TabSettings, self).delete(using, keep_parents) - def save(self, - force_insert=False, - force_update=False, - using=None, - update_fields=None): - cache_logic.invalidate_cache("tab_settings_%s" % self.key, - cache_logic.PERSISTENT) + def save( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): + cache_logic.invalidate_cache( + "tab_settings_%s" % self.key, cache_logic.PERSISTENT + ) super(TabSettings, self).save(force_insert, force_update, using, update_fields) + class School(models.Model): name = models.CharField(max_length=50, unique=True) @@ -69,8 +70,9 @@ def delete(self, using=None, keep_parents=False): judge_check = Judge.objects.filter(schools=self) if team_check.exists() or judge_check.exists(): raise Exception( - "School in use: [teams => %s,judges => %s]" % - ([t.name for t in team_check], [j.name for j in judge_check])) + "School in use: [teams => %s,judges => %s]" + % ([t.name for t in team_check], [j.name for j in judge_check]) + ) else: super(School, self).delete(using, keep_parents) @@ -97,13 +99,13 @@ class Debater(models.Model): novice_status = models.IntegerField(choices=NOVICE_CHOICES) tiebreaker = models.IntegerField(unique=True, null=True, blank=True) - def save(self, - force_insert=False, - force_update=False, - using=None, - update_fields=None): - while not self.tiebreaker or \ - Debater.objects.filter(tiebreaker=self.tiebreaker).exists(): + def save( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): + while ( + not self.tiebreaker + or Debater.objects.filter(tiebreaker=self.tiebreaker).exists() + ): self.tiebreaker = random.choice(range(0, 2**16)) super(Debater, self).save(force_insert, force_update, using, update_fields) @@ -135,11 +137,13 @@ class Meta: class Team(models.Model): name = models.CharField(max_length=30, unique=True) school = models.ForeignKey("School", on_delete=models.CASCADE) - hybrid_school = models.ForeignKey("School", - blank=True, - null=True, - on_delete=models.SET_NULL, - related_name="hybrid_school") + hybrid_school = models.ForeignKey( + "School", + blank=True, + null=True, + on_delete=models.SET_NULL, + related_name="hybrid_school", + ) debaters = models.ManyToManyField(Debater) UNSEEDED = 0 FREE_SEED = 1 @@ -153,20 +157,20 @@ class Team(models.Model): ) seed = models.IntegerField(choices=SEED_CHOICES) checked_in = models.BooleanField(default=True) - team_code = models.CharField(max_length=255, - blank=True, - null=True, - unique=True) + team_code = models.CharField(max_length=255, blank=True, null=True, unique=True) VARSITY = 0 NOVICE = 1 - BREAK_PREFERENCE_CHOICES = ( - (VARSITY, "Varsity"), - (NOVICE, "Novice") - ) + BREAK_PREFERENCE_CHOICES = ((VARSITY, "Varsity"), (NOVICE, "Novice")) - break_preference = models.IntegerField(default=0, - choices=BREAK_PREFERENCE_CHOICES) + break_preference = models.IntegerField(default=0, choices=BREAK_PREFERENCE_CHOICES) + tiebreaker = models.IntegerField(unique=True, null=True, blank=True) + + VARSITY = 0 + NOVICE = 1 + BREAK_PREFERENCE_CHOICES = ((VARSITY, "Varsity"), (NOVICE, "Novice")) + + break_preference = models.IntegerField(default=0, choices=BREAK_PREFERENCE_CHOICES) tiebreaker = models.IntegerField(unique=True, null=True, blank=True) def set_unique_team_code(self): @@ -184,17 +188,17 @@ def gen_haiku_and_clean(): self.team_code = code - def save(self, - force_insert=False, - force_update=False, - using=None, - update_fields=None): + def save( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): # Generate a team code for teams that don't have one if not self.team_code: self.set_unique_team_code() - while not self.tiebreaker or \ - Team.objects.filter(tiebreaker=self.tiebreaker).exists(): + while ( + not self.tiebreaker + or Team.objects.filter(tiebreaker=self.tiebreaker).exists() + ): self.tiebreaker = random.choice(range(0, 2**16)) super(Team, self).save(force_insert, force_update, using, update_fields) @@ -244,30 +248,23 @@ class Meta: class BreakingTeam(models.Model): VARSITY = 0 NOVICE = 1 - TYPE_CHOICES = ( - (VARSITY, "Varsity"), - (NOVICE, "Novice") - ) + TYPE_CHOICES = ((VARSITY, "Varsity"), (NOVICE, "Novice")) - team = models.OneToOneField("Team", - on_delete=models.CASCADE, - related_name="breaking_team") + team = models.OneToOneField( + "Team", on_delete=models.CASCADE, related_name="breaking_team" + ) seed = models.IntegerField(default=-1) effective_seed = models.IntegerField(default=-1) - type_of_team = models.IntegerField(default=VARSITY, - choices=TYPE_CHOICES) + type_of_team = models.IntegerField(default=VARSITY, choices=TYPE_CHOICES) class Judge(models.Model): name = models.CharField(max_length=30, unique=True) rank = models.DecimalField(max_digits=4, decimal_places=2) schools = models.ManyToManyField(School) - ballot_code = models.CharField(max_length=255, - blank=True, - null=True, - unique=True) + ballot_code = models.CharField(max_length=255, blank=True, null=True, unique=True) def set_unique_ballot_code(self): haikunator = Haikunator() @@ -278,28 +275,27 @@ def set_unique_ballot_code(self): self.ballot_code = code - def save(self, - force_insert=False, - force_update=False, - using=None, - update_fields=None): + def save( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): # Generate a random ballot code for judges that don't have one if not self.ballot_code: self.set_unique_ballot_code() - super(Judge, self).save(force_insert, force_update, using, - update_fields) + super(Judge, self).save(force_insert, force_update, using, update_fields) + + self.update_scratches() def is_checked_in_for_round(self, round_number): - return CheckIn.objects.filter(judge=self, - round_number=round_number).exists() + return CheckIn.objects.filter(judge=self, round_number=round_number).exists() def __str__(self): return self.name def affiliations_display(self): - return ", ".join([school.name for school in self.schools.all() \ - if not school.name == ""]) + return ", ".join( + [school.name for school in self.schools.all() if not school.name == ""] + ) def delete(self, using=None, keep_parents=False): checkins = CheckIn.objects.filter(judge=self) @@ -316,9 +312,11 @@ class Scratch(models.Model): team = models.ForeignKey(Team, related_name="scratches", on_delete=models.CASCADE) TEAM_SCRATCH = 0 TAB_SCRATCH = 1 + SCHOOL_SCRATCH = 2 TYPE_CHOICES = ( (TEAM_SCRATCH, "Discretionary Scratch"), (TAB_SCRATCH, "Tab Scratch"), + (SCHOOL_SCRATCH, "School Scratch"), ) scratch_type = models.IntegerField(choices=TYPE_CHOICES) @@ -327,7 +325,7 @@ class Meta: verbose_name_plural = "scratches" def __str__(self): - s_type = ("Team", "Tab")[self.scratch_type] + s_type = ("Team", "Tab", "School")[self.scratch_type] return "{} <={}=> {}".format(self.team, s_type, self.judge) @@ -341,14 +339,12 @@ def __str__(self): def delete(self, using=None, keep_parents=False): rounds = Round.objects.filter(room=self) if rounds.exists(): - raise Exception("Room is in round: %s" % ([str(r) - for r in rounds])) + raise Exception("Room is in round: %s" % ([str(r) for r in rounds])) else: super(Room, self).delete(using, keep_parents) def is_checked_in_for_round(self, round_number): - return RoomCheckIn.objects.filter(room=self, - round_number=round_number).exists() + return RoomCheckIn.objects.filter(room=self, round_number=round_number).exists() class Meta: ordering = ["name"] @@ -357,23 +353,25 @@ class Meta: class Outround(models.Model): VARSITY = 0 NOVICE = 1 - TYPE_OF_ROUND_CHOICES = ( - (VARSITY, "Varsity"), - (NOVICE, "Novice") - ) + + TYPE_OF_ROUND_CHOICES = ((VARSITY, "Varsity"), (NOVICE, "Novice")) num_teams = models.IntegerField() - type_of_round = models.IntegerField(default=VARSITY, - choices=TYPE_OF_ROUND_CHOICES) - gov_team = models.ForeignKey(Team, related_name="gov_team_outround", - on_delete=models.CASCADE) - opp_team = models.ForeignKey(Team, related_name="opp_team_outround", - on_delete=models.CASCADE) - chair = models.ForeignKey(Judge, - null=True, - blank=True, - on_delete=models.CASCADE, - related_name="chair_outround") + type_of_round = models.IntegerField(default=VARSITY, choices=TYPE_OF_ROUND_CHOICES) + gov_team = models.ForeignKey( + Team, related_name="gov_team_outround", on_delete=models.CASCADE + ) + opp_team = models.ForeignKey( + Team, related_name="opp_team_outround", on_delete=models.CASCADE + ) + chair = models.ForeignKey( + Judge, + null=True, + blank=True, + on_delete=models.CASCADE, + related_name="chair_outround", + ) + judges = models.ManyToManyField(Judge, blank=True, related_name="judges_outrounds") UNKNOWN = 0 GOV = 1 @@ -387,29 +385,25 @@ class Outround(models.Model): (GOV_VIA_FORFEIT, "GOV via Forfeit"), (OPP_VIA_FORFEIT, "OPP via Forfeit"), ) - room = models.ForeignKey(Room, - on_delete=models.CASCADE, - related_name="rooms_outrounds") + + room = models.ForeignKey( + Room, on_delete=models.CASCADE, related_name="rooms_outrounds" + ) victor = models.IntegerField(choices=VICTOR_CHOICES, default=0) sidelock = models.BooleanField(default=False) - CHOICES = ( - (UNKNOWN, "No"), - (GOV, "Gov"), - (OPP, "Opp") - ) - choice = models.IntegerField(default=UNKNOWN, - choices=CHOICES) + CHOICES = ((UNKNOWN, "No"), (GOV, "Gov"), (OPP, "Opp")) + choice = models.IntegerField(default=UNKNOWN, choices=CHOICES) def clean(self): if self.pk and self.chair not in self.judges.all(): raise ValidationError("Chair must be a judge in the round") def __str__(self): - return "Outround {} between {} and {}".format(self.num_teams, - self.gov_team, - self.opp_team) + return "Outround {} between {} and {}".format( + self.num_teams, self.gov_team, self.opp_team + ) @property def winner(self): @@ -431,15 +425,15 @@ def loser(self): class Round(models.Model): round_number = models.IntegerField() - gov_team = models.ForeignKey(Team, related_name="gov_team", - on_delete=models.CASCADE) - opp_team = models.ForeignKey(Team, related_name="opp_team", - on_delete=models.CASCADE) - chair = models.ForeignKey(Judge, - null=True, - blank=True, - on_delete=models.CASCADE, - related_name="chair") + gov_team = models.ForeignKey( + Team, related_name="gov_team", on_delete=models.CASCADE + ) + opp_team = models.ForeignKey( + Team, related_name="opp_team", on_delete=models.CASCADE + ) + chair = models.ForeignKey( + Judge, null=True, blank=True, on_delete=models.CASCADE, related_name="chair" + ) judges = models.ManyToManyField(Judge, blank=True, related_name="judges") NONE = 0 GOV = 1 @@ -472,24 +466,22 @@ def clean(self): raise ValidationError("Chair must be a judge in the round") def __str__(self): - return "Round {} between {} and {}".format(self.round_number, - self.gov_team, - self.opp_team) - - def save(self, - force_insert=False, - force_update=False, - using=None, - update_fields=None): + return "Round {} between {} and {}".format( + self.round_number, self.gov_team, self.opp_team + ) + + def save( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): no_shows = NoShow.objects.filter( round_number=self.round_number, - no_show_team__in=[self.gov_team, self.opp_team]) + no_show_team__in=[self.gov_team, self.opp_team], + ) if no_shows: no_shows.delete() - super(Round, self).save(force_insert, force_update, using, - update_fields) + super(Round, self).save(force_insert, force_update, using, update_fields) def delete(self, using=None, keep_parents=False): rounds = RoundStats.objects.filter(round=self) @@ -503,20 +495,21 @@ class Bye(models.Model): round_number = models.IntegerField() def __str__(self): - return "Bye in round " + str(self.round_number) + " for " + str( - self.bye_team) + return "Bye in round " + str(self.round_number) + " for " + str(self.bye_team) class NoShow(models.Model): - no_show_team = models.ForeignKey(Team, - related_name="no_shows", - on_delete=models.CASCADE) + no_show_team = models.ForeignKey( + Team, related_name="no_shows", on_delete=models.CASCADE + ) + round_number = models.IntegerField() lenient_late = models.BooleanField(default=False) def __str__(self): - return str(self.no_show_team) + " was no-show for round " + str( - self.round_number) + return ( + str(self.no_show_team) + " was no-show for round " + str(self.round_number) + ) class RoundStats(models.Model): @@ -531,8 +524,7 @@ class Meta: verbose_name_plural = "round stats" def __str__(self): - return "Results for %s in round %s" % (self.debater, - self.round.round_number) + return "Results for %s in round %s" % (self.debater, self.round.round_number) class CheckIn(models.Model): @@ -540,8 +532,7 @@ class CheckIn(models.Model): round_number = models.IntegerField() def __str__(self): - return "Judge %s is checked in for round %s" % (self.judge, - self.round_number) + return "Judge %s is checked in for round %s" % (self.judge, self.round_number) class RoomCheckIn(models.Model): @@ -549,5 +540,4 @@ class RoomCheckIn(models.Model): round_number = models.IntegerField() def __str__(self): - return "Room %s is checked in for round %s" % (self.room, - self.round_number) + return "Room %s is checked in for round %s" % (self.room, self.round_number) diff --git a/mittab/libs/data_import/__init__.py b/mittab/libs/data_import/__init__.py index 2e01fae5f..c0672baa7 100644 --- a/mittab/libs/data_import/__init__.py +++ b/mittab/libs/data_import/__init__.py @@ -17,8 +17,8 @@ def __init__(self, file_to_import, min_rows): self.min_rows = min_rows try: self.sheet = xlrd.open_workbook( - filename=None, - file_contents=file_to_import.read()).sheet_by_index(0) + filename=None, file_contents=file_to_import.read() + ).sheet_by_index(0) except: raise InvalidWorkbookException("Could not open workbook") @@ -55,11 +55,16 @@ def __init__(self, workbook): def import_row(self, row, row_number): pass + def after_import(self): + pass + def import_data(self): for row_number, row in enumerate(self.workbook.rows()): self.import_row(row, row_number) if self.errors: self.rollback() + else: + self.after_import() return self.errors def create(self, obj): diff --git a/mittab/libs/data_import/import_judges.py b/mittab/libs/data_import/import_judges.py index 237de7761..0eb579044 100644 --- a/mittab/libs/data_import/import_judges.py +++ b/mittab/libs/data_import/import_judges.py @@ -1,6 +1,7 @@ -from mittab.apps.tab.models import School +from mittab.apps.tab.models import School, Team, Scratch from mittab.apps.tab.forms import JudgeForm from mittab.libs.data_import import Workbook, WorkbookImporter, InvalidWorkbookException +from mittab.libs.tab_logic import add_scratches_for_single_judge_for_school_affiliation def import_judges(file_to_import): @@ -12,6 +13,11 @@ def import_judges(file_to_import): class JudgeImporter(WorkbookImporter): + def __init__(self): + """ """ + self.teams = Team.objects.all().prefetch_related("school", "hybrid_school") + self.scratches = [] + def import_row(self, row, row_number): judge_name = row[0] judge_rank = row[1] @@ -37,8 +43,14 @@ def import_row(self, row, row_number): data = {"name": judge_name, "rank": judge_rank, "schools": schools} form = JudgeForm(data=data) if form.is_valid(): - self.create(form) + judge = self.create(form) + self.scratches.extend( + add_scratches_for_single_judge_for_school_affiliation(judge, self.teams) + ) else: for _field, error_msgs in form.errors.items(): for error_msg in error_msgs: self.error("%s - %s" % (judge_name, error_msg), row_number) + + def after_import(): + Scratch.objects.bulk_create(self.scratcehs) diff --git a/mittab/libs/tab_logic/__init__.py b/mittab/libs/tab_logic/__init__.py index ffaf908df..c4b9d9781 100644 --- a/mittab/libs/tab_logic/__init__.py +++ b/mittab/libs/tab_logic/__init__.py @@ -49,15 +49,15 @@ def pair_round(): forfeit_teams = list(Team.objects.filter(checked_in=False)) for team in forfeit_teams: lenient_late = TabSettings.get("lenient_late", 0) >= current_round - no_show = NoShow(no_show_team=team, - round_number=current_round, - lenient_late=lenient_late) + no_show = NoShow( + no_show_team=team, round_number=current_round, lenient_late=lenient_late + ) no_show.save() # If it is the first round, pair by *seed* all_checked_in_teams = Team.objects.filter(checked_in=True).prefetch_related( - "gov_team", # poorly named relation, gets rounds as gov team - "opp_team", # poorly named relation, rounds as opp team + "gov_team", # poorly named relation, gets rounds as gov team + "opp_team", # poorly named relation, rounds as opp team # for all gov rounds, load the opp team's gov+opp rounds (opp-strength) "gov_team__opp_team__gov_team", "gov_team__opp_team__opp_team", @@ -82,9 +82,7 @@ def pair_round(): if len(list_of_teams) % 2 == 1: if TabSettings.get("fair_bye", 1) == 0: print("Bye: using only unseeded teams") - possible_teams = [ - t for t in list_of_teams if t.seed < Team.HALF_SEED - ] + possible_teams = [t for t in list_of_teams if t.seed < Team.HALF_SEED] else: print("Bye: using all teams") possible_teams = list_of_teams @@ -96,29 +94,25 @@ def pair_round(): # Sort the teams by seed. We must randomize beforehand so that similarly # seeded teams are paired randomly. random.shuffle(list_of_teams) - list_of_teams = sorted(list_of_teams, - key=lambda team: team.seed, - reverse=True) + list_of_teams = sorted(list_of_teams, key=lambda team: team.seed, reverse=True) # Otherwise, pair by *speaks* else: # Bucket all the teams into brackets # NOTE: We do not bucket teams that have only won by # forfeit/bye/lenient_late in every round because they have no speaks - middle_of_bracket, normal_pairing_teams = \ - get_middle_and_non_middle_teams(all_checked_in_teams) + middle_of_bracket, normal_pairing_teams = get_middle_and_non_middle_teams( + all_checked_in_teams + ) - team_buckets = [(tot_wins(team), team) - for team in normal_pairing_teams] + team_buckets = [(tot_wins(team), team) for team in normal_pairing_teams] list_of_teams = [ - rank_teams_except_record( - [team for (w, team) in team_buckets if w == i]) + rank_teams_except_record([team for (w, team) in team_buckets if w == i]) for i in range(current_round) ] for team in middle_of_bracket: wins = tot_wins(team) - print(("Pairing %s into the middle of the %s-win bracket" % - (team, wins))) + print(("Pairing %s into the middle of the %s-win bracket" % (team, wins))) bracket_size = len(list_of_teams[wins]) bracket_middle = bracket_size // 2 list_of_teams[wins].insert(bracket_middle, team) @@ -134,11 +128,12 @@ def pair_round(): # If there are no teams all down, give the bye to a one down team. if bracket == 0: byeint = len(list_of_teams[bracket]) - 1 - bye = Bye(bye_team=list_of_teams[bracket][byeint], - round_number=current_round) + bye = Bye( + bye_team=list_of_teams[bracket][byeint], + round_number=current_round, + ) bye.save() - list_of_teams[bracket].remove( - list_of_teams[bracket][byeint]) + list_of_teams[bracket].remove(list_of_teams[bracket][byeint]) elif bracket == 1 and not list_of_teams[0]: # in 1 up and no all down teams found_bye = False @@ -146,8 +141,10 @@ def pair_round(): if had_bye(list_of_teams[1][byeint]): pass elif not found_bye: - bye = Bye(bye_team=list_of_teams[1][byeint], - round_number=current_round) + bye = Bye( + bye_team=list_of_teams[1][byeint], + round_number=current_round, + ) bye.save() list_of_teams[1].remove(list_of_teams[1][byeint]) found_bye = True @@ -159,19 +156,18 @@ def pair_round(): i = len(list_of_teams[bracket - 1]) - 1 pullup_rounds = Round.objects.exclude(pullup=Round.NONE) teams_been_pulled_up = [ - r.gov_team for r in pullup_rounds - if r.pullup == Round.GOV + r.gov_team for r in pullup_rounds if r.pullup == Round.GOV ] - teams_been_pulled_up.extend([ - r.opp_team for r in pullup_rounds - if r.pullup == Round.OPP - ]) + teams_been_pulled_up.extend( + [r.opp_team for r in pullup_rounds if r.pullup == Round.OPP] + ) # try to pull-up the lowest-ranked team that hasn't been # pulled-up. Fall-back to the lowest-ranked team if all have # been pulled-up not_pulled_up_teams = [ - t for t in list_of_teams[bracket - 1] + t + for t in list_of_teams[bracket - 1] if t not in teams_been_pulled_up ] if not_pulled_up_teams: @@ -190,15 +186,16 @@ def pair_round(): for team in list(Team.objects.filter(checked_in=True)): # They have all wins and they haven't forfeited so # they need to get paired in - if team in middle_of_bracket and tot_wins( - team) == bracket: + if team in middle_of_bracket and tot_wins(team) == bracket: removed_teams += [team] list_of_teams[bracket].remove(team) list_of_teams[bracket] = rank_teams_except_record( - list_of_teams[bracket]) + list_of_teams[bracket] + ) for team in removed_teams: list_of_teams[bracket].insert( - len(list_of_teams[bracket]) // 2, team) + len(list_of_teams[bracket]) // 2, team + ) # Pass in the prepared nodes to the perfect pairing logic # to get a pairing for the round @@ -214,15 +211,18 @@ def pair_round(): if current_round == 1: random.shuffle(pairings, random=random.random) - pairings = sorted(pairings, - key=lambda team: highest_seed(team[0], team[1]), - reverse=True) + pairings = sorted( + pairings, key=lambda team: highest_seed(team[0], team[1]), reverse=True + ) # sort with pairing with highest ranked team first else: sorted_teams = [s.team for s in rank_teams()] - pairings = sorted(pairings, - key=lambda team: min(sorted_teams.index(team[0]), - sorted_teams.index(team[1]))) + pairings = sorted( + pairings, + key=lambda team: min( + sorted_teams.index(team[0]), sorted_teams.index(team[1]) + ), + ) # Assign rooms (does this need to be random? maybe bad to have top # ranked teams/judges in top rooms?) @@ -239,10 +239,9 @@ def pair_round(): # Enter into database all_rounds = [] for gov, opp, room in pairings: - round_obj = Round(round_number=current_round, - gov_team=gov, - opp_team=opp, - room=room) + round_obj = Round( + round_number=current_round, gov_team=gov, opp_team=opp, room=room + ) if gov in all_pull_ups: round_obj.pullup = Round.GOV elif opp in all_pull_ups: @@ -269,12 +268,13 @@ def have_enough_rooms(_round_to_check): def have_properly_entered_data(round_to_check): last_round = round_to_check - 1 - prev_rounds = Round.objects \ - .filter(round_number=last_round) \ - .prefetch_related("gov_team", "opp_team") + prev_rounds = Round.objects.filter(round_number=last_round).prefetch_related( + "gov_team", "opp_team" + ) prev_round_noshows = set( - NoShow.objects.filter(round_number=last_round) \ - .values_list("no_show_team_id", flat=True) + NoShow.objects.filter(round_number=last_round).values_list( + "no_show_team_id", flat=True + ) ) prev_round_byes = set( Bye.objects.filter(round_number=last_round).values_list("bye_team", flat=True) @@ -289,10 +289,12 @@ def have_properly_entered_data(round_to_check): for team in gov_team, opp_team: if team.id in prev_round_byes: raise errors.ByeAssignmentError( - "{} both had a bye and debated last round".format(team)) + "{} both had a bye and debated last round".format(team) + ) if team.id in prev_round_noshows: raise errors.NoShowAssignmentError( - "{} both debated and had a no show".format(team)) + "{} both debated and had a no show".format(team) + ) def validate_round_data(round_to_check): @@ -321,6 +323,23 @@ def validate_round_data(round_to_check): have_properly_entered_data(round_to_check) +def add_scratches_for_single_judge_for_school_affiliation(judge, teams): + """ + Generates a list of scratch objects to add for a given judge. + """ + to_return = [] + + for team in teams: + judge_schools = judge.schools.all() + + if team.school in judge_schools or team.hybrid_school in judge_schools: + to_return.append( + Scratch(judge=judge, team=team, scratch_type=Scratch.SCHOOL_SCRATCH) + ) + + return to_return + + def add_scratches_for_school_affil(): """ Add scratches for teams/judges from the same school @@ -329,14 +348,18 @@ def add_scratches_for_school_affil(): all_judges = Judge.objects.all().prefetch_related("schools", "scratches__team") all_teams = Team.objects.all().prefetch_related("school", "hybrid_school") + Scratch.objects.filter(scratch_type=Scratch.SCHOOL_SCRATCH).all().delete() + to_create = [] for judge in all_judges: for team in all_teams: judge_schools = judge.schools.all() if team.school in judge_schools or team.hybrid_school in judge_schools: - if not any(s.team == team for s in judge.scratches.all()): - to_create.append(Scratch(judge=judge, team=team, scratch_type=1)) + to_create.append( + add_scratches_for_single_judge_for_school_affiliation(judge, team) + ) + Scratch.objects.bulk_create(to_create) @@ -422,11 +445,13 @@ def sorted_pairings(round_number): "opp_team__debaters__team_set__byes", "opp_team__debaters__team_set__no_shows", "opp_team__debaters__roundstats_set", + "opp_team__debaters__roundstats_set__round", + ) + ) + round_pairing.sort(key=lambda x: team_comp(x, round_number), reverse=True) "opp_team__debaters__roundstats_set__round" ) ) - round_pairing.sort(key=lambda x: team_comp(x, round_number), - reverse=True) return round_pairing @@ -467,10 +492,17 @@ class TabFlags: JUDGE_NOT_CHECKED_IN_NEXT = 1 << 10 ALL_FLAGS = [ - TEAM_CHECKED_IN, TEAM_NOT_CHECKED_IN, JUDGE_CHECKED_IN_CUR, - JUDGE_NOT_CHECKED_IN_CUR, LOW_RANKED_JUDGE, MID_RANKED_JUDGE, - HIGH_RANKED_JUDGE, ROOM_ZERO_RANK, ROOM_NON_ZERO_RANK, - JUDGE_CHECKED_IN_NEXT, JUDGE_NOT_CHECKED_IN_NEXT + TEAM_CHECKED_IN, + TEAM_NOT_CHECKED_IN, + JUDGE_CHECKED_IN_CUR, + JUDGE_NOT_CHECKED_IN_CUR, + LOW_RANKED_JUDGE, + MID_RANKED_JUDGE, + HIGH_RANKED_JUDGE, + ROOM_ZERO_RANK, + ROOM_NON_ZERO_RANK, + JUDGE_CHECKED_IN_NEXT, + JUDGE_NOT_CHECKED_IN_NEXT, ] @staticmethod @@ -478,57 +510,70 @@ def translate_flag(flag, short=False): return { TabFlags.TEAM_NOT_CHECKED_IN: ("Team NOT Checked In", "*"), TabFlags.TEAM_CHECKED_IN: ("Team Checked In", ""), - TabFlags.JUDGE_CHECKED_IN_CUR: - ("Judge Checked In, Current Round", ""), - TabFlags.JUDGE_NOT_CHECKED_IN_CUR: - ("Judge NOT Checked In, Current Round", "*"), - TabFlags.JUDGE_CHECKED_IN_NEXT: - ("Judge Checked In, Next Round", ""), - TabFlags.JUDGE_NOT_CHECKED_IN_NEXT: - ("Judge NOT Checked In, Next Round", "!"), + TabFlags.JUDGE_CHECKED_IN_CUR: ("Judge Checked In, Current Round", ""), + TabFlags.JUDGE_NOT_CHECKED_IN_CUR: ( + "Judge NOT Checked In, Current Round", + "*", + ), + TabFlags.JUDGE_CHECKED_IN_NEXT: ("Judge Checked In, Next Round", ""), + TabFlags.JUDGE_NOT_CHECKED_IN_NEXT: ( + "Judge NOT Checked In, Next Round", + "!", + ), TabFlags.LOW_RANKED_JUDGE: ("Low Ranked Judge", "L"), TabFlags.MID_RANKED_JUDGE: ("Mid Ranked Judge", "M"), TabFlags.HIGH_RANKED_JUDGE: ("High Ranked Judge", "H"), TabFlags.ROOM_ZERO_RANK: ("Room has rank of 0", "*"), - TabFlags.ROOM_NON_ZERO_RANK: ("Room has rank > 0", "") + TabFlags.ROOM_NON_ZERO_RANK: ("Room has rank > 0", ""), }.get(flag, ("Flag Not Found", "U"))[short] @staticmethod def flags_to_symbols(flags): - return "".join([ - TabFlags.translate_flag(flag, True) for flag in TabFlags.ALL_FLAGS - if flags & flag == flag - ]) + return "".join( + [ + TabFlags.translate_flag(flag, True) + for flag in TabFlags.ALL_FLAGS + if flags & flag == flag + ] + ) @staticmethod def get_filters_and_symbols(all_flags): flat_flags = list(itertools.chain(*all_flags)) - filters = [[(flag, TabFlags.translate_flag(flag)) - for flag in flag_group] for flag_group in all_flags] - symbol_text = [(TabFlags.translate_flag(flag, True), - TabFlags.translate_flag(flag)) for flag in flat_flags - if TabFlags.translate_flag(flag, True)] + filters = [ + [(flag, TabFlags.translate_flag(flag)) for flag in flag_group] + for flag_group in all_flags + ] + symbol_text = [ + (TabFlags.translate_flag(flag, True), TabFlags.translate_flag(flag)) + for flag in flat_flags + if TabFlags.translate_flag(flag, True) + ] return filters, symbol_text def perfect_pairing(list_of_teams): - """ Uses the mwmatching library to assign teams in a pairing """ + """Uses the mwmatching library to assign teams in a pairing""" graph_edges = [] weights = get_weights() for i, team1 in enumerate(list_of_teams): for j, team2 in enumerate(list_of_teams): if i > j: - weight = calc_weight(team1, team2, i, j, - list_of_teams[len(list_of_teams) - i - 1], - list_of_teams[len(list_of_teams) - j - 1], - len(list_of_teams) - i - 1, - len(list_of_teams) - j - 1, - weights, - TabSettings.get("cur_round", 1), - TabSettings.get("tot_rounds", 5)) + weight = calc_weight( + team1, + team2, + i, + j, + list_of_teams[len(list_of_teams) - i - 1], + list_of_teams[len(list_of_teams) - j - 1], + len(list_of_teams) - i - 1, + len(list_of_teams) - j - 1, + weights, + TabSettings.get("cur_round", 1), + TabSettings.get("tot_rounds", 5), + ) graph_edges += [(i, j, weight)] - pairings_num = mwmatching.maxWeightMatching(graph_edges, - maxcardinality=True) + pairings_num = mwmatching.maxWeightMatching(graph_edges, maxcardinality=True) all_pairs = [] for pair in pairings_num: if pair < len(list_of_teams): @@ -557,8 +602,35 @@ def get_weights(): -def calc_weight(team_a, team_b, team_a_ind, team_b_ind, team_a_opt, team_b_opt, - team_a_opt_ind, team_b_opt_ind, weights, current_round, tot_rounds): +def get_weights(): + """ + Returns a map of all the weight-related tab settings to use without querying for + calculations + """ + return { + "power_pairing_multiple": TabSettings.get("power_pairing_multiple", -1), + "high_opp_penalty": TabSettings.get("high_opp_penalty", 0), + "high_gov_penalty": TabSettings.get("high_gov_penalty", -100), + "high_high_opp_penalty": TabSettings.get("higher_opp_penalty", -10), + "same_school_penalty": TabSettings.get("same_school_penalty", -1000), + "hit_pull_up_before": TabSettings.get("hit_pull_up_before", -10000), + "hit_team_before": TabSettings.get("hit_team_before", -100000), + } + + +def calc_weight( + team_a, + team_b, + team_a_ind, + team_b_ind, + team_a_opt, + team_b_opt, + team_a_opt_ind, + team_b_opt_ind, + weights, + current_round, + tot_rounds, +): """ Calculate the penalty for a given pairing @@ -573,13 +645,17 @@ def calc_weight(team_a, team_b, team_a_ind, team_b_ind, team_a_opt, team_b_opt, team_b_opt_ind - the position in the pairing of team_b_opt """ if current_round == 1: - weight = weights["power_pairing_multiple"] * ( - abs(team_a_opt.seed - team_b.seed) + - abs(team_b_opt.seed - team_a.seed)) / 2.0 + weight = ( + weights["power_pairing_multiple"] + * (abs(team_a_opt.seed - team_b.seed) + abs(team_b_opt.seed - team_a.seed)) + / 2.0 + ) else: - weight = weights["power_pairing_multiple"] * ( - abs(team_a_opt_ind - team_b_ind) + - abs(team_b_opt_ind - team_a_ind)) / 2.0 + weight = ( + weights["power_pairing_multiple"] + * (abs(team_a_opt_ind - team_b_ind) + abs(team_b_opt_ind - team_a_ind)) + / 2.0 + ) half = int(tot_rounds // 2) + 1 if num_opps(team_a) >= half and num_opps(team_b) >= half: @@ -595,7 +671,8 @@ def calc_weight(team_a, team_b, team_a_ind, team_b_ind, team_a_opt, team_b_opt, weight += weights["same_school_penalty"] if (hit_pull_up(team_a) and tot_wins(team_b) < tot_wins(team_a)) or ( - hit_pull_up(team_b) and tot_wins(team_a) < tot_wins(team_b)): + hit_pull_up(team_b) and tot_wins(team_a) < tot_wins(team_b) + ): weight += weights["hit_pull_up_before"] if hit_before(team_a, team_b):