From dabdaf7c62718459d77baa8e351b137cb012e63b Mon Sep 17 00:00:00 2001 From: Rodda John Date: Mon, 21 Oct 2019 17:52:38 -0400 Subject: [PATCH 01/27] Resolves issue --- .../tab/migrations/0013_auto_20191021_2136.py | 18 +++++++++++++++ mittab/apps/tab/models.py | 22 ++++++++++++++++++- mittab/libs/tab_logic/__init__.py | 9 +------- 3 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 mittab/apps/tab/migrations/0013_auto_20191021_2136.py 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 27287d397..fe426ce17 100644 --- a/mittab/apps/tab/models.py +++ b/mittab/apps/tab/models.py @@ -237,6 +237,8 @@ def save(self, 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() @@ -256,15 +258,33 @@ def delete(self, using=None, keep_parents=False): class Meta: ordering = ["name"] + def update_scratches(self): + all_teams = Team.objects.all() + + Scratch.objects.filter(scratch_type=Scratch.SCHOOL_SCRATCH, + judge=self).all().delete() + + for team in all_teams: + judge_schools = self.schools.all() + + if team.school in judge_schools or \ + team.hybrid_school in judge_schools: + if not Scratch.objects.filter(judge=self, team=team).exists(): + Scratch.objects.create(judge=self, + team=team, + scratch_type=Scratch.SCHOOL_SCRATCH) + class Scratch(models.Model): judge = models.ForeignKey(Judge, on_delete=models.CASCADE) team = models.ForeignKey(Team, 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) @@ -273,7 +293,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) diff --git a/mittab/libs/tab_logic/__init__.py b/mittab/libs/tab_logic/__init__.py index 946004dbd..3c46389bc 100644 --- a/mittab/libs/tab_logic/__init__.py +++ b/mittab/libs/tab_logic/__init__.py @@ -297,15 +297,8 @@ def add_scratches_for_school_affil(): Only do this if they haven't already been added """ all_judges = Judge.objects.all() - all_teams = Team.objects.all() 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 Scratch.objects.filter(judge=judge, team=team).exists(): - Scratch.objects.create(judge=judge, - team=team, - scratch_type=1) + judge.update_scratches() def highest_seed(team1, team2): From ff9c4de0048137a0acf82cbfb1e8828bfba3fe81 Mon Sep 17 00:00:00 2001 From: Rodda John Date: Sun, 26 Feb 2023 09:44:30 -0500 Subject: [PATCH 02/27] Formatting --- mittab/libs/assign_judges.py | 53 ++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/mittab/libs/assign_judges.py b/mittab/libs/assign_judges.py index a18c4eb93..a00c8da03 100644 --- a/mittab/libs/assign_judges.py +++ b/mittab/libs/assign_judges.py @@ -7,16 +7,20 @@ def add_judges(): current_round_number = TabSettings.get("cur_round") - 1 - judges = list(Judge.objects.filter(checkin__round_number=4).prefetch_related( - "judges", # poorly named relation for the round - "scratches", - )) + judges = list( + Judge.objects.filter( + checkin__round_number=current_round_number + ).prefetch_related( + "judges", # poorly named relation for the round + "scratches", + ) + ) pairings = tab_logic.sorted_pairings(current_round_number) # First clear any existing judge assignments - Round.judges.through.objects \ - .filter(round__round_number=current_round_number) \ - .delete() + Round.judges.through.objects.filter( + round__round_number=current_round_number + ).delete() # Try to have consistent ordering with the round display random.seed(1337) @@ -26,15 +30,16 @@ def add_judges(): # Order the judges and pairings by power ranking judges = sorted(judges, key=lambda j: j.rank, reverse=True) - pairings.sort(key=lambda x: tab_logic.team_comp(x, current_round_number), - reverse=True) + pairings.sort( + key=lambda x: tab_logic.team_comp(x, current_round_number), reverse=True + ) num_rounds = len(pairings) # Assign chairs (single judges) to each round using perfect pairing graph_edges = [] - for (judge_i, judge) in enumerate(judges): - for (pairing_i, pairing) in enumerate(pairings): + for judge_i, judge in enumerate(judges): + for pairing_i, pairing in enumerate(pairings): if not judge_conflict(judge, pairing.gov_team, pairing.opp_team): edge = ( pairing_i, @@ -50,12 +55,14 @@ def add_judges(): if not graph_edges: raise errors.JudgeAssignmentError( "Impossible to assign judges, consider reducing your gaps if you" - " are making panels, otherwise find some more judges.") + " are making panels, otherwise find some more judges." + ) elif -1 in judge_assignments[:num_rounds]: - pairing_list = judge_assignments[:len(pairings)] + pairing_list = judge_assignments[: len(pairings)] bad_pairing = pairings[pairing_list.index(-1)] raise errors.JudgeAssignmentError( - "Could not find a judge for: %s" % str(bad_pairing)) + "Could not find a judge for: %s" % str(bad_pairing) + ) else: raise errors.JudgeAssignmentError() @@ -75,8 +82,9 @@ def add_judges(): Round.objects.bulk_update(pairings, ["chair"]) Round.judges.through.objects.bulk_create(judge_round_joins) + def calc_weight(judge_i, pairing_i): - """ Calculate the relative badness of this judge assignment + """Calculate the relative badness of this judge assignment We want small negative numbers to be preferred to large negative numbers @@ -85,9 +93,18 @@ def calc_weight(judge_i, pairing_i): def judge_conflict(judge, team1, team2): - return any(s.team_id in (team1.id, team2.id,) for s in judge.scratches.all()) \ - or had_judge(judge, team1) \ - or had_judge(judge, team2) + return ( + any( + s.team_id + in ( + team1.id, + team2.id, + ) + for s in judge.scratches.all() + ) + or had_judge(judge, team1) + or had_judge(judge, team2) + ) def had_judge(judge, team): From 36bb64fe1c82b2b69cacb31605218418f087e663 Mon Sep 17 00:00:00 2001 From: Rodda John Date: Sun, 26 Feb 2023 09:45:52 -0500 Subject: [PATCH 03/27] Revert "Formatting" This reverts commit ff9c4de0048137a0acf82cbfb1e8828bfba3fe81. --- mittab/libs/assign_judges.py | 53 ++++++++++++------------------------ 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/mittab/libs/assign_judges.py b/mittab/libs/assign_judges.py index a00c8da03..a18c4eb93 100644 --- a/mittab/libs/assign_judges.py +++ b/mittab/libs/assign_judges.py @@ -7,20 +7,16 @@ def add_judges(): current_round_number = TabSettings.get("cur_round") - 1 - judges = list( - Judge.objects.filter( - checkin__round_number=current_round_number - ).prefetch_related( - "judges", # poorly named relation for the round - "scratches", - ) - ) + judges = list(Judge.objects.filter(checkin__round_number=4).prefetch_related( + "judges", # poorly named relation for the round + "scratches", + )) pairings = tab_logic.sorted_pairings(current_round_number) # First clear any existing judge assignments - Round.judges.through.objects.filter( - round__round_number=current_round_number - ).delete() + Round.judges.through.objects \ + .filter(round__round_number=current_round_number) \ + .delete() # Try to have consistent ordering with the round display random.seed(1337) @@ -30,16 +26,15 @@ def add_judges(): # Order the judges and pairings by power ranking judges = sorted(judges, key=lambda j: j.rank, reverse=True) - pairings.sort( - key=lambda x: tab_logic.team_comp(x, current_round_number), reverse=True - ) + pairings.sort(key=lambda x: tab_logic.team_comp(x, current_round_number), + reverse=True) num_rounds = len(pairings) # Assign chairs (single judges) to each round using perfect pairing graph_edges = [] - for judge_i, judge in enumerate(judges): - for pairing_i, pairing in enumerate(pairings): + for (judge_i, judge) in enumerate(judges): + for (pairing_i, pairing) in enumerate(pairings): if not judge_conflict(judge, pairing.gov_team, pairing.opp_team): edge = ( pairing_i, @@ -55,14 +50,12 @@ def add_judges(): if not graph_edges: raise errors.JudgeAssignmentError( "Impossible to assign judges, consider reducing your gaps if you" - " are making panels, otherwise find some more judges." - ) + " are making panels, otherwise find some more judges.") elif -1 in judge_assignments[:num_rounds]: - pairing_list = judge_assignments[: len(pairings)] + pairing_list = judge_assignments[:len(pairings)] bad_pairing = pairings[pairing_list.index(-1)] raise errors.JudgeAssignmentError( - "Could not find a judge for: %s" % str(bad_pairing) - ) + "Could not find a judge for: %s" % str(bad_pairing)) else: raise errors.JudgeAssignmentError() @@ -82,9 +75,8 @@ def add_judges(): Round.objects.bulk_update(pairings, ["chair"]) Round.judges.through.objects.bulk_create(judge_round_joins) - def calc_weight(judge_i, pairing_i): - """Calculate the relative badness of this judge assignment + """ Calculate the relative badness of this judge assignment We want small negative numbers to be preferred to large negative numbers @@ -93,18 +85,9 @@ def calc_weight(judge_i, pairing_i): def judge_conflict(judge, team1, team2): - return ( - any( - s.team_id - in ( - team1.id, - team2.id, - ) - for s in judge.scratches.all() - ) - or had_judge(judge, team1) - or had_judge(judge, team2) - ) + return any(s.team_id in (team1.id, team2.id,) for s in judge.scratches.all()) \ + or had_judge(judge, team1) \ + or had_judge(judge, team2) def had_judge(judge, team): From cb9e1dcd20b761c134a2be7979d5d4356ac7b812 Mon Sep 17 00:00:00 2001 From: "Rodda R. John" Date: Mon, 21 Oct 2019 18:43:35 -0400 Subject: [PATCH 04/27] Resolves (#273) --- mittab/templates/ballots/missing_ballots.html | 1 + 1 file changed, 1 insertion(+) diff --git a/mittab/templates/ballots/missing_ballots.html b/mittab/templates/ballots/missing_ballots.html index b25c8150d..f9863b8a4 100644 --- a/mittab/templates/ballots/missing_ballots.html +++ b/mittab/templates/ballots/missing_ballots.html @@ -11,6 +11,7 @@

Missing Ballots

This list will refresh every 30 seconds

    +

    Missing: {{ rounds|length }}

    {% for r in rounds %}
  • From 117bac8f63d92e51a358a6f36a0917b54230dc61 Mon Sep 17 00:00:00 2001 From: Ben Muschol Date: Mon, 4 Nov 2019 07:41:01 -0800 Subject: [PATCH 05/27] Replace Sqlite with MySQL (#277) --- .circleci/config.yml | 29 +++- .env | 10 ++ Dockerfile.web | 2 +- README.md | 18 +++ bin/setup | 4 + docker-compose.yml | 22 ++- .../management/commands/initialize_tourney.py | 5 +- .../apps/tab/management/commands/load_test.py | 138 ++++++++++++++++++ .../management/commands/simulate_rounds.py | 37 +---- mittab/apps/tab/management/commands/utils.py | 47 ++++++ mittab/apps/tab/models.py | 4 +- mittab/libs/backup.py | 80 ---------- mittab/libs/backup/__init__.py | 55 +++++++ mittab/libs/backup/strategies/local_dump.py | 58 ++++++++ mittab/settings.py | 8 +- requirements.txt | 2 + 16 files changed, 394 insertions(+), 125 deletions(-) create mode 100644 mittab/apps/tab/management/commands/load_test.py create mode 100644 mittab/apps/tab/management/commands/utils.py delete mode 100644 mittab/libs/backup.py create mode 100644 mittab/libs/backup/__init__.py create mode 100644 mittab/libs/backup/strategies/local_dump.py diff --git a/.circleci/config.yml b/.circleci/config.yml index d06297b5f..07f17925e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,6 +5,14 @@ jobs: build: docker: - image: circleci/python:3.7.3-node-browsers + environment: + MYSQL_PASSWORD: secret + - image: circleci/mysql:5.7 + environment: + MYSQL_ROOT_PASSWORD: secret + MYSQL_DATABASE: mittab + MYSQL_ROOT_HOST: 127.0.0.1 + parallelism: 4 working_directory: ~/repo @@ -30,12 +38,29 @@ jobs: - ./venv key: v1-dependencies-{{ checksum "requirements.txt" }} + - run: + # Our primary container isn't MYSQL so run a sleep command until it's ready. + name: Waiting for MySQL to be ready + command: | + for i in `seq 1 10`; + do + nc -z 127.0.0.1 3306 && echo Success && exit 0 + echo -n . + sleep 1 + done + echo Failed waiting for MySQL && exit 1 + + - run: + name: setup db + command: | + . venv/bin/activate + ./bin/setup password + - run: name: run tests command: | . venv/bin/activate - ./bin/setup password > /dev/null 2>&1 - pytest --junitxml=test-reports/junit.xml --cov=mittab --cov-report xml:cov.xml --circleci-parallelize --create-db mittab/ + pytest --junitxml=test-reports/junit.xml --cov=mittab --cov-report xml:cov.xml --circleci-parallelize mittab/ no_output_timeout: 20m - store_test_results: diff --git a/.env b/.env index e69de29bb..fdf1b6a97 100644 --- a/.env +++ b/.env @@ -0,0 +1,10 @@ +MYSQL_DATABASE=mittab +MYSQL_USER=root +MYSQL_HOST=localhost + +# to be overridden in .env.secret +MYSQL_PASSWORD=secret +MYSQL_ROOT_PASSWORD=secret + +# defined in the docker-compose file +MITTAB_DB_HOST=mysql diff --git a/Dockerfile.web b/Dockerfile.web index 78722471c..e23352ab0 100644 --- a/Dockerfile.web +++ b/Dockerfile.web @@ -2,7 +2,7 @@ FROM python:3.7 # install dependenices RUN apt-get update && apt-get upgrade -y && \ - apt-get install sqlite3 && apt-get install -y vim + apt-get install && apt-get install -y vim default-mysql-client # sets up nodejs to install npm RUN curl -sL https://deb.nodesource.com/setup_11.x | bash RUN apt-get install nodejs diff --git a/README.md b/README.md index 02e42c5fd..3c5c59372 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,24 @@ information** Currently the installation consists of downloading the code, installing requirements and then manually running the server. +### Pre-Requisite: Install MySQL + +Instructions for this will vary by platform. On Mac OS X, it comes +pre-installed. + +Otherwise, check [here](https://dev.mysql.com/doc/mysql/en/windows-installation.html) for Windows +and [here](https://dev.mysql.com/doc/refman/8.0/en/linux-installation.html) for Linux + +Before running, you need to install MySQL and create a database called `mittab` + +The database login credentials can be configured through the following env vars: + * `MYSQL_USER` (default `root`) + * `MYSQL_PASSWORD` (default `""`, if you use the default make sure you allow empty root passwords in your mysql settings) + * `MITTAB_DB_HOST` (default `127.0.0.1`, you probably have no need to change this) + * `MITTAB_PORT` (default `3306`, the default mysql port) + +### Running the server + ``` git clone cd mit-tab diff --git a/bin/setup b/bin/setup index ce5ae16ff..c6bd25328 100755 --- a/bin/setup +++ b/bin/setup @@ -1,5 +1,9 @@ #!/bin/bash +set -e +if [ -x "$(command -v mysqladmin)" ]; then + mysqladmin ping -h $MITTAB_DB_HOST -u $MYSQL_USER --password=$MYSQL_PASSWORD --wait=30 +fi python manage.py migrate --noinput npm install ./node_modules/.bin/webpack --config webpack.config.js --mode production diff --git a/docker-compose.yml b/docker-compose.yml index 689af4829..8407a2b63 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,12 +12,25 @@ services: volumes: - ./:/var/www/tab links: - - memcached:memcached + - mysql:mysql env_file: - .env - .env.secret command: /usr/local/bin/gunicorn mittab.wsgi:application -w 2 -b :8000 -t 300 + mysql: + image: mysql:5.7 + restart: always + ports: + - "3306:3306" + expose: + - "3306" + volumes: + - my-db:/var/lib/mysql + env_file: + - .env + - .env.secret + nginx: restart: always build: @@ -31,8 +44,5 @@ services: - web links: - web:web - - memcached: - image: memcached - ports: - - "11211:11211" +volumes: + my-db: diff --git a/mittab/apps/tab/management/commands/initialize_tourney.py b/mittab/apps/tab/management/commands/initialize_tourney.py index 371aaaffc..42bc06729 100644 --- a/mittab/apps/tab/management/commands/initialize_tourney.py +++ b/mittab/apps/tab/management/commands/initialize_tourney.py @@ -7,8 +7,9 @@ from django.contrib.auth.models import User from django.core.management.base import BaseCommand -from mittab.libs.backup import BACKUP_PREFIX from mittab.apps.tab.models import TabSettings +from mittab.libs.backup import backup_round +from mittab.libs.backup.strategies.local_dump import BACKUP_PREFIX class Command(BaseCommand): @@ -46,8 +47,8 @@ def handle(self, *args, **options): self.stdout.write( "Copying current tournament state to backup tournament directory: %s" % tournament_dir) + backup_round("before_new_tournament") try: - shutil.copy(path + "/pairing_db.sqlite3", tournament_dir) shutil.rmtree(tournament_dir + "/backups", ignore_errors=True) shutil.copytree(path + "/backups", tournament_dir + "/backups") except (IOError, os.error) as why: diff --git a/mittab/apps/tab/management/commands/load_test.py b/mittab/apps/tab/management/commands/load_test.py new file mode 100644 index 000000000..4468af776 --- /dev/null +++ b/mittab/apps/tab/management/commands/load_test.py @@ -0,0 +1,138 @@ +import re +from threading import Thread +import time + +from django.core.management.base import BaseCommand +import requests + +from mittab.apps.tab.models import Round, TabSettings +from mittab.apps.tab.management.commands import utils + + +class Command(BaseCommand): + help = "Load test the tournament, connecting via localhost and hitting the server" + + def add_arguments(self, parser): + parser.add_argument( + "--host", + dest="host", + help="The hostname of the server to hit", + nargs="?", + default="localhost:8000") + parser.add_argument( + "--connections", + dest="connections", + help="The number of concurrent connections to open", + nargs="?", + default=10, + type=int) + + def handle(self, *args, **options): + cur_round = TabSettings.get("cur_round") - 1 + host = options["host"] + + csrf_threads = [] + rounds = Round.objects.filter(round_number=cur_round, victor=Round.NONE) + for round_obj in rounds: + judge = round_obj.chair + csrf_threads.append(GetCsrfThread(host, judge.ballot_code, round_obj)) + + num_errors = 0 + while csrf_threads: + cur_csrf_threads = [] + for _ in range(min(len(csrf_threads), options["connections"])): + cur_csrf_threads.append(csrf_threads.pop()) + + for thr in cur_csrf_threads: + thr.start() + for thr in cur_csrf_threads: + thr.join() + + result_threads = [] + for thr in cur_csrf_threads: + num_errors += num_errors + csrf_token, num_errors = thr.result + if csrf_token is None: + print("no csrf token") + + result_thread = SubmitResultThread( + thr.host, + thr.ballot_code, + csrf_token, + thr.round_obj) + result_threads.append(result_thread) + + for thr in result_threads: + thr.start() + for thr in result_threads: + thr.join() + for thr in result_threads: + num_errors += thr.num_errors + print("Done with one batch! Sleeping!") + time.sleep(2) + + print("Done!") + print("Total errors: %s" % num_errors) + + +class SubmitResultThread(Thread): + MAX_ERRORS = 10 + + def __init__(self, host, ballot_code, csrf_token, round_obj): + super(SubmitResultThread, self).__init__() + self.host = host + self.ballot_code = ballot_code + self.csrf_token = csrf_token + self.round_obj = round_obj + self.num_errors = 0 + self.resp = None + + def run(self): + self.resp = self.get_resp() + + def get_resp(self): + if self.num_errors >= self.MAX_ERRORS: + return None + + result = utils.generate_random_results(self.round_obj, self.ballot_code) + result["csrfmiddlewaretoken"] = self.csrf_token + + resp = requests.post("http://%s/e_ballots/%s/" % (self.host, self.ballot_code), + result, + cookies={"csrftoken": self.csrf_token}) + if resp.status_code > 299: + self.num_errors += 1 + return self.get_resp() + else: + return resp.text + + +class GetCsrfThread(Thread): + REGEX = "name=\"csrfmiddlewaretoken\" value=\"([^\"]+)\"" + MAX_ERRORS = 10 + + def __init__(self, host, ballot_code, round_obj): + super(GetCsrfThread, self).__init__() + self.num_errors = 0 + self.host = host + self.ballot_code = ballot_code + self.round_obj = round_obj + self.result = (None, None) + + def run(self): + resp = self.get_resp() + if resp is None: + self.result = (None, self.num_errors) + else: + csrf = re.search(self.REGEX, resp).group(1) + self.result = (csrf, self.num_errors) + + def get_resp(self): + if self.num_errors >= self.MAX_ERRORS: + return None + resp = requests.get("http://%s/e_ballots/%s" % (self.host, self.ballot_code)) + if resp.status_code > 299: + self.num_errors += 1 + return self.get_resp() + else: + return resp.text diff --git a/mittab/apps/tab/management/commands/simulate_rounds.py b/mittab/apps/tab/management/commands/simulate_rounds.py index c3e3d5641..21a88f6d4 100644 --- a/mittab/apps/tab/management/commands/simulate_rounds.py +++ b/mittab/apps/tab/management/commands/simulate_rounds.py @@ -1,12 +1,10 @@ -import random - from django.core.management.base import BaseCommand from mittab.apps.tab.models import Round, TabSettings, RoundStats +from mittab.apps.tab.management.commands import utils class Command(BaseCommand): - SPEAKS_RANGE = list(range(15, 35)) def handle(self, *args, **options): cur_round = TabSettings.get("cur_round") - 1 @@ -18,35 +16,14 @@ def handle(self, *args, **options): self.__simulate_round(round_obj) def __simulate_round(self, round_obj): - winner = random.choice([Round.GOV, Round.OPP]) - speaks = sorted([random.choice(self.SPEAKS_RANGE) for _ in range(4)]) - - winning_team = round_obj.gov_team if winner == Round.GOV else round_obj.opp_team - winning_positions = ["pm", "mg" - ] if winner == Round.GOV else ["lo", "mo"] - - losing_team = round_obj.opp_team if winner == Round.GOV else round_obj.gov_team - losing_positions = ["lo", "mo" - ] if winner == Round.GOV else ["pm", "mg"] - - debaters_rank_order = [ - winning_team.debaters.first(), - winning_team.debaters.last(), - losing_team.debaters.first(), - losing_team.debaters.last(), - ] - - for rank in range(1, 5): - debater = debaters_rank_order[rank - 1] - speak = speaks.pop() - position = winning_positions.pop( - ) if rank <= 2 else losing_positions.pop() + results = utils.generate_random_results(round_obj) - stat = RoundStats(debater=debater, + for position in ["pm", "mg", "lo", "mo"]: + stat = RoundStats(debater=results[position + "_debater"], round=round_obj, - speaks=speak, - ranks=rank, + speaks=results[position + "_speaks"], + ranks=results[position + "_ranks"], debater_role=position) stat.save() - round_obj.victor = winner + round_obj.victor = results["winner"] round_obj.save() diff --git a/mittab/apps/tab/management/commands/utils.py b/mittab/apps/tab/management/commands/utils.py new file mode 100644 index 000000000..758accf0b --- /dev/null +++ b/mittab/apps/tab/management/commands/utils.py @@ -0,0 +1,47 @@ +import random + +from mittab.apps.tab.models import Round + + +SPEAKS_RANGE = list(range(15, 35)) + +def generate_random_results(round_obj, ballot_code=None): + winner = random.choice([Round.GOV, Round.OPP]) + speaks = sorted([random.choice(SPEAKS_RANGE) for _ in range(4)]) + + winning_team = round_obj.gov_team if winner == Round.GOV else round_obj.opp_team + losing_team = round_obj.opp_team if winner == Round.GOV else round_obj.gov_team + + if winner == Round.GOV: + losing_positions = ["lo", "mo"] + winning_positions = ["pm", "mg"] + else: + losing_positions = ["pm", "mg"] + winning_positions = ["lo", "mo"] + + debaters_rank_order = [ + winning_team.debaters.first(), + winning_team.debaters.last(), + losing_team.debaters.first(), + losing_team.debaters.last(), + ] + + form_data = { + "round_instance": round_obj.id, + "winner": winner + } + if ballot_code is not None: + form_data["ballot_code"] = ballot_code + + for rank in range(1, 5): + debater = debaters_rank_order[rank - 1] + speak = speaks.pop() + if rank <= 2: + position = winning_positions.pop() + else: + position = losing_positions.pop() + + form_data[position + "_ranks"] = rank + form_data[position + "_speaks"] = speak + form_data[position + "_debater"] = debater.id + return form_data diff --git a/mittab/apps/tab/models.py b/mittab/apps/tab/models.py index fe426ce17..1311ef18d 100644 --- a/mittab/apps/tab/models.py +++ b/mittab/apps/tab/models.py @@ -138,7 +138,7 @@ class Team(ModelWithTiebreaker): ) seed = models.IntegerField(choices=SEED_CHOICES) checked_in = models.BooleanField(default=True) - team_code = models.CharField(max_length=256, + team_code = models.CharField(max_length=255, blank=True, null=True, unique=True) @@ -211,7 +211,7 @@ 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=256, + ballot_code = models.CharField(max_length=255, blank=True, null=True, unique=True) diff --git a/mittab/libs/backup.py b/mittab/libs/backup.py deleted file mode 100644 index b03d89c06..000000000 --- a/mittab/libs/backup.py +++ /dev/null @@ -1,80 +0,0 @@ -import shutil -import time -import os -from wsgiref.util import FileWrapper - -from django.conf import settings - -from mittab.apps.tab.models import TabSettings -from mittab.libs import errors -from mittab.settings import BASE_DIR - -BACKUP_PREFIX = os.path.join(BASE_DIR, "mittab") -BACKUP_PATH = os.path.join(BACKUP_PREFIX, "backups") -DATABASE_PATH = settings.DATABASES["default"]["NAME"] - - -def get_backup_filename(filename): - if len(filename) < 3 or filename[-3:] != ".db": - filename += ".db" - return os.path.join(BACKUP_PATH, filename) - - -def backup_exists(filename): - return os.path.exists(get_backup_filename(filename)) - - -def backup_round(dst_filename=None, round_number=None, btime=None): - if round_number is None: - round_number = TabSettings.get("cur_round") - - if btime is None: - btime = int(time.time()) - - print("Trying to backup to backups directory") - if dst_filename is None: - dst_filename = "site_round_%i_%i" % (round_number, btime) - - if backup_exists(dst_filename): - dst_filename += "_%i" % btime - - return copy_db(DATABASE_PATH, get_backup_filename(dst_filename)) - - -def handle_backup(f): - dst_filename = get_backup_filename(f.name) - print(("Tried to write {}".format(dst_filename))) - try: - with open(dst_filename, "wb+") as destination: - for chunk in f.chunks(): - destination.write(chunk) - except Exception: - errors.emit_current_exception() - - -def list_backups(): - print("Checking backups directory") - if not os.path.exists(BACKUP_PATH): - os.makedirs(BACKUP_PATH) - - return os.listdir(BACKUP_PATH) - - -def restore_from_backup(src_filename): - print("Restoring from backups directory") - return copy_db(get_backup_filename(src_filename), DATABASE_PATH) - - -def copy_db(src_filename, dst_filename): - try: - shutil.copyfile(src_filename, dst_filename) - print(("Copied %s to %s" % (src_filename, dst_filename))) - return True - except Exception: - errors.emit_current_exception() - return False - - -def get_wrapped_file(src_filename): - src_filename = get_backup_filename(src_filename) - return FileWrapper(open(src_filename, "rb")), os.path.getsize(src_filename) diff --git a/mittab/libs/backup/__init__.py b/mittab/libs/backup/__init__.py new file mode 100644 index 000000000..7d87825d4 --- /dev/null +++ b/mittab/libs/backup/__init__.py @@ -0,0 +1,55 @@ +import shutil +import time +import os +from wsgiref.util import FileWrapper + +from django.conf import settings + +from mittab.apps.tab.models import TabSettings +from mittab.libs import errors +from mittab.settings import BASE_DIR +from mittab.libs.backup.strategies.local_dump import LocalDump + + +def _generate_unique_key(base): + if LocalDump(base).exists(): + return "%s_%s" % (base, int(time.time())) + else: + return base + +def backup_round(dst_filename=None, round_number=None, btime=None): + if round_number is None: + round_number = TabSettings.get("cur_round", "no-round-number") + + if btime is None: + btime = int(time.time()) + + print("Trying to backup to backups directory") + if dst_filename is None: + dst_filename = "site_round_%i_%i" % (round_number, btime) + + dst_filename = _generate_unique_key(dst_filename) + return LocalDump(dst_filename).backup() + + +def handle_backup(f): + dst_key = _generate_unique_key(f.name) + print(("Tried to write {}".format(dst_key))) + try: + return LocalDump.from_upload(dst_key, f) + except Exception: + errors.emit_current_exception() + + +def list_backups(): + print("Checking backups directory") + return [dump.key for dump in LocalDump.all()] + + +def restore_from_backup(src_key): + print("Restoring from backups directory") + return LocalDump(src_key).restore() + + +def get_wrapped_file(src_key): + return LocalDump(src_key).downloadable() diff --git a/mittab/libs/backup/strategies/local_dump.py b/mittab/libs/backup/strategies/local_dump.py new file mode 100644 index 000000000..a992b111b --- /dev/null +++ b/mittab/libs/backup/strategies/local_dump.py @@ -0,0 +1,58 @@ +import os +from io import StringIO +from shutil import copyfileobj +from wsgiref.util import FileWrapper + +from django.core.management import call_command + +from mittab.settings import BASE_DIR + + +BACKUP_PREFIX = os.path.join(BASE_DIR, "mittab") +BACKUP_PATH = os.path.join(BACKUP_PREFIX, "backups") +SUFFIX = ".dump.json" + +if not os.path.exists(BACKUP_PATH): + os.makedirs(BACKUP_PATH) + +class LocalDump: + def __init__(self, key): + self.key = key + + @classmethod + def all(cls): + all_names = os.listdir(BACKUP_PATH) + def key_from_filename(name): + return name[:-len(SUFFIX)] + return [cls(key_from_filename(name)) for name in all_names] + + @classmethod + def from_upload(cls, key, upload): + dst_filename = os.path.join(BACKUP_PATH, key + SUFFIX) + with open(dst_filename, "wb+") as destination: + for chunk in upload.chunks(): + destination.write(chunk) + return cls(key) + + def downloadable(self): + src_filename = self._get_backup_filename() + return FileWrapper(open(src_filename, "rb")), os.path.getsize(src_filename) + + def backup(self): + out = StringIO() + call_command("dumpdata", stdout=out) + with open(self._get_backup_filename(), "w") as f: + out.seek(0) + copyfileobj(out, f) + + def restore(self): + return call_command("loaddata", self._get_backup_filename()) + + def exists(self): + return os.path.exists(self._get_backup_filename()) + + def _get_backup_filename(self): + key = self.key + if len(key) < len(SUFFIX) or not key.endswith(SUFFIX): + key += ".dump.json" + return os.path.join(BACKUP_PATH, key) diff --git a/mittab/settings.py b/mittab/settings.py index 91a30abdd..28a34b9b9 100644 --- a/mittab/settings.py +++ b/mittab/settings.py @@ -39,8 +39,12 @@ DATABASES = { "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": os.path.join(BASE_DIR, "mittab", "pairing_db.sqlite3"), + "ENGINE": "django.db.backends.mysql", + "NAME": "mittab", + "USER": os.environ.get("MYSQL_USER", "root"), + "PASSWORD": os.environ.get("MYSQL_PASSWORD", ""), + "HOST": os.environ.get("MITTAB_DB_HOST", "127.0.0.1"), + "PORT": os.environ.get("MYSQL_PORT", "3306"), "ATOMIC_REQUESTS": True, } } diff --git a/requirements.txt b/requirements.txt index dd97a335a..2f3fae243 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ django-localflavor==1.0 django-webpack-loader==0.6.0 django-polymorphic==2.0.3 gunicorn==19.7.1 +mysqlclient==1.4.4 xlrd==0.9.4 xlwt==1.3.0 pylint==2.3.1 @@ -17,5 +18,6 @@ pytest-cov==2.7.1 pytest-django==3.4.8 PyYAML==5.1.2 raven==6.4.0 +requests==2.22.0 selenium==3.141.0 splinter==0.10.0 From 1c9e3aff6c3d468b43809e5940bbff4922a0b4de Mon Sep 17 00:00:00 2001 From: Ben Muschol Date: Mon, 11 Nov 2019 09:15:50 -0800 Subject: [PATCH 06/27] Fix backup/restore --- bin/dev-server | 1 + mittab/libs/backup/strategies/local_dump.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/dev-server b/bin/dev-server index 8c114a6bc..6435a049a 100755 --- a/bin/dev-server +++ b/bin/dev-server @@ -7,6 +7,7 @@ import sys server_env = os.environ.copy() server_env["DEBUG"] = "1" +server_env["MITTAB_DB_HOST"] = "127.0.0.1" webpack_args = ["./node_modules/.bin/webpack", "--config", "webpack.config.js", "--watch"] server_args = ["python", "manage.py", "runserver"] diff --git a/mittab/libs/backup/strategies/local_dump.py b/mittab/libs/backup/strategies/local_dump.py index a992b111b..7b0e4f4f4 100644 --- a/mittab/libs/backup/strategies/local_dump.py +++ b/mittab/libs/backup/strategies/local_dump.py @@ -40,12 +40,14 @@ def downloadable(self): def backup(self): out = StringIO() - call_command("dumpdata", stdout=out) + exclude = ["contenttypes", "auth.permission", "sessions.session"] + call_command("dumpdata", stdout=out, exclude=exclude, natural_foreign=True) with open(self._get_backup_filename(), "w") as f: out.seek(0) copyfileobj(out, f) def restore(self): + call_command("flush", interactive=False) return call_command("loaddata", self._get_backup_filename()) def exists(self): From 33d54f9cab89a0878213c8859f0a47c873c7deab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Tue, 12 Nov 2019 14:25:04 -0500 Subject: [PATCH 07/27] Add team codes to DebateXML export (#278) --- mittab/apps/tab/archive.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mittab/apps/tab/archive.py b/mittab/apps/tab/archive.py index 1ca986066..7867c8ea2 100644 --- a/mittab/apps/tab/archive.py +++ b/mittab/apps/tab/archive.py @@ -106,6 +106,8 @@ def add_participants(self): "id": TEAM_ID_PREFIX + str(team.id), "name": team.name }) + if team.team_code is not None: + team_tag.set("code", team.team_code) institutions = SCHOOL_ID_PREFIX + str(team.school_id) if team.hybrid_school_id is not None: From c6f61766f171850d474d746d5fbcdc2151453e94 Mon Sep 17 00:00:00 2001 From: Ben Muschol Date: Thu, 21 Nov 2019 21:08:38 -0800 Subject: [PATCH 08/27] Initial setup --- docker-compose.yml | 1 + mittab/settings.py | 1 + 2 files changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 8407a2b63..c64039fc7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,6 +27,7 @@ services: - "3306" volumes: - my-db:/var/lib/mysql + command: ['mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci'] env_file: - .env - .env.secret diff --git a/mittab/settings.py b/mittab/settings.py index 28a34b9b9..cb0ad0c66 100644 --- a/mittab/settings.py +++ b/mittab/settings.py @@ -41,6 +41,7 @@ "default": { "ENGINE": "django.db.backends.mysql", "NAME": "mittab", + "OPTIONS": {"charset": "utf8mb4"}, "USER": os.environ.get("MYSQL_USER", "root"), "PASSWORD": os.environ.get("MYSQL_PASSWORD", ""), "HOST": os.environ.get("MITTAB_DB_HOST", "127.0.0.1"), From 0754ce19164b80ca4172ef0ab16179f07bd2fc95 Mon Sep 17 00:00:00 2001 From: Ben Muschol Date: Thu, 21 Nov 2019 21:11:15 -0800 Subject: [PATCH 09/27] Get it working --- .../tab/migrations/0013_auto_20191122_0510.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 mittab/apps/tab/migrations/0013_auto_20191122_0510.py diff --git a/mittab/apps/tab/migrations/0013_auto_20191122_0510.py b/mittab/apps/tab/migrations/0013_auto_20191122_0510.py new file mode 100644 index 000000000..8d29216c8 --- /dev/null +++ b/mittab/apps/tab/migrations/0013_auto_20191122_0510.py @@ -0,0 +1,23 @@ +# Generated by Django 2.1.5 on 2019-11-22 05:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tab', '0012_merge_20191017_0109'), + ] + + operations = [ + migrations.AlterField( + model_name='judge', + name='ballot_code', + field=models.CharField(blank=True, max_length=255, null=True, unique=True), + ), + migrations.AlterField( + model_name='team', + name='team_code', + field=models.CharField(blank=True, max_length=255, null=True, unique=True), + ), + ] From 0f20010b9694289ef1cbb9f73893c2d12c40f393 Mon Sep 17 00:00:00 2001 From: Ben Muschol Date: Fri, 22 Nov 2019 14:09:09 -0800 Subject: [PATCH 10/27] Disable ATOMIC_REQUESTS (#281) --- mittab/settings.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mittab/settings.py b/mittab/settings.py index cb0ad0c66..089efe049 100644 --- a/mittab/settings.py +++ b/mittab/settings.py @@ -31,8 +31,6 @@ "mittab.apps.tab.middleware.Login", ) -ATOMIC_REQUESTS = True - ROOT_URLCONF = "mittab.urls" WSGI_APPLICATION = "mittab.wsgi.application" @@ -46,7 +44,6 @@ "PASSWORD": os.environ.get("MYSQL_PASSWORD", ""), "HOST": os.environ.get("MITTAB_DB_HOST", "127.0.0.1"), "PORT": os.environ.get("MYSQL_PORT", "3306"), - "ATOMIC_REQUESTS": True, } } From 5af2f35195e397055fd5fa6a7f3aab11c305aa33 Mon Sep 17 00:00:00 2001 From: Ben Muschol Date: Fri, 29 Nov 2019 13:08:51 -0500 Subject: [PATCH 11/27] Redirect traffic to a page w/o db access during backup write/restore (#285) --- mittab/apps/tab/middleware.py | 22 +++++++++++++++++++ mittab/libs/backup/__init__.py | 39 ++++++++++++++++++++++++---------- mittab/settings.py | 1 + 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/mittab/apps/tab/middleware.py b/mittab/apps/tab/middleware.py index 4ee5c0281..be5f5caf5 100644 --- a/mittab/apps/tab/middleware.py +++ b/mittab/apps/tab/middleware.py @@ -1,8 +1,10 @@ import re from django.contrib.auth.views import LoginView +from django.http import HttpResponse from mittab.apps.tab.helpers import redirect_and_flash_info +from mittab.libs.backup import is_backup_active LOGIN_WHITELIST = ("/accounts/login/", "/pairings/pairinglist/", "/pairings/missing_ballots/", "/e_ballots/", "/404/", @@ -13,6 +15,7 @@ class Login: """This middleware requires a login for every view""" + def __init__(self, get_response): self.get_response = get_response @@ -31,3 +34,22 @@ def __call__(self, request): path="/accounts/login/?next=%s" % request.path) else: return self.get_response(request) + +class FailoverDuringBackup: + """ + Redirect traffic during a backup to a page which won't do any database + reads/writes + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if is_backup_active(): + return HttpResponse( + """ + A backup is in process. Try again in a few seconds. + If you were submitting a form, you will need to re-submit it. + """ + ) + return self.get_response(request) diff --git a/mittab/libs/backup/__init__.py b/mittab/libs/backup/__init__.py index 7d87825d4..9e6833023 100644 --- a/mittab/libs/backup/__init__.py +++ b/mittab/libs/backup/__init__.py @@ -11,6 +11,18 @@ from mittab.libs.backup.strategies.local_dump import LocalDump +ACTIVE_BACKUP_KEY = "MITTAB_ACTIVE_BACKUP" +ACTIVE_BACKUP_VAL = "1" + + +class ActiveBackupContextManager: + def __enter__(self): + os.environ[ACTIVE_BACKUP_KEY] = ACTIVE_BACKUP_VAL + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + os.environ[ACTIVE_BACKUP_KEY] = "0" + def _generate_unique_key(base): if LocalDump(base).exists(): return "%s_%s" % (base, int(time.time())) @@ -18,18 +30,19 @@ def _generate_unique_key(base): return base def backup_round(dst_filename=None, round_number=None, btime=None): - if round_number is None: - round_number = TabSettings.get("cur_round", "no-round-number") + with ActiveBackupContextManager() as _: + if round_number is None: + round_number = TabSettings.get("cur_round", "no-round-number") - if btime is None: - btime = int(time.time()) + if btime is None: + btime = int(time.time()) - print("Trying to backup to backups directory") - if dst_filename is None: - dst_filename = "site_round_%i_%i" % (round_number, btime) + print("Trying to backup to backups directory") + if dst_filename is None: + dst_filename = "site_round_%i_%i" % (round_number, btime) - dst_filename = _generate_unique_key(dst_filename) - return LocalDump(dst_filename).backup() + dst_filename = _generate_unique_key(dst_filename) + return LocalDump(dst_filename).backup() def handle_backup(f): @@ -47,9 +60,13 @@ def list_backups(): def restore_from_backup(src_key): - print("Restoring from backups directory") - return LocalDump(src_key).restore() + with ActiveBackupContextManager() as _: + print("Restoring from backups directory") + return LocalDump(src_key).restore() def get_wrapped_file(src_key): return LocalDump(src_key).downloadable() + +def is_backup_active(): + return str(os.environ.get(ACTIVE_BACKUP_KEY, "0")) == ACTIVE_BACKUP_VAL diff --git a/mittab/settings.py b/mittab/settings.py index 089efe049..10923ccff 100644 --- a/mittab/settings.py +++ b/mittab/settings.py @@ -22,6 +22,7 @@ "webpack_loader", "bootstrap4", "polymorphic") MIDDLEWARE = ( + "mittab.apps.tab.middleware.FailoverDuringBackup", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", From df2559f6dcf7257a387825a130d790db9fc83f61 Mon Sep 17 00:00:00 2001 From: Ben Muschol Date: Sat, 30 Nov 2019 22:54:26 -0500 Subject: [PATCH 12/27] Use mysqldump for backups (#287) * Add mysqldump * Finish multi-part backup * Build my own docker image I guess * ? * Double the timeout * I'm dumb --- .circleci/config.yml | 3 +- Dockerfile.test | 6 ++ mittab/libs/backup/strategies/local_dump.py | 79 ++++++++++++++++----- nginx.conf | 8 +-- 4 files changed, 75 insertions(+), 21 deletions(-) create mode 100644 Dockerfile.test diff --git a/.circleci/config.yml b/.circleci/config.yml index 07f17925e..64dea6896 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,9 +4,10 @@ orbs: jobs: build: docker: - - image: circleci/python:3.7.3-node-browsers + - image: benmusch/mittab-test:0.0.1 environment: MYSQL_PASSWORD: secret + MITTAB_DB_HOST: 127.0.0.1 - image: circleci/mysql:5.7 environment: MYSQL_ROOT_PASSWORD: secret diff --git a/Dockerfile.test b/Dockerfile.test new file mode 100644 index 000000000..3076b24be --- /dev/null +++ b/Dockerfile.test @@ -0,0 +1,6 @@ +FROM circleci/python:3.7.3-node-browsers + +RUN sudo apt-get update +RUN sudo apt-get upgrade -y +RUN sudo apt-get install +RUN sudo apt-get install -y default-mysql-client diff --git a/mittab/libs/backup/strategies/local_dump.py b/mittab/libs/backup/strategies/local_dump.py index 7b0e4f4f4..9d50de648 100644 --- a/mittab/libs/backup/strategies/local_dump.py +++ b/mittab/libs/backup/strategies/local_dump.py @@ -1,20 +1,26 @@ import os -from io import StringIO -from shutil import copyfileobj +import time +import subprocess from wsgiref.util import FileWrapper -from django.core.management import call_command +from mittab import settings -from mittab.settings import BASE_DIR - -BACKUP_PREFIX = os.path.join(BASE_DIR, "mittab") +BACKUP_PREFIX = os.path.join(settings.BASE_DIR, "mittab") BACKUP_PATH = os.path.join(BACKUP_PREFIX, "backups") -SUFFIX = ".dump.json" +SUFFIX = ".dump.sql" if not os.path.exists(BACKUP_PATH): os.makedirs(BACKUP_PATH) +DB_SETTINGS = settings.DATABASES["default"] +DB_HOST = DB_SETTINGS["HOST"] +DB_NAME = DB_SETTINGS["NAME"] +DB_USER = DB_SETTINGS["USER"] +DB_PASS = DB_SETTINGS["PASSWORD"] +DB_PORT = DB_SETTINGS["PORT"] + + class LocalDump: def __init__(self, key): self.key = key @@ -39,16 +45,25 @@ def downloadable(self): return FileWrapper(open(src_filename, "rb")), os.path.getsize(src_filename) def backup(self): - out = StringIO() - exclude = ["contenttypes", "auth.permission", "sessions.session"] - call_command("dumpdata", stdout=out, exclude=exclude, natural_foreign=True) - with open(self._get_backup_filename(), "w") as f: - out.seek(0) - copyfileobj(out, f) + subprocess.check_call(self._dump_cmd(self._get_backup_filename())) def restore(self): - call_command("flush", interactive=False) - return call_command("loaddata", self._get_backup_filename()) + tmp_filename = "backup_before_restore_%s%s" % (int(time.time()), SUFFIX) + tmp_full_path = os.path.join(BACKUP_PATH, tmp_filename) + try: + subprocess.check_call(self._dump_cmd(tmp_full_path)) + except Exception as e: + os.remove(tmp_full_path) + raise e + + try: + with open(self._get_backup_filename()) as stdin: + subprocess.check_call(self._restore_cmd(), stdin=stdin) + os.remove(tmp_full_path) + except Exception as e: + with open(tmp_full_path) as stdin: + subprocess.check_call(self._restore_cmd(), stdin=stdin) + raise e def exists(self): return os.path.exists(self._get_backup_filename()) @@ -56,5 +71,37 @@ def exists(self): def _get_backup_filename(self): key = self.key if len(key) < len(SUFFIX) or not key.endswith(SUFFIX): - key += ".dump.json" + key += SUFFIX return os.path.join(BACKUP_PATH, key) + + def _restore_cmd(self): + cmd = [ + "mysql", + DB_NAME, + "--port={}".format(DB_PORT), + "--host={}".format(DB_HOST), + "--user={}".format(DB_USER), + ] + + if DB_PASS: + cmd.append("--password={}".format(DB_PASS)) + + return cmd + + + def _dump_cmd(self, dst): + cmd = [ + "mysqldump", + DB_NAME, + "--quick", + "--lock-all-tables", + "--port={}".format(DB_PORT), + "--host={}".format(DB_HOST), + "--user={}".format(DB_USER), + "--result-file={}".format(dst), + ] + + if DB_PASS: + cmd.append("--password={}".format(DB_PASS)) + + return cmd diff --git a/nginx.conf b/nginx.conf index 871aaf875..dc63fc971 100644 --- a/nginx.conf +++ b/nginx.conf @@ -16,10 +16,10 @@ server { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_connect_timeout 300; - proxy_send_timeout 300; - proxy_read_timeout 300; - send_timeout 300; + proxy_connect_timeout 600; + proxy_send_timeout 600; + proxy_read_timeout 600; + send_timeout 600; } } From 0e356fc7f0b2213e5308c0328749a66e04880724 Mon Sep 17 00:00:00 2001 From: Ben Muschol Date: Sat, 16 May 2020 14:44:11 -0700 Subject: [PATCH 13/27] Switch to pipenv for package management (#291) --- .circleci/config.yml | 45 ++--- .env => .env.example | 8 +- .env.secret | 0 .gitignore | 1 + Dockerfile.web | 6 +- Pipfile | 34 ++++ Pipfile.lock | 431 +++++++++++++++++++++++++++++++++++++++++++ README.md | 11 +- bin/dev-server | 1 - docker-compose.yml | 2 - requirements.txt | 23 --- 11 files changed, 500 insertions(+), 62 deletions(-) rename .env => .env.example (65%) delete mode 100644 .env.secret create mode 100644 Pipfile create mode 100644 Pipfile.lock delete mode 100644 requirements.txt diff --git a/.circleci/config.yml b/.circleci/config.yml index 64dea6896..5e8b4d26f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,6 +8,7 @@ jobs: environment: MYSQL_PASSWORD: secret MITTAB_DB_HOST: 127.0.0.1 + PIPENV_VENV_IN_PROJECT: true - image: circleci/mysql:5.7 environment: MYSQL_ROOT_PASSWORD: secret @@ -20,27 +21,23 @@ jobs: steps: - checkout - - restore_cache: - keys: - - v1-dependencies-{{ checksum "requirements.txt" }} - - v1-dependencies- + key: deps-v2-{{ checksum "Pipfile.lock" }} - run: name: install dependencies command: | - python3 -m venv venv - . venv/bin/activate - pip install -r requirements.txt + sudo pip install pipenv + pipenv install npm install - save_cache: paths: - - ./venv - key: v1-dependencies-{{ checksum "requirements.txt" }} + - .venv + key: deps-v2-{{ checksum "Pipfile.lock" }} - run: - # Our primary container isn't MYSQL so run a sleep command until it's ready. + # Our primary container isn't MYSQL so run a sleep command until it's ready. name: Waiting for MySQL to be ready command: | for i in `seq 1 10`; @@ -54,14 +51,12 @@ jobs: - run: name: setup db command: | - . venv/bin/activate - ./bin/setup password + pipenv run ./bin/setup password - run: name: run tests command: | - . venv/bin/activate - pytest --junitxml=test-reports/junit.xml --cov=mittab --cov-report xml:cov.xml --circleci-parallelize mittab/ + pipenv run pytest --junitxml=test-reports/junit.xml --cov=mittab --cov-report xml:cov.xml --circleci-parallelize mittab/ no_output_timeout: 20m - store_test_results: @@ -75,27 +70,27 @@ jobs: file: ./cov.xml lint: docker: - # note: this is 3.5.7 because pylint doesn't yet support 3.7 - - image: circleci/python:3.5.7-node + - image: benmusch/mittab-test:0.0.1 + environment: + PIPENV_VENV_IN_PROJECT: true steps: - checkout - restore_cache: - keys: - - v1-dependencies-{{ checksum "requirements.txt" }} - - v1-dependencies- - + key: deps-v2-{{ checksum "Pipfile.lock" }} - run: name: install dependencies command: | - python3 -m venv venv - . venv/bin/activate - pip install -r requirements.txt + sudo pip install pipenv + pipenv install npm install + - save_cache: + paths: + - .venv + key: deps-v2-{{ checksum "Pipfile.lock" }} - run: name: Lint -- Python command: | - . venv/bin/activate - pylint mittab + pipenv run pylint mittab - run: name: Lint -- JS command: npm run lint diff --git a/.env b/.env.example similarity index 65% rename from .env rename to .env.example index fdf1b6a97..530c4b1e2 100644 --- a/.env +++ b/.env.example @@ -3,8 +3,10 @@ MYSQL_USER=root MYSQL_HOST=localhost # to be overridden in .env.secret -MYSQL_PASSWORD=secret -MYSQL_ROOT_PASSWORD=secret +MYSQL_PASSWORD= +MYSQL_ROOT_PASSWORD= # defined in the docker-compose file -MITTAB_DB_HOST=mysql +MITTAB_DB_HOST=127.0.0.1 + +DEBUG=1 diff --git a/.env.secret b/.env.secret deleted file mode 100644 index e69de29bb..000000000 diff --git a/.gitignore b/.gitignore index 54d8e4b39..b5e669bda 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,4 @@ typings/ # Output of 'npm pack' *.tgz +.env diff --git a/Dockerfile.web b/Dockerfile.web index e23352ab0..7ae1f2750 100644 --- a/Dockerfile.web +++ b/Dockerfile.web @@ -8,10 +8,12 @@ RUN curl -sL https://deb.nodesource.com/setup_11.x | bash RUN apt-get install nodejs WORKDIR /var/www/tab -COPY requirements.txt ./ +COPY Pipfile ./ +COPY Pipfile.lock ./ COPY package.json ./ COPY package-lock.json ./ -RUN pip install -r requirements.txt +RUN pip install pipenv +RUN pipenv install --deploy --system # setup django COPY manage.py ./ diff --git a/Pipfile b/Pipfile new file mode 100644 index 000000000..4fe07a03e --- /dev/null +++ b/Pipfile @@ -0,0 +1,34 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] +haikunator = "==2.1" +mock = "==1.0.1" +django-bootstrap4 = "==0.0.8" +django-localflavor = "==1.0" +django-webpack-loader = "==0.6.0" +django-polymorphic = "==2.0.3" +gunicorn = "==19.7.1" +mysqlclient = "==1.4.4" +xlrd = "==0.9.4" +xlwt = "==1.3.0" +pylint = "==2.3.1" +pylint-django = "==2.0.9" +pylint-quotes = "==0.2.1" +pytest = "==4.4.1" +pytest-circleci-parallelized = "==0.0.4" +pytest-cov = "==2.7.1" +pytest-django = "==3.4.8" +raven = "==6.4.0" +requests = "==2.22.0" +selenium = "==3.141.0" +splinter = "==0.10.0" +Django = "==2.1.5" +PyYAML = "==5.1.2" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 000000000..3eb25b177 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,431 @@ +{ + "_meta": { + "hash": { + "sha256": "b9c06b9a7ba06483106b4f0fdc96795d9a91135a435e49c018b0a01be8eca778" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "astroid": { + "hashes": [ + "sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1", + "sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38" + ], + "version": "==2.4.1" + }, + "atomicwrites": { + "hashes": [ + "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197", + "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a" + ], + "version": "==1.4.0" + }, + "attrs": { + "hashes": [ + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + ], + "version": "==19.3.0" + }, + "certifi": { + "hashes": [ + "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", + "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" + ], + "version": "==2020.4.5.1" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "coverage": { + "hashes": [ + "sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a", + "sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355", + "sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65", + "sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7", + "sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9", + "sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1", + "sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0", + "sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55", + "sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c", + "sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6", + "sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef", + "sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019", + "sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e", + "sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0", + "sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf", + "sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24", + "sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2", + "sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c", + "sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4", + "sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0", + "sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd", + "sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04", + "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e", + "sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730", + "sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2", + "sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768", + "sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796", + "sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7", + "sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a", + "sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489", + "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052" + ], + "version": "==5.1" + }, + "django": { + "hashes": [ + "sha256:a32c22af23634e1d11425574dce756098e015a165be02e4690179889b207c7a8", + "sha256:d6393918da830530a9516bbbcbf7f1214c3d733738779f06b0f649f49cc698c3" + ], + "index": "pypi", + "version": "==2.1.5" + }, + "django-bootstrap4": { + "hashes": [ + "sha256:9f115534ae8d052d397201f3d716c10d7c9832b422e44dd7382418c6f274df18" + ], + "index": "pypi", + "version": "==0.0.8" + }, + "django-localflavor": { + "hashes": [ + "sha256:77f5f5642f1f7464b4d097215063bc9daf9b053b1bc1a9c993cadb02fe0fa7b8", + "sha256:c3443b2d4eb810e7762a8be2b693266827c03a034af874796ae0bd173c0e9180" + ], + "index": "pypi", + "version": "==1.0" + }, + "django-polymorphic": { + "hashes": [ + "sha256:1fb5505537bcaf71cfc951ff94c4e3ba83c761eaca04b7b2ce9cb63937634ea5", + "sha256:79e7df455fdc8c3d28d38b7ab8323fc21d109a162b8ca282119e0e9ce8db7bdb" + ], + "index": "pypi", + "version": "==2.0.3" + }, + "django-webpack-loader": { + "hashes": [ + "sha256:60bab6b9a037a5346fad12d2a70a6bc046afb33154cf75ed640b93d3ebd5f520", + "sha256:970b968c2a8975fb7eff56a3bab5d0d90d396740852d1e0c50c5cfe2b824199a" + ], + "index": "pypi", + "version": "==0.6.0" + }, + "gunicorn": { + "hashes": [ + "sha256:75af03c99389535f218cc596c7de74df4763803f7b63eb09d77e92b3956b36c6", + "sha256:eee1169f0ca667be05db3351a0960765620dad53f53434262ff8901b68a1b622" + ], + "index": "pypi", + "version": "==19.7.1" + }, + "haikunator": { + "hashes": [ + "sha256:66f68b15345b279f78a5fffd4ab56cfb19a9dbb1f41b7f442472efd4cb83458e", + "sha256:91ee3949a3a613cac037ddde0b16b17062e248376b11491436e49d5ddc75ff9b" + ], + "index": "pypi", + "version": "==2.1" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "importlib-metadata": { + "hashes": [ + "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", + "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" + ], + "markers": "python_version < '3.8'", + "version": "==1.6.0" + }, + "isort": { + "hashes": [ + "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", + "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" + ], + "version": "==4.3.21" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", + "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", + "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", + "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", + "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", + "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", + "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", + "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", + "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", + "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", + "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", + "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", + "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", + "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", + "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", + "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", + "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", + "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", + "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", + "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", + "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" + ], + "version": "==1.4.3" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "mock": { + "hashes": [ + "sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc", + "sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f" + ], + "index": "pypi", + "version": "==1.0.1" + }, + "more-itertools": { + "hashes": [ + "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be", + "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982" + ], + "markers": "python_version > '2.7'", + "version": "==8.3.0" + }, + "mysqlclient": { + "hashes": [ + "sha256:79a498ddda955e488f80c82a6392bf6e07c323d48db236033f33825665d8ba5c", + "sha256:8c3b61d89f7daaeab6aad6bf4c4bc3ef30bec1a8169f94dc59aea87ba2fabf80", + "sha256:9c737cc55a5dc8dd3583a942d5a9b21be58d16f00f5fefca4e575e7d9682e98c" + ], + "index": "pypi", + "version": "==1.4.4" + }, + "pluggy": { + "hashes": [ + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + ], + "version": "==0.13.1" + }, + "py": { + "hashes": [ + "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", + "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" + ], + "version": "==1.8.1" + }, + "pylint": { + "hashes": [ + "sha256:5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09", + "sha256:723e3db49555abaf9bf79dc474c6b9e2935ad82230b10c1138a71ea41ac0fff1" + ], + "index": "pypi", + "version": "==2.3.1" + }, + "pylint-django": { + "hashes": [ + "sha256:c0562562bffbdc97a26d007d818231348633282bec66ba445540a036a0ae76f5", + "sha256:e4abeef83f6f6577951ca0b2d12f73fc0c53dd33272fee4982c8cb42e4ae64ad" + ], + "index": "pypi", + "version": "==2.0.9" + }, + "pylint-plugin-utils": { + "hashes": [ + "sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a", + "sha256:57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a" + ], + "version": "==0.6" + }, + "pylint-quotes": { + "hashes": [ + "sha256:5332fa8b48ce5d8780df196d684f38c00614f8233fdc4c67efbdc0bbe7dc51d7", + "sha256:c53d2a63b4cd16c9fa426d243de6396910ff04c65001efdbea37427d401fce72" + ], + "index": "pypi", + "version": "==0.2.1" + }, + "pytest": { + "hashes": [ + "sha256:3773f4c235918987d51daf1db66d51c99fac654c81d6f2f709a046ab446d5e5d", + "sha256:b7802283b70ca24d7119b32915efa7c409982f59913c1a6c0640aacf118b95f5" + ], + "index": "pypi", + "version": "==4.4.1" + }, + "pytest-circleci-parallelized": { + "hashes": [ + "sha256:2cd5c232e4c7b7c2a53c021cb3fbd2fa601675904c9da25a152cd2c7a1ddef99" + ], + "index": "pypi", + "version": "==0.0.4" + }, + "pytest-cov": { + "hashes": [ + "sha256:2b097cde81a302e1047331b48cadacf23577e431b61e9c6f49a1170bbe3d3da6", + "sha256:e00ea4fdde970725482f1f35630d12f074e121a23801aabf2ae154ec6bdd343a" + ], + "index": "pypi", + "version": "==2.7.1" + }, + "pytest-django": { + "hashes": [ + "sha256:30d773f1768e8f214a3106f1090e00300ce6edfcac8c55fd13b675fe1cbd1c85", + "sha256:4d3283e774fe1d40630ee58bf34929b83875e4751b525eeb07a7506996eb42ee" + ], + "index": "pypi", + "version": "==3.4.8" + }, + "pytz": { + "hashes": [ + "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", + "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" + ], + "version": "==2020.1" + }, + "pyyaml": { + "hashes": [ + "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", + "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", + "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", + "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", + "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", + "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", + "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", + "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", + "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", + "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", + "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", + "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", + "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + ], + "index": "pypi", + "version": "==5.1.2" + }, + "raven": { + "hashes": [ + "sha256:2c9cd4d8c267f57db625305aaa89e7dd852d6864c13c7b84f4d4500df07bebd9", + "sha256:b8edbb3335ed6c23cb168ced37fb523c1b91d9f3b0eddb90934249977841a902" + ], + "index": "pypi", + "version": "==6.4.0" + }, + "requests": { + "hashes": [ + "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", + "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + ], + "index": "pypi", + "version": "==2.22.0" + }, + "selenium": { + "hashes": [ + "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c", + "sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d" + ], + "index": "pypi", + "version": "==3.141.0" + }, + "six": { + "hashes": [ + "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", + "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" + ], + "version": "==1.14.0" + }, + "splinter": { + "hashes": [ + "sha256:2d9f370536e6c1607824f5538e0bff9808bc02f086b07622b3790424dd3daff4", + "sha256:5d9913bddb6030979c18d6801578813b02bbf8a03b43fb057f093228ed876d62" + ], + "index": "pypi", + "version": "==0.10.0" + }, + "typed-ast": { + "hashes": [ + "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", + "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", + "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", + "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", + "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", + "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", + "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", + "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", + "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", + "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", + "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", + "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", + "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", + "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", + "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", + "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", + "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", + "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", + "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", + "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", + "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" + ], + "markers": "implementation_name == 'cpython' and python_version < '3.8'", + "version": "==1.4.1" + }, + "urllib3": { + "hashes": [ + "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", + "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" + ], + "version": "==1.25.9" + }, + "wrapt": { + "hashes": [ + "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" + ], + "version": "==1.12.1" + }, + "xlrd": { + "hashes": [ + "sha256:3b06b6c24970be82a38aa22f77d8f65bf5286dd801bfb62e6b9d168d08ba91cc", + "sha256:8e8d3359f39541a6ff937f4030db54864836a06e42988c452db5b6b86d29ea72" + ], + "index": "pypi", + "version": "==0.9.4" + }, + "xlwt": { + "hashes": [ + "sha256:a082260524678ba48a297d922cc385f58278b8aa68741596a87de01a9c628b2e", + "sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88" + ], + "index": "pypi", + "version": "==1.3.0" + }, + "zipp": { + "hashes": [ + "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", + "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" + ], + "version": "==3.1.0" + } + }, + "develop": {} +} diff --git a/README.md b/README.md index 3c5c59372..1b733f49f 100644 --- a/README.md +++ b/README.md @@ -44,18 +44,17 @@ git clone cd mit-tab # make sure to use python3 for the virtualenv -virtualenv venv -source venv/bin/activate -pip install -r requirements.txt +pip install pipenv +pipenv install # set-up webpack assets npm install # load test data. username: tab password: password -python manage.py loaddata testing_db +pipenv run python manage.py loaddata testing_db # Simultaneously runs webpack and the python server -./bin/dev-server +pipenv run ./bin/dev-server ``` **Note**: the `bin/dev-server` script is new and not tested on many set-ups. If you @@ -70,7 +69,7 @@ have any issues, you can accomplish the same thing by running: /node_modules/.bin/webpack --config webpack.config.js --watch # run the Django server: -DEBUG=1 python manage.py runserver +pipenv run python manage.py runserver ``` ### Testing diff --git a/bin/dev-server b/bin/dev-server index 6435a049a..f242ac420 100755 --- a/bin/dev-server +++ b/bin/dev-server @@ -16,7 +16,6 @@ webpack_proc = subprocess.Popen(webpack_args, stdout=sys.stdout, stderr=sys.stde server_proc = subprocess.Popen(server_args, stdout=sys.stdout, stderr=sys.stderr, env=server_env) def on_interrupt(*args): - print("Exiting webpack and python...", flush=True) webpack_proc.terminate() server_proc.terminate() sys.exit(0) diff --git a/docker-compose.yml b/docker-compose.yml index c64039fc7..ecc32b535 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,6 @@ services: - mysql:mysql env_file: - .env - - .env.secret command: /usr/local/bin/gunicorn mittab.wsgi:application -w 2 -b :8000 -t 300 mysql: @@ -30,7 +29,6 @@ services: command: ['mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci'] env_file: - .env - - .env.secret nginx: restart: always diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 2f3fae243..000000000 --- a/requirements.txt +++ /dev/null @@ -1,23 +0,0 @@ -Django==2.1.5 -haikunator==2.1 -mock==1.0.1 -django-bootstrap4==0.0.8 -django-localflavor==1.0 -django-webpack-loader==0.6.0 -django-polymorphic==2.0.3 -gunicorn==19.7.1 -mysqlclient==1.4.4 -xlrd==0.9.4 -xlwt==1.3.0 -pylint==2.3.1 -pylint-django==2.0.9 -pylint-quotes==0.2.1 -pytest==4.4.1 -pytest-circleci-parallelized==0.0.4 -pytest-cov==2.7.1 -pytest-django==3.4.8 -PyYAML==5.1.2 -raven==6.4.0 -requests==2.22.0 -selenium==3.141.0 -splinter==0.10.0 From 7348aa97dff7dba8a40ccc20a3fd955d20113861 Mon Sep 17 00:00:00 2001 From: "Rodda R. John" Date: Sat, 16 May 2020 16:49:35 -0700 Subject: [PATCH 14/27] Finalizing outrounds implementation (#292) --- .eslintignore | 1 + assets/js/index.js | 1 + assets/js/outround.js | 185 +++++ bin/dev-server | 2 +- docs/Advanced-Topics.md | 82 ++- docs/Outrounds.md | 29 + docs/index.md | 1 + mittab/apps/tab/admin.py | 15 + mittab/apps/tab/forms.py | 81 ++- mittab/apps/tab/judge_views.py | 5 +- .../management/commands/simulate_rounds.py | 5 +- mittab/apps/tab/middleware.py | 5 +- .../migrations/0012_breakingteam_outround.py | 38 + .../migrations/0013_merge_20191015_1156.py | 14 + .../migrations/0014_team_break_preference.py | 18 + .../tab/migrations/0015_outround_sidelock.py | 18 + .../tab/migrations/0016_outround_choice.py | 18 + .../migrations/0017_merge_20191106_1831.py | 14 + .../migrations/0018_merge_20200124_1633.py | 14 + mittab/apps/tab/models.py | 107 ++- mittab/apps/tab/outround_pairing_views.py | 663 ++++++++++++++++++ mittab/apps/tab/pairing_views.py | 2 + mittab/apps/tab/team_views.py | 5 + mittab/apps/tab/templatetags/tags.py | 5 + mittab/apps/tab/views.py | 5 +- mittab/libs/data_import/import_teams.py | 3 +- mittab/libs/errors.py | 4 + mittab/libs/outround_tab_logic/__init__.py | 5 + .../outround_tab_logic/bracket_generation.py | 27 + mittab/libs/outround_tab_logic/checks.py | 136 ++++ mittab/libs/outround_tab_logic/helpers.py | 2 + mittab/libs/outround_tab_logic/pairing.py | 182 +++++ .../tests/tab_logic/test_outround_pairing.py | 91 +++ mittab/templates/base/_navigation.html | 2 + mittab/templates/outrounds/_form.html | 24 + mittab/templates/outrounds/ballot.html | 24 + mittab/templates/outrounds/forum_result.html | 20 + mittab/templates/outrounds/pairing_base.html | 114 +++ mittab/templates/outrounds/pairing_card.html | 121 ++++ .../templates/outrounds/pretty_pairing.html | 146 ++++ mittab/templates/pairing/pairing_control.html | 7 + mittab/templates/registration/login.html | 10 + mittab/templates/tab/batch_checkin.html | 8 +- mittab/templates/tab/room_batch_checkin.html | 10 +- mittab/urls.py | 54 +- package.json | 3 +- settings.yaml | 39 +- 47 files changed, 2308 insertions(+), 57 deletions(-) create mode 100644 .eslintignore create mode 100644 assets/js/outround.js create mode 100644 docs/Outrounds.md create mode 100644 mittab/apps/tab/migrations/0012_breakingteam_outround.py create mode 100644 mittab/apps/tab/migrations/0013_merge_20191015_1156.py create mode 100644 mittab/apps/tab/migrations/0014_team_break_preference.py create mode 100644 mittab/apps/tab/migrations/0015_outround_sidelock.py create mode 100644 mittab/apps/tab/migrations/0016_outround_choice.py create mode 100644 mittab/apps/tab/migrations/0017_merge_20191106_1831.py create mode 100644 mittab/apps/tab/migrations/0018_merge_20200124_1633.py create mode 100644 mittab/apps/tab/outround_pairing_views.py create mode 100644 mittab/libs/outround_tab_logic/__init__.py create mode 100644 mittab/libs/outround_tab_logic/bracket_generation.py create mode 100644 mittab/libs/outround_tab_logic/checks.py create mode 100644 mittab/libs/outround_tab_logic/helpers.py create mode 100644 mittab/libs/outround_tab_logic/pairing.py create mode 100644 mittab/libs/tests/tab_logic/test_outround_pairing.py create mode 100644 mittab/templates/outrounds/_form.html create mode 100644 mittab/templates/outrounds/ballot.html create mode 100644 mittab/templates/outrounds/forum_result.html create mode 100644 mittab/templates/outrounds/pairing_base.html create mode 100644 mittab/templates/outrounds/pairing_card.html create mode 100644 mittab/templates/outrounds/pretty_pairing.html diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..056783c3a --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +assets/webpack_bundles diff --git a/assets/js/index.js b/assets/js/index.js index 50e567e1e..cf69a692e 100644 --- a/assets/js/index.js +++ b/assets/js/index.js @@ -1,5 +1,6 @@ import "../css/app.scss"; import "./pairing"; +import "./outround"; import $ from "jquery"; diff --git a/assets/js/outround.js b/assets/js/outround.js new file mode 100644 index 000000000..0ade0c35f --- /dev/null +++ b/assets/js/outround.js @@ -0,0 +1,185 @@ +import $ from "jquery"; + +import quickSearchInit from "./quickSearch"; + +function cycleChoice(event) { + const button = $(this); + event.preventDefault(); + + const outroundId = button.data("outround-id"); + $.ajax({ + url: `/outround_choice/${outroundId}`, + success(result) { + button.html(result.data); + } + }); +} + +function populateTabCard(tabCardElement) { + const teamId = tabCardElement.attr("team-id"); + $.ajax({ + url: `/team/${teamId}/stats`, + success(result) { + const stats = result.result; + const text = [ + stats.effective_outround_seed, + stats.outround_seed, + stats.wins, + stats.total_speaks.toFixed(2), + stats.govs, + stats.opps, + stats.seed + ].join(" / "); + tabCardElement.attr( + "title", + "Effective Seed / Outround Seed / Wins / Speaks / Govs / Opps / Seed" + ); + tabCardElement.attr("href", `/team/card/${teamId}`); + tabCardElement.text(text); + } + }); +} + +function assignTeam(e) { + e.preventDefault(); + const teamId = $(e.target).attr("team-id"); + const oldTeamId = $(e.target).attr("src-team-id"); + const roundId = $(e.target).attr("round-id"); + const position = $(e.target).attr("position"); + const url = `/outround/pairings/assign_team/${roundId}/${position}/${teamId}`; + const alertMsg = ` + An error occured. + Refresh the page and try to fix any inconsistencies you may notice. + `; + + $.ajax({ + url, + success(result) { + if (result.success) { + const $container = $(`.row[round-id=${roundId}] .${position}-team`); + $container.find(".team-assign-button").attr("team-id", result.team.id); + $container.find(".team-link").text(result.team.name); + $container.find(".team-link").attr("href", `/team/${result.team.id}`); + $container.find(".outround-tabcard").attr("team-id", result.team.id); + + populateTabCard($(`.outround-tabcard[team-id=${result.team.id}]`)); + + const $oldTeamTabCard = $(`.outround-tabcard[team-id=${oldTeamId}]`); + if ($oldTeamTabCard) { + populateTabCard($oldTeamTabCard); + } + } else { + window.alert(alertMsg); + } + }, + failure() { + window.alert(alertMsg); + } + }); +} + +function populateAlternativeTeams() { + const $parent = $(this).parent(); + const teamId = $parent.attr("team-id"); + const roundId = $parent.attr("round-id"); + const position = $parent.attr("position"); + const url = `/outround/${roundId}/${teamId}/alternative_teams/${position}`; + + $.ajax({ + url, + success(result) { + $parent.find(".dropdown-menu").html(result); + $parent + .find(".dropdown-menu") + .find(".team-assign") + .click(assignTeam); + quickSearchInit($parent.find("#quick-search")); + $parent.find("#quick-search").focus(); + } + }); +} + +function assignJudge(e) { + e.preventDefault(); + const roundId = $(e.target).attr("round-id"); + const judgeId = $(e.target).attr("judge-id"); + const curJudgeId = $(e.target).attr("current-judge-id"); + const url = `/outround/${roundId}/assign_judge/${judgeId}/${curJudgeId || + ""}`; + + let $buttonWrapper; + if (curJudgeId) { + $buttonWrapper = $(`span[round-id=${roundId}][judge-id=${curJudgeId}]`); + } else { + $buttonWrapper = $(`span[round-id=${roundId}].unassigned`).first(); + } + const $button = $buttonWrapper.find(".btn-sm"); + $button.addClass("disabled"); + + $.ajax({ + url, + success(result) { + $button.removeClass("disabled"); + $buttonWrapper.removeClass("unassigned"); + $buttonWrapper.attr("judge-id", result.judge_id); + + const rank = result.judge_rank.toFixed(2); + $button.html(`${result.judge_name} (${rank})`); + $(`.judges span[round-id=${roundId}] .judge-toggle`).removeClass("chair"); + $(`.judges span[round-id=${roundId}][judge-id=${result.chair_id}] + .judge-toggle`).addClass("chair"); + } + }); +} + +function populateAlternativeJudges() { + const $parent = $(this).parent(); + const judgeId = $parent.attr("judge-id"); + const roundId = $parent.attr("round-id"); + const url = `/outround/${roundId}/alternative_judges/${judgeId || ""}`; + + $.ajax({ + url, + success(result) { + $parent.find(".dropdown-menu").html(result); + $parent + .find(".dropdown-menu") + .find(".judge-assign") + .click(assignJudge); + quickSearchInit($parent.find("#quick-search")); + $parent.find("#quick-search").focus(); + } + }); +} + +function togglePairingRelease(event) { + const button = $(".outround-release"); + const numTeams = button.data("num_teams"); + const typeOfRound = button.data("type_of_round"); + + event.preventDefault(); + $.ajax({ + url: `/outround_pairing/release/${numTeams}/${typeOfRound}`, + success(result) { + if (result.pairing_released) { + $("#close-pairings").removeClass("d-none"); + $("#release-pairings").addClass("d-none"); + } else { + $("#close-pairings").addClass("d-none"); + $("#release-pairings").removeClass("d-none"); + } + } + }); +} + +$(document).ready(() => { + $(".team.outround-tabcard").each((_, element) => { + populateTabCard($(element)); + }); + $(".choice-update").each((_, element) => { + $(element).click(cycleChoice); + }); + $(".outround-judge-toggle").click(populateAlternativeJudges); + $(".outround-team-toggle").click(populateAlternativeTeams); + $(".btn.outround-release").click(togglePairingRelease); +}); diff --git a/bin/dev-server b/bin/dev-server index f242ac420..a4de84a74 100755 --- a/bin/dev-server +++ b/bin/dev-server @@ -10,7 +10,7 @@ server_env["DEBUG"] = "1" server_env["MITTAB_DB_HOST"] = "127.0.0.1" webpack_args = ["./node_modules/.bin/webpack", "--config", "webpack.config.js", "--watch"] -server_args = ["python", "manage.py", "runserver"] +server_args = ["python", "manage.py", "runserver", "0.0.0.0:8001"] webpack_proc = subprocess.Popen(webpack_args, stdout=sys.stdout, stderr=sys.stderr) server_proc = subprocess.Popen(server_args, stdout=sys.stdout, stderr=sys.stderr, env=server_env) diff --git a/docs/Advanced-Topics.md b/docs/Advanced-Topics.md index 1abeac9ba..94b2e7921 100644 --- a/docs/Advanced-Topics.md +++ b/docs/Advanced-Topics.md @@ -41,37 +41,57 @@ these settings if provided regardless of if it showed up in the interface before you created it ```eval_rst -+---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| Name | Default value | Purpose | -+=====================+===============+==================================================================================================================================================================================================================================================================================================================+ -| `cur_round` | `1` | Control the current round of the tournament. **Don't modify this** | -+---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| `tot_rounds` | `5` | Number of in-rounds at the tournament | -+---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| `min_speak` | `0` | The minimum speaker score allowed to be submitted by the tab staff | -+---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| `max_speak` | `50` | The maximum speaker score allowed to be submitted by the tab staff | -+---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| `max_eballot_speak` | `35` | The maximum speaker score allowed to be submitted by an e-ballot Note: This value will be allowed. In other words, if the value is `35`, a 35 does not have to be justified to tab staff but a 36 does | -+---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| `min_eballot_speak` | `15` | The minimum speaker score allowed to be submitted by an e-ballot Note: This value will be allowed. In other words, if the value is `15`, a 15 does not have to be justified to tab staff but a 14 does | -+---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| `lenient_late` | `0` | The latest round where people who do not show up are given average speaks instead of speaks of 0 by default. Note: This only applies _before_ the round. If you change this after the round was paired, it will not take effect. You can manually allow a lenient_late in the admin interface. Docs coming soon. | -+---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| `pairing_released` | `0` | `1` if the pairings are publicly visible, `0` when they are not | -+---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| `fair_bye` | `1` | `1` if only unseeded teams should be eligible for a first round bye, `0` if all teams should be eligible | -+---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| `use_team_codes` | `0` | `1` if team codes should be display on the public pairings view, `0` if normal team names should be display | -+---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| `teams_public` | `0` | `1` if the teams list is publicly visible, `0` if not. | -+---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| `judges_public` | `0` | `1` if the judges list is publicly visible, `0` if not. | -+---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| `debaters_public` | `1` | `1` if the debaters are publicly visible on the public teams list. | -+---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| `team_codes_backend`| `0` | `1` if the team codes are used for all non-public views, `0` if not. | -+---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Name | Default value | Purpose | ++=====================+===============+========================================================================================================================================================================================================================================================================================================================================================================+ +| `cur_round` | `1` | Control the current round of the tournament. **Don't modify this** | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `tot_rounds` | `5` | Number of in-rounds at the tournament | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `min_speak` | `0` | The minimum speaker score allowed to be submitted by the tab staff | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `max_speak` | `50` | The maximum speaker score allowed to be submitted by the tab staff | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `max_eballot_speak` | `35` | The maximum speaker score allowed to be submitted by an e-ballot Note: This value will be allowed. In other words, if the value is `35`, a 35 does not have to be justified to tab staff but a 36 does | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `min_eballot_speak` | `15` | The minimum speaker score allowed to be submitted by an e-ballot Note: This value will be allowed. In other words, if the value is `15`, a 15 does not have to be justified to tab staff but a 14 does | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `lenient_late` | `0` | The latest round where people who do not show up are given average speaks instead of speaks of 0 by default. Note: This only applies _before_ the round. If you change this after the round was paired, it will not take effect. You can manually allow a lenient_late in the admin interface. Docs coming soon. | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `pairing_released` | `0` | `1` if the pairings are publicly visible, `0` when they are not | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `fair_bye` | `1` | `1` if only unseeded teams should be eligible for a first round bye, `0` if all teams should be eligible | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `use_team_codes` | `0` | `1` if team codes should be display on the public pairings view, `0` if normal team names should be display | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `teams_public` | `0` | `1` if the teams list is publicly visible, `0` if not. | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `judges_public` | `0` | `1` if the judges list is publicly visible, `0` if not. | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `debaters_public` | `1` | `1` if the debaters are publicly visible on the public teams list. | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `team_codes_backend`| `0` | `1` if the team codes are used for all non-public views, `0` if not. | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `nov_teams_to_break`| `4` | The number of novice teams to be included in the break (exclusive of novice teams who are varsity breaking) | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `var_teams_to_break`| `8` | The number of varsity teams to be included in the break. | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `nov_panel_size` | `3` | The number of judges on a novice outs panel. | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `var_panel_size` | `3` | The number of judges on a varsity outs panel. | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `var_to_nov` | `2` | The offset of the novice break to the varsity break. If novice semis happen when varsity quarters happen, the offset should be 1. If novice semis happen when varsity octofinals happen, the offset should be 2. | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `var_teams_visible` | `256` | The number of teams above which the varsity outround is visible. For example, if it were 8, quarterfinals and above would be visible, if it were 4, semifinals and above would be visible. | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `nov_teams_visible` | `256` | The number of teams above which the novice outround is visible. For example, if it were 8, quarterfinals and above would be visible, if it were 4, semifinals and above would be visible. | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `gov_opp_display` | `0` | A toggle to switch the outrounds view from displaying Gov and Opp (when value is 1) and Team 1 and Team 2 (when value is 2) | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `sidelock` | `0` | A toggle to indicate whether you are side-locking outrounds. If 1, then the gov opps for outrounds will not be random, but will adhere to sidelocks, and will be indicated on the pairing card, and bolded on the front-facing pairing display. | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `choice` | `0` | A toggle to indicate whether you would like to display who has choice, whether you would like this to be be indicated on the pairing card, and bolded on the front-facing pairing display. | ++---------------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ ``` Running a Tournament with a Non-Standard number of rounds diff --git a/docs/Outrounds.md b/docs/Outrounds.md new file mode 100644 index 000000000..6a54f1a7c --- /dev/null +++ b/docs/Outrounds.md @@ -0,0 +1,29 @@ +Outrounds +======== + +Outrounds are now here! + +Handling the Break +------------------ + +When you reach the final in-round of your tournament, the button that is normally "Prepare Next Round" should read "Break 'em". If this does not happen, please consult your `tot_rounds` TabSetting and ensure it is set properly. + +This will then break the appropriate number of teams as determined by the `nov_teams_to_break` and `var_teams_to_break` TabSetting. + +All teams have a `break_preference` field which determines which break they'd prefer. The only instance where this matters is if a novice team varsity breaks, but their break preference is set to novice, at which point they would not break varsity and only break to the novice bracket. + +It will also perform a number of checks to ensure you have enough rooms and judges. It does support paneled rounds (as long as they are consistently paneled) -- please consult the `nov_panel_size` and `var_panel_setting` for more information. It will let you pair if you don't have enough judges, but it will warn you. + +The other tab setting that must be set correctly in order to ensure judges / rooms are not double booked is the `var_to_nov` variable. If you would like varsity octafinals to happen at the same time as novice quarterfinals, this value should be `2` as the quotient of number of teams in varsity break rounds to the simultaneous novice break round is 2. If they are happening at the same time, the value should be `1`, if they are octafinals at the same time as semifinals, then it should be 4, etc. + +Managing Pairings +---------------- + +Currently MIT Tab does NOT support judge assignment, this must be done by hand. However, assuming you have set `var_to_nov` correctly (see above) AND all scratches are entered, the dropdowns on the pairing view will allow you to place judges very quickly. Furthermore, gov opp will be assigned randomly, so be sure to change that according to whichever type of system your tournament has chosen to use. + +Please also not the release pairings button that appears on the pairings page as normally appears. This will toggle the visibility of that round's pairings. + +Entering Results +---------------- + +Currently MIT Tab does not care about the type of decision (2-1, consensus, etc), but only the result. Please enter these before advancing to the next out-round (this functions as it does for in rounds). diff --git a/docs/index.md b/docs/index.md index 6078047c3..476d2f41d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,4 +13,5 @@ Contents * [Adding Teams, Rooms, Judges, and Debaters](Adding-Teams,-Judges,-Rooms-and-Debaters.md) * [Before Round 1](Before-Round-1.md) * [During the Tournament](Running-a-Tournament.md) +* [Outrounds](Outrounds.md) * [Advanced topics](Advanced-Topics.md) diff --git a/mittab/apps/tab/admin.py b/mittab/apps/tab/admin.py index eb58b831b..79aba2484 100644 --- a/mittab/apps/tab/admin.py +++ b/mittab/apps/tab/admin.py @@ -12,11 +12,24 @@ class Meta: fields = "__all__" +class OutroundAdminForm(forms.ModelForm): + chair = forms.ModelChoiceField(queryset=models.Judge.objects.all()) + + class Meta: + model = models.Outround + fields = "__all__" + + class RoundAdmin(admin.ModelAdmin): form = RoundAdminForm filter_horizontal = ("judges", ) +class OutroundAdmin(admin.ModelAdmin): + form = OutroundAdminForm + filter_horizontal = ("judges", ) + + class TeamAdmin(admin.ModelAdmin): filter_horizontal = ("debaters", ) @@ -34,3 +47,5 @@ class TeamAdmin(admin.ModelAdmin): admin.site.register(models.Room) admin.site.register(models.Bye) admin.site.register(models.NoShow) +admin.site.register(models.BreakingTeam) +admin.site.register(models.Outround, OutroundAdmin) diff --git a/mittab/apps/tab/forms.py b/mittab/apps/tab/forms.py index 448151349..0923f18a8 100644 --- a/mittab/apps/tab/forms.py +++ b/mittab/apps/tab/forms.py @@ -42,9 +42,13 @@ def __init__(self, *args, **kwargs): checkins = [ c.round_number for c in RoomCheckIn.objects.filter(room=room) ] - for i in range(num_rounds): + for i in range(-1, num_rounds): + # 0 is included as zero represents outrounds + label = "Checked in for round %s?" % (i + 1) + if i == -1: + label = "Checked in for outrounds?" self.fields["checkin_%s" % i] = forms.BooleanField( - label="Checked in for round %s?" % (i + 1), + label=label, initial=i + 1 in checkins, required=False) except Exception: @@ -90,9 +94,13 @@ def __init__(self, *args, **kwargs): checkins = [ c.round_number for c in CheckIn.objects.filter(judge=judge) ] - for i in range(num_rounds): + for i in range(-1, num_rounds): + # 0 is included as zero represents outrounds + label = "Checked in for round %s?" % (i + 1) + if i == -1: + label = "Checked in for outrounds?" self.fields["checkin_%s" % i] = forms.BooleanField( - label="Checked in for round %s?" % (i + 1), + label=label, initial=i + 1 in checkins, required=False) except Exception: @@ -101,7 +109,7 @@ def __init__(self, *args, **kwargs): def save(self, commit=True): judge = super(JudgeForm, self).save(commit) num_rounds = TabSettings.objects.get(key="tot_rounds").value - for i in range(num_rounds): + for i in range(-1, num_rounds): if "checkin_%s" % (i) in self.cleaned_data: should_be_checked_in = self.cleaned_data["checkin_%s" % (i)] checked_in = CheckIn.objects.filter(judge=judge, @@ -593,3 +601,66 @@ def score_panel(result, discard_minority): pprint.pprint(ranked) return ranked, final_winner + + +class OutroundResultEntryForm(forms.Form): + winner = forms.ChoiceField(label="Which team won the round?", + choices=Outround.VICTOR_CHOICES) + + def __init__(self, *args, **kwargs): + # Have to pop these off before sending to the super constructor + round_object = kwargs.pop("round_instance") + no_fill = False + if "no_fill" in kwargs: + kwargs.pop("no_fill") + no_fill = True + super(OutroundResultEntryForm, self).__init__(*args, **kwargs) + # If we already have information, fill that into the form + if round_object.victor != 0 and not no_fill: + self.fields["winner"].initial = round_object.victor + + self.fields["round_instance"] = forms.IntegerField( + initial=round_object.pk, widget=forms.HiddenInput()) + + if round_object.victor == 0 or no_fill: + return + + def clean(self): + cleaned_data = self.cleaned_data + try: + if cleaned_data["winner"] == Round.NONE: + self.add_error("winner", + self.error_class(["Someone has to win!"])) + + if self.errors: + return + + except Exception: + errors.emit_current_exception() + self.add_error( + "winner", + self.error_class( + ["Non handled error, preventing data contamination"])) + return cleaned_data + + def save(self, _commit=True): + cleaned_data = self.cleaned_data + round_obj = Outround.objects.get(pk=cleaned_data["round_instance"]) + + round_obj.victor = cleaned_data["winner"] + round_obj.save() + + round_obj = Outround.objects.get(pk=cleaned_data["round_instance"]) + if round_obj.victor > 0: + winning_team_seed = round_obj.winner.breaking_team.effective_seed + losing_team_seed = round_obj.loser.breaking_team.effective_seed + + if losing_team_seed < winning_team_seed: + round_obj.winner.breaking_team.effective_seed = losing_team_seed + round_obj.winner.breaking_team.save() + + breaking_team = round_obj.loser.breaking_team + breaking_team.effective_seed = breaking_team.seed + breaking_team.save() + + return round_obj diff --git a/mittab/apps/tab/judge_views.py b/mittab/apps/tab/judge_views.py index d13b6c690..d4fcb335d 100644 --- a/mittab/apps/tab/judge_views.py +++ b/mittab/apps/tab/judge_views.py @@ -212,7 +212,7 @@ 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 round_numbers: + 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)) @@ -226,7 +226,8 @@ def batch_checkin(request): def judge_check_in(request, judge_id, round_number): judge_id, round_number = int(judge_id), int(round_number) - if round_number < 1 or round_number > TabSettings.get("tot_rounds"): + if round_number < 0 or round_number > TabSettings.get("tot_rounds"): + # This is so that outrounds don't throw an error raise Http404("Round does not exist") judge = get_object_or_404(Judge, pk=judge_id) diff --git a/mittab/apps/tab/management/commands/simulate_rounds.py b/mittab/apps/tab/management/commands/simulate_rounds.py index 21a88f6d4..57cc6acf3 100644 --- a/mittab/apps/tab/management/commands/simulate_rounds.py +++ b/mittab/apps/tab/management/commands/simulate_rounds.py @@ -1,6 +1,6 @@ from django.core.management.base import BaseCommand -from mittab.apps.tab.models import Round, TabSettings, RoundStats +from mittab.apps.tab.models import Round, TabSettings, RoundStats, Debater from mittab.apps.tab.management.commands import utils @@ -19,7 +19,8 @@ def __simulate_round(self, round_obj): results = utils.generate_random_results(round_obj) for position in ["pm", "mg", "lo", "mo"]: - stat = RoundStats(debater=results[position + "_debater"], + debater = Debater.objects.get(pk=results[position + "_debater"]) + stat = RoundStats(debater=debater, round=round_obj, speaks=results[position + "_speaks"], ranks=results[position + "_ranks"], diff --git a/mittab/apps/tab/middleware.py b/mittab/apps/tab/middleware.py index be5f5caf5..307f140bb 100644 --- a/mittab/apps/tab/middleware.py +++ b/mittab/apps/tab/middleware.py @@ -8,7 +8,10 @@ LOGIN_WHITELIST = ("/accounts/login/", "/pairings/pairinglist/", "/pairings/missing_ballots/", "/e_ballots/", "/404/", - "/403/", "/500/", "/teams/", "/judges/") + "/403/", "/500/", "/teams/", "/judges/", + "/outround_pairings/pairinglist/0/", + "/outround_pairings/pairinglist/1/", + "/json") EBALLOT_REGEX = re.compile(r"/e_ballots/\S+") diff --git a/mittab/apps/tab/migrations/0012_breakingteam_outround.py b/mittab/apps/tab/migrations/0012_breakingteam_outround.py new file mode 100644 index 000000000..75844590e --- /dev/null +++ b/mittab/apps/tab/migrations/0012_breakingteam_outround.py @@ -0,0 +1,38 @@ +# Generated by Django 2.1.5 on 2019-10-14 22:21 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tab', '0011_auto_20191012_2018'), + ] + + operations = [ + migrations.CreateModel( + name='BreakingTeam', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('seed', models.IntegerField(default=-1)), + ('effective_seed', models.IntegerField(default=-1)), + ('type_of_team', models.IntegerField(choices=[(0, 'Varsity'), (1, 'Novice')], default=0)), + ('team', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='breaking_team', to='tab.Team')), + ], + ), + migrations.CreateModel( + name='Outround', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('num_teams', models.IntegerField()), + ('type_of_round', models.IntegerField(choices=[(0, 'Varsity'), (1, 'Novice')], default=0)), + ('victor', models.IntegerField(choices=[(0, 'UNKNOWN'), (1, 'GOV'), (2, 'OPP'), (3, 'GOV via Forfeit'), (4, 'OPP via Forfeit')], default=0)), + ('chair', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='chair_outround', to='tab.Judge')), + ('gov_team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gov_team_outround', to='tab.Team')), + ('judges', models.ManyToManyField(blank=True, related_name='judges_outrounds', to='tab.Judge')), + ('opp_team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opp_team_outround', to='tab.Team')), + ('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rooms_outrounds', to='tab.Room')), + ], + ), + ] diff --git a/mittab/apps/tab/migrations/0013_merge_20191015_1156.py b/mittab/apps/tab/migrations/0013_merge_20191015_1156.py new file mode 100644 index 000000000..d403befe7 --- /dev/null +++ b/mittab/apps/tab/migrations/0013_merge_20191015_1156.py @@ -0,0 +1,14 @@ +# Generated by Django 2.1.5 on 2019-10-15 11:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tab', '0012_breakingteam_outround'), + ('tab', '0011_roomcheckin'), + ] + + operations = [ + ] diff --git a/mittab/apps/tab/migrations/0014_team_break_preference.py b/mittab/apps/tab/migrations/0014_team_break_preference.py new file mode 100644 index 000000000..02194d400 --- /dev/null +++ b/mittab/apps/tab/migrations/0014_team_break_preference.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.5 on 2019-10-16 13:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tab', '0013_merge_20191015_1156'), + ] + + operations = [ + migrations.AddField( + model_name='team', + name='break_preference', + field=models.IntegerField(choices=[(0, 'Varsity'), (1, 'Novice')], default=0), + ), + ] diff --git a/mittab/apps/tab/migrations/0015_outround_sidelock.py b/mittab/apps/tab/migrations/0015_outround_sidelock.py new file mode 100644 index 000000000..f5f0ad7c3 --- /dev/null +++ b/mittab/apps/tab/migrations/0015_outround_sidelock.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.5 on 2019-10-16 15:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tab', '0014_team_break_preference'), + ] + + operations = [ + migrations.AddField( + model_name='outround', + name='sidelock', + field=models.BooleanField(default=False), + ), + ] diff --git a/mittab/apps/tab/migrations/0016_outround_choice.py b/mittab/apps/tab/migrations/0016_outround_choice.py new file mode 100644 index 000000000..70cce5969 --- /dev/null +++ b/mittab/apps/tab/migrations/0016_outround_choice.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.5 on 2019-10-18 17:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tab', '0015_outround_sidelock'), + ] + + operations = [ + migrations.AddField( + model_name='outround', + name='choice', + field=models.IntegerField(choices=[(0, 'Unknown'), (1, 'Gov'), (2, 'Opp')], default=0), + ), + ] diff --git a/mittab/apps/tab/migrations/0017_merge_20191106_1831.py b/mittab/apps/tab/migrations/0017_merge_20191106_1831.py new file mode 100644 index 000000000..5cb45c350 --- /dev/null +++ b/mittab/apps/tab/migrations/0017_merge_20191106_1831.py @@ -0,0 +1,14 @@ +# Generated by Django 2.1.5 on 2019-11-06 18:31 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tab', '0012_merge_20191017_0109'), + ('tab', '0016_outround_choice'), + ] + + operations = [ + ] diff --git a/mittab/apps/tab/migrations/0018_merge_20200124_1633.py b/mittab/apps/tab/migrations/0018_merge_20200124_1633.py new file mode 100644 index 000000000..0d339674c --- /dev/null +++ b/mittab/apps/tab/migrations/0018_merge_20200124_1633.py @@ -0,0 +1,14 @@ +# Generated by Django 2.1.5 on 2020-01-24 16:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tab', '0017_merge_20191106_1831'), + ('tab', '0013_auto_20191122_0510'), + ] + + operations = [ + ] diff --git a/mittab/apps/tab/models.py b/mittab/apps/tab/models.py index 1311ef18d..c4e659b5f 100644 --- a/mittab/apps/tab/models.py +++ b/mittab/apps/tab/models.py @@ -143,6 +143,16 @@ class Team(ModelWithTiebreaker): null=True, unique=True) + VARSITY = 0 + NOVICE = 1 + BREAK_PREFERENCE_CHOICES = ( + (VARSITY, "Varsity"), + (NOVICE, "Novice") + ) + + break_preference = models.IntegerField(default=0, + choices=BREAK_PREFERENCE_CHOICES) + def set_unique_team_code(self): haikunator = Haikunator() @@ -207,6 +217,25 @@ class Meta: ordering = ["name"] +class BreakingTeam(models.Model): + VARSITY = 0 + NOVICE = 1 + TYPE_CHOICES = ( + (VARSITY, "Varsity"), + (NOVICE, "Novice") + ) + + 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) + + class Judge(models.Model): name = models.CharField(max_length=30, unique=True) rank = models.DecimalField(max_digits=4, decimal_places=2) @@ -247,7 +276,8 @@ def __str__(self): return self.name def affiliations_display(self): - return ", ".join([school.name for school in self.schools.all()]) + 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) @@ -320,6 +350,81 @@ class Meta: ordering = ["name"] +class Outround(models.Model): + VARSITY = 0 + NOVICE = 1 + 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") + judges = models.ManyToManyField(Judge, blank=True, related_name="judges_outrounds") + UNKNOWN = 0 + GOV = 1 + OPP = 2 + GOV_VIA_FORFEIT = 3 + OPP_VIA_FORFEIT = 4 + VICTOR_CHOICES = ( + (UNKNOWN, "UNKNOWN"), + (GOV, "GOV"), + (OPP, "OPP"), + (GOV_VIA_FORFEIT, "GOV via Forfeit"), + (OPP_VIA_FORFEIT, "OPP via Forfeit"), + ) + 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) + + 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) + + @property + def winner(self): + if self.victor in [self.GOV, self.GOV_VIA_FORFEIT]: + return self.gov_team + elif self.victor in [2, 4]: + return self.opp_team + return None + + @property + def loser(self): + if not self.winner: + return None + + if self.winner == self.gov_team: + return self.opp_team + return self.gov_team + + class Round(models.Model): round_number = models.IntegerField() gov_team = models.ForeignKey(Team, related_name="gov_team", diff --git a/mittab/apps/tab/outround_pairing_views.py b/mittab/apps/tab/outround_pairing_views.py new file mode 100644 index 000000000..f7115ed0b --- /dev/null +++ b/mittab/apps/tab/outround_pairing_views.py @@ -0,0 +1,663 @@ +import random +import math + +from django.shortcuts import render, get_object_or_404 +from django.http import JsonResponse +from django.contrib.auth.decorators import permission_required +from django.db.models import Q +from django.shortcuts import redirect, reverse +from django.utils import timezone + +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.apps.tab.forms import OutroundResultEntryForm +import mittab.libs.tab_logic as tab_logic +import mittab.libs.outround_tab_logic as outround_tab_logic +from mittab.libs.outround_tab_logic import offset_to_quotient +import mittab.libs.backup as backup + + +@permission_required("tab.tab_settings.can_change", login_url="/403/") +def pair_next_outround(request, num_teams, type_of_round): + if request.method == "POST": + backup.backup_round("before_pairing_%s_%s" % + (num_teams / 2, type_of_round)) + + Outround.objects.filter(num_teams__lt=num_teams, + type_of_round=type_of_round).delete() + + outround_tab_logic.pair(type_of_round) + + return redirect_and_flash_success( + request, "Success!", path=reverse("outround_pairing_view", + kwargs={ + "num_teams": int(num_teams / 2), + "type_of_round": type_of_round + })) + + # See if we can pair the round + title = "Pairing Outrounds" + current_round_number = 0 + + previous_round_number = TabSettings.get("tot_rounds", 5) + + check_status = [] + + judges = outround_tab_logic.have_enough_judges_type(type_of_round) + rooms = outround_tab_logic.have_enough_rooms_type(type_of_round) + + msg = "Enough judges checked in for Out-rounds? Need {0}, have {1}".format( + judges[1][1], judges[1][0]) + + if num_teams <= 2: + check_status.append(("Have more rounds?", "No", "Not enough teams")) + else: + check_status.append(("Have more rounds?", "Yes", "Have enough teams!")) + + if judges[0]: + check_status.append((msg, "Yes", "Judges are checked in")) + else: + check_status.append((msg, "No", "Not enough judges")) + + msg = "N/2 Rooms available Round Out-rounds? Need {0}, have {1}".format( + rooms[1][1], rooms[1][0]) + if rooms[0]: + check_status.append((msg, "Yes", "Rooms are checked in")) + else: + check_status.append((msg, "No", "Not enough rooms")) + + round_label = "[%s] Ro%s" % ("N" if type_of_round else "V", + num_teams) + msg = "All Rounds properly entered for Round %s" % ( + round_label) + ready_to_pair = "Yes" + ready_to_pair_alt = "Checks passed!" + try: + outround_tab_logic.have_properly_entered_data(num_teams, type_of_round) + check_status.append((msg, "Yes", "All rounds look good")) + except PrevRoundNotEnteredError as e: + ready_to_pair = "No" + ready_to_pair_alt = str(e) + check_status.append( + (msg, "No", "Not all rounds are entered. %s" % str(e))) + + return render(request, "pairing/pair_round.html", locals()) + + +def get_outround_options(var_teams_to_break, + nov_teams_to_break): + outround_options = [] + + while not math.log(var_teams_to_break, 2) % 1 == 0: + var_teams_to_break += 1 + + while not math.log(nov_teams_to_break, 2) % 1 == 0: + nov_teams_to_break += 1 + + while var_teams_to_break > 1: + if Outround.objects.filter(type_of_round=BreakingTeam.VARSITY, + num_teams=var_teams_to_break).exists(): + outround_options.append( + (reverse("outround_pairing_view", kwargs={ + "type_of_round": BreakingTeam.VARSITY, + "num_teams": int(var_teams_to_break)}), + "[V] Ro%s" % (int(var_teams_to_break),)) + ) + var_teams_to_break /= 2 + + while nov_teams_to_break > 1: + if Outround.objects.filter(type_of_round=BreakingTeam.NOVICE, + num_teams=nov_teams_to_break).exists(): + outround_options.append( + (reverse("outround_pairing_view", kwargs={ + "type_of_round": BreakingTeam.NOVICE, + "num_teams": int(nov_teams_to_break)}), + "[N] Ro%s" % (int(nov_teams_to_break),)) + ) + nov_teams_to_break /= 2 + + return outround_options + + +@permission_required("tab.tab_settings.can_change", login_url="/403/") +def break_teams(request): + if request.method == "POST": + # Perform the break + backup.backup_round("before_the_break_%s" % (timezone.now().strftime("%H:%M"),)) + + success, msg = outround_tab_logic.perform_the_break() + + if success: + return redirect_and_flash_success( + request, msg, path="/outround_pairing" + ) + return redirect_and_flash_error( + request, msg, path="/" + ) + + # See if we can pair the round + title = "Pairing Outrounds" + current_round_number = 0 + + previous_round_number = TabSettings.get("tot_rounds", 5) + + check_status = [] + + msg = "All Rounds properly entered for Round %s" % ( + previous_round_number) + + ready_to_pair = "Yes" + ready_to_pair_alt = "Checks passed!" + try: + tab_logic.have_properly_entered_data(current_round_number) + check_status.append((msg, "Yes", "All rounds look good")) + except PrevRoundNotEnteredError as e: + ready_to_pair = "No" + ready_to_pair_alt = str(e) + check_status.append( + (msg, "No", "Not all rounds are entered. %s" % str(e))) + except ByeAssignmentError as e: + ready_to_pair = "No" + ready_to_pair_alt = str(e) + check_status.append( + (msg, "No", "You have a bye and results. %s" % str(e))) + except NoShowAssignmentError as e: + ready_to_pair = "No" + ready_to_pair_alt = str(e) + check_status.append( + (msg, "No", "You have a noshow and results. %s" % str(e))) + + rooms = outround_tab_logic.have_enough_rooms_before_break() + msg = "N/2 Rooms available Round Out-rounds? Need {0}, have {1}".format( + rooms[1][1], rooms[1][0]) + if rooms[0]: + check_status.append((msg, "Yes", "Rooms are checked in")) + else: + check_status.append((msg, "No", "Not enough rooms")) + + return render(request, "pairing/pair_round.html", locals()) + + +def outround_pairing_view(request, + type_of_round=BreakingTeam.VARSITY, + num_teams=None): + + choice = TabSettings.get("choice", 0) + + if num_teams is None: + num_teams = TabSettings.get("var_teams_to_break", 8) + + while not math.log(num_teams, 2) % 1 == 0: + num_teams += 1 + + return redirect("outround_pairing_view", + type_of_round=BreakingTeam.VARSITY, + num_teams=num_teams) + + pairing_released = False + + if type_of_round == BreakingTeam.VARSITY: + pairing_released = TabSettings.get("var_teams_visible", 256) <= num_teams + elif type_of_round == BreakingTeam.NOVICE: + pairing_released = TabSettings.get("nov_teams_visible", 256) <= num_teams + + label = "[%s] Ro%s" % ("V" if type_of_round == BreakingTeam.VARSITY else "N", + num_teams) + nov_teams_to_break = TabSettings.get("nov_teams_to_break") + var_teams_to_break = TabSettings.get("var_teams_to_break") + + if not nov_teams_to_break or not var_teams_to_break: + return redirect_and_flash_error(request, + "Please check your break tab settings", + path="/") + + outround_options = get_outround_options(var_teams_to_break, + nov_teams_to_break) + + outrounds = Outround.objects.filter(type_of_round=type_of_round, + num_teams=num_teams).all() + + judges_per_panel = TabSettings.get("var_panel_size", 3) \ + if type_of_round == BreakingTeam.VARSITY \ + else TabSettings.get("nov_panel_size", 3) + judge_slots = [i for i in range(1, judges_per_panel + 1)] + + var_to_nov = TabSettings.get("var_to_nov", 2) + + var_to_nov = offset_to_quotient(var_to_nov) + + other_round_num = num_teams / var_to_nov + if type_of_round == BreakingTeam.NOVICE: + other_round_num = num_teams * var_to_nov + + other_round_type = BreakingTeam.VARSITY \ + if type_of_round == BreakingTeam.NOVICE \ + else BreakingTeam.NOVICE + + pairing_exists = len(outrounds) > 0 + + lost_outrounds = [t.loser.id for t in Outround.objects.all() if t.loser] + + excluded_teams = BreakingTeam.objects.filter( + type_of_team=type_of_round + ).exclude( + team__id__in=lost_outrounds + ) + + excluded_teams = [t.team for t in excluded_teams] + + excluded_teams = [t for t in excluded_teams if not Outround.objects.filter( + type_of_round=type_of_round, + num_teams=num_teams, + gov_team=t + ).exists()] + + excluded_teams = [t for t in excluded_teams if not Outround.objects.filter( + type_of_round=type_of_round, + num_teams=num_teams, + opp_team=t + ).exists()] + + excluded_judges = Judge.objects.exclude( + judges_outrounds__num_teams=num_teams, + judges_outrounds__type_of_round=type_of_round, + ).exclude( + judges_outrounds__type_of_round=other_round_type, + judges_outrounds__num_teams=other_round_num + ).filter( + checkin__round_number=0 + ) + + non_checkins = Judge.objects.exclude( + judges_outrounds__num_teams=num_teams, + judges_outrounds__type_of_round=type_of_round + ).exclude( + judges_outrounds__type_of_round=other_round_type, + judges_outrounds__num_teams=other_round_num + ).exclude( + checkin__round_number=0 + ) + + available_rooms = Room.objects.exclude( + rooms_outrounds__num_teams=num_teams, + rooms_outrounds__type_of_round=type_of_round + ).exclude( + rooms_outrounds__num_teams=other_round_num, + rooms_outrounds__type_of_round=other_round_type + ) + + checked_in_rooms = [r.room for r in RoomCheckIn.objects.filter(round_number=0)] + available_rooms = [r for r in available_rooms if r in checked_in_rooms] + + size = max(list( + map( + len, + [excluded_teams, excluded_judges, non_checkins, available_rooms] + ))) + # The minimum rank you want to warn on + warning = 5 + excluded_people = list( + zip(*[ + x + [""] * (size - len(x)) for x in [ + list(excluded_teams), + list(excluded_judges), + list(non_checkins), + list(available_rooms) + ] + ])) + + return render(request, + "outrounds/pairing_base.html", + locals()) + + +def alternative_judges(request, round_id, judge_id=None): + round_obj = Outround.objects.get(id=int(round_id)) + round_gov, round_opp = round_obj.gov_team, round_obj.opp_team + # All of these variables are for the convenience of the template + try: + current_judge_id = int(judge_id) + current_judge_obj = Judge.objects.get(id=current_judge_id) + current_judge_name = current_judge_obj.name + current_judge_rank = current_judge_obj.rank + except TypeError: + current_judge_id, current_judge_obj, current_judge_rank = "", "", "" + current_judge_name = "No judge" + + var_to_nov = TabSettings.get("var_to_nov", 2) + + var_to_nov = offset_to_quotient(var_to_nov) + + other_round_num = round_obj.num_teams / var_to_nov + if round_obj.type_of_round == BreakingTeam.NOVICE: + other_round_num = round_obj.num_teams * var_to_nov + + other_round_type = BreakingTeam.NOVICE \ + if round_obj.type_of_round == BreakingTeam.VARSITY \ + else BreakingTeam.VARSITY + + excluded_judges = Judge.objects.exclude( + judges_outrounds__num_teams=round_obj.num_teams, + judges_outrounds__type_of_round=round_obj.type_of_round + ).exclude( + judges_outrounds__num_teams=other_round_num, + judges_outrounds__type_of_round=other_round_type + ).filter( + checkin__round_number=0 + ) + + query = Q( + judges_outrounds__num_teams=round_obj.num_teams, + judges_outrounds__type_of_round=round_obj.type_of_round + ) + query = query | Q( + judges_outrounds__num_teams=other_round_num, + judges_outrounds__type_of_round=other_round_type + ) + + included_judges = Judge.objects.filter(query) \ + .filter(checkin__round_number=0) \ + .distinct() + + def can_judge(judge, team1, team2): + query = Q(judge=judge, team=team1) | Q(judge=judge, team=team2) + return not Scratch.objects.filter(query).exists() + + excluded_judges = [(j.name, j.id, float(j.rank)) + for j in excluded_judges if can_judge(j, round_gov, round_opp)] + included_judges = [(j.name, j.id, float(j.rank)) + for j in included_judges if can_judge(j, round_gov, round_opp)] + + included_judges = sorted(included_judges, key=lambda x: -x[2]) + excluded_judges = sorted(excluded_judges, key=lambda x: -x[2]) + + return render(request, "pairing/judge_dropdown.html", locals()) + + +def alternative_teams(request, round_id, current_team_id, position): + round_obj = Outround.objects.get(pk=round_id) + current_team = Team.objects.get(pk=current_team_id) + + breaking_teams_by_type = [t.team.id + for t in BreakingTeam.objects.filter( + type_of_team=current_team.breaking_team.type_of_team + )] + + excluded_teams = Team.objects.filter( + id__in=breaking_teams_by_type + ).exclude( + gov_team_outround__num_teams=round_obj.num_teams + ).exclude( + opp_team_outround__num_teams=round_obj.num_teams + ).exclude(pk=current_team_id) + + included_teams = Team.objects.filter( + id__in=breaking_teams_by_type + ).exclude( + pk__in=excluded_teams + ) + + return render(request, "pairing/team_dropdown.html", locals()) + + +@permission_required("tab.tab_settings.can_change", login_url="/403/") +def assign_team(request, round_id, position, team_id): + try: + round_obj = Outround.objects.get(id=int(round_id)) + team_obj = Team.objects.get(id=int(team_id)) + + if position.lower() == "gov": + round_obj.gov_team = team_obj + elif position.lower() == "opp": + round_obj.opp_team = team_obj + else: + raise ValueError("Got invalid position: " + position) + round_obj.save() + + data = { + "success": True, + "team": { + "id": team_obj.id, + "name": team_obj.name + }, + } + except Exception: + emit_current_exception() + data = {"success": False} + return JsonResponse(data) + + +@permission_required("tab.tab_settings.can_change", login_url="/403/") +def assign_judge(request, round_id, judge_id, remove_id=None): + try: + round_obj = Outround.objects.get(id=int(round_id)) + judge_obj = Judge.objects.get(id=int(judge_id)) + round_obj.judges.add(judge_obj) + + if remove_id is not None: + remove_obj = Judge.objects.get(id=int(remove_id)) + round_obj.judges.remove(remove_obj) + + if remove_obj == round_obj.chair: + round_obj.chair = round_obj.judges.order_by("-rank").first() + elif not round_obj.chair: + round_obj.chair = judge_obj + + round_obj.save() + data = { + "success": True, + "chair_id": round_obj.chair.id, + "round_id": round_obj.id, + "judge_name": judge_obj.name, + "judge_rank": float(judge_obj.rank), + "judge_id": judge_obj.id + } + except Exception: + emit_current_exception() + data = {"success": False} + return JsonResponse(data) + + +def enter_result(request, + round_id, + form_class=OutroundResultEntryForm): + + round_obj = Outround.objects.get(id=round_id) + + redirect_to = reverse("outround_pairing_view", + kwargs={ + "num_teams": round_obj.num_teams, + "type_of_round": round_obj.type_of_round + }) + + if request.method == "POST": + form = form_class(request.POST, round_instance=round_obj) + if form.is_valid(): + try: + form.save() + except ValueError: + return redirect_and_flash_error( + request, "Invalid round result, could not remedy.") + return redirect_and_flash_success(request, + "Result entered successfully", + path=redirect_to) + else: + form_kwargs = {"round_instance": round_obj} + form = form_class(**form_kwargs) + + return render( + request, "outrounds/ballot.html", { + "form": form, + "title": "Entering Ballot for {}".format(round_obj), + "gov_team": round_obj.gov_team, + "opp_team": round_obj.opp_team, + }) + + +def pretty_pair(request, type_of_round=BreakingTeam.VARSITY, printable=False): + gov_opp_display = TabSettings.get("gov_opp_display", 0) + + round_number = 256 + + if type_of_round == BreakingTeam.VARSITY: + round_number = TabSettings.get("var_teams_visible", 256) + else: + round_number = TabSettings.get("nov_teams_visible", 256) + + round_pairing = Outround.objects.filter( + num_teams__gte=round_number, + type_of_round=type_of_round + ) + + unique_values = round_pairing.values_list("num_teams") + unique_values = list(set([value[0] for value in unique_values])) + unique_values.sort(key=lambda v: v, reverse=True) + + outround_pairings = [] + + for value in unique_values: + lost_outrounds = [t.loser.id for t in Outround.objects.all() if t.loser] + + excluded_teams = BreakingTeam.objects.filter( + type_of_team=type_of_round + ).exclude( + team__id__in=lost_outrounds + ) + + excluded_teams = [t.team for t in excluded_teams] + + excluded_teams = [t for t in excluded_teams if not Outround.objects.filter( + type_of_round=type_of_round, + num_teams=value, + gov_team=t + ).exists()] + + excluded_teams = [t for t in excluded_teams if not Outround.objects.filter( + type_of_round=type_of_round, + num_teams=value, + opp_team=t + ).exists()] + + outround_pairings.append({ + "label": "[%s] Ro%s" % ("N" if type_of_round else "V", value), + "rounds": Outround.objects.filter(num_teams=value, + type_of_round=type_of_round), + "excluded": excluded_teams + }) + + label = "%s Outrounds Pairings" % ("Novice" if type_of_round else "Varsity",) + + round_pairing = list(round_pairing) + + #We want a random looking, but constant ordering of the rounds + random.seed(0xBEEF) + random.shuffle(round_pairing) + round_pairing.sort(key=lambda r: r.gov_team.name) + paired_teams = [team.gov_team for team in round_pairing + ] + [team.opp_team for team in round_pairing] + + team_count = len(paired_teams) + + pairing_exists = True + #pairing_exists = TabSettings.get("pairing_released", 0) == 1 + printable = printable + + sidelock = TabSettings.get("sidelock", 0) + choice = TabSettings.get("choice", 0) + + return render(request, "outrounds/pretty_pairing.html", locals()) + + +def pretty_pair_print(request, type_of_round=BreakingTeam.VARSITY): + return pretty_pair(request, type_of_round, True) + + +def toggle_pairing_released(request, type_of_round, num_teams): + old = 256 + + if type_of_round == BreakingTeam.VARSITY: + old = TabSettings.get("var_teams_visible", 256) + + if old == num_teams: + TabSettings.set("var_teams_visible", num_teams * 2) + else: + TabSettings.set("var_teams_visible", num_teams) + else: + old = TabSettings.get("nov_teams_visible", 256) + + if old == num_teams: + TabSettings.set("nov_teams_visible", num_teams * 2) + else: + TabSettings.set("nov_teams_visible", num_teams) + + data = {"success": True, "pairing_released": not old == num_teams} + return JsonResponse(data) + + +def update_choice(request, outround_id): + outround = get_object_or_404(Outround, pk=outround_id) + + outround.choice += 1 + + if outround.choice == 3: + outround.choice = 0 + + outround.save() + data = {"success": True, + "data": "%s choice" % ( + outround.get_choice_display(), + )} + + return JsonResponse(data) + +def forum_view(request, type_of_round): + outrounds = Outround.objects.exclude( + victor=Outround.UNKNOWN + ).filter( + type_of_round=type_of_round + ) + + rounds = outrounds.values_list("num_teams") + rounds = [r[0] for r in rounds] + rounds = list(set(rounds)) + rounds.sort(key=lambda r: r, reverse=True) + + results = [] + + for _round in rounds: + to_add = {} + to_display = outrounds.filter(num_teams=_round) + + to_add["label"] = "[%s] Ro%s" % ("N" if type_of_round else "V", _round) + to_add["results"] = [] + + for outround in to_display: + to_add["results"] += [ + """[%s] %s (%s, %s) from %s%s (%s) drops to + [%s] %s (%s, %s) from %s%s (%s)""" % ( + outround.loser.breaking_team.seed, + outround.loser.display, + outround.loser.debaters.first().name, + outround.loser.debaters.last().name, + outround.loser.school.name, + " / " + outround.loser.hybrid_school.name \ + if outround.loser.hybrid_school else "", + "GOV" if outround.loser == outround.gov_team else "OPP", + outround.winner.breaking_team.seed, + outround.winner.display, + outround.winner.debaters.first().name, + outround.winner.debaters.last().name, + outround.winner.school.name, + " / " + outround.winner.hybrid_school.name \ + if outround.winner.hybrid_school else "", + "GOV" if outround.winner == outround.gov_team else "OPP", + ) + ] + + results.append(to_add) + + return render(request, + "outrounds/forum_result.html", + locals()) diff --git a/mittab/apps/tab/pairing_views.py b/mittab/apps/tab/pairing_views.py index 4d1547e3d..b8e8e0161 100644 --- a/mittab/apps/tab/pairing_views.py +++ b/mittab/apps/tab/pairing_views.py @@ -211,6 +211,8 @@ def view_round(request, round_number): errors, excluded_teams = [], [] round_pairing = list(Round.objects.filter(round_number=round_number)) + tot_rounds = TabSettings.get("tot_rounds", 5) + random.seed(1337) random.shuffle(round_pairing) round_pairing.sort(key=lambda x: tab_logic.team_comp(x, round_number), diff --git a/mittab/apps/tab/team_views.py b/mittab/apps/tab/team_views.py index 30dbb8f34..11958543f 100644 --- a/mittab/apps/tab/team_views.py +++ b/mittab/apps/tab/team_views.py @@ -390,6 +390,11 @@ def team_stats(request, team_id): stats["total_speaks"] = tab_logic.tot_speaks(team) stats["govs"] = tab_logic.num_govs(team) stats["opps"] = tab_logic.num_opps(team) + + if hasattr(team, "breaking_team"): + stats["outround_seed"] = team.breaking_team.seed + stats["effective_outround_seed"] = team.breaking_team.effective_seed + data = {"success": True, "result": stats} except Team.DoesNotExist: data = {"success": False} diff --git a/mittab/apps/tab/templatetags/tags.py b/mittab/apps/tab/templatetags/tags.py index 57b29e50a..5ebe9f818 100644 --- a/mittab/apps/tab/templatetags/tags.py +++ b/mittab/apps/tab/templatetags/tags.py @@ -21,6 +21,11 @@ def round_form(form, gov_team, opp_team): return {"form": form, "gov_team": gov_team, "opp_team": opp_team} +@register.inclusion_tag("outrounds/_form.html") +def outround_form(form): + return {"form": form} + + @register.filter("is_file_field") def is_file_field(field): if not hasattr(field, "field"): diff --git a/mittab/apps/tab/views.py b/mittab/apps/tab/views.py index 0a9d52f0f..7ef3a9d2f 100644 --- a/mittab/apps/tab/views.py +++ b/mittab/apps/tab/views.py @@ -246,7 +246,7 @@ def batch_checkin(request): round_numbers = list([i + 1 for i in range(TabSettings.get("tot_rounds"))]) for room in Room.objects.all(): checkins = [] - for round_number in round_numbers: + for round_number in [0] + round_numbers: # 0 is for outrounds checkins.append(room.is_checked_in_for_round(round_number)) rooms_and_checkins.append((room, checkins)) @@ -260,7 +260,8 @@ def batch_checkin(request): def room_check_in(request, room_id, round_number): room_id, round_number = int(room_id), int(round_number) - if round_number < 1 or round_number > TabSettings.get("tot_rounds"): + if round_number < 0 or round_number > TabSettings.get("tot_rounds"): + # 0 is so that outrounds don't throw an error raise Http404("Round does not exist") room = get_object_or_404(Room, pk=room_id) diff --git a/mittab/libs/data_import/import_teams.py b/mittab/libs/data_import/import_teams.py index 1aa920ccf..170cc6120 100644 --- a/mittab/libs/data_import/import_teams.py +++ b/mittab/libs/data_import/import_teams.py @@ -107,7 +107,8 @@ def import_row(self, row, row_number): "school": school.id, "hybrid_school": hybrid_school and hybrid_school.id, "debaters": [deb1_form.instance.id, deb2_form.instance.id], - "seed": team_seed + "seed": team_seed, + "break_preference": Team.VARSITY }) if team_form.is_valid(): diff --git a/mittab/libs/errors.py b/mittab/libs/errors.py index 9ba697843..5e5fa57bd 100644 --- a/mittab/libs/errors.py +++ b/mittab/libs/errors.py @@ -32,6 +32,10 @@ class NotEnoughRoomsError(Exception): pass +class BadBreak(Exception): + pass + + class JudgeAssignmentError(Exception): def __init__(self, reason=None): super(JudgeAssignmentError, self).__init__() diff --git a/mittab/libs/outround_tab_logic/__init__.py b/mittab/libs/outround_tab_logic/__init__.py new file mode 100644 index 000000000..e1ccd5816 --- /dev/null +++ b/mittab/libs/outround_tab_logic/__init__.py @@ -0,0 +1,5 @@ +from mittab.libs.outround_tab_logic.pairing import pair, perform_the_break +from mittab.libs.outround_tab_logic.checks import have_enough_judges, \ + have_enough_rooms, have_properly_entered_data, have_enough_judges_type, \ + have_enough_rooms_type, have_enough_rooms_before_break +from mittab.libs.outround_tab_logic.helpers import offset_to_quotient diff --git a/mittab/libs/outround_tab_logic/bracket_generation.py b/mittab/libs/outround_tab_logic/bracket_generation.py new file mode 100644 index 000000000..115fe7039 --- /dev/null +++ b/mittab/libs/outround_tab_logic/bracket_generation.py @@ -0,0 +1,27 @@ +import math + + +# Recursive function that generated bracket + +def branch(seed, level, limit): + # Level is how deep in the recursion basically + + # Limit is the depth of the recursion to get to 1, ie, for 8 teams + # this value would be 4 (dividing by 2) + level_sum = (2 ** level) + 1 + + # How many teams there are at the current spot + # Seed is where you are currently, think of it as a branch, you + # could be at the 1 seed, or the 5th branch, it's a tree. + + if limit == level + 1: + return ((seed, level_sum - seed),) + elif seed / 2 == 1: + return branch(seed, level + 1, limit) + \ + branch(level_sum - seed, level + 1, limit) + else: + return branch(level_sum - seed, level + 1, limit) + \ + branch(seed, level + 1, limit) + +def gen_bracket(num_teams): + return branch(1, 1, math.log(num_teams, 2) + 1) diff --git a/mittab/libs/outround_tab_logic/checks.py b/mittab/libs/outround_tab_logic/checks.py new file mode 100644 index 000000000..eb4e36ff6 --- /dev/null +++ b/mittab/libs/outround_tab_logic/checks.py @@ -0,0 +1,136 @@ +from mittab.apps.tab.models import * +from mittab.libs.errors import PrevRoundNotEnteredError + +from mittab.libs.outround_tab_logic.helpers import offset_to_quotient + + +def lost_teams(): + return [outround.loser.id + for outround in Outround.objects.all() + if outround.loser] + + +def have_enough_judges_type(type_of_round): + loser_ids = [outround.loser.id + for outround in Outround.objects.all() + if outround.loser] + + panel_size = 3 + + if type_of_round == BreakingTeam.VARSITY: + panel_size = TabSettings.get("var_panel_size", 3) + else: + panel_size = TabSettings.get("nov_panel_size", 3) + + teams_count = BreakingTeam.objects.filter( + type_of_team=type_of_round + ).exclude(team__id__in=loser_ids).count() + + judges_needed = ( + teams_count // 2 + ) * panel_size + + var_to_nov = TabSettings.get("var_to_nov", 2) + + var_to_nov = offset_to_quotient(var_to_nov) + + num_teams = teams_count + + other_round_num = num_teams / var_to_nov + if type_of_round == BreakingTeam.NOVICE: + other_round_num = num_teams * var_to_nov + + other_round_type = BreakingTeam.VARSITY \ + if type_of_round == BreakingTeam.NOVICE \ + else BreakingTeam.NOVICE + + num_in_use = Judge.objects.filter( + judges_outrounds__num_teams=other_round_num, + judges_outrounds__type_of_round=other_round_type + ).count() + + num_judges = CheckIn.objects.filter(round_number=0).count() - num_in_use + + if num_judges < judges_needed: + return False, (num_judges, judges_needed) + return True, (num_judges, judges_needed) + + +def have_enough_judges(): + var = have_enough_judges_type(BreakingTeam.VARSITY) + nov = have_enough_judges_type(BreakingTeam.NOVICE) + + return ( + var[0] and nov[0], + (var[1][0], var[1][1] + nov[1][1]) + ) + + +def have_enough_rooms_type(type_of_round): + loser_ids = [outround.loser.id + for outround in Outround.objects.all() + if outround.loser] + + num_teams = BreakingTeam.objects.filter( + type_of_team=type_of_round + ).exclude(team__id__in=loser_ids).count() + + rooms_needed = ( + num_teams // 2 + ) + + var_to_nov = TabSettings.get("var_to_nov", 2) + + var_to_nov = offset_to_quotient(var_to_nov) + + other_round_num = num_teams / var_to_nov + if type_of_round == BreakingTeam.NOVICE: + other_round_num = num_teams * var_to_nov + + other_round_type = BreakingTeam.VARSITY \ + if type_of_round == BreakingTeam.NOVICE \ + else BreakingTeam.NOVICE + + num_in_use = Room.objects.filter( + rooms_outrounds__num_teams=other_round_num, + rooms_outrounds__type_of_round=other_round_type + ).count() + + num_rooms = RoomCheckIn.objects.filter(round_number=0).count() - num_in_use + + if num_rooms < rooms_needed: + return False, (num_rooms, rooms_needed) + return True, (num_rooms, rooms_needed) + + +def have_enough_rooms(): + var = have_enough_rooms_type(BreakingTeam.VARSITY) + nov = have_enough_rooms_type(BreakingTeam.NOVICE) + + return ( + var[0] and nov[0], + (var[1][0], var[1][1] + nov[1][1]) + ) + + +def have_enough_rooms_before_break(): + var_breaking = TabSettings.get("var_teams_to_break", 8) + nov_breaking = TabSettings.get("nov_teams_to_break", 4) + + rooms_needed = var_breaking // 2 + rooms_needed += nov_breaking // 2 + + num_rooms = RoomCheckIn.objects.filter(round_number=0).count() + + return ( + rooms_needed <= num_rooms, (num_rooms, rooms_needed) + ) + + +def have_properly_entered_data(num_teams, type_of_round): + outrounds = Outround.objects.filter(num_teams=num_teams, + type_of_round=type_of_round, + victor=0).exists() + + if outrounds: + raise PrevRoundNotEnteredError() diff --git a/mittab/libs/outround_tab_logic/helpers.py b/mittab/libs/outround_tab_logic/helpers.py new file mode 100644 index 000000000..e49f8ecef --- /dev/null +++ b/mittab/libs/outround_tab_logic/helpers.py @@ -0,0 +1,2 @@ +def offset_to_quotient(offset): + return 2 ** offset diff --git a/mittab/libs/outround_tab_logic/pairing.py b/mittab/libs/outround_tab_logic/pairing.py new file mode 100644 index 000000000..559dc05a1 --- /dev/null +++ b/mittab/libs/outround_tab_logic/pairing.py @@ -0,0 +1,182 @@ +import math + +from mittab.apps.tab.models import * + +from mittab.libs.outround_tab_logic.checks import have_enough_rooms +from mittab.libs.outround_tab_logic.bracket_generation import gen_bracket +from mittab.libs.outround_tab_logic.helpers import offset_to_quotient +from mittab.libs.tab_logic import ( + have_properly_entered_data, + add_scratches_for_school_affil +) +from mittab.libs import errors +import mittab.libs.cache_logic as cache_logic + +from mittab.apps.tab.team_views import get_team_rankings + + +def perform_the_break(): + teams, nov_teams = cache_logic.cache_fxn_key( + get_team_rankings, + "team_rankings", + None + ) + + nov_teams_to_break = TabSettings.get("nov_teams_to_break") + var_teams_to_break = TabSettings.get("var_teams_to_break") + + if not nov_teams_to_break or not var_teams_to_break: + return False, "Please check your break tab settings" + + # This forces a refresh of the breaking teams + Outround.objects.all().delete() + BreakingTeam.objects.all().delete() + + current_seed = 1 + for team in teams: + if not team[0].break_preference == Team.VARSITY: + continue + + if current_seed > var_teams_to_break: + break + + BreakingTeam.objects.create(team=team[0], + seed=current_seed, + effective_seed=current_seed, + type_of_team=BreakingTeam.VARSITY) + current_seed += 1 + + current_seed = 1 + for nov_team in nov_teams: + if current_seed > nov_teams_to_break: + break + + if BreakingTeam.objects.filter(team=nov_team[0]).exists(): + continue + + BreakingTeam.objects.create(team=nov_team[0], + seed=current_seed, + effective_seed=current_seed, + type_of_team=BreakingTeam.NOVICE) + + current_seed += 1 + + pair(BreakingTeam.VARSITY) + pair(BreakingTeam.NOVICE) + + return True, "Success!" + + +def is_pairing_possible(num_teams): + if num_teams > Team.objects.count(): + raise errors.NotEnoughTeamsError() + + if num_teams < 2: + raise errors.NotEnoughTeamsError() + + # Check there are enough rooms + if not have_enough_rooms()[0]: + raise errors.NotEnoughRoomsError() + + # If we have results, they should be entered and there should be no + # byes or noshows for teams that debated + round_to_check = TabSettings.get("tot_rounds", 5) + have_properly_entered_data(round_to_check) + + +def get_next_available_room(num_teams, type_of_break): + base_queryset = Outround.objects.filter(num_teams=num_teams, + type_of_round=type_of_break) + + var_to_nov = TabSettings.get("var_to_nov", 2) + + var_to_nov = offset_to_quotient(var_to_nov) + + other_queryset = Outround.objects.filter(type_of_round=not type_of_break) + + if type_of_break == BreakingTeam.VARSITY: + other_queryset = other_queryset.filter(num_teams=num_teams / var_to_nov) + else: + other_queryset = other_queryset.filter(num_teams=num_teams * var_to_nov) + + rooms = [r.room + for r in RoomCheckIn.objects.filter(round_number=0) \ + .prefetch_related("room")] + rooms.sort(key=lambda r: r.rank, reverse=True) + + for room in rooms: + if not base_queryset.filter(room=room).exists() and \ + not other_queryset.filter(room=room).exists(): + return room + return None + + +def gov_team(team_one, team_two): + sidelock = TabSettings.get("sidelock", 0) + + if sidelock: + if Round.objects.filter(gov_team=team_one.team, + opp_team=team_two.team).exists(): + return True, team_two + elif Round.objects.filter(gov_team=team_two.team, + opp_team=team_one.team).exists(): + return True, team_one + return False, team_one + + +def pair(type_of_break=BreakingTeam.VARSITY): + add_scratches_for_school_affil() + + lost_outround = [t.loser.id for t in Outround.objects.all() if t.loser] + + base_queryset = BreakingTeam.objects.filter( + type_of_team=type_of_break + ).exclude( + team__id__in=lost_outround + ) + + num_teams = base_queryset.count() + + teams_for_bracket = num_teams + + while not math.log(teams_for_bracket, 2) % 1 == 0: + teams_for_bracket += 1 + + num_teams = teams_for_bracket + + is_pairing_possible(num_teams) + + Outround.objects.filter( + num_teams=num_teams + ).filter( + type_of_round=type_of_break + ).all().delete() + + if type_of_break == BreakingTeam.VARSITY: + TabSettings.set("var_outrounds_public", 0) + else: + TabSettings.set("nov_outrounds_public", 0) + + bracket = gen_bracket(num_teams) + + for pairing in bracket: + team_one = base_queryset.filter(effective_seed=pairing[0]).first() + team_two = base_queryset.filter(effective_seed=pairing[1]).first() + + print(pairing) + + if not team_one or not team_two: + continue + + sidelock, gov = gov_team(team_one, team_two) + opp = team_one if gov == team_two else team_two + + Outround.objects.create( + num_teams=num_teams, + type_of_round=type_of_break, + gov_team=gov.team, + opp_team=opp.team, + room=get_next_available_room(num_teams, + type_of_break=type_of_break), + sidelock=sidelock + ) diff --git a/mittab/libs/tests/tab_logic/test_outround_pairing.py b/mittab/libs/tests/tab_logic/test_outround_pairing.py new file mode 100644 index 000000000..4a03d1a08 --- /dev/null +++ b/mittab/libs/tests/tab_logic/test_outround_pairing.py @@ -0,0 +1,91 @@ +import random + +from django.test import TestCase +import pytest + +from mittab.apps.tab.models import * +from mittab.libs import outround_tab_logic + + +@pytest.mark.django_db +class TestOutroundPairingLogic(TestCase): + fixtures = ["testing_finished_db"] + pytestmark = pytest.mark.django_db + + def generate_checkins(self): + for r in Room.objects.all(): + RoomCheckIn.objects.create(room=r, + round_number=0) + + def test_break(self): + self.generate_checkins() + + outround_tab_logic.perform_the_break() + + def test_pairing(self): + self.generate_checkins() + + outround_tab_logic.perform_the_break() + outround_tab_logic.pair(BreakingTeam.NOVICE) + outround_tab_logic.pair(BreakingTeam.VARSITY) + + def enter_results(self, type_of_round): + outrounds = Outround.objects.filter(type_of_round=type_of_round).all() + + for outround in outrounds: + if not outround.victor: + outround.victor = random.randint(1, 2) + outround.save() + + def confirm_pairing(self, outrounds, num_teams): + for outround in outrounds: + assert (outround.gov_team.breaking_team.effective_seed + outround.opp_team.breaking_team.effective_seed) == (num_teams + 1) + + def test_all_outrounds(self): + self.generate_checkins() + + outround_tab_logic.perform_the_break() + + var_teams_to_break = TabSettings.get("var_teams_to_break", 8) + + while var_teams_to_break > 2: + outround_tab_logic.pair(BreakingTeam.VARSITY) + + outrounds = Outround.objects.filter( + type_of_round=BreakingTeam.VARSITY, + num_teams=var_teams_to_break + ) + + self.confirm_pairing( + outrounds, var_teams_to_break + ) + + self.enter_results(BreakingTeam.VARSITY) + + var_teams_to_break /= 2 + + def test_partials(self): + self.generate_checkins() + + TabSettings.set("var_teams_to_break", 7) + + outround_tab_logic.perform_the_break() + + var_teams_to_break = 8 + + while var_teams_to_break > 2: + outround_tab_logic.pair(BreakingTeam.VARSITY) + + outrounds = Outround.objects.filter( + type_of_round=BreakingTeam.VARSITY, + num_teams=var_teams_to_break + ) + + self.confirm_pairing( + outrounds, var_teams_to_break + ) + + self.enter_results(BreakingTeam.VARSITY) + + var_teams_to_break /= 2 + diff --git a/mittab/templates/base/_navigation.html b/mittab/templates/base/_navigation.html index 1c00d7ef1..06babd81b 100644 --- a/mittab/templates/base/_navigation.html +++ b/mittab/templates/base/_navigation.html @@ -34,6 +34,7 @@ {% url "view_scratches" as view_scratches %} {% url "add_scratch" as add_scratch %} {% url "settings_form" as settings_form %} +{% url "outround_pairing_view_default" as outround_pairings %}
  • diff --git a/mittab/templates/outrounds/_form.html b/mittab/templates/outrounds/_form.html new file mode 100644 index 000000000..2fd8b668e --- /dev/null +++ b/mittab/templates/outrounds/_form.html @@ -0,0 +1,24 @@ +{% load bootstrap4 %} + +{% if form.errors %} +
    + Please correct the error{{ form.errors|pluralize }} below. + + {% bootstrap_form_errors form type='non_fields' %} +
    +{% endif %} + +{{ form.media }} + +{% for hidden in form.hidden_fields %} + {{ hidden }} +{% endfor %} + +
    +
    + {% bootstrap_field form.winner %} +
    + +
    +
    +
    diff --git a/mittab/templates/outrounds/ballot.html b/mittab/templates/outrounds/ballot.html new file mode 100644 index 000000000..df532d002 --- /dev/null +++ b/mittab/templates/outrounds/ballot.html @@ -0,0 +1,24 @@ +{% extends "base/__wide.html" %} +{% load tags %} + +{% block title %}Data Entry{% endblock %} + +{% block banner %} {{title}} {% endblock %} + +{% block content %} + +
    +
    +
    {% csrf_token %} + {% outround_form form %} + +
    +
    + +
    +
    +
    +
    +
    + +{% endblock %} diff --git a/mittab/templates/outrounds/forum_result.html b/mittab/templates/outrounds/forum_result.html new file mode 100644 index 000000000..70efd65ac --- /dev/null +++ b/mittab/templates/outrounds/forum_result.html @@ -0,0 +1,20 @@ + + + + + Forum Result Display + + + + + + {% for category in results %} +

    {{ category.label }}

    + + {% for result in category.results %} + {{ result }} +
    + {% endfor %} + {% endfor %} + diff --git a/mittab/templates/outrounds/pairing_base.html b/mittab/templates/outrounds/pairing_base.html new file mode 100644 index 000000000..d7d7006af --- /dev/null +++ b/mittab/templates/outrounds/pairing_base.html @@ -0,0 +1,114 @@ +{% extends "base/__wide.html" %} + +{% load tags %} + +{% block title %}Round Status{% endblock %} + +{% block content %} +
    +
    +
    +
    +

    + Round Status for {{ label }} + + {% if not errors or num_excluded == 0 %} Valid {% else %} Invalid {% endif %} pairing + +

    +
    +
    + +
    +
    +
    +
    +
    +
    Pairing Controls
    +
    {% csrf_token %} +
    + + Close Pairings + + + Release Pairings + + {% if num_teams > 2 %} + + Prepare Next Round + + {% endif %} +
    +
    +
    + + +
    +
    + {% for pairing in outrounds %} +
    + {% include "outrounds/pairing_card.html" %} +
    + {% endfor %} +
    +
    +
    +
    People Not Paired In
    + + + + + + + + {% for team, cjudge, judge, room in excluded_people %} + + + + + + + {% endfor %} +
    Unpaired in Teams ({{ excluded_teams|length }})Checked in Judges ({{ excluded_judges|length }})Non Checked in Judges ({{ non_checkins|length }})Available Rooms ({{ available_rooms|length }})
    + {% if team %} {{team.name}} {% endif %} + + {% if cjudge %} {{cjudge.name}} {% endif %} + + {% if judge %} {{judge.name}} {% endif %} + + {% if room %} {{room.name}} {% endif %} +
    +
    +
    +
    +
    +{% endblock %} diff --git a/mittab/templates/outrounds/pairing_card.html b/mittab/templates/outrounds/pairing_card.html new file mode 100644 index 000000000..2ce84fbc7 --- /dev/null +++ b/mittab/templates/outrounds/pairing_card.html @@ -0,0 +1,121 @@ +
    +
    +
    + + + +
    + {% for judge in pairing.judges.all %} + + + {{judge.name}} ({{judge.rank}}) + + + +
    + {% endfor %} + {% for slot in judge_slots %} + {% if pairing.judges.all|length < slot %} + + + N/A + + + +
    + {% endif %} + {% endfor %} +
    + +
    + {% if pairing.victor == 1 %} + GOV win + {% elif pairing.victor == 2 %} + OPP win + {% elif pairing.victor == 3 %} + GOV via Forfeit + {% elif pairing.victor == 4 %} + OPP via Forfeit + {% else %} + Enter Ballot + {%endif%} + + {{pairing.room.name}} + + + Edit in Admin + + {% if choice %} + + {{ pairing.get_choice_display }} choice + + {% endif %} +
    +
    +
    +
    diff --git a/mittab/templates/outrounds/pretty_pairing.html b/mittab/templates/outrounds/pretty_pairing.html new file mode 100644 index 000000000..c0af8296f --- /dev/null +++ b/mittab/templates/outrounds/pretty_pairing.html @@ -0,0 +1,146 @@ + + + + + {% load render_bundle from webpack_loader %} + {% render_bundle 'pairingDisplay' %} + + {{ label }} + + + + + + {% if printable %} + + {% for outround in outround_pairings %} +
    +
    {{ outround.label }}
    + + + + + {% for pairing in outround.rounds %} + + + + + + + {% endfor %} +
    + Government + + Opposition + + Judge + + Room +
    {{pairing.gov_team.display}}{{pairing.opp_team.display}} + {% for judge in pairing.judges.all %} + {% if pairing.judges.all|length > 1 and judge == pairing.chair %} + {{judge.name}}
    + {% else %} + {{judge.name}}
    + {% endif %} + {% endfor %} +
    {{pairing.room.name}}
    +
    + {% empty %} + Nothing is visible. + {% endfor %} + {% else %} + + +
    +

    {{ label }}

    + + + + + + +
    + {% if gov_opp_display %}Government{% else %}Team 1{% endif %} + + {% if gov_opp_display %}Opposition{% else %}Team 2{% endif %} + + Judge + + Room +
    +
    + +
    + + + {% for outround in outround_pairings %} +

    {{ outround.label }}

    + + {% for pairing in outround.rounds %} + + + + + + + + {% endfor %} + {% if outround.excluded|length > 0 %} + + + + + {% endif %} +
    + {% if sidelock and pairing.sidelock %}{% endif %} + {% if choice and pairing.choice == 1 %}{% endif %} + {{pairing.gov_team.display}} + {% if choice and pairing.choice == 1 %}{% endif %} + {% if sidelock and pairing.sidelock %}{% endif %} + + {% if choice and pairing.choice == 2 %}{% endif %} + {{pairing.opp_team.display}} + {% if choice and pairing.choice == 2 %}{% endif %} + + {% for judge in pairing.judges.all %} + {% if pairing.judges.all|length > 1 and judge == pairing.chair %} + {{judge.name}}
    + {% else %} + {{judge.name}}
    + {% endif %} + {% endfor %} +
    {{pairing.room.name}}
    Advancing Teams: + {% for team in outround.excluded %} + {{ team.display }}{% if not forloop.last %}, {% endif %} + {% endfor %} +
    + {% empty %} +
    Nothing is visible.
    + {% endfor %} + + + + {% endif %} + + + + + diff --git a/mittab/templates/pairing/pairing_control.html b/mittab/templates/pairing/pairing_control.html index b86d151e4..020cebf31 100644 --- a/mittab/templates/pairing/pairing_control.html +++ b/mittab/templates/pairing/pairing_control.html @@ -43,10 +43,17 @@
    Pairing Controls
    title="Release the pairings to the participants"> Release Pairings + {% if round_number == tot_rounds %} + + Break 'em + + {% else %} Prepare Next Round + {% endif %} diff --git a/mittab/templates/registration/login.html b/mittab/templates/registration/login.html index 5ce4a4383..2f33b653d 100644 --- a/mittab/templates/registration/login.html +++ b/mittab/templates/registration/login.html @@ -68,5 +68,15 @@

    Participant Access

    href="{% url 'public_teams' %}"> Team List + + + Varsity Outrounds + + + + Novice Outrounds + {% endblock %} diff --git a/mittab/templates/tab/batch_checkin.html b/mittab/templates/tab/batch_checkin.html index 456e8089e..de43c709b 100644 --- a/mittab/templates/tab/batch_checkin.html +++ b/mittab/templates/tab/batch_checkin.html @@ -13,6 +13,8 @@ + + {% for round_number in round_numbers %}{% endfor %} {% for judge, checkins in judges_and_checkins %} @@ -25,10 +27,10 @@
    -
    diff --git a/mittab/templates/tab/room_batch_checkin.html b/mittab/templates/tab/room_batch_checkin.html index 087d5492e..d8fdfc10e 100644 --- a/mittab/templates/tab/room_batch_checkin.html +++ b/mittab/templates/tab/room_batch_checkin.html @@ -12,7 +12,9 @@
    JudgeOutroundsRound {{ round_number }}
    - + + + {% for round_number in round_numbers %}{% endfor %} {% for room, checkins in rooms_and_checkins %} @@ -25,10 +27,10 @@
    -
    diff --git a/mittab/urls.py b/mittab/urls.py index 4a28ca66a..03b0f9b51 100644 --- a/mittab/urls.py +++ b/mittab/urls.py @@ -1,5 +1,6 @@ from django.views import i18n from django.conf.urls import url +from django.urls import path from django.contrib import admin from django.contrib.auth.views import LoginView @@ -8,6 +9,7 @@ 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 +import mittab.apps.tab.outround_pairing_views as outround_pairing_views admin.autodiscover() @@ -165,6 +167,56 @@ pairing_views.enter_e_ballot, name="enter_e_ballot"), + # Outround related + url(r"break/", + outround_pairing_views.break_teams, + name="break"), + path("outround_pairing//", + outround_pairing_views.outround_pairing_view, + name="outround_pairing_view"), + path("outround_pairing", + outround_pairing_views.outround_pairing_view, + name="outround_pairing_view_default"), + url(r"^outround/(\d+)/alternative_judges/(\d+)/$", + outround_pairing_views.alternative_judges, + name="outround_alternative_judges"), + url(r"^outround/(\d+)/(\d+)/alternative_teams/(gov|opp)/$", + outround_pairing_views.alternative_teams, + name="outround_alternative_teams"), + url(r"^outround/(\d+)/alternative_judges/$", + outround_pairing_views.alternative_judges, + name="outround_alternative_judges"), + url(r"^outround/(\d+)/assign_judge/(\d+)/$", + outround_pairing_views.assign_judge, + name="outround_assign_judge"), + url(r"^outround/pairings/assign_team/(\d+)/(gov|opp)/(\d+)/$", + outround_pairing_views.assign_team, + name="outround_assign_team"), + url(r"^outround/(\d+)/assign_judge/(\d+)/(\d+)/$", + outround_pairing_views.assign_judge, + name="outround_swap_judge"), + url(r"^outround/(\d+)/result/$", + outround_pairing_views.enter_result, + name="enter_result"), + path("outround_pairing/pair///", + outround_pairing_views.pair_next_outround, + name="next_outround"), + path("outround_pairings/pairinglist//", + outround_pairing_views.pretty_pair, + name="outround_pretty_pair"), + path("outround_pairings/pairinglist/printable//", + outround_pairing_views.pretty_pair_print, + name="outround_pretty_pair_print"), + path("outround_pairing/release///", + outround_pairing_views.toggle_pairing_released, + name="toggle_outround_pairing_released"), + path("outround_result/", + outround_pairing_views.forum_view, + name="forum_view"), + path("outround_choice/", + outround_pairing_views.update_choice, + name="update_choice"), + # Settings related url(r"^settings_form", views.settings_form, @@ -188,7 +240,7 @@ url(r"^archive/download/$", views.generate_archive, name="download_archive"), # Cache related - url(r"^cache_refresh", views.force_cache_refresh, name="cache_refresh") + url(r"^cache_refresh", views.force_cache_refresh, name="cache_refresh"), ] handler403 = "mittab.apps.tab.views.render_403" diff --git a/package.json b/package.json index 8237ec5ed..ff0ae50f0 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "lint": "eslint assets -c .eslintrc --ext js" + "lint": "eslint assets -c .eslintrc --ext js", + "fixlint": "eslint assets -c .eslintrc --ext js --fix" }, "repository": { "type": "git", diff --git a/settings.yaml b/settings.yaml index ce5f5764d..6b3439aff 100644 --- a/settings.yaml +++ b/settings.yaml @@ -1,10 +1,10 @@ - teams_public: - name: public_teams + name: teams_public description: "Check this box to make the team list publicly visible" value: false type: boolean - judges_public: - name: public_judges + name: judges_public description: "Check this box to make the judges list publicly visible" value: false type: boolean @@ -20,3 +20,38 @@ name: min_eballot_speak description: "The minimum speaker score allowed to be submitted by an e-ballot Note: This value will be allowed. In other words, if the value is 15, a 15 does not have to be justified to tab staff but a 14 does" value: 15 +- nov_teams_to_break: + name: nov_teams_to_break + description: "The number of novice teams to be included in the break (exclusive of novice teams who are varsity breaking)" + value: 4 +- var_teams_to_break: + name: var_teams_to_break + description: "The number of varsity teams to be included in the break." + value: 8 +- nov_panel_size: + name: nov_panel_size + description: "The number of judges on a novice outs panel" + value: 3 +- var_panel_size: + name: var_panel_size + description: "The number of judges on a varsity outs panel" + value: 3 +- var_to_nov: + name: var_to_nov + description: "The offset of the novice break to the varsity break. If novice semis happen when varsity quarters happen, the offset should be 1. If novice semis happen when varsity octofinals happen, the offset should be 2." + value: 2 +- gov_opp_display: + name: gov_opp_display + description: "A toggle to switch the outrounds view from displaying Gov and Opp (when value is 1) and Team 1 and Team 2 (when value is 2)" + value: false + type: boolean +- sidelock: + name: sidelock + description: "A toggle to indicate whether you are side-locking outrounds. If 1, then the gov opps for outrounds will not be random, but will adhere to sidelocks, and will be indicated on the pairing card, and bolded on the front-facing pairing display." + value: false + type: boolean +- choice: + name: choice + description: "A toggle to indicate whether you would like to display who has choice, whether you would like this to be be indicated on the pairing card, and bolded on the front-facing pairing display" + value: false + type: boolean From c32af010588ce22adf6af4c151e37836d7aba476 Mon Sep 17 00:00:00 2001 From: Ben Muschol Date: Thu, 10 Sep 2020 10:35:09 -0700 Subject: [PATCH 15/27] Some debugging --- bin/setup | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/setup b/bin/setup index c6bd25328..d1e895be9 100755 --- a/bin/setup +++ b/bin/setup @@ -1,6 +1,8 @@ #!/bin/bash set -e +set +x +printenv if [ -x "$(command -v mysqladmin)" ]; then mysqladmin ping -h $MITTAB_DB_HOST -u $MYSQL_USER --password=$MYSQL_PASSWORD --wait=30 fi From 3367328ebff25469673e098036b5d477c1ceb429 Mon Sep 17 00:00:00 2001 From: Ben Muschol Date: Thu, 10 Sep 2020 13:08:52 -0700 Subject: [PATCH 16/27] Some fixing --- .env.example | 2 +- docker-compose.yml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 530c4b1e2..917067895 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ MYSQL_DATABASE=mittab MYSQL_USER=root MYSQL_HOST=localhost -# to be overridden in .env.secret +# to be overridden in .env MYSQL_PASSWORD= MYSQL_ROOT_PASSWORD= diff --git a/docker-compose.yml b/docker-compose.yml index ecc32b535..0dbe18cd3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,10 +12,12 @@ services: volumes: - ./:/var/www/tab links: - - mysql:mysql + - "mysql:mysql" env_file: - .env command: /usr/local/bin/gunicorn mittab.wsgi:application -w 2 -b :8000 -t 300 + depends_on: + - "mysql" mysql: image: mysql:5.7 From 93bf7093381fac3efec3d7d387531d34ee7b2cfc Mon Sep 17 00:00:00 2001 From: Ben Muschol Date: Wed, 24 Mar 2021 17:26:23 -0400 Subject: [PATCH 17/27] Update setup --- bin/setup | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/setup b/bin/setup index d1e895be9..6f44fdb46 100755 --- a/bin/setup +++ b/bin/setup @@ -4,7 +4,7 @@ set +x printenv if [ -x "$(command -v mysqladmin)" ]; then - mysqladmin ping -h $MITTAB_DB_HOST -u $MYSQL_USER --password=$MYSQL_PASSWORD --wait=30 + mysqladmin ping -h $MITTAB_DB_HOST -u root --password=$MYSQL_ROOT_PASSWORD --wait=30 fi python manage.py migrate --noinput npm install From 4478e4f7259d3a7455eeae5289b14898763410ae Mon Sep 17 00:00:00 2001 From: Ben Muschol Date: Wed, 24 Mar 2021 17:26:44 -0400 Subject: [PATCH 18/27] Update settings.py --- mittab/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mittab/settings.py b/mittab/settings.py index 10923ccff..0f6bc6ec0 100644 --- a/mittab/settings.py +++ b/mittab/settings.py @@ -42,7 +42,7 @@ "NAME": "mittab", "OPTIONS": {"charset": "utf8mb4"}, "USER": os.environ.get("MYSQL_USER", "root"), - "PASSWORD": os.environ.get("MYSQL_PASSWORD", ""), + "PASSWORD": os.environ.get("MYSQL_ROOT_PASSWORD", ""), "HOST": os.environ.get("MITTAB_DB_HOST", "127.0.0.1"), "PORT": os.environ.get("MYSQL_PORT", "3306"), } From 3a95a54ba0fe5ce0b9d3b88c98aaa35e42c789db Mon Sep 17 00:00:00 2001 From: Ben Muschol Date: Thu, 2 Sep 2021 09:09:45 -0400 Subject: [PATCH 19/27] Bump node version --- Dockerfile.web | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.web b/Dockerfile.web index 7ae1f2750..cae1e0bf7 100644 --- a/Dockerfile.web +++ b/Dockerfile.web @@ -4,7 +4,7 @@ FROM python:3.7 RUN apt-get update && apt-get upgrade -y && \ apt-get install && apt-get install -y vim default-mysql-client # sets up nodejs to install npm -RUN curl -sL https://deb.nodesource.com/setup_11.x | bash +RUN curl -sL https://deb.nodesource.com/setup_12.x | bash RUN apt-get install nodejs WORKDIR /var/www/tab From 60fd7a6b14d28a07d5d443064c76b7c8ef7726d6 Mon Sep 17 00:00:00 2001 From: DanielS6 <91802001+DanielS6@users.noreply.github.com> Date: Tue, 19 Oct 2021 16:50:47 -0400 Subject: [PATCH 20/27] Tab-Policy: fix typo regarding byes (#320) --- docs/Tab-Policy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Tab-Policy.md b/docs/Tab-Policy.md index be649743e..1b67f6956 100644 --- a/docs/Tab-Policy.md +++ b/docs/Tab-Policy.md @@ -61,7 +61,7 @@ round. If a bye is needed to make the lowest bracket whole, a bye will be selected as follows: * In the first round the bye is selected randomly. -* In all preceding rounds the bye is selected as the lowest speaking team of +* In all succeeding rounds the bye is selected as the lowest speaking team of the all down bracket. * No team will get the bye more than once. * Byes are tabbed as a win with debaters receiving average speaks. From 5630590a525f69e81f940519a71b08e110ac3d76 Mon Sep 17 00:00:00 2001 From: Rodda John Date: Sun, 26 Feb 2023 10:00:05 -0500 Subject: [PATCH 21/27] updated --- .circleci/config.yml | 1 + .do/deploy.template.yaml | 79 + .gitignore | 2 + .node-version | 1 + .python-version | 2 +- Dockerfile | 34 + Dockerfile.nginx | 4 - Dockerfile.static | 27 + Dockerfile.web | 1 + Pipfile | 5 +- Pipfile.lock | 432 +++- README.md | 2 + assets/js/pairing.js | 42 +- bin/do-build.sh | 10 + bin/initialize.sh | 6 + bin/setup | 2 +- bin/setup_pythonanywhere.sh | 2 +- bin/start-server.sh | 33 + docker-compose.yml | 49 - fix_fixtures.py | 22 +- mittab/apps/tab/debater_views.py | 1 + mittab/apps/tab/fixtures/testing_db.json | 1976 ----------------- .../tab/fixtures/testing_finished_db.json | 1976 ----------------- .../management/commands/initialize_tourney.py | 38 - .../tab/migrations/0008_auto_20190520_1522.py | 24 - .../tab/migrations/0019_auto_20210408_1648.py | 18 + .../tab/migrations/0020_auto_20210409_1938.py | 46 + mittab/apps/tab/models.py | 295 ++- mittab/apps/tab/pairing_views.py | 80 +- mittab/apps/tab/team_views.py | 24 +- mittab/apps/tab/views.py | 12 +- mittab/libs/assign_judges.py | 223 +- mittab/libs/backup/__init__.py | 59 +- mittab/libs/backup/handlers.py | 66 + mittab/libs/backup/storage.py | 96 + mittab/libs/backup/strategies/local_dump.py | 107 - mittab/libs/cache_logic.py | 42 +- mittab/libs/data_import/__init__.py | 9 +- mittab/libs/data_import/import_judges.py | 16 +- mittab/libs/outround_tab_logic/pairing.py | 1 + mittab/libs/tab_logic/__init__.py | 432 ++-- mittab/libs/tab_logic/rankings.py | 32 +- mittab/libs/tab_logic/stats.py | 99 +- mittab/libs/tests/tab_logic/test_pairing.py | 10 +- mittab/libs/tests/test_case.py | 25 +- mittab/settings.py | 60 +- mittab/templates/outrounds/pairing_card.html | 2 +- mittab/templates/pairing/pairing_control.html | 1 + mittab/templates/pairing/pairing_display.html | 2 + mittab/urls.py | 11 +- nginx.conf | 8 +- 51 files changed, 1577 insertions(+), 4970 deletions(-) create mode 100644 .do/deploy.template.yaml create mode 100644 .node-version create mode 100644 Dockerfile delete mode 100644 Dockerfile.nginx create mode 100644 Dockerfile.static create mode 100755 bin/do-build.sh create mode 100644 bin/initialize.sh create mode 100755 bin/start-server.sh delete mode 100644 docker-compose.yml create mode 100644 mittab/apps/tab/migrations/0019_auto_20210408_1648.py create mode 100644 mittab/apps/tab/migrations/0020_auto_20210409_1938.py create mode 100644 mittab/libs/backup/handlers.py create mode 100644 mittab/libs/backup/storage.py delete mode 100644 mittab/libs/backup/strategies/local_dump.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 5e8b4d26f..359dcba72 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,6 +6,7 @@ jobs: docker: - image: benmusch/mittab-test:0.0.1 environment: + TEST_BROWSER: chrome MYSQL_PASSWORD: secret MITTAB_DB_HOST: 127.0.0.1 PIPENV_VENV_IN_PROJECT: true diff --git a/.do/deploy.template.yaml b/.do/deploy.template.yaml new file mode 100644 index 000000000..102a7d1aa --- /dev/null +++ b/.do/deploy.template.yaml @@ -0,0 +1,79 @@ +databases: +- cluster_name: mysql + db_name: defaultdb + db_user: doadmin + engine: MYSQL + name: mysql + production: true + version: "8" +envs: +- key: TAB_PASSWORD + scope: RUN_AND_BUILD_TIME + value: TODO +- key: BACKUP_STORAGE + scope: RUN_AND_BUILD_TIME + value: S3 +- key: BACKUP_PREFIX + scope: RUN_AND_BUILD_TIME + value: do-app/ +- key: BACKUP_S3_ENDPOINT + scope: RUN_AND_BUILD_TIME + value: https://nyc3.digitaloceanspaces.com +- key: BACKUP_BUCKET + scope: RUN_AND_BUILD_TIME + value: mittab-backups +- key: AWS_SECRET_ACCESS_KEY + scope: RUN_AND_BUILD_TIME + type: SECRET + value: EV[1:f3hvQkTC/Cy5+Ic0VNzIUwtAyG0AUJKT:A+ST33XUgZXpv1ahOKMIh4DGUeQCr4FPTARbB3PIkWkBQLmvqdGTtJukH76B4mkBnxDR7M39NWXWCY4=] +- key: AWS_ACCESS_KEY_ID + scope: RUN_AND_BUILD_TIME + type: SECRET + value: EV[1:l8UCyHKWyEmfgriuDQpgnH79+TOTnqhL:2QNnSilJIqIUs+A0XIJYJkY0o1iIkMi1fAFaLux/x+GjG/SF] +- key: AWS_DEFAULT_REGION + scope: RUN_AND_BUILD_TIME + value: nyc3 +- key: MYSQL_PASSWORD + scope: RUN_AND_BUILD_TIME + value: ${mysql.PASSWORD} +- key: MYSQL_DATABASE + scope: RUN_AND_BUILD_TIME + value: ${mysql.DATABASE} +- key: MYSQL_HOST + scope: RUN_AND_BUILD_TIME + value: ${mysql.HOST} +- key: MYSQL_PORT + scope: RUN_AND_BUILD_TIME + value: ${mysql.PORT} +- key: MYSQL_USER + scope: RUN_AND_BUILD_TIME + value: ${mysql.USERNAME} +- key: SENTRY_DSN + scope: RUN_AND_BUILD_TIME + value: https://ffbc8f385d2248db992f2f66ce0d7032:e6caead56a1d44c9aa09d2bcb9d9f31e@sentry.io/208171 +name: mit-tab +region: nyc +services: +- dockerfile_path: Dockerfile + envs: + - key: DATABASE_URL + scope: RUN_TIME + value: ${test.DATABASE_URL} + github: + branch: do-apps + repo: MIT-Tab/mit-tab + http_port: 8000 + instance_count: 1 + instance_size_slug: basic-xs + name: mit-tab + routes: + - path: / +static_sites: +- dockerfile_path: Dockerfile + github: + branch: do-apps + repo: MIT-Tab/mit-tab + name: static + output_dir: /var/www/tab/assets + routes: + - path: /static diff --git a/.gitignore b/.gitignore index b5e669bda..c1b710a5a 100644 --- a/.gitignore +++ b/.gitignore @@ -120,3 +120,5 @@ typings/ *.tgz .env + +*.dump.sql diff --git a/.node-version b/.node-version new file mode 100644 index 000000000..e68b86038 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +12.21.0 diff --git a/.python-version b/.python-version index 7dceab0f1..f06fb9e91 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -2.7.10 +3.7.10 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..d410e55ef --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +FROM python:3.7 + +# install dependenices +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y vim default-mysql-client + +WORKDIR /var/www/tab + +COPY Pipfile ./ +COPY Pipfile.lock ./ +COPY package.json ./ +COPY package-lock.json ./ +COPY manage.py ./ +COPY setup.py ./ +COPY webpack.config.js ./ +COPY ./mittab ./mittab +COPY ./bin ./bin +COPY ./assets ./assets + +RUN pip install pipenv +RUN pipenv install --deploy --system + +RUN curl -sL https://deb.nodesource.com/setup_12.x | bash +RUN apt-get install -y nodejs + +RUN npm install +RUN ./node_modules/.bin/webpack --config webpack.config.js --mode production +RUN python manage.py collectstatic --noinput + +RUN mkdir /var/tmp/django_cache + +EXPOSE 8000 +CMD ["/var/www/tab/bin/start-server.sh"] diff --git a/Dockerfile.nginx b/Dockerfile.nginx deleted file mode 100644 index 879326483..000000000 --- a/Dockerfile.nginx +++ /dev/null @@ -1,4 +0,0 @@ -FROM tutum/nginx -RUN apt-get update && apt-get install -y vim -RUN rm /etc/nginx/sites-enabled/default -COPY nginx.conf /etc/nginx/sites-enabled/mittab diff --git a/Dockerfile.static b/Dockerfile.static new file mode 100644 index 000000000..2cb34d496 --- /dev/null +++ b/Dockerfile.static @@ -0,0 +1,27 @@ +FROM python:3.7 + +# sets up nodejs to install npm +RUN curl -sL https://deb.nodesource.com/setup_12.x | bash +RUN apt-get install -y nodejs + +WORKDIR /var/www/tab + +COPY Pipfile ./ +COPY Pipfile.lock ./ +COPY package.json ./ +COPY package-lock.json ./ +COPY manage.py ./ +COPY setup.py ./ +COPY webpack.config.js ./ +COPY ./mittab ./mittab +COPY ./bin ./bin +COPY ./assets ./assets + +RUN pip install pipenv +RUN pipenv install --deploy --system + +RUN mkdir /var/tmp/django_cache + +RUN npm install +RUN ./node_modules/.bin/webpack --config webpack.config.js --mode production +RUN python manage.py collectstatic --noinput diff --git a/Dockerfile.web b/Dockerfile.web index cae1e0bf7..78ad0c07b 100644 --- a/Dockerfile.web +++ b/Dockerfile.web @@ -14,6 +14,7 @@ COPY package.json ./ COPY package-lock.json ./ RUN pip install pipenv RUN pipenv install --deploy --system +RUN mkdir /var/tmp/django_cache # setup django COPY manage.py ./ diff --git a/Pipfile b/Pipfile index 4fe07a03e..338bb3261 100644 --- a/Pipfile +++ b/Pipfile @@ -8,10 +8,11 @@ verify_ssl = true [packages] haikunator = "==2.1" mock = "==1.0.1" +boto3 = "*" django-bootstrap4 = "==0.0.8" django-localflavor = "==1.0" django-webpack-loader = "==0.6.0" -django-polymorphic = "==2.0.3" +django-silk = "*" gunicorn = "==19.7.1" mysqlclient = "==1.4.4" xlrd = "==0.9.4" @@ -27,7 +28,7 @@ raven = "==6.4.0" requests = "==2.22.0" selenium = "==3.141.0" splinter = "==0.10.0" -Django = "==2.1.5" +Django = "==2.2.10" PyYAML = "==5.1.2" [requires] diff --git a/Pipfile.lock b/Pipfile.lock index 3eb25b177..c7d7088de 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b9c06b9a7ba06483106b4f0fdc96795d9a91135a435e49c018b0a01be8eca778" + "sha256": "f79db4a6f1f8e0f7e58bc5d65b73044fecd08a84de5335465d7e749de8c21595" }, "pipfile-spec": 6, "requires": { @@ -18,31 +18,57 @@ "default": { "astroid": { "hashes": [ - "sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1", - "sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38" + "sha256:ad63b8552c70939568966811a088ef0bc880f99a24a00834abd0e3681b514f91", + "sha256:bea3f32799fbb8581f58431c12591bc20ce11cbc90ad82e2ea5717d94f2080d5" ], - "version": "==2.4.1" + "markers": "python_version >= '3.6'", + "version": "==2.5.3" }, "atomicwrites": { "hashes": [ "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197", "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.0" }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "version": "==19.3.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.3.0" + }, + "autopep8": { + "hashes": [ + "sha256:5454e6e9a3d02aae38f866eec0d9a7de4ab9f93c10a273fb0340f3d6d09f7514", + "sha256:f01b06a6808bc31698db907761e5890eb2295e287af53f6693b39ce55454034a" + ], + "version": "==1.5.6" + }, + "boto3": { + "hashes": [ + "sha256:a482135c30fa07eaf4370314dd0fb49117222a266d0423b2075aed3835ed1f04", + "sha256:d5ef160442925f5944e4cde88589f0f195f6c284f05613114fc6bbc35e342fa7" + ], + "index": "pypi", + "version": "==1.17.49" + }, + "botocore": { + "hashes": [ + "sha256:6a672ba41dd00e5c1c1824ca8143d180d88de8736d78c0b1f96b8d3cb0466561", + "sha256:f7f103fa0651c69dd360c7d0ecd874854303de5cc0869e0cbc2818a52baacc69" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.20.49" }, "certifi": { "hashes": [ - "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", - "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" ], - "version": "==2020.4.5.1" + "version": "==2020.12.5" }, "chardet": { "hashes": [ @@ -53,47 +79,69 @@ }, "coverage": { "hashes": [ - "sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a", - "sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355", - "sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65", - "sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7", - "sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9", - "sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1", - "sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0", - "sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55", - "sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c", - "sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6", - "sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef", - "sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019", - "sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e", - "sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0", - "sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf", - "sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24", - "sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2", - "sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c", - "sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4", - "sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0", - "sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd", - "sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04", - "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e", - "sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730", - "sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2", - "sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768", - "sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796", - "sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7", - "sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a", - "sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489", - "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052" - ], - "version": "==5.1" + "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", + "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", + "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", + "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", + "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", + "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", + "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", + "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", + "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", + "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", + "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", + "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", + "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", + "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", + "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", + "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", + "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", + "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", + "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", + "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", + "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", + "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", + "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", + "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", + "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", + "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", + "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", + "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", + "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", + "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", + "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", + "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", + "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", + "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", + "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", + "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", + "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", + "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", + "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", + "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", + "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", + "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", + "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", + "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", + "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", + "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", + "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", + "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", + "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", + "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", + "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", + "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==5.5" }, "django": { "hashes": [ - "sha256:a32c22af23634e1d11425574dce756098e015a165be02e4690179889b207c7a8", - "sha256:d6393918da830530a9516bbbcbf7f1214c3d733738779f06b0f649f49cc698c3" + "sha256:1226168be1b1c7efd0e66ee79b0e0b58b2caa7ed87717909cd8a57bb13a7079a", + "sha256:9a4635813e2d498a3c01b10c701fe4a515d76dd290aaa792ccb65ca4ccb6b038" ], "index": "pypi", - "version": "==2.1.5" + "version": "==2.2.10" }, "django-bootstrap4": { "hashes": [ @@ -110,13 +158,12 @@ "index": "pypi", "version": "==1.0" }, - "django-polymorphic": { + "django-silk": { "hashes": [ - "sha256:1fb5505537bcaf71cfc951ff94c4e3ba83c761eaca04b7b2ce9cb63937634ea5", - "sha256:79e7df455fdc8c3d28d38b7ab8323fc21d109a162b8ca282119e0e9ce8db7bdb" + "sha256:a331e55618fa62eaf3cf5a63f31bc1e91205efbeeca5e587c577498b0e251ed8" ], "index": "pypi", - "version": "==2.0.3" + "version": "==4.1.0" }, "django-webpack-loader": { "hashes": [ @@ -126,6 +173,12 @@ "index": "pypi", "version": "==0.6.0" }, + "gprof2dot": { + "hashes": [ + "sha256:1223189383b53dcc8ecfd45787ac48c0ed7b4dbc16ee8b88695d053eea1acabf" + ], + "version": "==2021.2.21" + }, "gunicorn": { "hashes": [ "sha256:75af03c99389535f218cc596c7de74df4763803f7b63eb09d77e92b3956b36c6", @@ -151,44 +204,121 @@ }, "importlib-metadata": { "hashes": [ - "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", - "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" + "sha256:c9db46394197244adf2f0b08ec5bc3cf16757e9590b02af1fca085c16c0d600a", + "sha256:d2d46ef77ffc85cbf7dac7e81dd663fde71c45326131bea8033b9bad42268ebe" ], "markers": "python_version < '3.8'", - "version": "==1.6.0" + "version": "==3.10.0" }, "isort": { "hashes": [ "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==4.3.21" }, + "jinja2": { + "hashes": [ + "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", + "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==2.11.3" + }, + "jmespath": { + "hashes": [ + "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", + "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==0.10.0" + }, "lazy-object-proxy": { "hashes": [ - "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", - "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", - "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", - "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", - "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", - "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", - "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", - "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", - "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", - "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", - "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", - "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", - "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", - "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", - "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", - "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", - "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", - "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", - "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", - "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", - "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" - ], - "version": "==1.4.3" + "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653", + "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61", + "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2", + "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837", + "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3", + "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43", + "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726", + "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3", + "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587", + "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8", + "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a", + "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd", + "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f", + "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad", + "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4", + "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b", + "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf", + "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981", + "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741", + "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e", + "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93", + "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.6.0" + }, + "markupsafe": { + "hashes": [ + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", + "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", + "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", + "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", + "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", + "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", + "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", + "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", + "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", + "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", + "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", + "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.1.1" }, "mccabe": { "hashes": [ @@ -207,11 +337,11 @@ }, "more-itertools": { "hashes": [ - "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be", - "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982" + "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced", + "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713" ], "markers": "python_version > '2.7'", - "version": "==8.3.0" + "version": "==8.7.0" }, "mysqlclient": { "hashes": [ @@ -227,14 +357,32 @@ "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.13.1" }, "py": { "hashes": [ - "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", - "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" + "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", + "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" ], - "version": "==1.8.1" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.10.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", + "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.7.0" + }, + "pygments": { + "hashes": [ + "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94", + "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8" + ], + "markers": "python_version >= '3.5'", + "version": "==2.8.1" }, "pylint": { "hashes": [ @@ -298,12 +446,20 @@ "index": "pypi", "version": "==3.4.8" }, + "python-dateutil": { + "hashes": [ + "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", + "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==2.8.1" + }, "pytz": { "hashes": [ - "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", - "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" + "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", + "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" ], - "version": "==2020.1" + "version": "==2021.1" }, "pyyaml": { "hashes": [ @@ -340,6 +496,13 @@ "index": "pypi", "version": "==2.22.0" }, + "s3transfer": { + "hashes": [ + "sha256:5d48b1fd2232141a9d5fb279709117aaba506cacea7f86f11bc392f06bfa8fc2", + "sha256:c5dadf598762899d8cfaecf68eba649cd25b0ce93b6c954b156aaa3eed160547" + ], + "version": "==0.3.6" + }, "selenium": { "hashes": [ "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c", @@ -350,10 +513,11 @@ }, "six": { "hashes": [ - "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", - "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "version": "==1.14.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.15.0" }, "splinter": { "hashes": [ @@ -363,39 +527,74 @@ "index": "pypi", "version": "==0.10.0" }, + "sqlparse": { + "hashes": [ + "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0", + "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8" + ], + "markers": "python_version >= '3.5'", + "version": "==0.4.1" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==0.10.2" + }, "typed-ast": { "hashes": [ - "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", - "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", - "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", - "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", - "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", - "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", - "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", - "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", - "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", - "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", - "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", - "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", - "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", - "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", - "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", - "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", - "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", - "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", - "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", - "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", - "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" + "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1", + "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d", + "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6", + "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd", + "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37", + "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151", + "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07", + "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440", + "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70", + "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496", + "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea", + "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400", + "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc", + "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606", + "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc", + "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581", + "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412", + "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a", + "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2", + "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787", + "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f", + "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937", + "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64", + "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487", + "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b", + "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41", + "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a", + "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3", + "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166", + "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10" ], "markers": "implementation_name == 'cpython' and python_version < '3.8'", - "version": "==1.4.1" + "version": "==1.4.2" + }, + "typing-extensions": { + "hashes": [ + "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", + "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", + "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" + ], + "markers": "python_version < '3.8'", + "version": "==3.7.4.3" }, "urllib3": { "hashes": [ - "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", - "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" + "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", + "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" ], - "version": "==1.25.9" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.25.11" }, "wrapt": { "hashes": [ @@ -421,10 +620,11 @@ }, "zipp": { "hashes": [ - "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", - "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" + "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", + "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" ], - "version": "==3.1.0" + "markers": "python_version >= '3.6'", + "version": "==3.4.1" } }, "develop": {} diff --git a/README.md b/README.md index 1b733f49f..be128203f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ +[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/mit-tab/mit-tab/tree/do-apps) + MIT-Tab is a web application to manage APDA debate tournaments. Looking to learn how to use mit-tab? [Check out the docs!](https://mit-tab.readthedocs.io/en/latest/) diff --git a/assets/js/pairing.js b/assets/js/pairing.js index 2da27802e..6d00dc01c 100644 --- a/assets/js/pairing.js +++ b/assets/js/pairing.js @@ -2,22 +2,24 @@ import $ from "jquery"; import quickSearchInit from "./quickSearch"; -function populateTabCard(tabCardElement) { - const teamId = tabCardElement.attr("team-id"); +function populateTabCards() { + const roundNumber = $("#round-number").data("round-number"); $.ajax({ - url: `/team/${teamId}/stats`, + url: `/round/${roundNumber}/stats`, success(result) { - const stats = result.result; - const text = [ - stats.wins, - stats.total_speaks.toFixed(2), - stats.govs, - stats.opps, - stats.seed - ].join(" / "); - tabCardElement.attr("title", "Wins / Speaks / Govs / Opps / Seed"); - tabCardElement.attr("href", `/team/card/${teamId}`); - tabCardElement.text(text); + Object.entries(result).forEach(([teamId, stats]) => { + const tabCardElement = $(`.tabcard[team-id=${teamId}]`); + const text = [ + stats.wins, + stats.total_speaks.toFixed(2), + stats.govs, + stats.opps, + stats.seed + ].join(" / "); + tabCardElement.attr("title", "Wins / Speaks / Govs / Opps / Seed"); + tabCardElement.attr("href", `/team/card/${teamId}`); + tabCardElement.text(text); + }); } }); } @@ -25,7 +27,6 @@ function populateTabCard(tabCardElement) { function assignTeam(e) { e.preventDefault(); const teamId = $(e.target).attr("team-id"); - const oldTeamId = $(e.target).attr("src-team-id"); const roundId = $(e.target).attr("round-id"); const position = $(e.target).attr("position"); const url = `/pairings/assign_team/${roundId}/${position}/${teamId}`; @@ -44,12 +45,7 @@ function assignTeam(e) { $container.find(".team-link").attr("href", `/team/${result.team.id}`); $container.find(".tabcard").attr("team-id", result.team.id); - populateTabCard($(`.tabcard[team-id=${result.team.id}]`)); - - const $oldTeamTabCard = $(`.tabcard[team-id=${oldTeamId}]`); - if ($oldTeamTabCard) { - populateTabCard($oldTeamTabCard); - } + populateTabCards(); } else { window.alert(alertMsg); } @@ -170,9 +166,7 @@ function togglePairingRelease(event) { } $(document).ready(() => { - $(".team.tabcard").each((_, element) => { - populateTabCard($(element)); - }); + populateTabCards(); $("#team_ranking").each((_, element) => { lazyLoad($(element).parent(), "/team/rank/"); }); diff --git a/bin/do-build.sh b/bin/do-build.sh new file mode 100755 index 000000000..1ad6b53eb --- /dev/null +++ b/bin/do-build.sh @@ -0,0 +1,10 @@ +#/usr/bin/env bash +set -e +set +x + +sudo curl -sL https://deb.nodesource.com/setup_12.x | bash +apt-get install -y nodejs + +npm install +./node_modules/.bin/webpack --config webpack.config.js --mode production +python manage.py collectstatic --noinput diff --git a/bin/initialize.sh b/bin/initialize.sh new file mode 100644 index 000000000..6e50beec7 --- /dev/null +++ b/bin/initialize.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -e +set +x + +python manage.py migrate --no-input +python manage.py initialize_tourney --tab-password $TAB_PASSWORD diff --git a/bin/setup b/bin/setup index 6f44fdb46..0c5748951 100755 --- a/bin/setup +++ b/bin/setup @@ -10,4 +10,4 @@ python manage.py migrate --noinput npm install ./node_modules/.bin/webpack --config webpack.config.js --mode production python manage.py collectstatic --noinput -python manage.py initialize_tourney --tab-password $1 '.' +python manage.py initialize_tourney --tab-password $1 diff --git a/bin/setup_pythonanywhere.sh b/bin/setup_pythonanywhere.sh index 2c6e79db3..ce9d50089 100755 --- a/bin/setup_pythonanywhere.sh +++ b/bin/setup_pythonanywhere.sh @@ -27,7 +27,7 @@ pip install -r requirements.txt echo -e "${BLUE}STEP 2: Set up the tournament${NC}" printf "Please enter a password for the 'tab' user: " read tab_password -python manage.py initialize_tourney --tab-password $tab_password $USER . +python manage.py initialize_tourney --tab-password $tab_password $USER echo -e "${BLUE}STEP 3: Collecting HTML, CSS and JS files${NC}" python manage.py collectstatic diff --git a/bin/start-server.sh b/bin/start-server.sh new file mode 100755 index 000000000..5ce7d644e --- /dev/null +++ b/bin/start-server.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -e +set +x + +cd /var/www/tab + +function execute-mysql() { + mysql -u $MYSQL_USER \ + -h $MYSQL_HOST \ + -D $MYSQL_DATABASE \ + -P $MYSQL_PORT \ + --password="$MYSQL_PASSWORD" \ + -e "$1" +} + +python manage.py migrate --noinput + +# Create a table tournament_intialized to use as a flag indicating the +# tournament has been initialzed +if [[ $(execute-mysql "show tables like 'tournament_initialized'") ]]; then + echo "Tournament already initialized, skipping init phase"; +else + echo "Initializing tournament"; + python manage.py initialize_tourney --tab-password $TAB_PASSWORD; + execute-mysql "CREATE TABLE tournament_initialized(id int not null, PRIMARY KEY (id));" +fi + +if [[ $TOURNAMENT_NAME == *-test ]]; then + python manage.py loaddata testing_db; +fi + +/usr/local/bin/gunicorn --worker-tmp-dir /dev/shm \ + mittab.wsgi:application -w 2 --bind 0.0.0.0:8000 -t 300 diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 0dbe18cd3..000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,49 +0,0 @@ -version: "2.0" -services: - web: - restart: always - build: - dockerfile: Dockerfile.web - context: . - args: - - "password" - expose: - - "8000" - volumes: - - ./:/var/www/tab - links: - - "mysql:mysql" - env_file: - - .env - command: /usr/local/bin/gunicorn mittab.wsgi:application -w 2 -b :8000 -t 300 - depends_on: - - "mysql" - - mysql: - image: mysql:5.7 - restart: always - ports: - - "3306:3306" - expose: - - "3306" - volumes: - - my-db:/var/lib/mysql - command: ['mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci'] - env_file: - - .env - - nginx: - restart: always - build: - context: . - dockerfile: Dockerfile.nginx - ports: - - "80:80" - volumes: - - ./staticfiles:/www/static - volumes_from: - - web - links: - - web:web -volumes: - my-db: diff --git a/fix_fixtures.py b/fix_fixtures.py index 0efc82023..4f595cfdf 100644 --- a/fix_fixtures.py +++ b/fix_fixtures.py @@ -13,26 +13,12 @@ USED_TIEBREAKERS = set() -def find_content_type(content_type_name): - for content_type in filter( - lambda obj: obj.get("model") == CONTENT_TYPE_MODEL, DATA): - fields = content_type.get("fields", {}) - if fields.get("model", None) == content_type_name and fields.get( - "app_label", None) == "tab": - return content_type["pk"] - - -def fix_type(type_name, ctype_id): +def fix_type(type_name): for model in filter(lambda obj: obj.get("model", None) == type_name, DATA): - model["fields"]["polymorphic_ctype_id"] = ctype_id - model["fields"]["tiebreaker"] = random.choice(range(0, 2**16)) - while model["fields"]["tiebreaker"] in USED_TIEBREAKERS: - model["fields"]["tiebreaker"] = random.choice(range(0, 2**16)) - USED_TIEBREAKERS.add(model["fields"]["tiebreaker"]) - + del model["fields"]["polymorphic_ctype_id"] -fix_type("tab.team", find_content_type("team")) -fix_type("tab.debater", find_content_type("debater")) +fix_type("tab.team") +fix_type("tab.debater") with open(LOCATION, "w") as out: json.dump(DATA, out) diff --git a/mittab/apps/tab/debater_views.py b/mittab/apps/tab/debater_views.py index 01a568322..eb556d2e4 100644 --- a/mittab/apps/tab/debater_views.py +++ b/mittab/apps/tab/debater_views.py @@ -116,6 +116,7 @@ def rank_debaters(request): debaters, nov_debaters = cache_logic.cache_fxn_key( get_speaker_rankings, "speaker_rankings", + cache_logic.DEFAULT, request ) diff --git a/mittab/apps/tab/fixtures/testing_db.json b/mittab/apps/tab/fixtures/testing_db.json index 3e1f52282..18327c7fc 100644 --- a/mittab/apps/tab/fixtures/testing_db.json +++ b/mittab/apps/tab/fixtures/testing_db.json @@ -372,10 +372,6 @@ "model": "tab.debater", "pk": 1, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Alfreda Harvey", "novice_status": 0 @@ -385,10 +381,6 @@ "model": "tab.debater", "pk": 2, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sylvia Mcclure", "novice_status": 0 @@ -398,10 +390,6 @@ "model": "tab.debater", "pk": 3, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Judy Tanner", "novice_status": 0 @@ -411,10 +399,6 @@ "model": "tab.debater", "pk": 4, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Janet Morris", "novice_status": 0 @@ -424,10 +408,6 @@ "model": "tab.debater", "pk": 5, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Gloria Floyd", "novice_status": 1 @@ -437,10 +417,6 @@ "model": "tab.debater", "pk": 7, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kellie Taylor", "novice_status": 1 @@ -450,10 +426,6 @@ "model": "tab.debater", "pk": 8, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Fay White", "novice_status": 1 @@ -463,10 +435,6 @@ "model": "tab.debater", "pk": 9, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Juliana Sanford", "novice_status": 1 @@ -476,10 +444,6 @@ "model": "tab.debater", "pk": 11, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Elizabeth Underwood", "novice_status": 1 @@ -489,10 +453,6 @@ "model": "tab.debater", "pk": 12, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Leanne Yates", "novice_status": 0 @@ -502,10 +462,6 @@ "model": "tab.debater", "pk": 13, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Minnie Cantrell", "novice_status": 1 @@ -515,10 +471,6 @@ "model": "tab.debater", "pk": 14, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lisa Brown", "novice_status": 1 @@ -528,10 +480,6 @@ "model": "tab.debater", "pk": 15, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Whitney Crosby", "novice_status": 1 @@ -541,10 +489,6 @@ "model": "tab.debater", "pk": 16, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Rae Little", "novice_status": 0 @@ -554,10 +498,6 @@ "model": "tab.debater", "pk": 17, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Andrea Arnold", "novice_status": 1 @@ -567,10 +507,6 @@ "model": "tab.debater", "pk": 18, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Simone Hampton", "novice_status": 0 @@ -580,10 +516,6 @@ "model": "tab.debater", "pk": 19, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Erna Wiley", "novice_status": 0 @@ -593,10 +525,6 @@ "model": "tab.debater", "pk": 20, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Ursula Perez", "novice_status": 1 @@ -606,10 +534,6 @@ "model": "tab.debater", "pk": 21, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Josefa Moore", "novice_status": 0 @@ -619,10 +543,6 @@ "model": "tab.debater", "pk": 22, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Hattie Bray", "novice_status": 1 @@ -632,10 +552,6 @@ "model": "tab.debater", "pk": 23, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lea Weaver", "novice_status": 0 @@ -645,10 +561,6 @@ "model": "tab.debater", "pk": 24, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Monica Harmon", "novice_status": 1 @@ -658,10 +570,6 @@ "model": "tab.debater", "pk": 25, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Brooke Hood", "novice_status": 0 @@ -671,10 +579,6 @@ "model": "tab.debater", "pk": 26, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lora Melendez", "novice_status": 0 @@ -684,10 +588,6 @@ "model": "tab.debater", "pk": 27, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lourdes Allen", "novice_status": 0 @@ -697,10 +597,6 @@ "model": "tab.debater", "pk": 28, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Angelita Blake", "novice_status": 1 @@ -710,10 +606,6 @@ "model": "tab.debater", "pk": 29, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jordan Huffman", "novice_status": 1 @@ -723,10 +615,6 @@ "model": "tab.debater", "pk": 30, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kathy Beard", "novice_status": 1 @@ -736,10 +624,6 @@ "model": "tab.debater", "pk": 31, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Laurie Barrett", "novice_status": 1 @@ -749,10 +633,6 @@ "model": "tab.debater", "pk": 32, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Catalina Mack", "novice_status": 1 @@ -762,10 +642,6 @@ "model": "tab.debater", "pk": 33, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Patricia Waters", "novice_status": 1 @@ -775,10 +651,6 @@ "model": "tab.debater", "pk": 34, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Caroline Franco", "novice_status": 1 @@ -788,10 +660,6 @@ "model": "tab.debater", "pk": 35, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Tracy Jackson", "novice_status": 1 @@ -801,10 +669,6 @@ "model": "tab.debater", "pk": 36, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Antoinette Mercer", "novice_status": 1 @@ -814,10 +678,6 @@ "model": "tab.debater", "pk": 37, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Rosalyn Leon", "novice_status": 1 @@ -827,10 +687,6 @@ "model": "tab.debater", "pk": 38, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Catherine Burris", "novice_status": 1 @@ -840,10 +696,6 @@ "model": "tab.debater", "pk": 39, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Petra Solis", "novice_status": 1 @@ -853,10 +705,6 @@ "model": "tab.debater", "pk": 40, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Marisa Hodges", "novice_status": 1 @@ -866,10 +714,6 @@ "model": "tab.debater", "pk": 41, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Aline Lott", "novice_status": 1 @@ -879,10 +723,6 @@ "model": "tab.debater", "pk": 42, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kristin Romero", "novice_status": 1 @@ -892,10 +732,6 @@ "model": "tab.debater", "pk": 43, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Fern Abbott", "novice_status": 1 @@ -905,10 +741,6 @@ "model": "tab.debater", "pk": 44, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Essie Wilkerson", "novice_status": 1 @@ -918,10 +750,6 @@ "model": "tab.debater", "pk": 45, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Wilma Montgomery", "novice_status": 0 @@ -931,10 +759,6 @@ "model": "tab.debater", "pk": 46, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jeannine Burton", "novice_status": 1 @@ -944,10 +768,6 @@ "model": "tab.debater", "pk": 47, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Angie Adams", "novice_status": 0 @@ -957,10 +777,6 @@ "model": "tab.debater", "pk": 48, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Selma Roberson", "novice_status": 1 @@ -970,10 +786,6 @@ "model": "tab.debater", "pk": 49, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Anita Mendez", "novice_status": 0 @@ -983,10 +795,6 @@ "model": "tab.debater", "pk": 50, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Evelyn Kelly", "novice_status": 1 @@ -996,10 +804,6 @@ "model": "tab.debater", "pk": 51, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Marylou Ellis", "novice_status": 0 @@ -1009,10 +813,6 @@ "model": "tab.debater", "pk": 52, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Marianne Tate", "novice_status": 1 @@ -1022,10 +822,6 @@ "model": "tab.debater", "pk": 53, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Hester Adkins", "novice_status": 1 @@ -1035,10 +831,6 @@ "model": "tab.debater", "pk": 54, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Charlotte Singleton", "novice_status": 0 @@ -1048,10 +840,6 @@ "model": "tab.debater", "pk": 55, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Tammy Hendrix", "novice_status": 0 @@ -1061,10 +849,6 @@ "model": "tab.debater", "pk": 56, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Mara Carlson", "novice_status": 0 @@ -1074,10 +858,6 @@ "model": "tab.debater", "pk": 58, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Alexandria Barnett", "novice_status": 0 @@ -1087,10 +867,6 @@ "model": "tab.debater", "pk": 59, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Staci Russell", "novice_status": 0 @@ -1100,10 +876,6 @@ "model": "tab.debater", "pk": 60, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kay Kemp", "novice_status": 0 @@ -1113,10 +885,6 @@ "model": "tab.debater", "pk": 61, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Valarie Curry", "novice_status": 0 @@ -1126,10 +894,6 @@ "model": "tab.debater", "pk": 62, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Gracie Blackwell", "novice_status": 0 @@ -1139,10 +903,6 @@ "model": "tab.debater", "pk": 63, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Graciela Riley", "novice_status": 0 @@ -1152,10 +912,6 @@ "model": "tab.debater", "pk": 64, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Tracey Irwin", "novice_status": 0 @@ -1165,10 +921,6 @@ "model": "tab.debater", "pk": 65, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Neva Ware", "novice_status": 0 @@ -1178,10 +930,6 @@ "model": "tab.debater", "pk": 66, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kimberly Rodriquez", "novice_status": 0 @@ -1191,10 +939,6 @@ "model": "tab.debater", "pk": 67, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Melinda Sellers", "novice_status": 1 @@ -1204,10 +948,6 @@ "model": "tab.debater", "pk": 68, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Charity Garza", "novice_status": 0 @@ -1217,10 +957,6 @@ "model": "tab.debater", "pk": 69, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Clarice Nolan", "novice_status": 0 @@ -1230,10 +966,6 @@ "model": "tab.debater", "pk": 70, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Erin Greer", "novice_status": 1 @@ -1243,10 +975,6 @@ "model": "tab.debater", "pk": 71, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Candice Charles", "novice_status": 1 @@ -1256,10 +984,6 @@ "model": "tab.debater", "pk": 72, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Rowena Garrett", "novice_status": 1 @@ -1269,10 +993,6 @@ "model": "tab.debater", "pk": 73, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Roxie Sanchez", "novice_status": 1 @@ -1282,10 +1002,6 @@ "model": "tab.debater", "pk": 74, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Margery Cotton", "novice_status": 1 @@ -1295,10 +1011,6 @@ "model": "tab.debater", "pk": 75, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Angelique Wall", "novice_status": 1 @@ -1308,10 +1020,6 @@ "model": "tab.debater", "pk": 76, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Helen Hensley", "novice_status": 1 @@ -1321,10 +1029,6 @@ "model": "tab.debater", "pk": 77, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lindsay Osborne", "novice_status": 1 @@ -1334,10 +1038,6 @@ "model": "tab.debater", "pk": 78, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Cheryl Terry", "novice_status": 1 @@ -1347,10 +1047,6 @@ "model": "tab.debater", "pk": 79, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Marlene Drake", "novice_status": 0 @@ -1360,10 +1056,6 @@ "model": "tab.debater", "pk": 80, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kim Chaney", "novice_status": 1 @@ -1373,10 +1065,6 @@ "model": "tab.debater", "pk": 81, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kelly Knapp", "novice_status": 1 @@ -1386,10 +1074,6 @@ "model": "tab.debater", "pk": 82, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Gretchen Goff", "novice_status": 1 @@ -1399,10 +1083,6 @@ "model": "tab.debater", "pk": 83, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Magdalena Tucker", "novice_status": 0 @@ -1412,10 +1092,6 @@ "model": "tab.debater", "pk": 84, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Beatriz Whitney", "novice_status": 0 @@ -1425,10 +1101,6 @@ "model": "tab.debater", "pk": 85, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lelia Shelton", "novice_status": 0 @@ -1438,10 +1110,6 @@ "model": "tab.debater", "pk": 86, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Susanne Bryant", "novice_status": 1 @@ -1451,10 +1119,6 @@ "model": "tab.debater", "pk": 87, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jenny Whitfield", "novice_status": 0 @@ -1464,10 +1128,6 @@ "model": "tab.debater", "pk": 88, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Bernadine Boyle", "novice_status": 1 @@ -1477,10 +1137,6 @@ "model": "tab.debater", "pk": 90, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Margo Hopkins", "novice_status": 1 @@ -1490,10 +1146,6 @@ "model": "tab.debater", "pk": 93, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Maryellen Guthrie", "novice_status": 0 @@ -1503,10 +1155,6 @@ "model": "tab.debater", "pk": 94, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Clarissa Castaneda", "novice_status": 0 @@ -1516,10 +1164,6 @@ "model": "tab.debater", "pk": 95, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Paige Ramsey", "novice_status": 1 @@ -1529,10 +1173,6 @@ "model": "tab.debater", "pk": 96, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sheila Velazquez", "novice_status": 0 @@ -1542,10 +1182,6 @@ "model": "tab.debater", "pk": 97, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Alejandra Meyers", "novice_status": 0 @@ -1555,10 +1191,6 @@ "model": "tab.debater", "pk": 98, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Christian Bell", "novice_status": 1 @@ -1568,10 +1200,6 @@ "model": "tab.debater", "pk": 99, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Bonnie Skinner", "novice_status": 1 @@ -1581,10 +1209,6 @@ "model": "tab.debater", "pk": 100, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Marissa Sexton", "novice_status": 1 @@ -1594,10 +1218,6 @@ "model": "tab.debater", "pk": 101, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Flossie Welch", "novice_status": 1 @@ -1607,10 +1227,6 @@ "model": "tab.debater", "pk": 102, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Christie Copeland", "novice_status": 0 @@ -1620,10 +1236,6 @@ "model": "tab.debater", "pk": 103, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Rosario Hebert", "novice_status": 1 @@ -1633,10 +1245,6 @@ "model": "tab.debater", "pk": 104, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kris Vaughn", "novice_status": 0 @@ -1646,10 +1254,6 @@ "model": "tab.debater", "pk": 105, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Christa Kent", "novice_status": 0 @@ -1659,10 +1263,6 @@ "model": "tab.debater", "pk": 106, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Ashlee Fitzgerald", "novice_status": 0 @@ -1672,10 +1272,6 @@ "model": "tab.debater", "pk": 107, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Beverley Knight", "novice_status": 1 @@ -1685,10 +1281,6 @@ "model": "tab.debater", "pk": 108, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Francisca Browning", "novice_status": 1 @@ -1698,10 +1290,6 @@ "model": "tab.debater", "pk": 109, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lawanda Bates", "novice_status": 0 @@ -1711,10 +1299,6 @@ "model": "tab.debater", "pk": 110, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jacklyn Hanson", "novice_status": 0 @@ -1724,10 +1308,6 @@ "model": "tab.debater", "pk": 111, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Katherine Riggs", "novice_status": 1 @@ -1737,10 +1317,6 @@ "model": "tab.debater", "pk": 112, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Pansy Alexander", "novice_status": 0 @@ -1750,10 +1326,6 @@ "model": "tab.debater", "pk": 113, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Elise Russo", "novice_status": 0 @@ -1763,10 +1335,6 @@ "model": "tab.debater", "pk": 114, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Josephine Griffin", "novice_status": 0 @@ -1776,10 +1344,6 @@ "model": "tab.debater", "pk": 115, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Nanette Hickman", "novice_status": 0 @@ -1789,10 +1353,6 @@ "model": "tab.debater", "pk": 116, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Luz Eaton", "novice_status": 0 @@ -1802,10 +1362,6 @@ "model": "tab.debater", "pk": 117, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lacey Yang", "novice_status": 0 @@ -1815,10 +1371,6 @@ "model": "tab.debater", "pk": 118, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Katina Mcintyre", "novice_status": 0 @@ -1828,10 +1380,6 @@ "model": "tab.debater", "pk": 119, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Leslie Vasquez", "novice_status": 0 @@ -1841,10 +1389,6 @@ "model": "tab.debater", "pk": 120, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Socorro Lindsey", "novice_status": 0 @@ -1854,10 +1398,6 @@ "model": "tab.debater", "pk": 121, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Shelly Kane", "novice_status": 1 @@ -1867,10 +1407,6 @@ "model": "tab.debater", "pk": 122, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Rosie Frazier", "novice_status": 1 @@ -1880,10 +1416,6 @@ "model": "tab.debater", "pk": 123, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Mamie Foster", "novice_status": 1 @@ -1893,10 +1425,6 @@ "model": "tab.debater", "pk": 124, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Leonor Lopez", "novice_status": 1 @@ -1906,10 +1434,6 @@ "model": "tab.debater", "pk": 125, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Claudette Kline", "novice_status": 1 @@ -1919,10 +1443,6 @@ "model": "tab.debater", "pk": 126, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Rosanne Obrien", "novice_status": 0 @@ -1932,10 +1452,6 @@ "model": "tab.debater", "pk": 129, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jan Hale", "novice_status": 1 @@ -1945,10 +1461,6 @@ "model": "tab.debater", "pk": 131, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Brigitte Mayo", "novice_status": 1 @@ -1958,10 +1470,6 @@ "model": "tab.debater", "pk": 132, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Winifred Ross", "novice_status": 1 @@ -1971,10 +1479,6 @@ "model": "tab.debater", "pk": 133, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Yvonne Boone", "novice_status": 0 @@ -1984,10 +1488,6 @@ "model": "tab.debater", "pk": 134, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Caitlin Evans", "novice_status": 0 @@ -1997,10 +1497,6 @@ "model": "tab.debater", "pk": 135, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Joann Larson", "novice_status": 1 @@ -2010,10 +1506,6 @@ "model": "tab.debater", "pk": 136, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Allyson Stevens", "novice_status": 1 @@ -2023,10 +1515,6 @@ "model": "tab.debater", "pk": 137, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Ernestine Mccormick", "novice_status": 0 @@ -2036,10 +1524,6 @@ "model": "tab.debater", "pk": 138, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Velma Munoz", "novice_status": 1 @@ -2049,10 +1533,6 @@ "model": "tab.debater", "pk": 139, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Traci James", "novice_status": 0 @@ -2062,10 +1542,6 @@ "model": "tab.debater", "pk": 140, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Maribel Hubbard", "novice_status": 1 @@ -2075,10 +1551,6 @@ "model": "tab.debater", "pk": 141, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lynette Fuller", "novice_status": 1 @@ -2088,10 +1560,6 @@ "model": "tab.debater", "pk": 142, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "June Pacheco", "novice_status": 0 @@ -2101,10 +1569,6 @@ "model": "tab.debater", "pk": 143, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Shanna Brennan", "novice_status": 1 @@ -2114,10 +1578,6 @@ "model": "tab.debater", "pk": 144, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Hallie Schroeder", "novice_status": 0 @@ -2127,10 +1587,6 @@ "model": "tab.debater", "pk": 145, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Alyce Mooney", "novice_status": 0 @@ -2140,10 +1596,6 @@ "model": "tab.debater", "pk": 146, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jill Vincent", "novice_status": 1 @@ -2153,10 +1605,6 @@ "model": "tab.debater", "pk": 147, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Gayle Hatfield", "novice_status": 0 @@ -2166,10 +1614,6 @@ "model": "tab.debater", "pk": 148, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jerri Chandler", "novice_status": 0 @@ -2179,10 +1623,6 @@ "model": "tab.debater", "pk": 149, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Myrtle Ellison", "novice_status": 1 @@ -2192,10 +1632,6 @@ "model": "tab.debater", "pk": 150, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jacqueline Jordan", "novice_status": 0 @@ -2205,10 +1641,6 @@ "model": "tab.debater", "pk": 151, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Saundra Clarke", "novice_status": 0 @@ -2218,10 +1650,6 @@ "model": "tab.debater", "pk": 152, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Isabella Daugherty", "novice_status": 0 @@ -2231,10 +1659,6 @@ "model": "tab.debater", "pk": 153, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Beverly Morales", "novice_status": 0 @@ -2244,10 +1668,6 @@ "model": "tab.debater", "pk": 154, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jamie Holder", "novice_status": 0 @@ -2257,10 +1677,6 @@ "model": "tab.debater", "pk": 155, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Tammi Jimenez", "novice_status": 0 @@ -2270,10 +1686,6 @@ "model": "tab.debater", "pk": 156, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Amalia Kinney", "novice_status": 0 @@ -2283,10 +1695,6 @@ "model": "tab.debater", "pk": 157, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Priscilla Everett", "novice_status": 0 @@ -2296,10 +1704,6 @@ "model": "tab.debater", "pk": 158, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Corina Calhoun", "novice_status": 0 @@ -2309,10 +1713,6 @@ "model": "tab.debater", "pk": 160, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Katelyn Key", "novice_status": 1 @@ -2322,10 +1722,6 @@ "model": "tab.debater", "pk": 161, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Vanessa Hernandez", "novice_status": 1 @@ -2335,10 +1731,6 @@ "model": "tab.debater", "pk": 163, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Gail Cooper", "novice_status": 1 @@ -2348,10 +1740,6 @@ "model": "tab.debater", "pk": 164, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Florine Ferrell", "novice_status": 0 @@ -2361,10 +1749,6 @@ "model": "tab.debater", "pk": 165, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sophie Mcguire", "novice_status": 1 @@ -2374,10 +1758,6 @@ "model": "tab.debater", "pk": 166, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Krystal Rodriguez", "novice_status": 0 @@ -2387,10 +1767,6 @@ "model": "tab.debater", "pk": 167, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Imelda Avery", "novice_status": 0 @@ -2400,10 +1776,6 @@ "model": "tab.debater", "pk": 168, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Leta Saunders", "novice_status": 1 @@ -2413,10 +1785,6 @@ "model": "tab.debater", "pk": 169, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Melisa Combs", "novice_status": 0 @@ -2426,10 +1794,6 @@ "model": "tab.debater", "pk": 170, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Robyn Calderon", "novice_status": 1 @@ -2439,10 +1803,6 @@ "model": "tab.debater", "pk": 171, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Nannie Wright", "novice_status": 1 @@ -2452,10 +1812,6 @@ "model": "tab.debater", "pk": 172, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "April Bowen", "novice_status": 1 @@ -2465,10 +1821,6 @@ "model": "tab.debater", "pk": 173, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lizzie Parsons", "novice_status": 1 @@ -2478,10 +1830,6 @@ "model": "tab.debater", "pk": 174, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Elinor Bradley", "novice_status": 1 @@ -2491,10 +1839,6 @@ "model": "tab.debater", "pk": 175, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Rosalind Soto", "novice_status": 0 @@ -2504,10 +1848,6 @@ "model": "tab.debater", "pk": 176, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lara Valentine", "novice_status": 1 @@ -2517,10 +1857,6 @@ "model": "tab.debater", "pk": 177, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Pat Strong", "novice_status": 1 @@ -2530,10 +1866,6 @@ "model": "tab.debater", "pk": 178, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jenna Doyle", "novice_status": 1 @@ -2543,10 +1875,6 @@ "model": "tab.debater", "pk": 179, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Terry Dyer", "novice_status": 1 @@ -2556,10 +1884,6 @@ "model": "tab.debater", "pk": 180, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Cindy Newman", "novice_status": 1 @@ -2569,10 +1893,6 @@ "model": "tab.debater", "pk": 181, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lindsey Cohen", "novice_status": 1 @@ -2582,10 +1902,6 @@ "model": "tab.debater", "pk": 182, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kathie Rivas", "novice_status": 1 @@ -2595,10 +1911,6 @@ "model": "tab.debater", "pk": 183, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sheena Branch", "novice_status": 1 @@ -2608,10 +1920,6 @@ "model": "tab.debater", "pk": 186, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lynnette Hunt", "novice_status": 1 @@ -2621,10 +1929,6 @@ "model": "tab.debater", "pk": 188, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lauri Giles", "novice_status": 1 @@ -2634,10 +1938,6 @@ "model": "tab.debater", "pk": 189, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Mona Gould", "novice_status": 1 @@ -2647,10 +1947,6 @@ "model": "tab.debater", "pk": 190, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Gay Blair", "novice_status": 1 @@ -2660,10 +1956,6 @@ "model": "tab.debater", "pk": 191, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Deann Fernandez", "novice_status": 1 @@ -2673,10 +1965,6 @@ "model": "tab.debater", "pk": 192, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Holly Buckley", "novice_status": 1 @@ -2686,10 +1974,6 @@ "model": "tab.debater", "pk": 193, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Gladys Kelley", "novice_status": 0 @@ -2699,10 +1983,6 @@ "model": "tab.debater", "pk": 194, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lila Sandoval", "novice_status": 1 @@ -2712,10 +1992,6 @@ "model": "tab.debater", "pk": 195, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sherry David", "novice_status": 1 @@ -2725,10 +2001,6 @@ "model": "tab.debater", "pk": 196, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Miriam Hester", "novice_status": 0 @@ -2738,10 +2010,6 @@ "model": "tab.debater", "pk": 197, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Georgia Lindsay", "novice_status": 0 @@ -2751,10 +2019,6 @@ "model": "tab.debater", "pk": 198, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Fanny Gentry", "novice_status": 0 @@ -2764,10 +2028,6 @@ "model": "tab.debater", "pk": 199, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Louella Sampson", "novice_status": 1 @@ -2777,10 +2037,6 @@ "model": "tab.debater", "pk": 200, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Briana Watson", "novice_status": 0 @@ -2790,10 +2046,6 @@ "model": "tab.debater", "pk": 201, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jessie Olsen", "novice_status": 0 @@ -2803,10 +2055,6 @@ "model": "tab.debater", "pk": 202, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Johanna Burt", "novice_status": 0 @@ -2816,10 +2064,6 @@ "model": "tab.debater", "pk": 204, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Tanya Manning", "novice_status": 0 @@ -2829,10 +2073,6 @@ "model": "tab.debater", "pk": 205, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Grace Armstrong", "novice_status": 0 @@ -2842,10 +2082,6 @@ "model": "tab.debater", "pk": 206, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Mildred Robbins", "novice_status": 0 @@ -2855,10 +2091,6 @@ "model": "tab.debater", "pk": 207, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Evangeline Lowe", "novice_status": 0 @@ -2868,10 +2100,6 @@ "model": "tab.debater", "pk": 208, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Misty Reilly", "novice_status": 0 @@ -2881,10 +2109,6 @@ "model": "tab.debater", "pk": 209, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Tricia Haley", "novice_status": 0 @@ -2894,10 +2118,6 @@ "model": "tab.debater", "pk": 210, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Madelyn Ball", "novice_status": 0 @@ -2907,10 +2127,6 @@ "model": "tab.debater", "pk": 211, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Millie Guzman", "novice_status": 1 @@ -2920,10 +2136,6 @@ "model": "tab.debater", "pk": 212, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Reyna Gray", "novice_status": 1 @@ -2933,10 +2145,6 @@ "model": "tab.debater", "pk": 213, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Tracie Chase", "novice_status": 1 @@ -2946,10 +2154,6 @@ "model": "tab.debater", "pk": 214, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Beulah Stewart", "novice_status": 1 @@ -2959,10 +2163,6 @@ "model": "tab.debater", "pk": 215, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Judith Tyler", "novice_status": 0 @@ -2972,10 +2172,6 @@ "model": "tab.debater", "pk": 216, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Brandie Walsh", "novice_status": 1 @@ -2985,10 +2181,6 @@ "model": "tab.debater", "pk": 217, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Yesenia Wilkinson", "novice_status": 1 @@ -2998,10 +2190,6 @@ "model": "tab.debater", "pk": 218, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Willa Garner", "novice_status": 1 @@ -3011,10 +2199,6 @@ "model": "tab.debater", "pk": 219, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Taylor Johns", "novice_status": 0 @@ -3024,10 +2208,6 @@ "model": "tab.debater", "pk": 220, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Cecilia Mccoy", "novice_status": 0 @@ -3037,10 +2217,6 @@ "model": "tab.debater", "pk": 221, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Cara Parker", "novice_status": 1 @@ -3050,10 +2226,6 @@ "model": "tab.debater", "pk": 222, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Margret Marks", "novice_status": 0 @@ -3063,10 +2235,6 @@ "model": "tab.debater", "pk": 223, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Benita Bradshaw", "novice_status": 0 @@ -3076,10 +2244,6 @@ "model": "tab.debater", "pk": 224, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Hazel Patton", "novice_status": 1 @@ -3089,10 +2253,6 @@ "model": "tab.debater", "pk": 225, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Candy Massey", "novice_status": 1 @@ -3102,10 +2262,6 @@ "model": "tab.debater", "pk": 227, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Phoebe Livingston", "novice_status": 1 @@ -3115,10 +2271,6 @@ "model": "tab.debater", "pk": 228, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Minerva Pearson", "novice_status": 1 @@ -3128,10 +2280,6 @@ "model": "tab.debater", "pk": 229, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Luann Moon", "novice_status": 0 @@ -3141,10 +2289,6 @@ "model": "tab.debater", "pk": 230, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Trina Kirby", "novice_status": 1 @@ -3154,10 +2298,6 @@ "model": "tab.debater", "pk": 231, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Crystal Simpson", "novice_status": 1 @@ -3167,10 +2307,6 @@ "model": "tab.debater", "pk": 232, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Melissa Grimes", "novice_status": 1 @@ -3180,10 +2316,6 @@ "model": "tab.debater", "pk": 233, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "May Houston", "novice_status": 1 @@ -3193,10 +2325,6 @@ "model": "tab.debater", "pk": 234, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Isabelle Joyce", "novice_status": 1 @@ -3206,10 +2334,6 @@ "model": "tab.debater", "pk": 235, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Aimee Levine", "novice_status": 0 @@ -3219,10 +2343,6 @@ "model": "tab.debater", "pk": 236, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Mary Crawford", "novice_status": 1 @@ -3232,10 +2352,6 @@ "model": "tab.debater", "pk": 237, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Meredith Reynolds", "novice_status": 1 @@ -3245,10 +2361,6 @@ "model": "tab.debater", "pk": 238, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lidia Trevino", "novice_status": 0 @@ -3258,10 +2370,6 @@ "model": "tab.debater", "pk": 239, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sophia Bruce", "novice_status": 1 @@ -3271,10 +2379,6 @@ "model": "tab.debater", "pk": 240, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Shawn Greene", "novice_status": 0 @@ -3284,10 +2388,6 @@ "model": "tab.debater", "pk": 242, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Shawna Cochran", "novice_status": 0 @@ -3297,10 +2397,6 @@ "model": "tab.debater", "pk": 243, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jody Jacobs", "novice_status": 0 @@ -3310,10 +2406,6 @@ "model": "tab.debater", "pk": 244, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lorna Todd", "novice_status": 1 @@ -3323,10 +2415,6 @@ "model": "tab.debater", "pk": 245, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Ericka Petersen", "novice_status": 0 @@ -3336,10 +2424,6 @@ "model": "tab.debater", "pk": 246, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sharon Lee", "novice_status": 0 @@ -3349,10 +2433,6 @@ "model": "tab.debater", "pk": 247, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Nichole Gillespie", "novice_status": 1 @@ -3362,10 +2442,6 @@ "model": "tab.debater", "pk": 248, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Christina Cline", "novice_status": 0 @@ -3375,10 +2451,6 @@ "model": "tab.debater", "pk": 249, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Flora Mclean", "novice_status": 1 @@ -3388,10 +2460,6 @@ "model": "tab.debater", "pk": 250, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Iva Sharpe", "novice_status": 0 @@ -3401,10 +2469,6 @@ "model": "tab.debater", "pk": 251, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Coleen Conley", "novice_status": 1 @@ -3414,10 +2478,6 @@ "model": "tab.debater", "pk": 253, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Nancy Garrison", "novice_status": 1 @@ -3427,10 +2487,6 @@ "model": "tab.debater", "pk": 254, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jayne Sherman", "novice_status": 0 @@ -3440,10 +2496,6 @@ "model": "tab.debater", "pk": 255, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Vonda Duke", "novice_status": 1 @@ -3453,10 +2505,6 @@ "model": "tab.debater", "pk": 256, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Addie Joseph", "novice_status": 1 @@ -3466,10 +2514,6 @@ "model": "tab.debater", "pk": 257, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Paulette Newton", "novice_status": 0 @@ -3479,10 +2523,6 @@ "model": "tab.debater", "pk": 258, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Noemi Stuart", "novice_status": 0 @@ -3492,10 +2532,6 @@ "model": "tab.debater", "pk": 259, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Molly Carver", "novice_status": 1 @@ -3505,10 +2541,6 @@ "model": "tab.debater", "pk": 260, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Teresa Merrill", "novice_status": 1 @@ -3518,10 +2550,6 @@ "model": "tab.debater", "pk": 262, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lakeisha Richard", "novice_status": 0 @@ -3531,10 +2559,6 @@ "model": "tab.debater", "pk": 263, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Concepcion Booker", "novice_status": 1 @@ -3544,10 +2568,6 @@ "model": "tab.debater", "pk": 264, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Mae Fisher", "novice_status": 1 @@ -3557,10 +2577,6 @@ "model": "tab.debater", "pk": 265, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Beatrice Potter", "novice_status": 1 @@ -3570,10 +2586,6 @@ "model": "tab.debater", "pk": 266, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Deanna Dalton", "novice_status": 1 @@ -3583,10 +2595,6 @@ "model": "tab.debater", "pk": 267, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Nelda Santiago", "novice_status": 1 @@ -3596,10 +2604,6 @@ "model": "tab.debater", "pk": 268, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Casandra Holden", "novice_status": 0 @@ -3609,10 +2613,6 @@ "model": "tab.debater", "pk": 269, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Janine Brooks", "novice_status": 0 @@ -3622,10 +2622,6 @@ "model": "tab.debater", "pk": 270, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Stacey Bond", "novice_status": 0 @@ -3635,10 +2631,6 @@ "model": "tab.debater", "pk": 271, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Stacy Bailey", "novice_status": 0 @@ -3648,10 +2640,6 @@ "model": "tab.debater", "pk": 272, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Casey Sanders", "novice_status": 0 @@ -3661,10 +2649,6 @@ "model": "tab.debater", "pk": 273, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sandra Casey", "novice_status": 1 @@ -3674,10 +2658,6 @@ "model": "tab.debater", "pk": 274, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Johnnie Valenzuela", "novice_status": 0 @@ -3687,10 +2667,6 @@ "model": "tab.debater", "pk": 275, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kelley Pope", "novice_status": 0 @@ -3700,10 +2676,6 @@ "model": "tab.debater", "pk": 276, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lillie Mcleod", "novice_status": 1 @@ -3713,10 +2685,6 @@ "model": "tab.debater", "pk": 277, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Violet Rowland", "novice_status": 0 @@ -3726,10 +2694,6 @@ "model": "tab.debater", "pk": 278, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Guadalupe Roach", "novice_status": 1 @@ -3739,10 +2703,6 @@ "model": "tab.debater", "pk": 279, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Elisabeth Fry", "novice_status": 1 @@ -3752,10 +2712,6 @@ "model": "tab.debater", "pk": 280, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lavonne Oconnor", "novice_status": 0 @@ -3765,10 +2721,6 @@ "model": "tab.debater", "pk": 282, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lillian Bonner", "novice_status": 1 @@ -3778,10 +2730,6 @@ "model": "tab.debater", "pk": 283, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lucinda Strickland", "novice_status": 0 @@ -3791,10 +2739,6 @@ "model": "tab.debater", "pk": 284, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Clare Flowers", "novice_status": 0 @@ -3804,10 +2748,6 @@ "model": "tab.debater", "pk": 285, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Tiffany Lynn", "novice_status": 1 @@ -3817,10 +2757,6 @@ "model": "tab.debater", "pk": 286, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Shauna Stone", "novice_status": 1 @@ -3830,10 +2766,6 @@ "model": "tab.debater", "pk": 287, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sandy Odonnell", "novice_status": 1 @@ -3843,10 +2775,6 @@ "model": "tab.debater", "pk": 288, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Abigail Whitley", "novice_status": 1 @@ -3856,10 +2784,6 @@ "model": "tab.debater", "pk": 289, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sherri Blevins", "novice_status": 1 @@ -3869,10 +2793,6 @@ "model": "tab.debater", "pk": 290, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Robert Leonard", "novice_status": 1 @@ -3882,10 +2802,6 @@ "model": "tab.debater", "pk": 291, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Tami Larsen", "novice_status": 1 @@ -3895,10 +2811,6 @@ "model": "tab.debater", "pk": 292, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Irene Hewitt", "novice_status": 1 @@ -3908,10 +2820,6 @@ "model": "tab.debater", "pk": 293, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Ruthie Gilbert", "novice_status": 1 @@ -3921,10 +2829,6 @@ "model": "tab.debater", "pk": 294, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Corrine Cherry", "novice_status": 1 @@ -3934,10 +2838,6 @@ "model": "tab.debater", "pk": 295, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Daisy Mcdowell", "novice_status": 1 @@ -3947,10 +2847,6 @@ "model": "tab.debater", "pk": 297, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Edna Bennett", "novice_status": 1 @@ -3960,10 +2856,6 @@ "model": "tab.debater", "pk": 299, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "James Mcbride", "novice_status": 1 @@ -3973,10 +2865,6 @@ "model": "tab.debater", "pk": 300, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Earnestine Pitts", "novice_status": 0 @@ -3986,10 +2874,6 @@ "model": "tab.debater", "pk": 301, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Karina Cardenas", "novice_status": 0 @@ -3999,10 +2883,6 @@ "model": "tab.debater", "pk": 302, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Marsha Zamora", "novice_status": 1 @@ -4012,10 +2892,6 @@ "model": "tab.debater", "pk": 303, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Rachel Cleveland", "novice_status": 0 @@ -4025,10 +2901,6 @@ "model": "tab.debater", "pk": 304, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Ladonna Murphy", "novice_status": 1 @@ -4038,10 +2910,6 @@ "model": "tab.debater", "pk": 305, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Serena Mcgee", "novice_status": 0 @@ -4051,10 +2919,6 @@ "model": "tab.debater", "pk": 306, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jeannie Foley", "novice_status": 1 @@ -4064,10 +2928,6 @@ "model": "tab.debater", "pk": 307, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Ellen Conway", "novice_status": 0 @@ -4077,10 +2937,6 @@ "model": "tab.debater", "pk": 308, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Ada Day", "novice_status": 0 @@ -4090,10 +2946,6 @@ "model": "tab.debater", "pk": 309, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Melanie Hammond", "novice_status": 0 @@ -4103,10 +2955,6 @@ "model": "tab.debater", "pk": 310, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Roseann Crane", "novice_status": 0 @@ -4116,10 +2964,6 @@ "model": "tab.debater", "pk": 311, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kitty Reyes", "novice_status": 0 @@ -4129,10 +2973,6 @@ "model": "tab.debater", "pk": 312, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Margaret Madden", "novice_status": 0 @@ -4142,10 +2982,6 @@ "model": "tab.debater", "pk": 313, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Gabrielle Valencia", "novice_status": 0 @@ -4155,10 +2991,6 @@ "model": "tab.debater", "pk": 314, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Faye Shaw", "novice_status": 0 @@ -4168,10 +3000,6 @@ "model": "tab.debater", "pk": 315, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Dorothea Frederick", "novice_status": 0 @@ -4181,10 +3009,6 @@ "model": "tab.debater", "pk": 316, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Terra Mcdonald", "novice_status": 0 @@ -4194,10 +3018,6 @@ "model": "tab.debater", "pk": 317, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sheri Vega", "novice_status": 1 @@ -4207,10 +3027,6 @@ "model": "tab.debater", "pk": 318, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Daphne Owen", "novice_status": 1 @@ -4220,10 +3036,6 @@ "model": "tab.debater", "pk": 319, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sheryl Klein", "novice_status": 1 @@ -4233,10 +3045,6 @@ "model": "tab.debater", "pk": 320, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jami Kidd", "novice_status": 1 @@ -4246,10 +3054,6 @@ "model": "tab.debater", "pk": 323, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Leona Callahan", "novice_status": 1 @@ -4259,10 +3063,6 @@ "model": "tab.debater", "pk": 324, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Penny York", "novice_status": 1 @@ -4272,10 +3072,6 @@ "model": "tab.debater", "pk": 325, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Carey Estes", "novice_status": 1 @@ -4285,10 +3081,6 @@ "model": "tab.debater", "pk": 326, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Linda Silva", "novice_status": 1 @@ -4298,10 +3090,6 @@ "model": "tab.debater", "pk": 327, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Tina Barr", "novice_status": 0 @@ -4311,10 +3099,6 @@ "model": "tab.debater", "pk": 328, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Ilene Frost", "novice_status": 0 @@ -4324,10 +3108,6 @@ "model": "tab.debater", "pk": 329, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Deena Reid", "novice_status": 0 @@ -4337,10 +3117,6 @@ "model": "tab.debater", "pk": 330, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Penelope Haynes", "novice_status": 0 @@ -4350,10 +3126,6 @@ "model": "tab.debater", "pk": 331, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jasmine Robertson", "novice_status": 0 @@ -4363,10 +3135,6 @@ "model": "tab.debater", "pk": 332, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Stephanie Grant", "novice_status": 0 @@ -4376,10 +3144,6 @@ "model": "tab.debater", "pk": 333, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Olive Gilliam", "novice_status": 1 @@ -4389,10 +3153,6 @@ "model": "tab.debater", "pk": 334, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Maggie Long", "novice_status": 0 @@ -4402,10 +3162,6 @@ "model": "tab.debater", "pk": 335, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Roxanne King", "novice_status": 0 @@ -4415,10 +3171,6 @@ "model": "tab.debater", "pk": 336, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Justine Malone", "novice_status": 0 @@ -4428,10 +3180,6 @@ "model": "tab.debater", "pk": 337, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Diann Prince", "novice_status": 0 @@ -4441,10 +3189,6 @@ "model": "tab.debater", "pk": 338, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Alba Randall", "novice_status": 0 @@ -4454,10 +3198,6 @@ "model": "tab.debater", "pk": 339, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Martina Hunter", "novice_status": 0 @@ -4467,10 +3207,6 @@ "model": "tab.debater", "pk": 340, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Valerie Chapman", "novice_status": 1 @@ -4480,10 +3216,6 @@ "model": "tab.debater", "pk": 341, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Angelina Lambert", "novice_status": 1 @@ -4493,10 +3225,6 @@ "model": "tab.debater", "pk": 342, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Vivian Holt", "novice_status": 1 @@ -4506,10 +3234,6 @@ "model": "tab.debater", "pk": 343, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Alma Dodson", "novice_status": 1 @@ -4519,10 +3243,6 @@ "model": "tab.debater", "pk": 344, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Edith Ayala", "novice_status": 1 @@ -4532,10 +3252,6 @@ "model": "tab.debater", "pk": 345, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Melody Steele", "novice_status": 1 @@ -4545,10 +3261,6 @@ "model": "tab.debater", "pk": 346, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Gertrude Cote", "novice_status": 0 @@ -4558,10 +3270,6 @@ "model": "tab.debater", "pk": 347, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Barbara Wyatt", "novice_status": 0 @@ -4571,10 +3279,6 @@ "model": "tab.debater", "pk": 348, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Berta Galloway", "novice_status": 0 @@ -4584,10 +3288,6 @@ "model": "tab.debater", "pk": 349, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Cathy Gallegos", "novice_status": 0 @@ -4597,10 +3297,6 @@ "model": "tab.debater", "pk": 350, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jaclyn Mcmahon", "novice_status": 1 @@ -4610,10 +3306,6 @@ "model": "tab.debater", "pk": 351, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Angeline Garcia", "novice_status": 1 @@ -4623,10 +3315,6 @@ "model": "tab.debater", "pk": 352, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Latoya Dickerson", "novice_status": 1 @@ -4636,10 +3324,6 @@ "model": "tab.debater", "pk": 353, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Ingrid Durham", "novice_status": 1 @@ -4649,10 +3333,6 @@ "model": "tab.debater", "pk": 355, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sally Ray", "novice_status": 0 @@ -4662,10 +3342,6 @@ "model": "tab.debater", "pk": 356, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kari Hardin", "novice_status": 1 @@ -4675,10 +3351,6 @@ "model": "tab.debater", "pk": 358, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Bianca Bridges", "novice_status": 1 @@ -4688,10 +3360,6 @@ "model": "tab.debater", "pk": 359, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Janette Hays", "novice_status": 1 @@ -4701,10 +3369,6 @@ "model": "tab.debater", "pk": 360, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Gena Mejia", "novice_status": 1 @@ -4714,10 +3378,6 @@ "model": "tab.debater", "pk": 361, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Shelby Holloway", "novice_status": 1 @@ -4727,10 +3387,6 @@ "model": "tab.debater", "pk": 362, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "John Love", "novice_status": 1 @@ -4740,10 +3396,6 @@ "model": "tab.debater", "pk": 363, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Althea Sullivan", "novice_status": 1 @@ -4753,10 +3405,6 @@ "model": "tab.team", "pk": 1, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Chicago LM", "school": 2, @@ -4774,10 +3422,6 @@ "model": "tab.team", "pk": 2, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Syracuse Coriolanus", "school": 3, @@ -4795,10 +3439,6 @@ "model": "tab.team", "pk": 4, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Syracuse Cymbeline", "school": 3, @@ -4816,10 +3456,6 @@ "model": "tab.team", "pk": 6, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Syracuse Timon", "school": 3, @@ -4837,10 +3473,6 @@ "model": "tab.team", "pk": 7, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wellesley Chairman Meow", "school": 4, @@ -4858,10 +3490,6 @@ "model": "tab.team", "pk": 8, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "NU CF", "school": 6, @@ -4879,10 +3507,6 @@ "model": "tab.team", "pk": 9, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wellesley Birkenstock A", "school": 4, @@ -4900,10 +3524,6 @@ "model": "tab.team", "pk": 10, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "NU Powerpuff Girls A", "school": 6, @@ -4921,10 +3541,6 @@ "model": "tab.team", "pk": 11, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Fordham LCD Catsystem", "school": 33, @@ -4942,10 +3558,6 @@ "model": "tab.team", "pk": 12, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "NU Return of the Pups", "school": 6, @@ -4963,10 +3575,6 @@ "model": "tab.team", "pk": 13, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wellesley Failed System", "school": 4, @@ -4984,10 +3592,6 @@ "model": "tab.team", "pk": 14, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Fordham GZ", "school": 33, @@ -5005,10 +3609,6 @@ "model": "tab.team", "pk": 15, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wellesley Elle Me Dit", "school": 4, @@ -5026,10 +3626,6 @@ "model": "tab.team", "pk": 16, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "West Point MP", "school": 9, @@ -5047,10 +3643,6 @@ "model": "tab.team", "pk": 17, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Duke PS", "school": 34, @@ -5068,10 +3660,6 @@ "model": "tab.team", "pk": 18, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wellesley Bob Dylan", "school": 4, @@ -5089,10 +3677,6 @@ "model": "tab.team", "pk": 19, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Duke SA", "school": 34, @@ -5110,10 +3694,6 @@ "model": "tab.team", "pk": 21, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Amherst PL", "school": 28, @@ -5131,10 +3711,6 @@ "model": "tab.team", "pk": 22, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wellesley/F&M Hybrid", "school": 4, @@ -5152,10 +3728,6 @@ "model": "tab.team", "pk": 23, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Maryland KS", "school": 35, @@ -5173,10 +3745,6 @@ "model": "tab.team", "pk": 24, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "West Point Medical", "school": 9, @@ -5194,10 +3762,6 @@ "model": "tab.team", "pk": 25, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Hopkins IS", "school": 5, @@ -5215,10 +3779,6 @@ "model": "tab.team", "pk": 26, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "West Point QM", "school": 9, @@ -5236,10 +3796,6 @@ "model": "tab.team", "pk": 27, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Amherst RJ", "school": 28, @@ -5257,10 +3813,6 @@ "model": "tab.team", "pk": 28, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Hopkins Church of Chirt", "school": 5, @@ -5278,10 +3830,6 @@ "model": "tab.team", "pk": 29, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "West Point Armor", "school": 9, @@ -5299,10 +3847,6 @@ "model": "tab.team", "pk": 30, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Columbia A, Why Not A+", "school": 7, @@ -5320,10 +3864,6 @@ "model": "tab.team", "pk": 31, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Pitt Disposable Dignity", "school": 30, @@ -5341,10 +3881,6 @@ "model": "tab.team", "pk": 32, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "West Point Infantry", "school": 9, @@ -5362,10 +3898,6 @@ "model": "tab.team", "pk": 33, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Columbia Blue Ivy (Carter)", "school": 7, @@ -5383,10 +3915,6 @@ "model": "tab.team", "pk": 34, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Pitt Magnanimous Sultans", "school": 30, @@ -5404,10 +3932,6 @@ "model": "tab.team", "pk": 35, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "West Point EOD", "school": 9, @@ -5425,10 +3949,6 @@ "model": "tab.team", "pk": 36, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Columbia North West", "school": 7, @@ -5446,10 +3966,6 @@ "model": "tab.team", "pk": 37, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Berkeley 3", "school": 10, @@ -5467,10 +3983,6 @@ "model": "tab.team", "pk": 38, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Columbia Moon Unit Zappa: You'", "school": 7, @@ -5488,10 +4000,6 @@ "model": "tab.team", "pk": 39, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Berkeley 2", "school": 10, @@ -5509,10 +4017,6 @@ "model": "tab.team", "pk": 40, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Columbia Blanket Jackson", "school": 7, @@ -5530,10 +4034,6 @@ "model": "tab.team", "pk": 41, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Berkeley 1", "school": 10, @@ -5551,10 +4051,6 @@ "model": "tab.team", "pk": 42, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brandeis EN", "school": 31, @@ -5572,10 +4068,6 @@ "model": "tab.team", "pk": 43, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Columbia/Stanford Sr8bros", "school": 11, @@ -5593,10 +4085,6 @@ "model": "tab.team", "pk": 44, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "GW MM", "school": 13, @@ -5614,10 +4102,6 @@ "model": "tab.team", "pk": 47, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "NYU A", "school": 8, @@ -5635,10 +4119,6 @@ "model": "tab.team", "pk": 48, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "AU/GW Hybrid", "school": 13, @@ -5656,10 +4136,6 @@ "model": "tab.team", "pk": 49, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "NYU B", "school": 8, @@ -5677,10 +4153,6 @@ "model": "tab.team", "pk": 50, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "GW Roundup", "school": 13, @@ -5698,10 +4170,6 @@ "model": "tab.team", "pk": 51, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "NYU C", "school": 8, @@ -5719,10 +4187,6 @@ "model": "tab.team", "pk": 52, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "AU Patrick Bateman", "school": 27, @@ -5740,10 +4204,6 @@ "model": "tab.team", "pk": 54, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "NYU D", "school": 8, @@ -5761,10 +4221,6 @@ "model": "tab.team", "pk": 55, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "GW B", "school": 13, @@ -5782,10 +4238,6 @@ "model": "tab.team", "pk": 56, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "AU Elle Woods", "school": 27, @@ -5803,10 +4255,6 @@ "model": "tab.team", "pk": 57, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Stanford Rall Bad Bitchezzz", "school": 11, @@ -5824,10 +4272,6 @@ "model": "tab.team", "pk": 58, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "AU Frasier Crane", "school": 27, @@ -5845,10 +4289,6 @@ "model": "tab.team", "pk": 59, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "GW Last SOTY/Penultimate SOTY", "school": 13, @@ -5866,10 +4306,6 @@ "model": "tab.team", "pk": 60, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Penn JT", "school": 12, @@ -5887,10 +4323,6 @@ "model": "tab.team", "pk": 61, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "AU Harvey Specter", "school": 27, @@ -5908,10 +4340,6 @@ "model": "tab.team", "pk": 62, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Smith Gryffindor", "school": 15, @@ -5929,10 +4357,6 @@ "model": "tab.team", "pk": 63, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Penn MW", "school": 12, @@ -5950,10 +4374,6 @@ "model": "tab.team", "pk": 65, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Penn DS", "school": 12, @@ -5971,10 +4391,6 @@ "model": "tab.team", "pk": 66, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Smith Ravenclaw", "school": 15, @@ -5992,10 +4408,6 @@ "model": "tab.team", "pk": 67, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Smith Slytherin", "school": 15, @@ -6013,10 +4425,6 @@ "model": "tab.team", "pk": 68, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Tufts EK", "school": 14, @@ -6034,10 +4442,6 @@ "model": "tab.team", "pk": 69, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Georgetown Bend and Snap", "school": 23, @@ -6055,10 +4459,6 @@ "model": "tab.team", "pk": 70, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Tufts ES", "school": 14, @@ -6076,10 +4476,6 @@ "model": "tab.team", "pk": 71, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "W&M Django's Novices", "school": 16, @@ -6097,10 +4493,6 @@ "model": "tab.team", "pk": 72, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Tufts Bendrew Husick", "school": 14, @@ -6118,10 +4510,6 @@ "model": "tab.team", "pk": 73, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "W&M/UAlbany Hybrid", "school": 42, @@ -6139,10 +4527,6 @@ "model": "tab.team", "pk": 74, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Tufts DR", "school": 14, @@ -6160,10 +4544,6 @@ "model": "tab.team", "pk": 75, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "W&M A", "school": 16, @@ -6181,10 +4561,6 @@ "model": "tab.team", "pk": 76, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Tufts TC", "school": 14, @@ -6202,10 +4578,6 @@ "model": "tab.team", "pk": 77, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wesleyan Amy Gardner", "school": 21, @@ -6223,10 +4595,6 @@ "model": "tab.team", "pk": 78, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Swat MR", "school": 36, @@ -6244,10 +4612,6 @@ "model": "tab.team", "pk": 79, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "BU BP", "school": 17, @@ -6265,10 +4629,6 @@ "model": "tab.team", "pk": 81, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brown GP", "school": 18, @@ -6286,10 +4646,6 @@ "model": "tab.team", "pk": 82, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "BU Cargo Shorts", "school": 17, @@ -6307,10 +4663,6 @@ "model": "tab.team", "pk": 83, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Swat ND", "school": 36, @@ -6328,10 +4680,6 @@ "model": "tab.team", "pk": 84, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wesleyan Donna Moss", "school": 21, @@ -6349,10 +4697,6 @@ "model": "tab.team", "pk": 85, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brown Veal of Ignorance", "school": 18, @@ -6370,10 +4714,6 @@ "model": "tab.team", "pk": 86, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "BU NN", "school": 17, @@ -6391,10 +4731,6 @@ "model": "tab.team", "pk": 87, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brown MN", "school": 18, @@ -6412,10 +4748,6 @@ "model": "tab.team", "pk": 89, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wesleyan Toby Ziegler", "school": 21, @@ -6433,10 +4765,6 @@ "model": "tab.team", "pk": 90, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "BU VJ", "school": 17, @@ -6454,10 +4782,6 @@ "model": "tab.team", "pk": 92, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wesleyan Leo McGarry", "school": 21, @@ -6475,10 +4799,6 @@ "model": "tab.team", "pk": 93, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "BU RA", "school": 17, @@ -6496,10 +4816,6 @@ "model": "tab.team", "pk": 94, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brown CY", "school": 18, @@ -6517,10 +4833,6 @@ "model": "tab.team", "pk": 95, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "BU Batman", "school": 17, @@ -6538,10 +4850,6 @@ "model": "tab.team", "pk": 97, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brown Final Machination", "school": 18, @@ -6559,10 +4867,6 @@ "model": "tab.team", "pk": 98, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Princeton A", "school": 19, @@ -6580,10 +4884,6 @@ "model": "tab.team", "pk": 99, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brown Amish Sex", "school": 18, @@ -6601,10 +4901,6 @@ "model": "tab.team", "pk": 100, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Princeton Legal Liability", "school": 19, @@ -6622,10 +4918,6 @@ "model": "tab.team", "pk": 101, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wesleyan CJ Cregg", "school": 21, @@ -6643,10 +4935,6 @@ "model": "tab.team", "pk": 102, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Princeton SJ", "school": 19, @@ -6664,10 +4952,6 @@ "model": "tab.team", "pk": 103, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Swat BH", "school": 36, @@ -6685,10 +4969,6 @@ "model": "tab.team", "pk": 104, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wesleyan Josh Lyman", "school": 21, @@ -6706,10 +4986,6 @@ "model": "tab.team", "pk": 105, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Princeton RT", "school": 19, @@ -6727,10 +5003,6 @@ "model": "tab.team", "pk": 106, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Princeton MC", "school": 19, @@ -6748,10 +5020,6 @@ "model": "tab.team", "pk": 107, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "MHC Firebreathing Rubberduckie", "school": 26, @@ -6769,10 +5037,6 @@ "model": "tab.team", "pk": 108, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Maryland DZ", "school": 35, @@ -6790,10 +5054,6 @@ "model": "tab.team", "pk": 109, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "MHC Whose Lines", "school": 26, @@ -6811,10 +5071,6 @@ "model": "tab.team", "pk": 110, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Maryland Sailor Scouts", "school": 35, @@ -6832,10 +5088,6 @@ "model": "tab.team", "pk": 111, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Binghamton TH", "school": 32, @@ -6853,10 +5105,6 @@ "model": "tab.team", "pk": 112, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "MHC Defined Lines", "school": 26, @@ -6874,10 +5122,6 @@ "model": "tab.team", "pk": 113, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "F&M Lennonists", "school": 20, @@ -6895,10 +5139,6 @@ "model": "tab.team", "pk": 114, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Bates Maybach Keys", "school": 25, @@ -6916,10 +5156,6 @@ "model": "tab.team", "pk": 115, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Maryland FK", "school": 35, @@ -6937,10 +5173,6 @@ "model": "tab.team", "pk": 116, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Binghamton PC", "school": 32, @@ -6958,10 +5190,6 @@ "model": "tab.team", "pk": 117, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "F&M B", "school": 20, @@ -6979,10 +5207,6 @@ "model": "tab.team", "pk": 118, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Bates Menage", "school": 25, @@ -7000,10 +5224,6 @@ "model": "tab.team", "pk": 119, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Bates Swahili", "school": 25, @@ -7021,10 +5241,6 @@ "model": "tab.team", "pk": 120, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Maryland/Yale B^2 + 2BS + S^2", "school": 1, @@ -7042,10 +5258,6 @@ "model": "tab.team", "pk": 121, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Binghamton LW", "school": 32, @@ -7063,10 +5275,6 @@ "model": "tab.team", "pk": 122, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Rutgers War Bears", "school": 22, @@ -7084,10 +5292,6 @@ "model": "tab.team", "pk": 123, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "PC HB", "school": 38, @@ -7105,10 +5309,6 @@ "model": "tab.team", "pk": 124, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Bates Blouse", "school": 25, @@ -7126,10 +5326,6 @@ "model": "tab.team", "pk": 126, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Binghamton DS", "school": 32, @@ -7147,10 +5343,6 @@ "model": "tab.team", "pk": 127, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "PC OP", "school": 38, @@ -7168,10 +5360,6 @@ "model": "tab.team", "pk": 128, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Bates Spouse", "school": 25, @@ -7189,10 +5377,6 @@ "model": "tab.team", "pk": 129, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "PC AP", "school": 38, @@ -7210,10 +5394,6 @@ "model": "tab.team", "pk": 131, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Bates House", "school": 25, @@ -7231,10 +5411,6 @@ "model": "tab.team", "pk": 132, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "PC LL", "school": 38, @@ -7252,10 +5428,6 @@ "model": "tab.team", "pk": 133, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Bates Merchants of Yeezus", "school": 25, @@ -7273,10 +5445,6 @@ "model": "tab.team", "pk": 134, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Rutgers LP", "school": 22, @@ -7294,10 +5462,6 @@ "model": "tab.team", "pk": 135, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "MIT We Can't Stop", "school": 37, @@ -7315,10 +5479,6 @@ "model": "tab.team", "pk": 137, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Williams Sorry We Can't Think ", "school": 39, @@ -7336,10 +5496,6 @@ "model": "tab.team", "pk": 138, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "MIT Wrecking Ball", "school": 37, @@ -7357,10 +5513,6 @@ "model": "tab.team", "pk": 139, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Rutgers HP", "school": 22, @@ -7378,10 +5530,6 @@ "model": "tab.team", "pk": 140, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Stony Brook Muliminal Messages", "school": 29, @@ -7399,10 +5547,6 @@ "model": "tab.team", "pk": 141, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Middlebury Fuck, The Novices U", "school": 24, @@ -7420,10 +5564,6 @@ "model": "tab.team", "pk": 142, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Stony Brook Brown Out", "school": 29, @@ -7441,10 +5581,6 @@ "model": "tab.team", "pk": 143, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Rutgers Linsanity Sosa", "school": 22, @@ -7462,10 +5598,6 @@ "model": "tab.team", "pk": 145, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Stony Brook Malay Fighters", "school": 29, @@ -7483,10 +5615,6 @@ "model": "tab.team", "pk": 146, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "MIT Party in the USA", "school": 37, @@ -7504,10 +5632,6 @@ "model": "tab.team", "pk": 147, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Middlebury pB and j", "school": 24, @@ -7525,10 +5649,6 @@ "model": "tab.team", "pk": 148, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Stony Brook NN", "school": 29, @@ -7546,10 +5666,6 @@ "model": "tab.team", "pk": 150, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Middlebury Midd Kid A", "school": 24, @@ -7567,10 +5683,6 @@ "model": "tab.team", "pk": 151, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "MIT Best of Both Worlds", "school": 37, @@ -7588,10 +5700,6 @@ "model": "tab.team", "pk": 152, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "MIT CSB", "school": 37, @@ -7609,10 +5717,6 @@ "model": "tab.team", "pk": 153, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Yale LR", "school": 1, @@ -7630,10 +5734,6 @@ "model": "tab.team", "pk": 154, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Yale Deezy", "school": 1, @@ -7651,10 +5751,6 @@ "model": "tab.team", "pk": 155, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Yale SZ", "school": 1, @@ -7672,10 +5768,6 @@ "model": "tab.team", "pk": 156, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Yale BB", "school": 1, @@ -7693,10 +5785,6 @@ "model": "tab.team", "pk": 157, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Yale HJ", "school": 1, @@ -7714,10 +5802,6 @@ "model": "tab.team", "pk": 158, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Yale HR", "school": 1, @@ -7735,10 +5819,6 @@ "model": "tab.team", "pk": 160, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Yale CaliBoys", "school": 1, @@ -7756,10 +5836,6 @@ "model": "tab.team", "pk": 162, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Yale BP", "school": 1, @@ -7777,10 +5853,6 @@ "model": "tab.team", "pk": 163, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Yale SWP", "school": 1, @@ -7798,10 +5870,6 @@ "model": "tab.team", "pk": 164, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Yale/CCSF Swahili A", "school": 43, @@ -7819,10 +5887,6 @@ "model": "tab.team", "pk": 165, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Middlebury Snark A", "school": 24, @@ -7840,10 +5904,6 @@ "model": "tab.team", "pk": 166, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brandeis AL", "school": 31, @@ -7861,10 +5921,6 @@ "model": "tab.team", "pk": 167, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brandeis GW", "school": 31, @@ -7882,10 +5938,6 @@ "model": "tab.team", "pk": 168, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brandeis Yaas Gaga", "school": 31, @@ -7903,10 +5955,6 @@ "model": "tab.team", "pk": 169, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brandeis C'Mon", "school": 31, @@ -7924,10 +5972,6 @@ "model": "tab.team", "pk": 170, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brandeis Crazy Kids", "school": 31, @@ -7945,10 +5989,6 @@ "model": "tab.team", "pk": 171, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brandeis Tik Tok", "school": 31, @@ -7966,10 +6006,6 @@ "model": "tab.team", "pk": 172, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Rutgers/Brown Power & Control", "school": 18, @@ -7987,10 +6023,6 @@ "model": "tab.team", "pk": 173, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Rutgers Swirl", "school": 22, @@ -8008,10 +6040,6 @@ "model": "tab.team", "pk": 174, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brandeis JS", "school": 31, @@ -8029,10 +6057,6 @@ "model": "tab.team", "pk": 175, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brandeis LA", "school": 31, diff --git a/mittab/apps/tab/fixtures/testing_finished_db.json b/mittab/apps/tab/fixtures/testing_finished_db.json index ea66f2816..f17aab954 100644 --- a/mittab/apps/tab/fixtures/testing_finished_db.json +++ b/mittab/apps/tab/fixtures/testing_finished_db.json @@ -372,10 +372,6 @@ "model": "tab.debater", "pk": 1, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Alfreda Harvey", "novice_status": 0 @@ -385,10 +381,6 @@ "model": "tab.debater", "pk": 2, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sylvia Mcclure", "novice_status": 0 @@ -398,10 +390,6 @@ "model": "tab.debater", "pk": 3, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Judy Tanner", "novice_status": 0 @@ -411,10 +399,6 @@ "model": "tab.debater", "pk": 4, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Janet Morris", "novice_status": 0 @@ -424,10 +408,6 @@ "model": "tab.debater", "pk": 5, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Gloria Floyd", "novice_status": 1 @@ -437,10 +417,6 @@ "model": "tab.debater", "pk": 7, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kellie Taylor", "novice_status": 1 @@ -450,10 +426,6 @@ "model": "tab.debater", "pk": 8, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Fay White", "novice_status": 1 @@ -463,10 +435,6 @@ "model": "tab.debater", "pk": 9, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Juliana Sanford", "novice_status": 1 @@ -476,10 +444,6 @@ "model": "tab.debater", "pk": 11, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Elizabeth Underwood", "novice_status": 1 @@ -489,10 +453,6 @@ "model": "tab.debater", "pk": 12, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Leanne Yates", "novice_status": 0 @@ -502,10 +462,6 @@ "model": "tab.debater", "pk": 13, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Minnie Cantrell", "novice_status": 1 @@ -515,10 +471,6 @@ "model": "tab.debater", "pk": 14, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lisa Brown", "novice_status": 1 @@ -528,10 +480,6 @@ "model": "tab.debater", "pk": 15, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Whitney Crosby", "novice_status": 1 @@ -541,10 +489,6 @@ "model": "tab.debater", "pk": 16, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Rae Little", "novice_status": 0 @@ -554,10 +498,6 @@ "model": "tab.debater", "pk": 17, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Andrea Arnold", "novice_status": 1 @@ -567,10 +507,6 @@ "model": "tab.debater", "pk": 18, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Simone Hampton", "novice_status": 0 @@ -580,10 +516,6 @@ "model": "tab.debater", "pk": 19, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Erna Wiley", "novice_status": 0 @@ -593,10 +525,6 @@ "model": "tab.debater", "pk": 20, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Ursula Perez", "novice_status": 1 @@ -606,10 +534,6 @@ "model": "tab.debater", "pk": 21, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Josefa Moore", "novice_status": 0 @@ -619,10 +543,6 @@ "model": "tab.debater", "pk": 22, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Hattie Bray", "novice_status": 1 @@ -632,10 +552,6 @@ "model": "tab.debater", "pk": 23, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lea Weaver", "novice_status": 0 @@ -645,10 +561,6 @@ "model": "tab.debater", "pk": 24, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Monica Harmon", "novice_status": 1 @@ -658,10 +570,6 @@ "model": "tab.debater", "pk": 25, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Brooke Hood", "novice_status": 0 @@ -671,10 +579,6 @@ "model": "tab.debater", "pk": 26, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lora Melendez", "novice_status": 0 @@ -684,10 +588,6 @@ "model": "tab.debater", "pk": 27, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lourdes Allen", "novice_status": 0 @@ -697,10 +597,6 @@ "model": "tab.debater", "pk": 28, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Angelita Blake", "novice_status": 1 @@ -710,10 +606,6 @@ "model": "tab.debater", "pk": 29, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jordan Huffman", "novice_status": 1 @@ -723,10 +615,6 @@ "model": "tab.debater", "pk": 30, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kathy Beard", "novice_status": 1 @@ -736,10 +624,6 @@ "model": "tab.debater", "pk": 31, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Laurie Barrett", "novice_status": 1 @@ -749,10 +633,6 @@ "model": "tab.debater", "pk": 32, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Catalina Mack", "novice_status": 1 @@ -762,10 +642,6 @@ "model": "tab.debater", "pk": 33, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Patricia Waters", "novice_status": 1 @@ -775,10 +651,6 @@ "model": "tab.debater", "pk": 34, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Caroline Franco", "novice_status": 1 @@ -788,10 +660,6 @@ "model": "tab.debater", "pk": 35, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Tracy Jackson", "novice_status": 1 @@ -801,10 +669,6 @@ "model": "tab.debater", "pk": 36, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Antoinette Mercer", "novice_status": 1 @@ -814,10 +678,6 @@ "model": "tab.debater", "pk": 37, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Rosalyn Leon", "novice_status": 1 @@ -827,10 +687,6 @@ "model": "tab.debater", "pk": 38, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Catherine Burris", "novice_status": 1 @@ -840,10 +696,6 @@ "model": "tab.debater", "pk": 39, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Petra Solis", "novice_status": 1 @@ -853,10 +705,6 @@ "model": "tab.debater", "pk": 40, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Marisa Hodges", "novice_status": 1 @@ -866,10 +714,6 @@ "model": "tab.debater", "pk": 41, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Aline Lott", "novice_status": 1 @@ -879,10 +723,6 @@ "model": "tab.debater", "pk": 42, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kristin Romero", "novice_status": 1 @@ -892,10 +732,6 @@ "model": "tab.debater", "pk": 43, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Fern Abbott", "novice_status": 1 @@ -905,10 +741,6 @@ "model": "tab.debater", "pk": 44, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Essie Wilkerson", "novice_status": 1 @@ -918,10 +750,6 @@ "model": "tab.debater", "pk": 45, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Wilma Montgomery", "novice_status": 0 @@ -931,10 +759,6 @@ "model": "tab.debater", "pk": 46, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jeannine Burton", "novice_status": 1 @@ -944,10 +768,6 @@ "model": "tab.debater", "pk": 47, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Angie Adams", "novice_status": 0 @@ -957,10 +777,6 @@ "model": "tab.debater", "pk": 48, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Selma Roberson", "novice_status": 1 @@ -970,10 +786,6 @@ "model": "tab.debater", "pk": 49, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Anita Mendez", "novice_status": 0 @@ -983,10 +795,6 @@ "model": "tab.debater", "pk": 50, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Evelyn Kelly", "novice_status": 1 @@ -996,10 +804,6 @@ "model": "tab.debater", "pk": 51, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Marylou Ellis", "novice_status": 0 @@ -1009,10 +813,6 @@ "model": "tab.debater", "pk": 52, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Marianne Tate", "novice_status": 1 @@ -1022,10 +822,6 @@ "model": "tab.debater", "pk": 53, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Hester Adkins", "novice_status": 1 @@ -1035,10 +831,6 @@ "model": "tab.debater", "pk": 54, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Charlotte Singleton", "novice_status": 0 @@ -1048,10 +840,6 @@ "model": "tab.debater", "pk": 55, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Tammy Hendrix", "novice_status": 0 @@ -1061,10 +849,6 @@ "model": "tab.debater", "pk": 56, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Mara Carlson", "novice_status": 0 @@ -1074,10 +858,6 @@ "model": "tab.debater", "pk": 58, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Alexandria Barnett", "novice_status": 0 @@ -1087,10 +867,6 @@ "model": "tab.debater", "pk": 59, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Staci Russell", "novice_status": 0 @@ -1100,10 +876,6 @@ "model": "tab.debater", "pk": 60, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kay Kemp", "novice_status": 0 @@ -1113,10 +885,6 @@ "model": "tab.debater", "pk": 61, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Valarie Curry", "novice_status": 0 @@ -1126,10 +894,6 @@ "model": "tab.debater", "pk": 62, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Gracie Blackwell", "novice_status": 0 @@ -1139,10 +903,6 @@ "model": "tab.debater", "pk": 63, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Graciela Riley", "novice_status": 0 @@ -1152,10 +912,6 @@ "model": "tab.debater", "pk": 64, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Tracey Irwin", "novice_status": 0 @@ -1165,10 +921,6 @@ "model": "tab.debater", "pk": 65, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Neva Ware", "novice_status": 0 @@ -1178,10 +930,6 @@ "model": "tab.debater", "pk": 66, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kimberly Rodriquez", "novice_status": 0 @@ -1191,10 +939,6 @@ "model": "tab.debater", "pk": 67, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Melinda Sellers", "novice_status": 1 @@ -1204,10 +948,6 @@ "model": "tab.debater", "pk": 68, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Charity Garza", "novice_status": 0 @@ -1217,10 +957,6 @@ "model": "tab.debater", "pk": 69, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Clarice Nolan", "novice_status": 0 @@ -1230,10 +966,6 @@ "model": "tab.debater", "pk": 70, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Erin Greer", "novice_status": 1 @@ -1243,10 +975,6 @@ "model": "tab.debater", "pk": 71, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Candice Charles", "novice_status": 1 @@ -1256,10 +984,6 @@ "model": "tab.debater", "pk": 72, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Rowena Garrett", "novice_status": 1 @@ -1269,10 +993,6 @@ "model": "tab.debater", "pk": 73, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Roxie Sanchez", "novice_status": 1 @@ -1282,10 +1002,6 @@ "model": "tab.debater", "pk": 74, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Margery Cotton", "novice_status": 1 @@ -1295,10 +1011,6 @@ "model": "tab.debater", "pk": 75, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Angelique Wall", "novice_status": 1 @@ -1308,10 +1020,6 @@ "model": "tab.debater", "pk": 76, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Helen Hensley", "novice_status": 1 @@ -1321,10 +1029,6 @@ "model": "tab.debater", "pk": 77, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lindsay Osborne", "novice_status": 1 @@ -1334,10 +1038,6 @@ "model": "tab.debater", "pk": 78, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Cheryl Terry", "novice_status": 1 @@ -1347,10 +1047,6 @@ "model": "tab.debater", "pk": 79, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Marlene Drake", "novice_status": 0 @@ -1360,10 +1056,6 @@ "model": "tab.debater", "pk": 80, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kim Chaney", "novice_status": 1 @@ -1373,10 +1065,6 @@ "model": "tab.debater", "pk": 81, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kelly Knapp", "novice_status": 1 @@ -1386,10 +1074,6 @@ "model": "tab.debater", "pk": 82, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Gretchen Goff", "novice_status": 1 @@ -1399,10 +1083,6 @@ "model": "tab.debater", "pk": 83, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Magdalena Tucker", "novice_status": 0 @@ -1412,10 +1092,6 @@ "model": "tab.debater", "pk": 84, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Beatriz Whitney", "novice_status": 0 @@ -1425,10 +1101,6 @@ "model": "tab.debater", "pk": 85, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lelia Shelton", "novice_status": 0 @@ -1438,10 +1110,6 @@ "model": "tab.debater", "pk": 86, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Susanne Bryant", "novice_status": 1 @@ -1451,10 +1119,6 @@ "model": "tab.debater", "pk": 87, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jenny Whitfield", "novice_status": 0 @@ -1464,10 +1128,6 @@ "model": "tab.debater", "pk": 88, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Bernadine Boyle", "novice_status": 1 @@ -1477,10 +1137,6 @@ "model": "tab.debater", "pk": 90, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Margo Hopkins", "novice_status": 1 @@ -1490,10 +1146,6 @@ "model": "tab.debater", "pk": 93, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Maryellen Guthrie", "novice_status": 0 @@ -1503,10 +1155,6 @@ "model": "tab.debater", "pk": 94, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Clarissa Castaneda", "novice_status": 0 @@ -1516,10 +1164,6 @@ "model": "tab.debater", "pk": 95, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Paige Ramsey", "novice_status": 1 @@ -1529,10 +1173,6 @@ "model": "tab.debater", "pk": 96, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sheila Velazquez", "novice_status": 0 @@ -1542,10 +1182,6 @@ "model": "tab.debater", "pk": 97, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Alejandra Meyers", "novice_status": 0 @@ -1555,10 +1191,6 @@ "model": "tab.debater", "pk": 98, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Christian Bell", "novice_status": 1 @@ -1568,10 +1200,6 @@ "model": "tab.debater", "pk": 99, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Bonnie Skinner", "novice_status": 1 @@ -1581,10 +1209,6 @@ "model": "tab.debater", "pk": 100, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Marissa Sexton", "novice_status": 1 @@ -1594,10 +1218,6 @@ "model": "tab.debater", "pk": 101, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Flossie Welch", "novice_status": 1 @@ -1607,10 +1227,6 @@ "model": "tab.debater", "pk": 102, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Christie Copeland", "novice_status": 0 @@ -1620,10 +1236,6 @@ "model": "tab.debater", "pk": 103, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Rosario Hebert", "novice_status": 1 @@ -1633,10 +1245,6 @@ "model": "tab.debater", "pk": 104, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kris Vaughn", "novice_status": 0 @@ -1646,10 +1254,6 @@ "model": "tab.debater", "pk": 105, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Christa Kent", "novice_status": 0 @@ -1659,10 +1263,6 @@ "model": "tab.debater", "pk": 106, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Ashlee Fitzgerald", "novice_status": 0 @@ -1672,10 +1272,6 @@ "model": "tab.debater", "pk": 107, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Beverley Knight", "novice_status": 1 @@ -1685,10 +1281,6 @@ "model": "tab.debater", "pk": 108, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Francisca Browning", "novice_status": 1 @@ -1698,10 +1290,6 @@ "model": "tab.debater", "pk": 109, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lawanda Bates", "novice_status": 0 @@ -1711,10 +1299,6 @@ "model": "tab.debater", "pk": 110, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jacklyn Hanson", "novice_status": 0 @@ -1724,10 +1308,6 @@ "model": "tab.debater", "pk": 111, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Katherine Riggs", "novice_status": 1 @@ -1737,10 +1317,6 @@ "model": "tab.debater", "pk": 112, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Pansy Alexander", "novice_status": 0 @@ -1750,10 +1326,6 @@ "model": "tab.debater", "pk": 113, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Elise Russo", "novice_status": 0 @@ -1763,10 +1335,6 @@ "model": "tab.debater", "pk": 114, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Josephine Griffin", "novice_status": 0 @@ -1776,10 +1344,6 @@ "model": "tab.debater", "pk": 115, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Nanette Hickman", "novice_status": 0 @@ -1789,10 +1353,6 @@ "model": "tab.debater", "pk": 116, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Luz Eaton", "novice_status": 0 @@ -1802,10 +1362,6 @@ "model": "tab.debater", "pk": 117, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lacey Yang", "novice_status": 0 @@ -1815,10 +1371,6 @@ "model": "tab.debater", "pk": 118, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Katina Mcintyre", "novice_status": 0 @@ -1828,10 +1380,6 @@ "model": "tab.debater", "pk": 119, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Leslie Vasquez", "novice_status": 0 @@ -1841,10 +1389,6 @@ "model": "tab.debater", "pk": 120, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Socorro Lindsey", "novice_status": 0 @@ -1854,10 +1398,6 @@ "model": "tab.debater", "pk": 121, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Shelly Kane", "novice_status": 1 @@ -1867,10 +1407,6 @@ "model": "tab.debater", "pk": 122, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Rosie Frazier", "novice_status": 1 @@ -1880,10 +1416,6 @@ "model": "tab.debater", "pk": 123, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Mamie Foster", "novice_status": 1 @@ -1893,10 +1425,6 @@ "model": "tab.debater", "pk": 124, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Leonor Lopez", "novice_status": 1 @@ -1906,10 +1434,6 @@ "model": "tab.debater", "pk": 125, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Claudette Kline", "novice_status": 1 @@ -1919,10 +1443,6 @@ "model": "tab.debater", "pk": 126, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Rosanne Obrien", "novice_status": 0 @@ -1932,10 +1452,6 @@ "model": "tab.debater", "pk": 129, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jan Hale", "novice_status": 1 @@ -1945,10 +1461,6 @@ "model": "tab.debater", "pk": 131, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Brigitte Mayo", "novice_status": 1 @@ -1958,10 +1470,6 @@ "model": "tab.debater", "pk": 132, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Winifred Ross", "novice_status": 1 @@ -1971,10 +1479,6 @@ "model": "tab.debater", "pk": 133, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Yvonne Boone", "novice_status": 0 @@ -1984,10 +1488,6 @@ "model": "tab.debater", "pk": 134, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Caitlin Evans", "novice_status": 0 @@ -1997,10 +1497,6 @@ "model": "tab.debater", "pk": 135, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Joann Larson", "novice_status": 1 @@ -2010,10 +1506,6 @@ "model": "tab.debater", "pk": 136, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Allyson Stevens", "novice_status": 1 @@ -2023,10 +1515,6 @@ "model": "tab.debater", "pk": 137, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Ernestine Mccormick", "novice_status": 0 @@ -2036,10 +1524,6 @@ "model": "tab.debater", "pk": 138, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Velma Munoz", "novice_status": 1 @@ -2049,10 +1533,6 @@ "model": "tab.debater", "pk": 139, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Traci James", "novice_status": 0 @@ -2062,10 +1542,6 @@ "model": "tab.debater", "pk": 140, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Maribel Hubbard", "novice_status": 1 @@ -2075,10 +1551,6 @@ "model": "tab.debater", "pk": 141, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lynette Fuller", "novice_status": 1 @@ -2088,10 +1560,6 @@ "model": "tab.debater", "pk": 142, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "June Pacheco", "novice_status": 0 @@ -2101,10 +1569,6 @@ "model": "tab.debater", "pk": 143, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Shanna Brennan", "novice_status": 1 @@ -2114,10 +1578,6 @@ "model": "tab.debater", "pk": 144, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Hallie Schroeder", "novice_status": 0 @@ -2127,10 +1587,6 @@ "model": "tab.debater", "pk": 145, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Alyce Mooney", "novice_status": 0 @@ -2140,10 +1596,6 @@ "model": "tab.debater", "pk": 146, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jill Vincent", "novice_status": 1 @@ -2153,10 +1605,6 @@ "model": "tab.debater", "pk": 147, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Gayle Hatfield", "novice_status": 0 @@ -2166,10 +1614,6 @@ "model": "tab.debater", "pk": 148, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jerri Chandler", "novice_status": 0 @@ -2179,10 +1623,6 @@ "model": "tab.debater", "pk": 149, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Myrtle Ellison", "novice_status": 1 @@ -2192,10 +1632,6 @@ "model": "tab.debater", "pk": 150, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jacqueline Jordan", "novice_status": 0 @@ -2205,10 +1641,6 @@ "model": "tab.debater", "pk": 151, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Saundra Clarke", "novice_status": 0 @@ -2218,10 +1650,6 @@ "model": "tab.debater", "pk": 152, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Isabella Daugherty", "novice_status": 0 @@ -2231,10 +1659,6 @@ "model": "tab.debater", "pk": 153, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Beverly Morales", "novice_status": 0 @@ -2244,10 +1668,6 @@ "model": "tab.debater", "pk": 154, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jamie Holder", "novice_status": 0 @@ -2257,10 +1677,6 @@ "model": "tab.debater", "pk": 155, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Tammi Jimenez", "novice_status": 0 @@ -2270,10 +1686,6 @@ "model": "tab.debater", "pk": 156, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Amalia Kinney", "novice_status": 0 @@ -2283,10 +1695,6 @@ "model": "tab.debater", "pk": 157, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Priscilla Everett", "novice_status": 0 @@ -2296,10 +1704,6 @@ "model": "tab.debater", "pk": 158, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Corina Calhoun", "novice_status": 0 @@ -2309,10 +1713,6 @@ "model": "tab.debater", "pk": 160, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Katelyn Key", "novice_status": 1 @@ -2322,10 +1722,6 @@ "model": "tab.debater", "pk": 161, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Vanessa Hernandez", "novice_status": 1 @@ -2335,10 +1731,6 @@ "model": "tab.debater", "pk": 163, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Gail Cooper", "novice_status": 1 @@ -2348,10 +1740,6 @@ "model": "tab.debater", "pk": 164, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Florine Ferrell", "novice_status": 0 @@ -2361,10 +1749,6 @@ "model": "tab.debater", "pk": 165, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sophie Mcguire", "novice_status": 1 @@ -2374,10 +1758,6 @@ "model": "tab.debater", "pk": 166, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Krystal Rodriguez", "novice_status": 0 @@ -2387,10 +1767,6 @@ "model": "tab.debater", "pk": 167, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Imelda Avery", "novice_status": 0 @@ -2400,10 +1776,6 @@ "model": "tab.debater", "pk": 168, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Leta Saunders", "novice_status": 1 @@ -2413,10 +1785,6 @@ "model": "tab.debater", "pk": 169, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Melisa Combs", "novice_status": 0 @@ -2426,10 +1794,6 @@ "model": "tab.debater", "pk": 170, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Robyn Calderon", "novice_status": 1 @@ -2439,10 +1803,6 @@ "model": "tab.debater", "pk": 171, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Nannie Wright", "novice_status": 1 @@ -2452,10 +1812,6 @@ "model": "tab.debater", "pk": 172, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "April Bowen", "novice_status": 1 @@ -2465,10 +1821,6 @@ "model": "tab.debater", "pk": 173, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lizzie Parsons", "novice_status": 1 @@ -2478,10 +1830,6 @@ "model": "tab.debater", "pk": 174, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Elinor Bradley", "novice_status": 1 @@ -2491,10 +1839,6 @@ "model": "tab.debater", "pk": 175, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Rosalind Soto", "novice_status": 0 @@ -2504,10 +1848,6 @@ "model": "tab.debater", "pk": 176, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lara Valentine", "novice_status": 1 @@ -2517,10 +1857,6 @@ "model": "tab.debater", "pk": 177, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Pat Strong", "novice_status": 1 @@ -2530,10 +1866,6 @@ "model": "tab.debater", "pk": 178, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jenna Doyle", "novice_status": 1 @@ -2543,10 +1875,6 @@ "model": "tab.debater", "pk": 179, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Terry Dyer", "novice_status": 1 @@ -2556,10 +1884,6 @@ "model": "tab.debater", "pk": 180, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Cindy Newman", "novice_status": 1 @@ -2569,10 +1893,6 @@ "model": "tab.debater", "pk": 181, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lindsey Cohen", "novice_status": 1 @@ -2582,10 +1902,6 @@ "model": "tab.debater", "pk": 182, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kathie Rivas", "novice_status": 1 @@ -2595,10 +1911,6 @@ "model": "tab.debater", "pk": 183, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sheena Branch", "novice_status": 1 @@ -2608,10 +1920,6 @@ "model": "tab.debater", "pk": 186, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lynnette Hunt", "novice_status": 1 @@ -2621,10 +1929,6 @@ "model": "tab.debater", "pk": 188, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lauri Giles", "novice_status": 1 @@ -2634,10 +1938,6 @@ "model": "tab.debater", "pk": 189, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Mona Gould", "novice_status": 1 @@ -2647,10 +1947,6 @@ "model": "tab.debater", "pk": 190, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Gay Blair", "novice_status": 1 @@ -2660,10 +1956,6 @@ "model": "tab.debater", "pk": 191, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Deann Fernandez", "novice_status": 1 @@ -2673,10 +1965,6 @@ "model": "tab.debater", "pk": 192, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Holly Buckley", "novice_status": 1 @@ -2686,10 +1974,6 @@ "model": "tab.debater", "pk": 193, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Gladys Kelley", "novice_status": 0 @@ -2699,10 +1983,6 @@ "model": "tab.debater", "pk": 194, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lila Sandoval", "novice_status": 1 @@ -2712,10 +1992,6 @@ "model": "tab.debater", "pk": 195, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sherry David", "novice_status": 1 @@ -2725,10 +2001,6 @@ "model": "tab.debater", "pk": 196, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Miriam Hester", "novice_status": 0 @@ -2738,10 +2010,6 @@ "model": "tab.debater", "pk": 197, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Georgia Lindsay", "novice_status": 0 @@ -2751,10 +2019,6 @@ "model": "tab.debater", "pk": 198, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Fanny Gentry", "novice_status": 0 @@ -2764,10 +2028,6 @@ "model": "tab.debater", "pk": 199, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Louella Sampson", "novice_status": 1 @@ -2777,10 +2037,6 @@ "model": "tab.debater", "pk": 200, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Briana Watson", "novice_status": 0 @@ -2790,10 +2046,6 @@ "model": "tab.debater", "pk": 201, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jessie Olsen", "novice_status": 0 @@ -2803,10 +2055,6 @@ "model": "tab.debater", "pk": 202, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Johanna Burt", "novice_status": 0 @@ -2816,10 +2064,6 @@ "model": "tab.debater", "pk": 204, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Tanya Manning", "novice_status": 0 @@ -2829,10 +2073,6 @@ "model": "tab.debater", "pk": 205, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Grace Armstrong", "novice_status": 0 @@ -2842,10 +2082,6 @@ "model": "tab.debater", "pk": 206, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Mildred Robbins", "novice_status": 0 @@ -2855,10 +2091,6 @@ "model": "tab.debater", "pk": 207, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Evangeline Lowe", "novice_status": 0 @@ -2868,10 +2100,6 @@ "model": "tab.debater", "pk": 208, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Misty Reilly", "novice_status": 0 @@ -2881,10 +2109,6 @@ "model": "tab.debater", "pk": 209, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Tricia Haley", "novice_status": 0 @@ -2894,10 +2118,6 @@ "model": "tab.debater", "pk": 210, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Madelyn Ball", "novice_status": 0 @@ -2907,10 +2127,6 @@ "model": "tab.debater", "pk": 211, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Millie Guzman", "novice_status": 1 @@ -2920,10 +2136,6 @@ "model": "tab.debater", "pk": 212, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Reyna Gray", "novice_status": 1 @@ -2933,10 +2145,6 @@ "model": "tab.debater", "pk": 213, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Tracie Chase", "novice_status": 1 @@ -2946,10 +2154,6 @@ "model": "tab.debater", "pk": 214, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Beulah Stewart", "novice_status": 1 @@ -2959,10 +2163,6 @@ "model": "tab.debater", "pk": 215, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Judith Tyler", "novice_status": 0 @@ -2972,10 +2172,6 @@ "model": "tab.debater", "pk": 216, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Brandie Walsh", "novice_status": 1 @@ -2985,10 +2181,6 @@ "model": "tab.debater", "pk": 217, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Yesenia Wilkinson", "novice_status": 1 @@ -2998,10 +2190,6 @@ "model": "tab.debater", "pk": 218, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Willa Garner", "novice_status": 1 @@ -3011,10 +2199,6 @@ "model": "tab.debater", "pk": 219, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Taylor Johns", "novice_status": 0 @@ -3024,10 +2208,6 @@ "model": "tab.debater", "pk": 220, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Cecilia Mccoy", "novice_status": 0 @@ -3037,10 +2217,6 @@ "model": "tab.debater", "pk": 221, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Cara Parker", "novice_status": 1 @@ -3050,10 +2226,6 @@ "model": "tab.debater", "pk": 222, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Margret Marks", "novice_status": 0 @@ -3063,10 +2235,6 @@ "model": "tab.debater", "pk": 223, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Benita Bradshaw", "novice_status": 0 @@ -3076,10 +2244,6 @@ "model": "tab.debater", "pk": 224, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Hazel Patton", "novice_status": 1 @@ -3089,10 +2253,6 @@ "model": "tab.debater", "pk": 225, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Candy Massey", "novice_status": 1 @@ -3102,10 +2262,6 @@ "model": "tab.debater", "pk": 227, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Phoebe Livingston", "novice_status": 1 @@ -3115,10 +2271,6 @@ "model": "tab.debater", "pk": 228, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Minerva Pearson", "novice_status": 1 @@ -3128,10 +2280,6 @@ "model": "tab.debater", "pk": 229, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Luann Moon", "novice_status": 0 @@ -3141,10 +2289,6 @@ "model": "tab.debater", "pk": 230, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Trina Kirby", "novice_status": 1 @@ -3154,10 +2298,6 @@ "model": "tab.debater", "pk": 231, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Crystal Simpson", "novice_status": 1 @@ -3167,10 +2307,6 @@ "model": "tab.debater", "pk": 232, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Melissa Grimes", "novice_status": 1 @@ -3180,10 +2316,6 @@ "model": "tab.debater", "pk": 233, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "May Houston", "novice_status": 1 @@ -3193,10 +2325,6 @@ "model": "tab.debater", "pk": 234, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Isabelle Joyce", "novice_status": 1 @@ -3206,10 +2334,6 @@ "model": "tab.debater", "pk": 235, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Aimee Levine", "novice_status": 0 @@ -3219,10 +2343,6 @@ "model": "tab.debater", "pk": 236, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Mary Crawford", "novice_status": 1 @@ -3232,10 +2352,6 @@ "model": "tab.debater", "pk": 237, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Meredith Reynolds", "novice_status": 1 @@ -3245,10 +2361,6 @@ "model": "tab.debater", "pk": 238, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lidia Trevino", "novice_status": 0 @@ -3258,10 +2370,6 @@ "model": "tab.debater", "pk": 239, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sophia Bruce", "novice_status": 1 @@ -3271,10 +2379,6 @@ "model": "tab.debater", "pk": 240, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Shawn Greene", "novice_status": 0 @@ -3284,10 +2388,6 @@ "model": "tab.debater", "pk": 242, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Shawna Cochran", "novice_status": 0 @@ -3297,10 +2397,6 @@ "model": "tab.debater", "pk": 243, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jody Jacobs", "novice_status": 0 @@ -3310,10 +2406,6 @@ "model": "tab.debater", "pk": 244, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lorna Todd", "novice_status": 1 @@ -3323,10 +2415,6 @@ "model": "tab.debater", "pk": 245, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Ericka Petersen", "novice_status": 0 @@ -3336,10 +2424,6 @@ "model": "tab.debater", "pk": 246, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sharon Lee", "novice_status": 0 @@ -3349,10 +2433,6 @@ "model": "tab.debater", "pk": 247, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Nichole Gillespie", "novice_status": 1 @@ -3362,10 +2442,6 @@ "model": "tab.debater", "pk": 248, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Christina Cline", "novice_status": 0 @@ -3375,10 +2451,6 @@ "model": "tab.debater", "pk": 249, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Flora Mclean", "novice_status": 1 @@ -3388,10 +2460,6 @@ "model": "tab.debater", "pk": 250, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Iva Sharpe", "novice_status": 0 @@ -3401,10 +2469,6 @@ "model": "tab.debater", "pk": 251, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Coleen Conley", "novice_status": 1 @@ -3414,10 +2478,6 @@ "model": "tab.debater", "pk": 253, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Nancy Garrison", "novice_status": 1 @@ -3427,10 +2487,6 @@ "model": "tab.debater", "pk": 254, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jayne Sherman", "novice_status": 0 @@ -3440,10 +2496,6 @@ "model": "tab.debater", "pk": 255, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Vonda Duke", "novice_status": 1 @@ -3453,10 +2505,6 @@ "model": "tab.debater", "pk": 256, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Addie Joseph", "novice_status": 1 @@ -3466,10 +2514,6 @@ "model": "tab.debater", "pk": 257, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Paulette Newton", "novice_status": 0 @@ -3479,10 +2523,6 @@ "model": "tab.debater", "pk": 258, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Noemi Stuart", "novice_status": 0 @@ -3492,10 +2532,6 @@ "model": "tab.debater", "pk": 259, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Molly Carver", "novice_status": 1 @@ -3505,10 +2541,6 @@ "model": "tab.debater", "pk": 260, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Teresa Merrill", "novice_status": 1 @@ -3518,10 +2550,6 @@ "model": "tab.debater", "pk": 262, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lakeisha Richard", "novice_status": 0 @@ -3531,10 +2559,6 @@ "model": "tab.debater", "pk": 263, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Concepcion Booker", "novice_status": 1 @@ -3544,10 +2568,6 @@ "model": "tab.debater", "pk": 264, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Mae Fisher", "novice_status": 1 @@ -3557,10 +2577,6 @@ "model": "tab.debater", "pk": 265, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Beatrice Potter", "novice_status": 1 @@ -3570,10 +2586,6 @@ "model": "tab.debater", "pk": 266, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Deanna Dalton", "novice_status": 1 @@ -3583,10 +2595,6 @@ "model": "tab.debater", "pk": 267, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Nelda Santiago", "novice_status": 1 @@ -3596,10 +2604,6 @@ "model": "tab.debater", "pk": 268, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Casandra Holden", "novice_status": 0 @@ -3609,10 +2613,6 @@ "model": "tab.debater", "pk": 269, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Janine Brooks", "novice_status": 0 @@ -3622,10 +2622,6 @@ "model": "tab.debater", "pk": 270, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Stacey Bond", "novice_status": 0 @@ -3635,10 +2631,6 @@ "model": "tab.debater", "pk": 271, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Stacy Bailey", "novice_status": 0 @@ -3648,10 +2640,6 @@ "model": "tab.debater", "pk": 272, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Casey Sanders", "novice_status": 0 @@ -3661,10 +2649,6 @@ "model": "tab.debater", "pk": 273, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sandra Casey", "novice_status": 1 @@ -3674,10 +2658,6 @@ "model": "tab.debater", "pk": 274, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Johnnie Valenzuela", "novice_status": 0 @@ -3687,10 +2667,6 @@ "model": "tab.debater", "pk": 275, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kelley Pope", "novice_status": 0 @@ -3700,10 +2676,6 @@ "model": "tab.debater", "pk": 276, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lillie Mcleod", "novice_status": 1 @@ -3713,10 +2685,6 @@ "model": "tab.debater", "pk": 277, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Violet Rowland", "novice_status": 0 @@ -3726,10 +2694,6 @@ "model": "tab.debater", "pk": 278, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Guadalupe Roach", "novice_status": 1 @@ -3739,10 +2703,6 @@ "model": "tab.debater", "pk": 279, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Elisabeth Fry", "novice_status": 1 @@ -3752,10 +2712,6 @@ "model": "tab.debater", "pk": 280, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lavonne Oconnor", "novice_status": 0 @@ -3765,10 +2721,6 @@ "model": "tab.debater", "pk": 282, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lillian Bonner", "novice_status": 1 @@ -3778,10 +2730,6 @@ "model": "tab.debater", "pk": 283, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Lucinda Strickland", "novice_status": 0 @@ -3791,10 +2739,6 @@ "model": "tab.debater", "pk": 284, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Clare Flowers", "novice_status": 0 @@ -3804,10 +2748,6 @@ "model": "tab.debater", "pk": 285, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Tiffany Lynn", "novice_status": 1 @@ -3817,10 +2757,6 @@ "model": "tab.debater", "pk": 286, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Shauna Stone", "novice_status": 1 @@ -3830,10 +2766,6 @@ "model": "tab.debater", "pk": 287, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sandy Odonnell", "novice_status": 1 @@ -3843,10 +2775,6 @@ "model": "tab.debater", "pk": 288, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Abigail Whitley", "novice_status": 1 @@ -3856,10 +2784,6 @@ "model": "tab.debater", "pk": 289, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sherri Blevins", "novice_status": 1 @@ -3869,10 +2793,6 @@ "model": "tab.debater", "pk": 290, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Robert Leonard", "novice_status": 1 @@ -3882,10 +2802,6 @@ "model": "tab.debater", "pk": 291, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Tami Larsen", "novice_status": 1 @@ -3895,10 +2811,6 @@ "model": "tab.debater", "pk": 292, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Irene Hewitt", "novice_status": 1 @@ -3908,10 +2820,6 @@ "model": "tab.debater", "pk": 293, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Ruthie Gilbert", "novice_status": 1 @@ -3921,10 +2829,6 @@ "model": "tab.debater", "pk": 294, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Corrine Cherry", "novice_status": 1 @@ -3934,10 +2838,6 @@ "model": "tab.debater", "pk": 295, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Daisy Mcdowell", "novice_status": 1 @@ -3947,10 +2847,6 @@ "model": "tab.debater", "pk": 297, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Edna Bennett", "novice_status": 1 @@ -3960,10 +2856,6 @@ "model": "tab.debater", "pk": 299, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "James Mcbride", "novice_status": 1 @@ -3973,10 +2865,6 @@ "model": "tab.debater", "pk": 300, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Earnestine Pitts", "novice_status": 0 @@ -3986,10 +2874,6 @@ "model": "tab.debater", "pk": 301, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Karina Cardenas", "novice_status": 0 @@ -3999,10 +2883,6 @@ "model": "tab.debater", "pk": 302, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Marsha Zamora", "novice_status": 1 @@ -4012,10 +2892,6 @@ "model": "tab.debater", "pk": 303, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Rachel Cleveland", "novice_status": 0 @@ -4025,10 +2901,6 @@ "model": "tab.debater", "pk": 304, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Ladonna Murphy", "novice_status": 1 @@ -4038,10 +2910,6 @@ "model": "tab.debater", "pk": 305, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Serena Mcgee", "novice_status": 0 @@ -4051,10 +2919,6 @@ "model": "tab.debater", "pk": 306, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jeannie Foley", "novice_status": 1 @@ -4064,10 +2928,6 @@ "model": "tab.debater", "pk": 307, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Ellen Conway", "novice_status": 0 @@ -4077,10 +2937,6 @@ "model": "tab.debater", "pk": 308, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Ada Day", "novice_status": 0 @@ -4090,10 +2946,6 @@ "model": "tab.debater", "pk": 309, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Melanie Hammond", "novice_status": 0 @@ -4103,10 +2955,6 @@ "model": "tab.debater", "pk": 310, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Roseann Crane", "novice_status": 0 @@ -4116,10 +2964,6 @@ "model": "tab.debater", "pk": 311, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kitty Reyes", "novice_status": 0 @@ -4129,10 +2973,6 @@ "model": "tab.debater", "pk": 312, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Margaret Madden", "novice_status": 0 @@ -4142,10 +2982,6 @@ "model": "tab.debater", "pk": 313, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Gabrielle Valencia", "novice_status": 0 @@ -4155,10 +2991,6 @@ "model": "tab.debater", "pk": 314, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Faye Shaw", "novice_status": 0 @@ -4168,10 +3000,6 @@ "model": "tab.debater", "pk": 315, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Dorothea Frederick", "novice_status": 0 @@ -4181,10 +3009,6 @@ "model": "tab.debater", "pk": 316, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Terra Mcdonald", "novice_status": 0 @@ -4194,10 +3018,6 @@ "model": "tab.debater", "pk": 317, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sheri Vega", "novice_status": 1 @@ -4207,10 +3027,6 @@ "model": "tab.debater", "pk": 318, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Daphne Owen", "novice_status": 1 @@ -4220,10 +3036,6 @@ "model": "tab.debater", "pk": 319, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sheryl Klein", "novice_status": 1 @@ -4233,10 +3045,6 @@ "model": "tab.debater", "pk": 320, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jami Kidd", "novice_status": 1 @@ -4246,10 +3054,6 @@ "model": "tab.debater", "pk": 323, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Leona Callahan", "novice_status": 1 @@ -4259,10 +3063,6 @@ "model": "tab.debater", "pk": 324, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Penny York", "novice_status": 1 @@ -4272,10 +3072,6 @@ "model": "tab.debater", "pk": 325, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Carey Estes", "novice_status": 1 @@ -4285,10 +3081,6 @@ "model": "tab.debater", "pk": 326, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Linda Silva", "novice_status": 1 @@ -4298,10 +3090,6 @@ "model": "tab.debater", "pk": 327, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Tina Barr", "novice_status": 0 @@ -4311,10 +3099,6 @@ "model": "tab.debater", "pk": 328, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Ilene Frost", "novice_status": 0 @@ -4324,10 +3108,6 @@ "model": "tab.debater", "pk": 329, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Deena Reid", "novice_status": 0 @@ -4337,10 +3117,6 @@ "model": "tab.debater", "pk": 330, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Penelope Haynes", "novice_status": 0 @@ -4350,10 +3126,6 @@ "model": "tab.debater", "pk": 331, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jasmine Robertson", "novice_status": 0 @@ -4363,10 +3135,6 @@ "model": "tab.debater", "pk": 332, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Stephanie Grant", "novice_status": 0 @@ -4376,10 +3144,6 @@ "model": "tab.debater", "pk": 333, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Olive Gilliam", "novice_status": 1 @@ -4389,10 +3153,6 @@ "model": "tab.debater", "pk": 334, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Maggie Long", "novice_status": 0 @@ -4402,10 +3162,6 @@ "model": "tab.debater", "pk": 335, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Roxanne King", "novice_status": 0 @@ -4415,10 +3171,6 @@ "model": "tab.debater", "pk": 336, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Justine Malone", "novice_status": 0 @@ -4428,10 +3180,6 @@ "model": "tab.debater", "pk": 337, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Diann Prince", "novice_status": 0 @@ -4441,10 +3189,6 @@ "model": "tab.debater", "pk": 338, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Alba Randall", "novice_status": 0 @@ -4454,10 +3198,6 @@ "model": "tab.debater", "pk": 339, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Martina Hunter", "novice_status": 0 @@ -4467,10 +3207,6 @@ "model": "tab.debater", "pk": 340, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Valerie Chapman", "novice_status": 1 @@ -4480,10 +3216,6 @@ "model": "tab.debater", "pk": 341, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Angelina Lambert", "novice_status": 1 @@ -4493,10 +3225,6 @@ "model": "tab.debater", "pk": 342, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Vivian Holt", "novice_status": 1 @@ -4506,10 +3234,6 @@ "model": "tab.debater", "pk": 343, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Alma Dodson", "novice_status": 1 @@ -4519,10 +3243,6 @@ "model": "tab.debater", "pk": 344, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Edith Ayala", "novice_status": 1 @@ -4532,10 +3252,6 @@ "model": "tab.debater", "pk": 345, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Melody Steele", "novice_status": 1 @@ -4545,10 +3261,6 @@ "model": "tab.debater", "pk": 346, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Gertrude Cote", "novice_status": 0 @@ -4558,10 +3270,6 @@ "model": "tab.debater", "pk": 347, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Barbara Wyatt", "novice_status": 0 @@ -4571,10 +3279,6 @@ "model": "tab.debater", "pk": 348, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Berta Galloway", "novice_status": 0 @@ -4584,10 +3288,6 @@ "model": "tab.debater", "pk": 349, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Cathy Gallegos", "novice_status": 0 @@ -4597,10 +3297,6 @@ "model": "tab.debater", "pk": 350, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Jaclyn Mcmahon", "novice_status": 1 @@ -4610,10 +3306,6 @@ "model": "tab.debater", "pk": 351, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Angeline Garcia", "novice_status": 1 @@ -4623,10 +3315,6 @@ "model": "tab.debater", "pk": 352, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Latoya Dickerson", "novice_status": 1 @@ -4636,10 +3324,6 @@ "model": "tab.debater", "pk": 353, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Ingrid Durham", "novice_status": 1 @@ -4649,10 +3333,6 @@ "model": "tab.debater", "pk": 355, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Sally Ray", "novice_status": 0 @@ -4662,10 +3342,6 @@ "model": "tab.debater", "pk": 356, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Kari Hardin", "novice_status": 1 @@ -4675,10 +3351,6 @@ "model": "tab.debater", "pk": 358, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Bianca Bridges", "novice_status": 1 @@ -4688,10 +3360,6 @@ "model": "tab.debater", "pk": 359, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Janette Hays", "novice_status": 1 @@ -4701,10 +3369,6 @@ "model": "tab.debater", "pk": 360, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Gena Mejia", "novice_status": 1 @@ -4714,10 +3378,6 @@ "model": "tab.debater", "pk": 361, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Shelby Holloway", "novice_status": 1 @@ -4727,10 +3387,6 @@ "model": "tab.debater", "pk": 362, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "John Love", "novice_status": 1 @@ -4740,10 +3396,6 @@ "model": "tab.debater", "pk": 363, "fields": { - "polymorphic_ctype": [ - "tab", - "debater" - ], "tiebreaker": null, "name": "Althea Sullivan", "novice_status": 1 @@ -4753,10 +3405,6 @@ "model": "tab.team", "pk": 1, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Chicago LM", "school": 2, @@ -4774,10 +3422,6 @@ "model": "tab.team", "pk": 2, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Syracuse Coriolanus", "school": 3, @@ -4795,10 +3439,6 @@ "model": "tab.team", "pk": 4, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Syracuse Cymbeline", "school": 3, @@ -4816,10 +3456,6 @@ "model": "tab.team", "pk": 6, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Syracuse Timon", "school": 3, @@ -4837,10 +3473,6 @@ "model": "tab.team", "pk": 7, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wellesley Chairman Meow", "school": 4, @@ -4858,10 +3490,6 @@ "model": "tab.team", "pk": 8, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "NU CF", "school": 6, @@ -4879,10 +3507,6 @@ "model": "tab.team", "pk": 9, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wellesley Birkenstock A", "school": 4, @@ -4900,10 +3524,6 @@ "model": "tab.team", "pk": 10, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "NU Powerpuff Girls A", "school": 6, @@ -4921,10 +3541,6 @@ "model": "tab.team", "pk": 11, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Fordham LCD Catsystem", "school": 33, @@ -4942,10 +3558,6 @@ "model": "tab.team", "pk": 12, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "NU Return of the Pups", "school": 6, @@ -4963,10 +3575,6 @@ "model": "tab.team", "pk": 13, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wellesley Failed System", "school": 4, @@ -4984,10 +3592,6 @@ "model": "tab.team", "pk": 14, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Fordham GZ", "school": 33, @@ -5005,10 +3609,6 @@ "model": "tab.team", "pk": 15, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wellesley Elle Me Dit", "school": 4, @@ -5026,10 +3626,6 @@ "model": "tab.team", "pk": 16, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "West Point MP", "school": 9, @@ -5047,10 +3643,6 @@ "model": "tab.team", "pk": 17, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Duke PS", "school": 34, @@ -5068,10 +3660,6 @@ "model": "tab.team", "pk": 18, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wellesley Bob Dylan", "school": 4, @@ -5089,10 +3677,6 @@ "model": "tab.team", "pk": 19, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Duke SA", "school": 34, @@ -5110,10 +3694,6 @@ "model": "tab.team", "pk": 21, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Amherst PL", "school": 28, @@ -5131,10 +3711,6 @@ "model": "tab.team", "pk": 22, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wellesley/F&M Hybrid", "school": 4, @@ -5152,10 +3728,6 @@ "model": "tab.team", "pk": 23, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Maryland KS", "school": 35, @@ -5173,10 +3745,6 @@ "model": "tab.team", "pk": 24, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "West Point Medical", "school": 9, @@ -5194,10 +3762,6 @@ "model": "tab.team", "pk": 25, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Hopkins IS", "school": 5, @@ -5215,10 +3779,6 @@ "model": "tab.team", "pk": 26, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "West Point QM", "school": 9, @@ -5236,10 +3796,6 @@ "model": "tab.team", "pk": 27, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Amherst RJ", "school": 28, @@ -5257,10 +3813,6 @@ "model": "tab.team", "pk": 28, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Hopkins Church of Chirt", "school": 5, @@ -5278,10 +3830,6 @@ "model": "tab.team", "pk": 29, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "West Point Armor", "school": 9, @@ -5299,10 +3847,6 @@ "model": "tab.team", "pk": 30, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Columbia A, Why Not A+", "school": 7, @@ -5320,10 +3864,6 @@ "model": "tab.team", "pk": 31, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Pitt Disposable Dignity", "school": 30, @@ -5341,10 +3881,6 @@ "model": "tab.team", "pk": 32, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "West Point Infantry", "school": 9, @@ -5362,10 +3898,6 @@ "model": "tab.team", "pk": 33, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Columbia Blue Ivy (Carter)", "school": 7, @@ -5383,10 +3915,6 @@ "model": "tab.team", "pk": 34, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Pitt Magnanimous Sultans", "school": 30, @@ -5404,10 +3932,6 @@ "model": "tab.team", "pk": 35, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "West Point EOD", "school": 9, @@ -5425,10 +3949,6 @@ "model": "tab.team", "pk": 36, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Columbia North West", "school": 7, @@ -5446,10 +3966,6 @@ "model": "tab.team", "pk": 37, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Berkeley 3", "school": 10, @@ -5467,10 +3983,6 @@ "model": "tab.team", "pk": 38, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Columbia Moon Unit Zappa: You'", "school": 7, @@ -5488,10 +4000,6 @@ "model": "tab.team", "pk": 39, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Berkeley 2", "school": 10, @@ -5509,10 +4017,6 @@ "model": "tab.team", "pk": 40, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Columbia Blanket Jackson", "school": 7, @@ -5530,10 +4034,6 @@ "model": "tab.team", "pk": 41, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Berkeley 1", "school": 10, @@ -5551,10 +4051,6 @@ "model": "tab.team", "pk": 42, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brandeis EN", "school": 31, @@ -5572,10 +4068,6 @@ "model": "tab.team", "pk": 43, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Columbia/Stanford Sr8bros", "school": 11, @@ -5593,10 +4085,6 @@ "model": "tab.team", "pk": 44, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "GW MM", "school": 13, @@ -5614,10 +4102,6 @@ "model": "tab.team", "pk": 47, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "NYU A", "school": 8, @@ -5635,10 +4119,6 @@ "model": "tab.team", "pk": 48, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "AU/GW Hybrid", "school": 13, @@ -5656,10 +4136,6 @@ "model": "tab.team", "pk": 49, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "NYU B", "school": 8, @@ -5677,10 +4153,6 @@ "model": "tab.team", "pk": 50, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "GW Roundup", "school": 13, @@ -5698,10 +4170,6 @@ "model": "tab.team", "pk": 51, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "NYU C", "school": 8, @@ -5719,10 +4187,6 @@ "model": "tab.team", "pk": 52, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "AU Patrick Bateman", "school": 27, @@ -5740,10 +4204,6 @@ "model": "tab.team", "pk": 54, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "NYU D", "school": 8, @@ -5761,10 +4221,6 @@ "model": "tab.team", "pk": 55, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "GW B", "school": 13, @@ -5782,10 +4238,6 @@ "model": "tab.team", "pk": 56, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "AU Elle Woods", "school": 27, @@ -5803,10 +4255,6 @@ "model": "tab.team", "pk": 57, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Stanford Rall Bad Bitchezzz", "school": 11, @@ -5824,10 +4272,6 @@ "model": "tab.team", "pk": 58, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "AU Frasier Crane", "school": 27, @@ -5845,10 +4289,6 @@ "model": "tab.team", "pk": 59, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "GW Last SOTY/Penultimate SOTY", "school": 13, @@ -5866,10 +4306,6 @@ "model": "tab.team", "pk": 60, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Penn JT", "school": 12, @@ -5887,10 +4323,6 @@ "model": "tab.team", "pk": 61, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "AU Harvey Specter", "school": 27, @@ -5908,10 +4340,6 @@ "model": "tab.team", "pk": 62, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Smith Gryffindor", "school": 15, @@ -5929,10 +4357,6 @@ "model": "tab.team", "pk": 63, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Penn MW", "school": 12, @@ -5950,10 +4374,6 @@ "model": "tab.team", "pk": 65, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Penn DS", "school": 12, @@ -5971,10 +4391,6 @@ "model": "tab.team", "pk": 66, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Smith Ravenclaw", "school": 15, @@ -5992,10 +4408,6 @@ "model": "tab.team", "pk": 67, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Smith Slytherin", "school": 15, @@ -6013,10 +4425,6 @@ "model": "tab.team", "pk": 68, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Tufts EK", "school": 14, @@ -6034,10 +4442,6 @@ "model": "tab.team", "pk": 69, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Georgetown Bend and Snap", "school": 23, @@ -6055,10 +4459,6 @@ "model": "tab.team", "pk": 70, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Tufts ES", "school": 14, @@ -6076,10 +4476,6 @@ "model": "tab.team", "pk": 71, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "W&M Django's Novices", "school": 16, @@ -6097,10 +4493,6 @@ "model": "tab.team", "pk": 72, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Tufts Bendrew Husick", "school": 14, @@ -6118,10 +4510,6 @@ "model": "tab.team", "pk": 73, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "W&M/UAlbany Hybrid", "school": 42, @@ -6139,10 +4527,6 @@ "model": "tab.team", "pk": 74, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Tufts DR", "school": 14, @@ -6160,10 +4544,6 @@ "model": "tab.team", "pk": 75, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "W&M A", "school": 16, @@ -6181,10 +4561,6 @@ "model": "tab.team", "pk": 76, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Tufts TC", "school": 14, @@ -6202,10 +4578,6 @@ "model": "tab.team", "pk": 77, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wesleyan Amy Gardner", "school": 21, @@ -6223,10 +4595,6 @@ "model": "tab.team", "pk": 78, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Swat MR", "school": 36, @@ -6244,10 +4612,6 @@ "model": "tab.team", "pk": 79, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "BU BP", "school": 17, @@ -6265,10 +4629,6 @@ "model": "tab.team", "pk": 81, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brown GP", "school": 18, @@ -6286,10 +4646,6 @@ "model": "tab.team", "pk": 82, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "BU Cargo Shorts", "school": 17, @@ -6307,10 +4663,6 @@ "model": "tab.team", "pk": 83, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Swat ND", "school": 36, @@ -6328,10 +4680,6 @@ "model": "tab.team", "pk": 84, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wesleyan Donna Moss", "school": 21, @@ -6349,10 +4697,6 @@ "model": "tab.team", "pk": 85, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brown Veal of Ignorance", "school": 18, @@ -6370,10 +4714,6 @@ "model": "tab.team", "pk": 86, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "BU NN", "school": 17, @@ -6391,10 +4731,6 @@ "model": "tab.team", "pk": 87, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brown MN", "school": 18, @@ -6412,10 +4748,6 @@ "model": "tab.team", "pk": 89, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wesleyan Toby Ziegler", "school": 21, @@ -6433,10 +4765,6 @@ "model": "tab.team", "pk": 90, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "BU VJ", "school": 17, @@ -6454,10 +4782,6 @@ "model": "tab.team", "pk": 92, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wesleyan Leo McGarry", "school": 21, @@ -6475,10 +4799,6 @@ "model": "tab.team", "pk": 93, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "BU RA", "school": 17, @@ -6496,10 +4816,6 @@ "model": "tab.team", "pk": 94, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brown CY", "school": 18, @@ -6517,10 +4833,6 @@ "model": "tab.team", "pk": 95, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "BU Batman", "school": 17, @@ -6538,10 +4850,6 @@ "model": "tab.team", "pk": 97, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brown Final Machination", "school": 18, @@ -6559,10 +4867,6 @@ "model": "tab.team", "pk": 98, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Princeton A", "school": 19, @@ -6580,10 +4884,6 @@ "model": "tab.team", "pk": 99, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brown Amish Sex", "school": 18, @@ -6601,10 +4901,6 @@ "model": "tab.team", "pk": 100, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Princeton Legal Liability", "school": 19, @@ -6622,10 +4918,6 @@ "model": "tab.team", "pk": 101, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wesleyan CJ Cregg", "school": 21, @@ -6643,10 +4935,6 @@ "model": "tab.team", "pk": 102, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Princeton SJ", "school": 19, @@ -6664,10 +4952,6 @@ "model": "tab.team", "pk": 103, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Swat BH", "school": 36, @@ -6685,10 +4969,6 @@ "model": "tab.team", "pk": 104, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Wesleyan Josh Lyman", "school": 21, @@ -6706,10 +4986,6 @@ "model": "tab.team", "pk": 105, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Princeton RT", "school": 19, @@ -6727,10 +5003,6 @@ "model": "tab.team", "pk": 106, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Princeton MC", "school": 19, @@ -6748,10 +5020,6 @@ "model": "tab.team", "pk": 107, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "MHC Firebreathing Rubberduckie", "school": 26, @@ -6769,10 +5037,6 @@ "model": "tab.team", "pk": 108, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Maryland DZ", "school": 35, @@ -6790,10 +5054,6 @@ "model": "tab.team", "pk": 109, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "MHC Whose Lines", "school": 26, @@ -6811,10 +5071,6 @@ "model": "tab.team", "pk": 110, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Maryland Sailor Scouts", "school": 35, @@ -6832,10 +5088,6 @@ "model": "tab.team", "pk": 111, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Binghamton TH", "school": 32, @@ -6853,10 +5105,6 @@ "model": "tab.team", "pk": 112, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "MHC Defined Lines", "school": 26, @@ -6874,10 +5122,6 @@ "model": "tab.team", "pk": 113, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "F&M Lennonists", "school": 20, @@ -6895,10 +5139,6 @@ "model": "tab.team", "pk": 114, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Bates Maybach Keys", "school": 25, @@ -6916,10 +5156,6 @@ "model": "tab.team", "pk": 115, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Maryland FK", "school": 35, @@ -6937,10 +5173,6 @@ "model": "tab.team", "pk": 116, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Binghamton PC", "school": 32, @@ -6958,10 +5190,6 @@ "model": "tab.team", "pk": 117, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "F&M B", "school": 20, @@ -6979,10 +5207,6 @@ "model": "tab.team", "pk": 118, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Bates Menage", "school": 25, @@ -7000,10 +5224,6 @@ "model": "tab.team", "pk": 119, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Bates Swahili", "school": 25, @@ -7021,10 +5241,6 @@ "model": "tab.team", "pk": 120, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Maryland/Yale B^2 + 2BS + S^2", "school": 1, @@ -7042,10 +5258,6 @@ "model": "tab.team", "pk": 121, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Binghamton LW", "school": 32, @@ -7063,10 +5275,6 @@ "model": "tab.team", "pk": 122, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Rutgers War Bears", "school": 22, @@ -7084,10 +5292,6 @@ "model": "tab.team", "pk": 123, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "PC HB", "school": 38, @@ -7105,10 +5309,6 @@ "model": "tab.team", "pk": 124, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Bates Blouse", "school": 25, @@ -7126,10 +5326,6 @@ "model": "tab.team", "pk": 126, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Binghamton DS", "school": 32, @@ -7147,10 +5343,6 @@ "model": "tab.team", "pk": 127, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "PC OP", "school": 38, @@ -7168,10 +5360,6 @@ "model": "tab.team", "pk": 128, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Bates Spouse", "school": 25, @@ -7189,10 +5377,6 @@ "model": "tab.team", "pk": 129, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "PC AP", "school": 38, @@ -7210,10 +5394,6 @@ "model": "tab.team", "pk": 131, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Bates House", "school": 25, @@ -7231,10 +5411,6 @@ "model": "tab.team", "pk": 132, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "PC LL", "school": 38, @@ -7252,10 +5428,6 @@ "model": "tab.team", "pk": 133, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Bates Merchants of Yeezus", "school": 25, @@ -7273,10 +5445,6 @@ "model": "tab.team", "pk": 134, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Rutgers LP", "school": 22, @@ -7294,10 +5462,6 @@ "model": "tab.team", "pk": 135, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "MIT We Can't Stop", "school": 37, @@ -7315,10 +5479,6 @@ "model": "tab.team", "pk": 137, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Williams Sorry We Can't Think ", "school": 39, @@ -7336,10 +5496,6 @@ "model": "tab.team", "pk": 138, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "MIT Wrecking Ball", "school": 37, @@ -7357,10 +5513,6 @@ "model": "tab.team", "pk": 139, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Rutgers HP", "school": 22, @@ -7378,10 +5530,6 @@ "model": "tab.team", "pk": 140, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Stony Brook Muliminal Messages", "school": 29, @@ -7399,10 +5547,6 @@ "model": "tab.team", "pk": 141, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Middlebury Fuck, The Novices U", "school": 24, @@ -7420,10 +5564,6 @@ "model": "tab.team", "pk": 142, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Stony Brook Brown Out", "school": 29, @@ -7441,10 +5581,6 @@ "model": "tab.team", "pk": 143, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Rutgers Linsanity Sosa", "school": 22, @@ -7462,10 +5598,6 @@ "model": "tab.team", "pk": 145, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Stony Brook Malay Fighters", "school": 29, @@ -7483,10 +5615,6 @@ "model": "tab.team", "pk": 146, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "MIT Party in the USA", "school": 37, @@ -7504,10 +5632,6 @@ "model": "tab.team", "pk": 147, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Middlebury pB and j", "school": 24, @@ -7525,10 +5649,6 @@ "model": "tab.team", "pk": 148, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Stony Brook NN", "school": 29, @@ -7546,10 +5666,6 @@ "model": "tab.team", "pk": 150, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Middlebury Midd Kid A", "school": 24, @@ -7567,10 +5683,6 @@ "model": "tab.team", "pk": 151, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "MIT Best of Both Worlds", "school": 37, @@ -7588,10 +5700,6 @@ "model": "tab.team", "pk": 152, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "MIT CSB", "school": 37, @@ -7609,10 +5717,6 @@ "model": "tab.team", "pk": 153, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Yale LR", "school": 1, @@ -7630,10 +5734,6 @@ "model": "tab.team", "pk": 154, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Yale Deezy", "school": 1, @@ -7651,10 +5751,6 @@ "model": "tab.team", "pk": 155, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Yale SZ", "school": 1, @@ -7672,10 +5768,6 @@ "model": "tab.team", "pk": 156, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Yale BB", "school": 1, @@ -7693,10 +5785,6 @@ "model": "tab.team", "pk": 157, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Yale HJ", "school": 1, @@ -7714,10 +5802,6 @@ "model": "tab.team", "pk": 158, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Yale HR", "school": 1, @@ -7735,10 +5819,6 @@ "model": "tab.team", "pk": 160, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Yale CaliBoys", "school": 1, @@ -7756,10 +5836,6 @@ "model": "tab.team", "pk": 162, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Yale BP", "school": 1, @@ -7777,10 +5853,6 @@ "model": "tab.team", "pk": 163, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Yale SWP", "school": 1, @@ -7798,10 +5870,6 @@ "model": "tab.team", "pk": 164, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Yale/CCSF Swahili A", "school": 43, @@ -7819,10 +5887,6 @@ "model": "tab.team", "pk": 165, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Middlebury Snark A", "school": 24, @@ -7840,10 +5904,6 @@ "model": "tab.team", "pk": 166, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brandeis AL", "school": 31, @@ -7861,10 +5921,6 @@ "model": "tab.team", "pk": 167, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brandeis GW", "school": 31, @@ -7882,10 +5938,6 @@ "model": "tab.team", "pk": 168, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brandeis Yaas Gaga", "school": 31, @@ -7903,10 +5955,6 @@ "model": "tab.team", "pk": 169, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brandeis C'Mon", "school": 31, @@ -7924,10 +5972,6 @@ "model": "tab.team", "pk": 170, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brandeis Crazy Kids", "school": 31, @@ -7945,10 +5989,6 @@ "model": "tab.team", "pk": 171, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brandeis Tik Tok", "school": 31, @@ -7966,10 +6006,6 @@ "model": "tab.team", "pk": 172, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Rutgers/Brown Power & Control", "school": 18, @@ -7987,10 +6023,6 @@ "model": "tab.team", "pk": 173, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Rutgers Swirl", "school": 22, @@ -8008,10 +6040,6 @@ "model": "tab.team", "pk": 174, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brandeis JS", "school": 31, @@ -8029,10 +6057,6 @@ "model": "tab.team", "pk": 175, "fields": { - "polymorphic_ctype": [ - "tab", - "team" - ], "tiebreaker": null, "name": "Brandeis LA", "school": 31, diff --git a/mittab/apps/tab/management/commands/initialize_tourney.py b/mittab/apps/tab/management/commands/initialize_tourney.py index 42bc06729..006a4d11f 100644 --- a/mittab/apps/tab/management/commands/initialize_tourney.py +++ b/mittab/apps/tab/management/commands/initialize_tourney.py @@ -1,7 +1,5 @@ import os -import shutil import sys -import time from django.core.management import call_command from django.contrib.auth.models import User @@ -9,7 +7,6 @@ from mittab.apps.tab.models import TabSettings from mittab.libs.backup import backup_round -from mittab.libs.backup.strategies.local_dump import BACKUP_PREFIX class Command(BaseCommand): @@ -28,34 +25,9 @@ def add_arguments(self, parser): help="Password for the entry user", nargs="?", default=User.objects.make_random_password(length=8)) - parser.add_argument("backup_directory") def handle(self, *args, **options): - backup_dir = options["backup_directory"] - path = BACKUP_PREFIX - - self.stdout.write( - "Creating directory for current tournament in backup directory") - tournament_dir = os.path.join(backup_dir, str(int(time.time()))) - - if not os.path.exists(tournament_dir): - os.makedirs(tournament_dir) - - if not os.path.exists(path + "/backups"): - os.makedirs(path + "/backups") - - self.stdout.write( - "Copying current tournament state to backup tournament directory: %s" - % tournament_dir) backup_round("before_new_tournament") - try: - shutil.rmtree(tournament_dir + "/backups", ignore_errors=True) - shutil.copytree(path + "/backups", tournament_dir + "/backups") - except (IOError, os.error) as why: - self.stdout.write("Failed to backup current tournament state") - print(why) - sys.exit(1) - self.stdout.write("Clearing data from database") try: call_command("flush", interactive=False) @@ -77,16 +49,6 @@ def handle(self, *args, **options): TabSettings.set("lenient_late", 0) TabSettings.set("cur_round", 1) - self.stdout.write("Cleaning up old backups") - try: - shutil.rmtree(path + "/backups") - os.makedirs(path + "/backups") - except (IOError, os.error) as why: - self.stdout.write( - "Failed to copy clean database to pairing_db.sqlite3") - print(why) - sys.exit(1) - self.stdout.write( "Done setting up tournament, after backing up old one. " "New tournament information:") diff --git a/mittab/apps/tab/migrations/0008_auto_20190520_1522.py b/mittab/apps/tab/migrations/0008_auto_20190520_1522.py index e21408ce2..0961d81ec 100644 --- a/mittab/apps/tab/migrations/0008_auto_20190520_1522.py +++ b/mittab/apps/tab/migrations/0008_auto_20190520_1522.py @@ -6,29 +6,6 @@ import django.db.models.deletion -def forwards_func_team(apps, schema_editor): - MyModel = apps.get_model("tab", "Team") - ContentType = apps.get_model("contenttypes", "ContentType") - - new_ct = ContentType.objects.get_for_model(MyModel) - MyModel.objects.filter(polymorphic_ctype__isnull=True).update( - polymorphic_ctype=new_ct) - - -def forwards_func_deb(apps, schema_editor): - MyModel = apps.get_model("tab", "Debater") - ContentType = apps.get_model("contenttypes", "ContentType") - - new_ct = ContentType.objects.get_for_model(MyModel) - MyModel.objects.filter(polymorphic_ctype__isnull=True).update( - polymorphic_ctype=new_ct) - - -def forwards_func(apps, schema_editor): - forwards_func_deb(apps, schema_editor) - forwards_func_team(apps, schema_editor) - - class Migration(migrations.Migration): dependencies = [ @@ -67,5 +44,4 @@ class Migration(migrations.Migration): name='tiebreaker', field=models.IntegerField(blank=True, null=True, unique=True), ), - migrations.RunPython(forwards_func, migrations.RunPython.noop), ] diff --git a/mittab/apps/tab/migrations/0019_auto_20210408_1648.py b/mittab/apps/tab/migrations/0019_auto_20210408_1648.py new file mode 100644 index 000000000..f2c7715bd --- /dev/null +++ b/mittab/apps/tab/migrations/0019_auto_20210408_1648.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2021-04-08 16:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tab', '0018_merge_20200124_1633'), + ] + + operations = [ + migrations.AlterField( + model_name='outround', + name='choice', + field=models.IntegerField(choices=[(0, 'No'), (1, 'Gov'), (2, 'Opp')], default=0), + ), + ] diff --git a/mittab/apps/tab/migrations/0020_auto_20210409_1938.py b/mittab/apps/tab/migrations/0020_auto_20210409_1938.py new file mode 100644 index 000000000..f6ce3f68d --- /dev/null +++ b/mittab/apps/tab/migrations/0020_auto_20210409_1938.py @@ -0,0 +1,46 @@ +# Generated by Django 2.2.10 on 2021-04-09 19:38 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tab', '0019_auto_20210408_1648'), + ] + + operations = [ + migrations.AlterModelOptions( + name='team', + options={'ordering': ['pk']}, + ), + migrations.RemoveField( + model_name='debater', + name='polymorphic_ctype', + ), + migrations.RemoveField( + model_name='team', + name='polymorphic_ctype', + ), + migrations.AlterField( + model_name='bye', + name='bye_team', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='byes', to='tab.Team'), + ), + migrations.AlterField( + model_name='noshow', + name='no_show_team', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='no_shows', to='tab.Team'), + ), + migrations.AlterField( + model_name='scratch', + name='judge', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scratches', to='tab.Judge'), + ), + migrations.AlterField( + model_name='scratch', + name='team', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scratchs', to='tab.Team'), + ), + ] diff --git a/mittab/apps/tab/models.py b/mittab/apps/tab/models.py index c4e659b5f..049509c4c 100644 --- a/mittab/apps/tab/models.py +++ b/mittab/apps/tab/models.py @@ -3,21 +3,8 @@ from haikunator import Haikunator from django.db import models from django.core.exceptions import ValidationError -from polymorphic.models import PolymorphicModel - -class ModelWithTiebreaker(PolymorphicModel): - tiebreaker = models.IntegerField(unique=True, null=True, blank=True) - - def save(self, *args, **kwargs): - while not self.tiebreaker or \ - self.__class__.objects.filter(tiebreaker=self.tiebreaker).exists(): - self.tiebreaker = random.choice(range(0, 2**16)) - - super(ModelWithTiebreaker, self).save(*args, **kwargs) - - class Meta: - abstract = True +from mittab.libs import cache_logic class TabSettings(models.Model): @@ -32,12 +19,21 @@ def __str__(self): @classmethod def get(cls, key, default=None): - if cls.objects.filter(key=key).exists(): - return cls.objects.get(key=key).value - else: - if default is None: - raise ValueError("Invalid key '%s'" % key) + def safe_get(): + setting = cls.objects.filter(key=key).first() + return setting.value if setting is not None else None + + result = cache_logic.cache_fxn_key( + safe_get, + "tab_settings_%s" % key, + cache_logic.PERSISTENT, + ) + if result is None and default is None: + raise ValueError("No TabSetting with key '%s'" % key) + elif result is None: return default + else: + return result @classmethod def set(cls, key, value): @@ -48,6 +44,20 @@ def set(cls, key, value): else: 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 + ) + 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 + ) + super(TabSettings, self).save(force_insert, force_update, using, update_fields) + class School(models.Model): name = models.CharField(max_length=50, unique=True) @@ -60,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) @@ -77,7 +88,7 @@ class Meta: ordering = ["name"] -class Debater(ModelWithTiebreaker): +class Debater(models.Model): name = models.CharField(max_length=30, unique=True) VARSITY = 0 NOVICE = 1 @@ -86,6 +97,17 @@ class Debater(ModelWithTiebreaker): (NOVICE, "Novice"), ) 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() + ): + self.tiebreaker = random.choice(range(0, 2**16)) + super(Debater, self).save(force_insert, force_update, using, update_fields) @property def num_teams(self): @@ -93,18 +115,13 @@ def num_teams(self): @property def display(self): - if self.num_teams: - return self.name - return "{} (NO TEAM)".format(self.name) + return self.name def __str__(self): return self.name def team(self): - # this ordering is just a work-around to say consistent with imperfect test data - # Ideally, we will enforce only 1 team membership via validation - # https://github.com/MIT-Tab/mit-tab/issues/218 - return self.team_set.order_by("pk").first() + return self.team_set.first() def delete(self, using=None, keep_parents=False): teams = Team.objects.filter(debaters=self) @@ -117,14 +134,16 @@ class Meta: ordering = ["name"] -class Team(ModelWithTiebreaker): +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 @@ -138,20 +157,14 @@ class Team(ModelWithTiebreaker): ) 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) def set_unique_team_code(self): haikunator = Haikunator() @@ -168,12 +181,20 @@ def gen_haiku_and_clean(): self.team_code = code - def save(self, *args, **kwargs): + 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() - super(Team, self).save(*args, **kwargs) + 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) @property def display_backend(self): @@ -214,36 +235,29 @@ def debaters_display(self): return "" class Meta: - ordering = ["name"] + ordering = ["pk"] 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() @@ -254,30 +268,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) @@ -288,26 +299,10 @@ def delete(self, using=None, keep_parents=False): class Meta: ordering = ["name"] - def update_scratches(self): - all_teams = Team.objects.all() - - Scratch.objects.filter(scratch_type=Scratch.SCHOOL_SCRATCH, - judge=self).all().delete() - - for team in all_teams: - judge_schools = self.schools.all() - - if team.school in judge_schools or \ - team.hybrid_school in judge_schools: - if not Scratch.objects.filter(judge=self, team=team).exists(): - Scratch.objects.create(judge=self, - team=team, - scratch_type=Scratch.SCHOOL_SCRATCH) - class Scratch(models.Model): - judge = models.ForeignKey(Judge, on_delete=models.CASCADE) - team = models.ForeignKey(Team, on_delete=models.CASCADE) + judge = models.ForeignKey(Judge, related_name="scratches", on_delete=models.CASCADE) + team = models.ForeignKey(Team, related_name="scratches", on_delete=models.CASCADE) TEAM_SCRATCH = 0 TAB_SCRATCH = 1 SCHOOL_SCRATCH = 2 @@ -337,14 +332,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"] @@ -353,23 +346,23 @@ 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 @@ -383,29 +376,24 @@ 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): @@ -427,15 +415,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 @@ -468,24 +456,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) @@ -495,22 +481,24 @@ def delete(self, using=None, keep_parents=False): class Bye(models.Model): - bye_team = models.ForeignKey(Team, on_delete=models.CASCADE) + bye_team = models.ForeignKey(Team, related_name="byes", on_delete=models.CASCADE) 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, 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): @@ -525,8 +513,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): @@ -534,8 +521,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): @@ -543,5 +529,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/apps/tab/pairing_views.py b/mittab/apps/tab/pairing_views.py index b8e8e0161..0cd7ff0f2 100644 --- a/mittab/apps/tab/pairing_views.py +++ b/mittab/apps/tab/pairing_views.py @@ -95,34 +95,14 @@ def pair_round(request): def assign_judges_to_pairing(request): current_round_number = TabSettings.objects.get(key="cur_round").value - 1 if request.method == "POST": - panel_points, errors = [], [] - potential_panel_points = [ - k for k in list(request.POST.keys()) if k.startswith("panel_") - ] - for point in potential_panel_points: - try: - point = int(point.split("_")[1]) - num = float(request.POST["panel_{0}".format(point)]) - if num > 0.0: - panel_points.append((Round.objects.get(id=point), num)) - except Exception as e: - emit_current_exception() - errors.append(e) - - panel_points.reverse() - rounds = list(Round.objects.filter(round_number=current_round_number)) - judges = [ - ci.judge - for ci in CheckIn.objects.filter(round_number=current_round_number) - ] try: backup.backup_round("round_%s_before_judge_assignment" % current_round_number) - assign_judges.add_judges(rounds, judges, panel_points) - except Exception as e: + assign_judges.add_judges() + except Exception: emit_current_exception() - return redirect_and_flash_error( - request, "Got error during judge assignment") + return redirect_and_flash_error(request, + "Got error during judge assignment") return redirect("/pairings/status/") @@ -139,12 +119,11 @@ def view_backup(request, filename): @permission_required("tab.tab_settings.can_change", login_url="/403/") -def download_backup(request, filename): - print("Trying to download {}".format(filename)) - wrapper, size = backup.get_wrapped_file(filename) - response = HttpResponse(wrapper, content_type="text/plain") - response["Content-Length"] = size - response["Content-Disposition"] = "attachment; filename=%s" % filename +def download_backup(request, key): + print("Trying to download {}".format(key)) + data = backup.get_backup_content(key) + response = HttpResponse(data, content_type="text/plain") + response["Content-Disposition"] = "attachment; filename=%s" % key return response @@ -153,7 +132,7 @@ def upload_backup(request): if request.method == "POST": form = UploadBackupForm(request.POST, request.FILES) if form.is_valid(): - backup.handle_backup(request.FILES["file"]) + backup.upload_backup(request.FILES["file"]) return redirect_and_flash_success( request, "Backup {} uploaded successfully".format( request.FILES["file"].name)) @@ -203,21 +182,16 @@ def restore_backup(request, filename): def view_status(request): - current_round_number = TabSettings.objects.get(key="cur_round").value - 1 + current_round_number = TabSettings.get("cur_round") - 1 return view_round(request, current_round_number) def view_round(request, round_number): errors, excluded_teams = [], [] - round_pairing = list(Round.objects.filter(round_number=round_number)) tot_rounds = TabSettings.get("tot_rounds", 5) - random.seed(1337) - random.shuffle(round_pairing) - round_pairing.sort(key=lambda x: tab_logic.team_comp(x, round_number), - reverse=True) - + round_pairing = tab_logic.sorted_pairings(round_number) #For the template since we can't pass in something nicer like a hash round_info = [pair for pair in round_pairing] @@ -311,6 +285,36 @@ def alternative_teams(request, round_id, current_team_id, position): return render(request, "pairing/team_dropdown.html", locals()) +def team_stats(request, round_number): + """ + Returns the tab card data for all teams in the pairings of this given round number + """ + pairings = tab_logic.sorted_pairings(round_number) + stats_by_team_id = {} + + def stats_for_team(team): + stats = {} + stats["seed"] = Team.get_seed_display(team).split(" ")[0] + stats["wins"] = tab_logic.tot_wins(team) + stats["total_speaks"] = tab_logic.tot_speaks(team) + stats["govs"] = tab_logic.num_govs(team) + stats["opps"] = tab_logic.num_opps(team) + + if hasattr(team, "breaking_team"): + stats["outround_seed"] = team.breaking_team.seed + stats["effective_outround_seed"] = team.breaking_team.effective_seed + + return stats + + for round_obj in pairings: + if round_obj.gov_team: + stats_by_team_id[round_obj.gov_team_id] = stats_for_team(round_obj.gov_team) + if round_obj.opp_team: + stats_by_team_id[round_obj.opp_team_id] = stats_for_team(round_obj.gov_team) + + return JsonResponse(stats_by_team_id) + + @permission_required("tab.tab_settings.can_change", login_url="/403/") def assign_team(request, round_id, position, team_id): try: diff --git a/mittab/apps/tab/team_views.py b/mittab/apps/tab/team_views.py index 11958543f..a32b5a074 100644 --- a/mittab/apps/tab/team_views.py +++ b/mittab/apps/tab/team_views.py @@ -1,4 +1,4 @@ -from django.http import HttpResponseRedirect, JsonResponse +from django.http import HttpResponseRedirect from django.contrib.auth.decorators import permission_required from django.shortcuts import render @@ -370,6 +370,7 @@ def rank_teams(request): teams, nov_teams = cache_logic.cache_fxn_key( get_team_rankings, "team_rankings", + cache_logic.DEFAULT, request ) @@ -378,24 +379,3 @@ def rank_teams(request): "novice": nov_teams, "title": "Team Rankings" }) - - -def team_stats(request, team_id): - team_id = int(team_id) - try: - team = Team.objects.get(pk=team_id) - stats = {} - stats["seed"] = Team.get_seed_display(team).split(" ")[0] - stats["wins"] = tab_logic.tot_wins(team) - stats["total_speaks"] = tab_logic.tot_speaks(team) - stats["govs"] = tab_logic.num_govs(team) - stats["opps"] = tab_logic.num_opps(team) - - if hasattr(team, "breaking_team"): - stats["outround_seed"] = team.breaking_team.seed - stats["effective_outround_seed"] = team.breaking_team.effective_seed - - data = {"success": True, "result": stats} - except Team.DoesNotExist: - data = {"success": False} - return JsonResponse(data) diff --git a/mittab/apps/tab/views.py b/mittab/apps/tab/views.py index 7ef3a9d2f..f67964a03 100644 --- a/mittab/apps/tab/views.py +++ b/mittab/apps/tab/views.py @@ -18,12 +18,6 @@ def index(request): - number_teams = Team.objects.count() - number_judges = Judge.objects.count() - number_schools = School.objects.count() - number_debaters = Debater.objects.count() - number_rooms = Room.objects.count() - school_list = [(school.pk, school.name) for school in School.objects.all()] judge_list = [(judge.pk, judge.name) for judge in Judge.objects.all()] team_list = [(team.pk, team.display_backend) for team in Team.objects.all()] @@ -31,6 +25,12 @@ def index(request): for debater in Debater.objects.all()] room_list = [(room.pk, room.name) for room in Room.objects.all()] + number_teams = len(team_list) + number_judges = len(judge_list) + number_schools = len(school_list) + number_debaters = len(debater_list) + number_rooms = len(room_list) + return render(request, "common/index.html", locals()) diff --git a/mittab/libs/assign_judges.py b/mittab/libs/assign_judges.py index 15836ec29..a18c4eb93 100644 --- a/mittab/libs/assign_judges.py +++ b/mittab/libs/assign_judges.py @@ -4,12 +4,19 @@ from mittab.apps.tab.models import * -def add_judges(pairings, judges, panel_points): - # First clear any existing judge assignments - for pairing in pairings: - pairing.judges.clear() +def add_judges(): + current_round_number = TabSettings.get("cur_round") - 1 + + judges = list(Judge.objects.filter(checkin__round_number=4).prefetch_related( + "judges", # poorly named relation for the round + "scratches", + )) + pairings = tab_logic.sorted_pairings(current_round_number) - current_round_number = TabSettings.objects.get(key="cur_round").value - 1 + # First clear any existing judge assignments + Round.judges.through.objects \ + .filter(round__round_number=current_round_number) \ + .delete() # Try to have consistent ordering with the round display random.seed(1337) @@ -22,150 +29,51 @@ def add_judges(pairings, judges, panel_points): pairings.sort(key=lambda x: tab_logic.team_comp(x, current_round_number), reverse=True) - pairing_groups = [list() for panel_point in panel_points] + [list()] - panel_gaps = {} - current_group = 0 - for pairing in pairings: - pairing_groups[current_group].append(pairing) - if current_group < len( - panel_points) and pairing == panel_points[current_group][0]: - panel_gaps[current_group] = panel_points[current_group][1] - current_group += 1 - - for (group_i, group) in enumerate(pairing_groups): - num_rounds = len(group) - # Assign chairs (single judges) to each round using perfect pairing - graph_edges = [] - for (judge_i, judge) in enumerate(judges): - for (pairing_i, pairing) in enumerate(group): - if not judge_conflict(judge, pairing.gov_team, - pairing.opp_team): - graph_edges.append((pairing_i, judge_i + len(group), - calc_weight(judge_i, pairing_i))) - judge_assignments = mwmatching.maxWeightMatching(graph_edges, - maxcardinality=True) - # If there is no possible assignment of chairs, raise an error - if -1 in judge_assignments[:num_rounds] or (num_rounds > 0 - and not graph_edges): - if not graph_edges: - raise errors.JudgeAssignmentError( - "Impossible to assign judges, consider reducing your gaps if you" - " are making panels, otherwise find some more judges.") - elif -1 in judge_assignments[:num_rounds]: - pairing_list = judge_assignments[:len(pairings)] - bad_pairing = pairings[pairing_list.index(-1)] - raise errors.JudgeAssignmentError( - "Could not find a judge for: %s" % str(bad_pairing)) - else: - raise errors.JudgeAssignmentError() - - # Save the judges to the pairings - for i in range(num_rounds): - group[i].judges.add(judges[judge_assignments[i] - num_rounds]) - group[i].chair = judges[judge_assignments[i] - num_rounds] - group[i].save() - - # Remove any assigned judges from the judging pool - for pairing in group: - for judge in pairing.judges.all(): - judges.remove(judge) - - # Function that tries to panel num_to_panel rounds of the potential_pairings - # Has built in logic to retry with lower number of panels if we fail due - # to either scratches or wanting to many rounds - def try_paneling(potential_pairings, all_judges, num_to_panel, gap): - if not potential_pairings or num_to_panel <= 0: - # Base case, failed to panel - print("Failed to panel") - return {} - - def sort_key(round_obj): - team_comp_result = tab_logic.team_comp(round_obj, - current_round_number) - get_rank = lambda judge: judge.rank - rank_tuple = (argmin(round_obj.judges.all(), get_rank).rank, ) - return rank_tuple + tuple([-1 * i for i in team_comp_result]) - - rounds = sorted(potential_pairings, key=sort_key) - base_judge = argmax( - rounds[:num_to_panel][-1].judges.all(), lambda j: j.rank) - print("Found maximally ranked judge {0}".format(base_judge)) - potential_panelists = [ - j for j in all_judges - if j.rank > (float(base_judge.rank) - float(gap)) - ] - print("Potential panelists:", potential_panelists) - # If we don't have enough potential panelists, try again with fewer panels - if len(potential_panelists) < 2 * num_to_panel: - print("Not enough judges to panel!: ", - len(potential_panelists), num_to_panel) - return try_paneling(potential_pairings, all_judges, - num_to_panel - 1, gap) - - panel_assignments = [] - rounds_to_panel = rounds[:num_to_panel] - num_to_panel = len(rounds_to_panel) - for pairing in rounds_to_panel: - panel_assignments.append([j for j in pairing.judges.all()]) - - # Do it twice so we get panels of 3 - for _ in (0, 1): - graph_edges = [] - for (judge_i, judge) in enumerate(potential_panelists): - for (pairing_i, pairing) in enumerate(rounds_to_panel): - if not judge_conflict(judge, pairing.gov_team, - pairing.opp_team): - judges = panel_assignments[pairing_i] + [judge] - graph_edges.append( - (pairing_i, judge_i + num_to_panel, - calc_weight_panel(judges))) - judge_assignments = mwmatching.maxWeightMatching( - graph_edges, maxcardinality=True) - print(judge_assignments) - if ((-1 in judge_assignments[:num_to_panel]) - or (num_to_panel > 0 and not graph_edges)): - print("Scratches are causing a retry") - return try_paneling(potential_pairings, all_judges, - num_to_panel - 1, gap) - # Save the judges to the potential panel assignments - judges_used = [] - for i in range(num_to_panel): - judge = potential_panelists[judge_assignments[i] - - num_to_panel] - panel_assignments[i].append(judge) - judges_used.append(judge) - # Remove any used judges from the potential panelist pool - for judge in judges_used: - print("Removing {0}".format(judge)) - potential_panelists.remove(judge) - - print("panels: ", panel_assignments) - result = {} - for (panel_i, panel) in enumerate(panel_assignments): - result[rounds_to_panel[panel_i]] = panel - return result - - # Use the try_paneling function for any rounds that have been marked as panel - # points, note that we start with trying to panel the entire bracket and - # rely on try_paneling's retries to fix it - if group_i in panel_gaps and panel_gaps[group_i]: - panels = try_paneling(group, judges, len(group), - panel_gaps[group_i]) - for (pairing, panelists) in panels.items(): - for panelist in panelists: - if panelist not in pairing.judges.all(): - pairing.judges.add(panelist) - judges.remove(panelist) - pairing.save() - - -def argmin(seq, fun): - return min([(fun(i), i) for i in seq])[1] - - -def argmax(seq, fun): - return max([(fun(i), i) for i in seq])[1] - + num_rounds = len(pairings) + + # Assign chairs (single judges) to each round using perfect pairing + graph_edges = [] + for (judge_i, judge) in enumerate(judges): + for (pairing_i, pairing) in enumerate(pairings): + if not judge_conflict(judge, pairing.gov_team, pairing.opp_team): + edge = ( + pairing_i, + num_rounds + judge_i, + calc_weight(judge_i, pairing_i), + ) + graph_edges.append(edge) + + judge_assignments = mwmatching.maxWeightMatching(graph_edges, maxcardinality=True) + + # If there is no possible assignment of chairs, raise an error + if -1 in judge_assignments[:num_rounds] or (num_rounds > 0 and not graph_edges): + if not graph_edges: + raise errors.JudgeAssignmentError( + "Impossible to assign judges, consider reducing your gaps if you" + " are making panels, otherwise find some more judges.") + elif -1 in judge_assignments[:num_rounds]: + pairing_list = judge_assignments[:len(pairings)] + bad_pairing = pairings[pairing_list.index(-1)] + raise errors.JudgeAssignmentError( + "Could not find a judge for: %s" % str(bad_pairing)) + else: + raise errors.JudgeAssignmentError() + + # Because we can't bulk-update the judges field of rounds (it's many-to-many), + # we use the join table model and bulk-create it + judge_round_joins = [] + for pairing_i, padded_judge_i in enumerate(judge_assignments[:num_rounds]): + judge_i = padded_judge_i - num_rounds + + round_obj = pairings[pairing_i] + judge = judges[judge_i] + + round_obj.chair = judge + judge_round_joins.append(Round.judges.through(judge=judge, round=round_obj)) + + # Save the judges to the pairings + Round.objects.bulk_update(pairings, ["chair"]) + Round.judges.through.objects.bulk_create(judge_round_joins) def calc_weight(judge_i, pairing_i): """ Calculate the relative badness of this judge assignment @@ -176,24 +84,17 @@ def calc_weight(judge_i, pairing_i): return -1 * abs(judge_i - (-1 * pairing_i)) -def calc_weight_panel(judges): - judge_ranks = [float(j.rank) for j in judges] - avg = round(float(sum(judge_ranks)) / len(judge_ranks)) - sum_squares = sum([((r - avg)**2) for r in judge_ranks]) - # Use the sum_squares so we get highest panelists with lowest judges - return 1000000 * sum(judge_ranks) + sum_squares - - def judge_conflict(judge, team1, team2): - return Scratch.objects.filter(judge=judge, team=team1).exists() \ + return any(s.team_id in (team1.id, team2.id,) for s in judge.scratches.all()) \ or had_judge(judge, team1) \ - or Scratch.objects.filter(judge=judge, team=team2).exists() \ or had_judge(judge, team2) def had_judge(judge, team): - return Round.objects.filter(gov_team=team, judges=judge).exists() \ - or Round.objects.filter(opp_team=team, judges=judge).exists() + for round_obj in judge.judges.all(): + if round_obj.gov_team_id == team.id or round_obj.opp_team_id == team.id: + return True + return False def can_judge_teams(list_of_judges, team1, team2): diff --git a/mittab/libs/backup/__init__.py b/mittab/libs/backup/__init__.py index 9e6833023..126fdfde2 100644 --- a/mittab/libs/backup/__init__.py +++ b/mittab/libs/backup/__init__.py @@ -1,20 +1,27 @@ -import shutil -import time +import io import os +import tempfile +import time from wsgiref.util import FileWrapper -from django.conf import settings from mittab.apps.tab.models import TabSettings -from mittab.libs import errors -from mittab.settings import BASE_DIR -from mittab.libs.backup.strategies.local_dump import LocalDump - +from mittab.libs import errors, cache_logic +from mittab.libs.backup.handlers import MysqlDumpRestorer +from mittab.libs.backup.storage import LocalFilesystem, ObjectStorage +from mittab import settings ACTIVE_BACKUP_KEY = "MITTAB_ACTIVE_BACKUP" ACTIVE_BACKUP_VAL = "1" +if settings.BACKUPS["use_s3"]: + BACKUP_STORAGE = ObjectStorage() +else: + BACKUP_STORAGE = LocalFilesystem() +BACKUP_HANDLER = MysqlDumpRestorer() + +# Note: Improve this to be.... something better and more lock-y class ActiveBackupContextManager: def __enter__(self): os.environ[ACTIVE_BACKUP_KEY] = ACTIVE_BACKUP_VAL @@ -22,14 +29,15 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, exc_traceback): os.environ[ACTIVE_BACKUP_KEY] = "0" + cache_logic.clear_cache() def _generate_unique_key(base): - if LocalDump(base).exists(): + if base in BACKUP_STORAGE: return "%s_%s" % (base, int(time.time())) else: return base -def backup_round(dst_filename=None, round_number=None, btime=None): +def backup_round(key=None, round_number=None, btime=None): with ActiveBackupContextManager() as _: if round_number is None: round_number = TabSettings.get("cur_round", "no-round-number") @@ -38,35 +46,30 @@ def backup_round(dst_filename=None, round_number=None, btime=None): btime = int(time.time()) print("Trying to backup to backups directory") - if dst_filename is None: - dst_filename = "site_round_%i_%i" % (round_number, btime) - - dst_filename = _generate_unique_key(dst_filename) - return LocalDump(dst_filename).backup() - - -def handle_backup(f): - dst_key = _generate_unique_key(f.name) - print(("Tried to write {}".format(dst_key))) + if key is None: + key = "site_round_%i_%i" % (round_number, btime) + key = _generate_unique_key(key) + BACKUP_STORAGE[key] = BACKUP_HANDLER.dump() + +def upload_backup(f): + key = _generate_unique_key(f.name) + print(("Tried to write {}".format(key))) try: - return LocalDump.from_upload(dst_key, f) + BACKUP_STORAGE[key] = f.read() except Exception: errors.emit_current_exception() +def get_backup_content(key): + return BACKUP_STORAGE[key] def list_backups(): print("Checking backups directory") - return [dump.key for dump in LocalDump.all()] - + return BACKUP_STORAGE.keys() -def restore_from_backup(src_key): +def restore_from_backup(key): with ActiveBackupContextManager() as _: print("Restoring from backups directory") - return LocalDump(src_key).restore() - - -def get_wrapped_file(src_key): - return LocalDump(src_key).downloadable() + BACKUP_HANDLER.restore(BACKUP_STORAGE[key]) def is_backup_active(): return str(os.environ.get(ACTIVE_BACKUP_KEY, "0")) == ACTIVE_BACKUP_VAL diff --git a/mittab/libs/backup/handlers.py b/mittab/libs/backup/handlers.py new file mode 100644 index 000000000..5e21b1a84 --- /dev/null +++ b/mittab/libs/backup/handlers.py @@ -0,0 +1,66 @@ +import subprocess + +from mittab import settings + +DB_SETTINGS = settings.DATABASES["default"] +DB_HOST = DB_SETTINGS["HOST"] +DB_NAME = DB_SETTINGS["NAME"] +DB_USER = DB_SETTINGS["USER"] +DB_PASS = DB_SETTINGS["PASSWORD"] +DB_PORT = DB_SETTINGS["PORT"] + +class MysqlDumpRestorer: + + def dump(self): + return subprocess.check_output(self._dump_cmd()) + + def restore(self, content): + """ + This is a multi-stage restore to avoid the worst-case scenario + where you dump the existing db, but the restore from the new db fails, + crashing the app with an empty database + + The process is: + 1. Dump existing DB to a new file + 2. Restore from given database + 3. If error for #2, restore from the dumped file again + + Can be improved by using rename database, just need to test that out first + """ + before = self.dump() + try: + subprocess.check_output(self._restore_cmd(), input=content) + except Exception as e: + subprocess.check_output(self._restore_cmd(), input=before) + raise e + + + def _restore_cmd(self): # Note: refactor to just use a python mysql client + cmd = [ + "mysql", + DB_NAME, + "--port={}".format(DB_PORT), + "--host={}".format(DB_HOST), + "--user={}".format(DB_USER), + ] + + if DB_PASS: + cmd.append("--password={}".format(DB_PASS)) + + return cmd + + def _dump_cmd(self): + cmd = [ + "mysqldump", + DB_NAME, + "--quick", + "--lock-all-tables", + "--port={}".format(DB_PORT), + "--host={}".format(DB_HOST), + "--user={}".format(DB_USER), + ] + + if DB_PASS: + cmd.append("--password={}".format(DB_PASS)) + + return cmd diff --git a/mittab/libs/backup/storage.py b/mittab/libs/backup/storage.py new file mode 100644 index 000000000..5ac4d3dfb --- /dev/null +++ b/mittab/libs/backup/storage.py @@ -0,0 +1,96 @@ +import os +import tempfile + +from botocore.exceptions import ClientError +import boto3 +from mittab import settings + + +BACKUP_PREFIX = settings.BACKUPS["prefix"] +BUCKET_NAME = settings.BACKUPS["bucket_name"] +S3_ENDPOINT = settings.BACKUPS["s3_endpoint"] +SUFFIX = ".dump.sql" + +def with_backup_dir(func): + def wrapper(*args, **kwargs): + if not os.path.exists(BACKUP_PREFIX): + os.makedirs(BACKUP_PREFIX) + return func(*args, **kwargs) + return wrapper + +class LocalFilesystem: + @with_backup_dir + def keys(self): + return [name[:-len(SUFFIX)] for name in os.listdir(BACKUP_PREFIX)] + + @with_backup_dir + def __setitem__(self, key, content): + dst_filename = os.path.join(BACKUP_PREFIX, key + SUFFIX) + with open(dst_filename, "wb+") as destination: + destination.write(content) + + def __getitem__(self, key): + with open(self._get_backup_filename(key), "rb") as f: + return f.read() + + def __contains__(self, key): + return os.path.exists(self._get_backup_filename(key)) + + def _get_backup_filename(self, key): + if len(key) < len(SUFFIX) or not key.endswith(SUFFIX): + key += SUFFIX + return os.path.join(BACKUP_PREFIX, key) + + +class ObjectStorage: + def __init__(self): + if not BUCKET_NAME: + raise ValueError("Need bucket name for S3 storage") + if not BACKUP_PREFIX: + raise ValueError("Need backup path for S3 storage") + + if S3_ENDPOINT is None: + self.s3_client = boto3.client("s3") + else: + self.s3_client = boto3.client("s3", endpoint_url=S3_ENDPOINT) + + def keys(self): + paginator = self.s3_client.get_paginator("list_objects_v2") + to_return = [] + for page in paginator.paginate(Bucket=BUCKET_NAME, Prefix=BACKUP_PREFIX): + keys = map( + lambda obj: obj["Key"][(len(BACKUP_PREFIX) + 1):-len(SUFFIX)], + page.get("Contents", [])) + to_return += list(keys) + return to_return + + def __contains__(self, key): + try: + self.s3_client.head_object(Bucket=BUCKET_NAME, Key=self._object_path(key)) + except ClientError as e: + return int(e.response["Error"]["Code"]) != 404 + return True + + def __setitem__(self, key, content): + with tempfile.TemporaryFile(mode="w+b") as f: + f.write(content) + f.seek(0) + self.s3_client.upload_fileobj(f, BUCKET_NAME, self._object_path(key)) + + def __getitem__(self, key): + with tempfile.TemporaryFile(mode="w+b") as f: + try: + self.s3_client.download_fileobj( + BUCKET_NAME, + self._object_path(key), + f) + except ClientError as e: + if int(e.response["Error"]["Code"]) == 404: + return KeyError(key) + else: + raise e + f.seek(0) + return f.read() + + def _object_path(self, key): + return "%s/%s%s" % (BACKUP_PREFIX, key, SUFFIX) diff --git a/mittab/libs/backup/strategies/local_dump.py b/mittab/libs/backup/strategies/local_dump.py deleted file mode 100644 index 9d50de648..000000000 --- a/mittab/libs/backup/strategies/local_dump.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -import time -import subprocess -from wsgiref.util import FileWrapper - -from mittab import settings - - -BACKUP_PREFIX = os.path.join(settings.BASE_DIR, "mittab") -BACKUP_PATH = os.path.join(BACKUP_PREFIX, "backups") -SUFFIX = ".dump.sql" - -if not os.path.exists(BACKUP_PATH): - os.makedirs(BACKUP_PATH) - -DB_SETTINGS = settings.DATABASES["default"] -DB_HOST = DB_SETTINGS["HOST"] -DB_NAME = DB_SETTINGS["NAME"] -DB_USER = DB_SETTINGS["USER"] -DB_PASS = DB_SETTINGS["PASSWORD"] -DB_PORT = DB_SETTINGS["PORT"] - - -class LocalDump: - def __init__(self, key): - self.key = key - - @classmethod - def all(cls): - all_names = os.listdir(BACKUP_PATH) - def key_from_filename(name): - return name[:-len(SUFFIX)] - return [cls(key_from_filename(name)) for name in all_names] - - @classmethod - def from_upload(cls, key, upload): - dst_filename = os.path.join(BACKUP_PATH, key + SUFFIX) - with open(dst_filename, "wb+") as destination: - for chunk in upload.chunks(): - destination.write(chunk) - return cls(key) - - def downloadable(self): - src_filename = self._get_backup_filename() - return FileWrapper(open(src_filename, "rb")), os.path.getsize(src_filename) - - def backup(self): - subprocess.check_call(self._dump_cmd(self._get_backup_filename())) - - def restore(self): - tmp_filename = "backup_before_restore_%s%s" % (int(time.time()), SUFFIX) - tmp_full_path = os.path.join(BACKUP_PATH, tmp_filename) - try: - subprocess.check_call(self._dump_cmd(tmp_full_path)) - except Exception as e: - os.remove(tmp_full_path) - raise e - - try: - with open(self._get_backup_filename()) as stdin: - subprocess.check_call(self._restore_cmd(), stdin=stdin) - os.remove(tmp_full_path) - except Exception as e: - with open(tmp_full_path) as stdin: - subprocess.check_call(self._restore_cmd(), stdin=stdin) - raise e - - def exists(self): - return os.path.exists(self._get_backup_filename()) - - def _get_backup_filename(self): - key = self.key - if len(key) < len(SUFFIX) or not key.endswith(SUFFIX): - key += SUFFIX - return os.path.join(BACKUP_PATH, key) - - def _restore_cmd(self): - cmd = [ - "mysql", - DB_NAME, - "--port={}".format(DB_PORT), - "--host={}".format(DB_HOST), - "--user={}".format(DB_USER), - ] - - if DB_PASS: - cmd.append("--password={}".format(DB_PASS)) - - return cmd - - - def _dump_cmd(self, dst): - cmd = [ - "mysqldump", - DB_NAME, - "--quick", - "--lock-all-tables", - "--port={}".format(DB_PORT), - "--host={}".format(DB_HOST), - "--user={}".format(DB_USER), - "--result-file={}".format(dst), - ] - - if DB_PASS: - cmd.append("--password={}".format(DB_PASS)) - - return cmd diff --git a/mittab/libs/cache_logic.py b/mittab/libs/cache_logic.py index ccea8b83c..0c19e4f67 100644 --- a/mittab/libs/cache_logic.py +++ b/mittab/libs/cache_logic.py @@ -2,22 +2,34 @@ from hashlib import sha1 import random -from django.core.cache import cache as _djcache +from django.core.cache import caches CACHE_TIMEOUT = 20 +DEFAULT = "default" +PERSISTENT = "filesystem" -def cache_fxn_key(fxn, key, *args, **kwargs): - result = _djcache.get(key) - if not result: +def cache_fxn_key(fxn, key, cache_name, *args, **kwargs): + """ + Cache the result of a function call under the specified key + + For example, the calling this like: + cache_fxn_key(lambda a: a ** 2, 'squared', DEFAULT, 1) + + would square `1` and store the result under the key 'squared' + all subsequent function calls would read from the cache until it's cleared + """ + if key not in caches[cache_name]: result = fxn(*args, **kwargs) - _djcache.set(key, result) - return result + caches[cache_name].set(key, result) + return result + else: + return caches[cache_name].get(key) -def invalidate_cache(key): - _djcache.delete(key) +def invalidate_cache(key, cache_name=DEFAULT): + caches[cache_name].delete(key) def cache(seconds=CACHE_TIMEOUT, stampede=CACHE_TIMEOUT): @@ -39,14 +51,13 @@ def myExpensiveMethod(parm1, parm2, parm3): def do_cache(f): def wrapper(*args, **kwargs): - key = sha1(("%s%s%s%s" % (f.__module__, f.__name__, args, - kwargs)).encode("utf-8")).hexdigest() - result = _djcache.get(key) + key = sha1(("%s%s%s%s" % (f.__module__, f.__name__, args, kwargs)) \ + .encode("utf-8")).hexdigest() + result = caches[DEFAULT].get(key) if result is None: - #print "busting cache" result = f(*args, **kwargs) - _djcache.set(key, result, - random.randint(seconds, seconds + stampede)) + caches[DEFAULT].set(key, result, + random.randint(seconds, seconds + stampede)) return result return wrapper @@ -55,4 +66,5 @@ def wrapper(*args, **kwargs): def clear_cache(): - _djcache.clear() + caches[DEFAULT].clear() + caches[PERSISTENT].clear() 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/outround_tab_logic/pairing.py b/mittab/libs/outround_tab_logic/pairing.py index 559dc05a1..a6cd95bcf 100644 --- a/mittab/libs/outround_tab_logic/pairing.py +++ b/mittab/libs/outround_tab_logic/pairing.py @@ -19,6 +19,7 @@ def perform_the_break(): teams, nov_teams = cache_logic.cache_fxn_key( get_team_rankings, "team_rankings", + cache_logic.DEFAULT, None ) diff --git a/mittab/libs/tab_logic/__init__.py b/mittab/libs/tab_logic/__init__.py index 3c46389bc..aea509f68 100644 --- a/mittab/libs/tab_logic/__init__.py +++ b/mittab/libs/tab_logic/__init__.py @@ -43,30 +43,46 @@ def pair_round(): # add scratches for teams/judges from the same school # NOTE that this only happens if they haven't already been added add_scratches_for_school_affil() - - list_of_teams = [None] * current_round all_pull_ups = [] # Record no-shows 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 + # 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", + "gov_team__opp_team__byes", + # for all opp rounds, load the gov team's gov+opp rounds (opp-strength) + "opp_team__gov_team__gov_team", + "opp_team__gov_team__opp_team", + "opp_team__gov_team__byes", + "byes", + "no_shows", + "debaters", + "debaters__roundstats_set", + "debaters__roundstats_set__round", + "debaters__team_set", + "debaters__team_set__no_shows", + ) + if current_round == 1: - list_of_teams = list(Team.objects.filter(checked_in=True)) + list_of_teams = list(all_checked_in_teams) # If there are an odd number of teams, give a random team the bye 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 @@ -78,31 +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 = middle_of_bracket_teams() - all_checked_in_teams = Team.objects.filter(checked_in=True) - normal_pairing_teams = all_checked_in_teams.exclude( - pk__in=[t.id for t in middle_of_bracket]) + 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) @@ -112,16 +122,18 @@ def pair_round(): # 2) If we are in 1-up bracket and there are no all down # teams, give someone a bye # 3) Otherwise, find a pull up from the next bracket + for bracket in reversed(list(range(current_round))): if len(list_of_teams[bracket]) % 2 != 0: # 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 @@ -129,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 @@ -142,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: @@ -173,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 @@ -191,25 +205,30 @@ def pair_round(): temp = perfect_pairing(list_of_teams) else: temp = perfect_pairing(list_of_teams[bracket]) - print("Pairing round %i of size %i" % (bracket, len(temp))) + print("Pairing bracket %i of size %i" % (bracket, len(temp))) for pair in temp: pairings.append([pair[0], pair[1], None]) 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?) - rooms = RoomCheckIn.objects.filter(round_number=current_round) + rooms = RoomCheckIn.objects.filter(round_number=current_round).prefetch_related( + "room" + ) rooms = map(lambda r: r.room, rooms) rooms = sorted(rooms, key=lambda r: r.rank, reverse=True) @@ -217,16 +236,17 @@ def pair_round(): pairing[2] = rooms[i] # 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: round_obj.pullup = Round.OPP - round_obj.save() + all_rounds.append(round_obj) + Round.objects.bulk_create(all_rounds) def have_enough_judges(round_to_check): @@ -247,7 +267,18 @@ 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) + 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 + ) + ) + prev_round_byes = set( + Bye.objects.filter(round_number=last_round).values_list("bye_team", flat=True) + ) + for prev_round in prev_rounds: # There should be a result if prev_round.victor == Round.NONE: @@ -255,14 +286,14 @@ def have_properly_entered_data(round_to_check): # Both teams should not have byes or noshows gov_team, opp_team = prev_round.gov_team, prev_round.opp_team for team in gov_team, opp_team: - had_noshow = NoShow.objects.filter(no_show_team=team, - round_number=last_round) - if had_bye(team, last_round): + if team.id in prev_round_byes: raise errors.ByeAssignmentError( - "{} both had a bye and debated last round".format(team)) - if had_noshow: + "{} 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): @@ -291,14 +322,44 @@ 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 Only do this if they haven't already been added """ - all_judges = Judge.objects.all() + 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: - judge.update_scratches() + for team in all_teams: + judge_schools = judge.schools.all() + if team.school in judge_schools or team.hybrid_school in judge_schools: + to_create.append( + add_scratches_for_single_judge_for_school_affiliation(judge, team) + ) + + Scratch.objects.bulk_create(to_create) def highest_seed(team1, team2): @@ -307,13 +368,20 @@ def highest_seed(team1, team2): # Check if two teams have hit before def hit_before(team1, team2): - return Round.objects.filter(gov_team=team1, opp_team=team2).exists() or \ - Round.objects.filter(gov_team=team2, opp_team=team1).exists() + for round_obj in team1.gov_team.all(): + if round_obj.opp_team == team2: + return True + for round_obj in team1.opp_team.all(): + if round_obj.opp_team == team2: + return True + return False -def middle_of_bracket_teams(): +def get_middle_and_non_middle_teams(all_teams): """ - Finds teams whose only rounds have been one of the following results: + Given a list of teams, splits the list into two. The first value will be + a list of teams who should be in the middle of the bracket because all of their + rounds have been one of the following: 1 - win by forfeit 2 - lenient_late rounds @@ -321,17 +389,66 @@ def middle_of_bracket_teams(): These teams have speaks of zero but _should_ have average speaks, so they should be paired into the middle of their bracket. Randomized for fairness + + The second list will be all of the teams not in the original list """ - teams = [] - for team in Team.objects.filter(checked_in=True): - avg_speaks_rounds = Bye.objects.filter(bye_team=team).count() - avg_speaks_rounds += NoShow.objects.filter(no_show_team=team, - lenient_late=True).count() + middle_of_bracket, non_middle_of_bracket = [], [] + all_teams = list(all_teams) + random.shuffle(all_teams) + round_count = TabSettings.get("cur_round") - 1 + + for team in all_teams: + avg_speaks_rounds = team.byes.count() + for no_show in team.no_shows.all(): + if no_show.lenient_late: + avg_speaks_rounds += 1 avg_speaks_rounds += num_forfeit_wins(team) - if TabSettings.get("cur_round") - 1 == avg_speaks_rounds: - teams.append(team) - random.shuffle(teams) - return teams + + if round_count == avg_speaks_rounds: + middle_of_bracket.append(team) + else: + non_middle_of_bracket.append(team) + + return middle_of_bracket, non_middle_of_bracket + + +def sorted_pairings(round_number): + """ + Helper function to get the sorted pairings for a round while minimizing the + number of DB queries required to calculate it + """ + round_pairing = list( + Round.objects.filter(round_number=round_number).prefetch_related( + "judges", + "chair", + "room", + "gov_team", + "opp_team", + "gov_team__breaking_team", + "gov_team__gov_team", # poorly named relation, points to rounds as gov + "gov_team__opp_team", # poorly named relation, points to rounds as gov + "gov_team__byes", + "gov_team__no_shows", + "gov_team__debaters__team_set", + "gov_team__debaters__team_set__byes", + "gov_team__debaters__team_set__no_shows", + "gov_team__debaters__roundstats_set", + "gov_team__debaters__roundstats_set__round", + "opp_team__breaking_team", + "opp_team__gov_team", # poorly named relation, points to rounds as gov + "opp_team__opp_team", # poorly named relation, points to rounds as gov + "opp_team__byes", + "opp_team__no_shows", + "opp_team__debaters__team_set", + "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) + + return round_pairing def team_comp(pairing, round_number): @@ -339,9 +456,11 @@ def team_comp(pairing, round_number): if round_number == 1: return (max(gov.seed, opp.seed), min(gov.seed, opp.seed)) else: - return (max(tot_wins(gov), - tot_wins(opp)), max(tot_speaks(gov), tot_speaks(opp)), - min(tot_speaks(gov), tot_speaks(opp))) + return ( + max(tot_wins(gov), tot_wins(opp)), + max(tot_speaks(gov), tot_speaks(opp)), + min(tot_speaks(gov), tot_speaks(opp)), + ) def team_score_except_record(team): @@ -368,10 +487,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 @@ -379,53 +505,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) + 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): @@ -438,8 +581,35 @@ def perfect_pairing(list_of_teams): return determine_gov_opp(all_pairs) -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): +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 @@ -453,45 +623,39 @@ def calc_weight(team_a, team_b, team_a_ind, team_b_ind, team_a_opt, team_b_opt, team_a_opt_ind - the position in the pairing of team_a_opt team_b_opt_ind - the position in the pairing of team_b_opt """ - - current_round = TabSettings.get("cur_round", 1) - tot_rounds = TabSettings.get("tot_rounds", 5) - 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) - if current_round == 1: - weight = 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 = 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: - weight += high_opp_penalty + weight += weights["high_opp_penalty"] if num_opps(team_a) >= half + 1 and num_opps(team_b) >= half + 1: - weight += high_high_opp_penalty + weight += weights["high_high_opp_penalty"] if num_govs(team_a) >= half and num_govs(team_b) >= half: - weight += high_gov_penalty + weight += weights["high_gov_penalty"] - if team_a.school == team_b.school: - weight += same_school_penalty + if team_a.school_id == team_b.school_id: + 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)): - weight += hit_pull_up_before + 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): - weight += hit_team_before + weight += weights["hit_team_before"] return weight diff --git a/mittab/libs/tab_logic/rankings.py b/mittab/libs/tab_logic/rankings.py index db2050fd7..1554383ed 100644 --- a/mittab/libs/tab_logic/rankings.py +++ b/mittab/libs/tab_logic/rankings.py @@ -6,16 +6,40 @@ def rank_speakers(): + debaters = Debater.objects.prefetch_related( + "team_set", + "team_set__byes", + "team_set__no_shows", + "roundstats_set", + ).all() return sorted([ DebaterScore(d) - for d in Debater.objects.prefetch_related("team_set").all() + for d in debaters ]) def rank_teams(): - return sorted([ - TeamScore(d) for d in Team.objects.prefetch_related("debaters").all() - ]) + all_teams = Team.objects.all().prefetch_related( + "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", + "gov_team__opp_team__byes", + # for all opp rounds, load the gov team's gov+opp rounds (opp-strength) + "opp_team__gov_team__gov_team", + "opp_team__gov_team__opp_team", + "opp_team__gov_team__byes", + "byes", + "no_shows", + "debaters", + "debaters__roundstats_set", + "debaters__roundstats_set__round", + "debaters__team_set", + "debaters__team_set__no_shows", + "debaters__team_set__byes", + ) + return sorted(TeamScore(d) for d in all_teams) class Stat: diff --git a/mittab/libs/tab_logic/stats.py b/mittab/libs/tab_logic/stats.py index 6829f07ea..94c3016ff 100644 --- a/mittab/libs/tab_logic/stats.py +++ b/mittab/libs/tab_logic/stats.py @@ -1,7 +1,6 @@ from collections import defaultdict -from django.db.models import Q -from mittab.apps.tab.models import Round, Bye, NoShow, TabSettings +from mittab.apps.tab.models import Round, TabSettings from mittab.libs.cache_logic import cache MAXIMUM_DEBATER_RANKS = 3.5 @@ -13,19 +12,21 @@ def num_byes(team): - return Bye.objects.filter(bye_team=team).count() - + return team.byes.count() def num_forfeit_wins(team): - return Round.objects.filter( - Q(gov_team=team, victor=Round.GOV_VIA_FORFEIT) - | Q(opp_team=team, victor=Round.OPP_VIA_FORFEIT) - | Q(gov_team=team, victor=Round.ALL_WIN) - | Q(opp_team=team, victor=Round.ALL_WIN)).count() - + num_wins = 0 + for round_obj in team.gov_team.all(): + if round_obj.victor in (Round.ALL_WIN, Round.GOV_VIA_FORFEIT,): + num_wins += 1 + for round_obj in team.opp_team.all(): + if round_obj.victor in (Round.ALL_WIN, Round.OPP_VIA_FORFEIT,): + num_wins += 1 + return num_wins def won_by_forfeit(round_obj, team): - if round_obj.opp_team != team and round_obj.gov_team != team: + if team is None or \ + (round_obj.opp_team_id != team.id and round_obj.gov_team_id != team.id): return False elif round_obj.victor == Round.ALL_WIN: return True @@ -37,7 +38,8 @@ def won_by_forfeit(round_obj, team): def forfeited_round(round_obj, team): - if round_obj.opp_team != team and round_obj.gov_team != team: + if team is None or \ + (round_obj.opp_team_id != team.id and round_obj.gov_team_id != team.id): return False elif round_obj.victor == Round.GOV_VIA_FORFEIT: return round_obj.opp_team == team @@ -47,29 +49,36 @@ def forfeited_round(round_obj, team): def hit_pull_up(team): - return Round.objects.filter(gov_team=team, pullup=Round.OPP).exists() or \ - Round.objects.filter(opp_team=team, pullup=Round.GOV).exists() + return any(r.pullup == Round.OPP for r in team.gov_team.all()) or \ + any(r.pullup == Round.GOV for r in team.opp_team.all()) def pull_up_count(team): - return Round.objects.filter(gov_team=team, pullup=Round.GOV).exists() + \ - Round.objects.filter(opp_team=team, pullup=Round.OPP).exists() + count = 0 + for round_obj in team.gov_team.all(): + if round_obj.pullup == Round.GOV: + count += 1 + + for round_obj in team.opp_team.all(): + if round_obj.pullup == Round.OPP: + count += 1 + + return count def num_opps(team): - return Round.objects.filter(opp_team=team).count() + return len(team.opp_team.all()) def num_govs(team): - return Round.objects.filter(gov_team=team).count() + return len(team.gov_team.all()) def had_bye(team, round_number=None): if round_number is None: - return Bye.objects.filter(bye_team=team).exists() + return team.byes.exists() else: - return Bye.objects.filter(bye_team=team, - round_number=round_number).exists() + any(b.round_number == round_number for b in team.byes.all()) ############## @@ -79,9 +88,17 @@ def had_bye(team, round_number=None): @cache() def tot_wins(team): - normal_wins = Round.objects.filter( - Q(gov_team=team, victor=Round.GOV) - | Q(opp_team=team, victor=Round.OPP)).count() + """ + Calculate total wins, using in-memory iteration rather than db queries to avoid n+1 + problems + """ + normal_wins = 0 + for round_obj in team.opp_team.all(): + if round_obj.victor == Round.OPP: + normal_wins += 1 + for round_obj in team.gov_team.all(): + if round_obj.victor == Round.GOV: + normal_wins += 1 return normal_wins + num_byes(team) + num_forfeit_wins(team) @@ -144,13 +161,13 @@ def opp_strength(team): opponent_count = 0 opponent_wins = 0 - gov_rounds = Round.objects.filter(gov_team=team) - opp_rounds = Round.objects.filter(opp_team=team) + gov_rounds = team.gov_team + opp_rounds = team.opp_team - for round_obj in gov_rounds: + for round_obj in gov_rounds.all(): opponent_wins += tot_wins(round_obj.opp_team) opponent_count += 1 - for round_obj in opp_rounds: + for round_obj in opp_rounds.all(): opponent_wins += tot_wins(round_obj.gov_team) opponent_count += 1 @@ -236,7 +253,6 @@ def speaks_for_debater(debater, average_ironmen=True): team = debater.team() # We start counting at 1, so when cur_round says 6 that means that we are # in round 5 and should have 5 speaks - num_speaks = TabSettings.get("cur_round") - 1 debater_roundstats = debater.roundstats_set.all() debater_speaks = [] @@ -247,6 +263,7 @@ def speaks_for_debater(debater, average_ironmen=True): for roundstat in debater_roundstats: speaks_per_round[roundstat.round.round_number].append(roundstat) + num_speaks = TabSettings.get("cur_round") - 1 for round_number in range(1, num_speaks + 1): roundstats = speaks_per_round[round_number] if roundstats: @@ -289,10 +306,17 @@ def debater_abnormal_round_speaks(debater, round_number): Uses average speaks """ team = debater.team() - had_noshow = NoShow.objects.filter(round_number=round_number, - no_show_team=team) + if team is None: + return MINIMUM_DEBATER_SPEAKS + + had_noshow = None + for no_show in team.no_shows.all(): + if no_show.round_number == round_number: + had_noshow = no_show + break + if had_bye(team, round_number) or (had_noshow - and had_noshow.first().lenient_late): + and had_noshow.lenient_late): return avg_deb_speaks(debater) elif had_noshow: return MINIMUM_DEBATER_SPEAKS @@ -432,10 +456,15 @@ def debater_abnormal_round_ranks(debater, round_number): Uses average ranks """ team = debater.team() - had_noshow = NoShow.objects.filter(round_number=round_number, - no_show_team=team) + had_noshow = None + if team is None: + return MINIMUM_DEBATER_SPEAKS + for no_show in team.no_shows.all(): + if no_show.round_number == round_number: + had_noshow = no_show + break if had_bye(team, round_number) or (had_noshow - and had_noshow.first().lenient_late): + and had_noshow.lenient_late): return avg_deb_ranks(debater) elif had_noshow: return MAXIMUM_DEBATER_RANKS diff --git a/mittab/libs/tests/tab_logic/test_pairing.py b/mittab/libs/tests/tab_logic/test_pairing.py index e855d9a5e..bd66d57da 100644 --- a/mittab/libs/tests/tab_logic/test_pairing.py +++ b/mittab/libs/tests/tab_logic/test_pairing.py @@ -45,17 +45,11 @@ def generate_checkins(self): checkin.save() def assign_judges(self): - cur_round = self.round_number() - panel_points = [] - rounds = list(Round.objects.filter(round_number=cur_round)) self.generate_checkins() - judges = [ - ci.judge for ci in CheckIn.objects.filter(round_number=cur_round) - ] - assign_judges.add_judges(rounds, judges, panel_points) + assign_judges.add_judges() def round_number(self): - return TabSettings.objects.get(key="cur_round").value - 1 + return TabSettings.get("cur_round") - 1 def check_pairing(self, round_number, last): assert self.round_number() == round_number diff --git a/mittab/libs/tests/test_case.py b/mittab/libs/tests/test_case.py index 810f96130..adbeb0ae9 100644 --- a/mittab/libs/tests/test_case.py +++ b/mittab/libs/tests/test_case.py @@ -1,9 +1,12 @@ +import os import time from django.test import LiveServerTestCase from selenium import webdriver from splinter import Browser +from mittab.apps.tab.models import TabSettings + class BaseWebTestCase(LiveServerTestCase): """ @@ -15,15 +18,21 @@ class BaseWebTestCase(LiveServerTestCase): wait_seconds = 3.0 def setUp(self): - chrome_options = webdriver.ChromeOptions() - chrome_options.add_argument("--window-size=1920,1080") - chrome_options.add_argument("--start-maximized") - chrome_options.add_argument("--no-sandbox") - self.browser = Browser("chrome", - headless=False, - wait_time=30, - options=chrome_options) + if os.environ.get("TEST_BROWSER", "chrome") == "chrome": + chrome_options = webdriver.ChromeOptions() + chrome_options.add_argument("--window-size=1920,1080") + chrome_options.add_argument("--start-maximized") + chrome_options.add_argument("--no-sandbox") + self.browser = Browser("chrome", + headless=False, + wait_time=30, + options=chrome_options) + else: + self.browser = Browser("firefox", + headless=False, + wait_time=30) self.browser.driver.set_page_load_timeout(240) + TabSettings.set("cur_round", 1) super(BaseWebTestCase, self).setUp() def tearDown(self): diff --git a/mittab/settings.py b/mittab/settings.py index 0f6bc6ec0..d045d0698 100644 --- a/mittab/settings.py +++ b/mittab/settings.py @@ -19,7 +19,7 @@ "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "mittab.apps.tab", "raven.contrib.django.raven_compat", - "webpack_loader", "bootstrap4", "polymorphic") + "webpack_loader", "bootstrap4",) MIDDLEWARE = ( "mittab.apps.tab.middleware.FailoverDuringBackup", @@ -32,22 +32,38 @@ "mittab.apps.tab.middleware.Login", ) +if os.environ.get("SILK_ENABLED"): + INSTALLED_APPS = INSTALLED_APPS + ("silk",) + MIDDLEWARE = MIDDLEWARE + ("silk.middleware.SilkyMiddleware",) + SILK_ENABLED = True +else: + SILK_ENABLED = False + ROOT_URLCONF = "mittab.urls" WSGI_APPLICATION = "mittab.wsgi.application" DATABASES = { "default": { - "ENGINE": "django.db.backends.mysql", - "NAME": "mittab", - "OPTIONS": {"charset": "utf8mb4"}, - "USER": os.environ.get("MYSQL_USER", "root"), - "PASSWORD": os.environ.get("MYSQL_ROOT_PASSWORD", ""), - "HOST": os.environ.get("MITTAB_DB_HOST", "127.0.0.1"), - "PORT": os.environ.get("MYSQL_PORT", "3306"), + "ENGINE": "django.db.backends.mysql", + "OPTIONS": {"charset": "utf8mb4"}, + "NAME": os.environ.get("MYSQL_DATABASE", "mittab"), + "USER": os.environ.get("MYSQL_USER", "root"), + "PASSWORD": os.environ.get("MYSQL_PASSWORD", ""), + "HOST": os.environ.get("MYSQL_HOST", "127.0.0.1"), + "PORT": os.environ.get("MYSQL_PORT", "3306"), } } +BACKUPS = { + "use_s3": os.environ.get("BACKUP_STORAGE", "") == "S3", + "prefix": os.environ.get( + "BACKUP_PREFIX", + os.path.join(BASE_DIR, "mittab", "backups")), + "bucket_name": os.environ.get("BACKUP_BUCKET"), + "s3_endpoint": os.environ.get("BACKUP_S3_ENDPOINT"), +} + # Error monitoring # https://docs.sentry.io/clients/python/integrations/django/ if os.environ.get("SENTRY_DSN"): @@ -103,3 +119,31 @@ MESSAGE_STORAGE = "django.contrib.messages.storage.session.SessionStorage" SETTING_YAML_PATH = os.path.join(BASE_DIR, "settings.yaml") + +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + }, + "filesystem": { + "BACKEND": "django.core.cache.backends.filebased.FileBasedCache", + "LOCATION": "/var/tmp/django_cache", + } +} + +if os.environ.get("MITTAB_LOG_QUERIES"): + LOGGING = { + "version": 1, + "handlers": { + "console": { + "class": "logging.StreamHandler", + }, + }, + "loggers": { + "django.db.backends": { + "level": "DEBUG", + }, + }, + "root": { + "handlers": ["console"], + } + } diff --git a/mittab/templates/outrounds/pairing_card.html b/mittab/templates/outrounds/pairing_card.html index 2ce84fbc7..c7af98ae8 100644 --- a/mittab/templates/outrounds/pairing_card.html +++ b/mittab/templates/outrounds/pairing_card.html @@ -8,7 +8,7 @@
    RoomRoomOutroundsRound {{ round_number }}
    - {{pairing.gov_team.display_backend}} + {{pairing.gov_team.display_backend}} GOV diff --git a/mittab/templates/pairing/pairing_control.html b/mittab/templates/pairing/pairing_control.html index 020cebf31..212df65ed 100644 --- a/mittab/templates/pairing/pairing_control.html +++ b/mittab/templates/pairing/pairing_control.html @@ -3,6 +3,7 @@ {% block title %}Round Status{% endblock %} {% block content %} +
    diff --git a/mittab/templates/pairing/pairing_display.html b/mittab/templates/pairing/pairing_display.html index 490ea3f9c..a5c7f6dbc 100644 --- a/mittab/templates/pairing/pairing_display.html +++ b/mittab/templates/pairing/pairing_display.html @@ -7,6 +7,8 @@ Pairings for Round {{round_number}} + + diff --git a/mittab/urls.py b/mittab/urls.py index 03b0f9b51..e28b80192 100644 --- a/mittab/urls.py +++ b/mittab/urls.py @@ -1,9 +1,10 @@ from django.views import i18n -from django.conf.urls import url +from django.conf.urls import url, include from django.urls import path from django.contrib import admin from django.contrib.auth.views import LoginView +import mittab.settings as settings import mittab.apps.tab.views as views import mittab.apps.tab.judge_views as judge_views import mittab.apps.tab.team_views as team_views @@ -80,7 +81,6 @@ url(r"^team/(\d+)/scratches/view/", team_views.view_scratches, name="view_scratches_team"), - url(r"^team/(\d+)/stats/", team_views.team_stats, name="team_stats"), url(r"^view_teams/$", team_views.view_teams, name="view_teams"), url(r"^enter_team/$", team_views.enter_team, name="enter_team"), url(r"^all_tab_cards/$", team_views.all_tab_cards, name="all_tab_cards"), @@ -109,6 +109,7 @@ pairing_views.view_rounds, name="view_rounds"), url(r"^round/(\d+)/$", pairing_views.view_round, name="view_round"), + url(r"^round/(\d+)/stats/$", pairing_views.team_stats, name="team_stats"), url(r"^round/(\d+)/result/$", pairing_views.enter_result, name="enter_result"), @@ -243,6 +244,12 @@ url(r"^cache_refresh", views.force_cache_refresh, name="cache_refresh"), ] +if settings.SILK_ENABLED: + urlpatterns += [ + # Profiler + url(r"^silk/", include("silk.urls", namespace="silk")) + ] + handler403 = "mittab.apps.tab.views.render_403" handler404 = "mittab.apps.tab.views.render_404" handler500 = "mittab.apps.tab.views.render_500" diff --git a/nginx.conf b/nginx.conf index dc63fc971..13966d7d9 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,18 +1,18 @@ server { - listen 80; + listen 8010; server_name mittab.org; charset utf-8; - access_log /var/log/access.log; - error_log /var/log/error.log; + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; location /static { alias /var/www/tab/staticfiles; } location / { - proxy_pass http://web:8000; + proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; From ee43049a4c5c81ef0d656f37d2f271c48b3dce1a Mon Sep 17 00:00:00 2001 From: Ben Muschol Date: Thu, 10 Feb 2022 15:15:46 -0500 Subject: [PATCH 22/27] Update docker to include the settings.yaml file --- Dockerfile | 1 + Dockerfile.web | 1 + 2 files changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index d410e55ef..dedf474ab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,7 @@ COPY package-lock.json ./ COPY manage.py ./ COPY setup.py ./ COPY webpack.config.js ./ +COPY settings.yaml ./ COPY ./mittab ./mittab COPY ./bin ./bin COPY ./assets ./assets diff --git a/Dockerfile.web b/Dockerfile.web index 78ad0c07b..a58989831 100644 --- a/Dockerfile.web +++ b/Dockerfile.web @@ -21,3 +21,4 @@ COPY manage.py ./ COPY setup.py ./ COPY ./mittab ./mittab COPY ./assets ./assets +COPY settings.yaml ./ From f718ade768ad70bd339fba8497e9b7e564eafcba Mon Sep 17 00:00:00 2001 From: Ben Muschol Date: Tue, 1 Mar 2022 15:22:54 -0500 Subject: [PATCH 23/27] Fix bug with bye speaks (#328) --- mittab/libs/tab_logic/stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mittab/libs/tab_logic/stats.py b/mittab/libs/tab_logic/stats.py index 94c3016ff..13f2627db 100644 --- a/mittab/libs/tab_logic/stats.py +++ b/mittab/libs/tab_logic/stats.py @@ -78,7 +78,7 @@ def had_bye(team, round_number=None): if round_number is None: return team.byes.exists() else: - any(b.round_number == round_number for b in team.byes.all()) + return any(b.round_number == round_number for b in team.byes.all()) ############## From 02e3356295751518828406bac03b2a29ac08675c Mon Sep 17 00:00:00 2001 From: Ben Muschol Date: Tue, 1 Mar 2022 16:50:36 -0500 Subject: [PATCH 24/27] Fix bug displaying stats in pairings (#329) Co-authored-by: Ben Muschol --- assets/js/pairing.js | 2 +- mittab/apps/tab/pairing_views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/js/pairing.js b/assets/js/pairing.js index 6d00dc01c..fb9cfad3a 100644 --- a/assets/js/pairing.js +++ b/assets/js/pairing.js @@ -18,7 +18,7 @@ function populateTabCards() { ].join(" / "); tabCardElement.attr("title", "Wins / Speaks / Govs / Opps / Seed"); tabCardElement.attr("href", `/team/card/${teamId}`); - tabCardElement.text(text); + tabCardElement.text(`${text}`); }); } }); diff --git a/mittab/apps/tab/pairing_views.py b/mittab/apps/tab/pairing_views.py index 0cd7ff0f2..36134d2a2 100644 --- a/mittab/apps/tab/pairing_views.py +++ b/mittab/apps/tab/pairing_views.py @@ -310,7 +310,7 @@ def stats_for_team(team): if round_obj.gov_team: stats_by_team_id[round_obj.gov_team_id] = stats_for_team(round_obj.gov_team) if round_obj.opp_team: - stats_by_team_id[round_obj.opp_team_id] = stats_for_team(round_obj.gov_team) + stats_by_team_id[round_obj.opp_team_id] = stats_for_team(round_obj.opp_team) return JsonResponse(stats_by_team_id) From 9f93a54bcc5e17f50fdcfbc79a003078bed6ecb8 Mon Sep 17 00:00:00 2001 From: Rodda John Date: Sun, 26 Feb 2023 09:44:30 -0500 Subject: [PATCH 25/27] Formatting --- mittab/libs/assign_judges.py | 53 ++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/mittab/libs/assign_judges.py b/mittab/libs/assign_judges.py index a18c4eb93..a00c8da03 100644 --- a/mittab/libs/assign_judges.py +++ b/mittab/libs/assign_judges.py @@ -7,16 +7,20 @@ def add_judges(): current_round_number = TabSettings.get("cur_round") - 1 - judges = list(Judge.objects.filter(checkin__round_number=4).prefetch_related( - "judges", # poorly named relation for the round - "scratches", - )) + judges = list( + Judge.objects.filter( + checkin__round_number=current_round_number + ).prefetch_related( + "judges", # poorly named relation for the round + "scratches", + ) + ) pairings = tab_logic.sorted_pairings(current_round_number) # First clear any existing judge assignments - Round.judges.through.objects \ - .filter(round__round_number=current_round_number) \ - .delete() + Round.judges.through.objects.filter( + round__round_number=current_round_number + ).delete() # Try to have consistent ordering with the round display random.seed(1337) @@ -26,15 +30,16 @@ def add_judges(): # Order the judges and pairings by power ranking judges = sorted(judges, key=lambda j: j.rank, reverse=True) - pairings.sort(key=lambda x: tab_logic.team_comp(x, current_round_number), - reverse=True) + pairings.sort( + key=lambda x: tab_logic.team_comp(x, current_round_number), reverse=True + ) num_rounds = len(pairings) # Assign chairs (single judges) to each round using perfect pairing graph_edges = [] - for (judge_i, judge) in enumerate(judges): - for (pairing_i, pairing) in enumerate(pairings): + for judge_i, judge in enumerate(judges): + for pairing_i, pairing in enumerate(pairings): if not judge_conflict(judge, pairing.gov_team, pairing.opp_team): edge = ( pairing_i, @@ -50,12 +55,14 @@ def add_judges(): if not graph_edges: raise errors.JudgeAssignmentError( "Impossible to assign judges, consider reducing your gaps if you" - " are making panels, otherwise find some more judges.") + " are making panels, otherwise find some more judges." + ) elif -1 in judge_assignments[:num_rounds]: - pairing_list = judge_assignments[:len(pairings)] + pairing_list = judge_assignments[: len(pairings)] bad_pairing = pairings[pairing_list.index(-1)] raise errors.JudgeAssignmentError( - "Could not find a judge for: %s" % str(bad_pairing)) + "Could not find a judge for: %s" % str(bad_pairing) + ) else: raise errors.JudgeAssignmentError() @@ -75,8 +82,9 @@ def add_judges(): Round.objects.bulk_update(pairings, ["chair"]) Round.judges.through.objects.bulk_create(judge_round_joins) + def calc_weight(judge_i, pairing_i): - """ Calculate the relative badness of this judge assignment + """Calculate the relative badness of this judge assignment We want small negative numbers to be preferred to large negative numbers @@ -85,9 +93,18 @@ def calc_weight(judge_i, pairing_i): def judge_conflict(judge, team1, team2): - return any(s.team_id in (team1.id, team2.id,) for s in judge.scratches.all()) \ - or had_judge(judge, team1) \ - or had_judge(judge, team2) + return ( + any( + s.team_id + in ( + team1.id, + team2.id, + ) + for s in judge.scratches.all() + ) + or had_judge(judge, team1) + or had_judge(judge, team2) + ) def had_judge(judge, team): From b1c63bf1e5b619abf2a3b99c385859ee109ab074 Mon Sep 17 00:00:00 2001 From: Rodda John Date: Sun, 26 Feb 2023 09:45:52 -0500 Subject: [PATCH 26/27] Revert "Formatting" This reverts commit ff9c4de0048137a0acf82cbfb1e8828bfba3fe81. --- mittab/libs/assign_judges.py | 53 ++++++++++++------------------------ 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/mittab/libs/assign_judges.py b/mittab/libs/assign_judges.py index a00c8da03..a18c4eb93 100644 --- a/mittab/libs/assign_judges.py +++ b/mittab/libs/assign_judges.py @@ -7,20 +7,16 @@ def add_judges(): current_round_number = TabSettings.get("cur_round") - 1 - judges = list( - Judge.objects.filter( - checkin__round_number=current_round_number - ).prefetch_related( - "judges", # poorly named relation for the round - "scratches", - ) - ) + judges = list(Judge.objects.filter(checkin__round_number=4).prefetch_related( + "judges", # poorly named relation for the round + "scratches", + )) pairings = tab_logic.sorted_pairings(current_round_number) # First clear any existing judge assignments - Round.judges.through.objects.filter( - round__round_number=current_round_number - ).delete() + Round.judges.through.objects \ + .filter(round__round_number=current_round_number) \ + .delete() # Try to have consistent ordering with the round display random.seed(1337) @@ -30,16 +26,15 @@ def add_judges(): # Order the judges and pairings by power ranking judges = sorted(judges, key=lambda j: j.rank, reverse=True) - pairings.sort( - key=lambda x: tab_logic.team_comp(x, current_round_number), reverse=True - ) + pairings.sort(key=lambda x: tab_logic.team_comp(x, current_round_number), + reverse=True) num_rounds = len(pairings) # Assign chairs (single judges) to each round using perfect pairing graph_edges = [] - for judge_i, judge in enumerate(judges): - for pairing_i, pairing in enumerate(pairings): + for (judge_i, judge) in enumerate(judges): + for (pairing_i, pairing) in enumerate(pairings): if not judge_conflict(judge, pairing.gov_team, pairing.opp_team): edge = ( pairing_i, @@ -55,14 +50,12 @@ def add_judges(): if not graph_edges: raise errors.JudgeAssignmentError( "Impossible to assign judges, consider reducing your gaps if you" - " are making panels, otherwise find some more judges." - ) + " are making panels, otherwise find some more judges.") elif -1 in judge_assignments[:num_rounds]: - pairing_list = judge_assignments[: len(pairings)] + pairing_list = judge_assignments[:len(pairings)] bad_pairing = pairings[pairing_list.index(-1)] raise errors.JudgeAssignmentError( - "Could not find a judge for: %s" % str(bad_pairing) - ) + "Could not find a judge for: %s" % str(bad_pairing)) else: raise errors.JudgeAssignmentError() @@ -82,9 +75,8 @@ def add_judges(): Round.objects.bulk_update(pairings, ["chair"]) Round.judges.through.objects.bulk_create(judge_round_joins) - def calc_weight(judge_i, pairing_i): - """Calculate the relative badness of this judge assignment + """ Calculate the relative badness of this judge assignment We want small negative numbers to be preferred to large negative numbers @@ -93,18 +85,9 @@ def calc_weight(judge_i, pairing_i): def judge_conflict(judge, team1, team2): - return ( - any( - s.team_id - in ( - team1.id, - team2.id, - ) - for s in judge.scratches.all() - ) - or had_judge(judge, team1) - or had_judge(judge, team2) - ) + return any(s.team_id in (team1.id, team2.id,) for s in judge.scratches.all()) \ + or had_judge(judge, team1) \ + or had_judge(judge, team2) def had_judge(judge, team): From 37c7a7c0cfd6556d59a24d1d65afe9f11d0e17c0 Mon Sep 17 00:00:00 2001 From: Rodda John Date: Sun, 26 Feb 2023 10:32:25 -0500 Subject: [PATCH 27/27] Judge scratches are now updated whenever a judge is updated - On create - On save - Batch import --- mittab/apps/tab/judge_views.py | 142 +++++++++++++++++++-------------- 1 file changed, 81 insertions(+), 61 deletions(-) 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")