Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 32 additions & 4 deletions assets/js/batchCheckin.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,30 @@ const updateCheckinCounts = (entityType, roundDeltas) => {
});
};

const updateToggleLabel = ($cb, checked) => {
const prefix = $cb.data("labelPrefix") || "Checked";
const labelOn = $cb.data("labelOn") || "In";
const labelOff = $cb.data("labelOff") || "Out";
const status = checked ? labelOn : labelOff;
$cb.next("label").text(`${prefix} ${status}`);
};

const updateJudgeHighlights = (ids, rounds, expected) => {
ids.forEach((id) => {
rounds.forEach((round) => {
const selector = `#judge td.judge-checkin-cell[data-judge-id='${id}'][data-round='${round}']`;
const $cell = $(selector);
if (!$cell.length) return;
$cell.toggleClass("checkin-cell-expected table-success", expected);
if (expected) {
$cell.attr("title", "Expected this round");
} else {
$cell.removeAttr("title");
}
});
});
};

const submitCheckIn = (checkboxes, checked) => {
const $boxes = $(checkboxes);
if (!$boxes.length) return;
Expand Down Expand Up @@ -47,10 +71,14 @@ const submitCheckIn = (checkboxes, checked) => {
rounds: [...new Set(rounds)].filter((r) => r != null),
})
.done(() => {
const status = checked ? "In" : "Out";
$boxes.each((_, cb) =>
$(cb).prop("checked", checked).next("label").text(`Checked ${status}`),
);
$boxes.each((_, cb) => {
const $cb = $(cb);
$cb.prop("checked", checked);
updateToggleLabel($cb, checked);
});
if (entityType === "judge_expected") {
updateJudgeHighlights(ids, [...new Set(rounds)], checked);
}
if (Object.keys(roundDeltas).length) {
updateCheckinCounts(entityType, roundDeltas);
}
Expand Down
9 changes: 9 additions & 0 deletions docs/Adding-Teams,-Judges,-Rooms-and-Debaters.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ file
You can use [these templates](https://drive.google.com/drive/folders/1ElIk0bM9uMpuewmOxb2e3-cWiLhCYv_5?usp=sharing)
for the teams, judges, and rooms files

### Judge spreadsheet columns

- Column A: Judge Name
- Column B: Judge Rank
- Columns C onward: Affiliated schools (one per column). Blank cells are ignored, so you can leave extra cells empty without creating phantom schools.
- The final *N* columns (where *N* is the value of the **Total Rounds** setting) represent the rounds you expect the judge to attend (`R1` through `RN`). Use `Y`, `Yes`, `True`, `1`, `X`, or `In` to mark a judge as expected for that round; leave the cell empty (or anything else) if they are not expected.

When the spreadsheet is imported, the expected-round columns populate the “Expected Judges” tab in batch check-in and the public judge list.

**NOTE:** The files _must_ be `.xlsx` files. Not `.xls`, `.csv`, or anything
similar

Expand Down
26 changes: 26 additions & 0 deletions mittab/apps/tab/migrations/0033_auto_20251114_2230.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 3.2.25 on 2025-11-14 22:30

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('tab', '0032_manualjudgeassignment'),
]

operations = [
migrations.CreateModel(
name='JudgeExpectedCheckIn',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('round_number', models.IntegerField()),
('judge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='expected_checkins', to='tab.judge')),
],
),
migrations.AddConstraint(
model_name='judgeexpectedcheckin',
constraint=models.UniqueConstraint(fields=('judge', 'round_number'), name='unique_judge_expectation_per_round'),
),
]
24 changes: 24 additions & 0 deletions mittab/apps/tab/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,10 @@ def is_checked_in_for_round(self, round_number):
return any(checkin.round_number == round_number
for checkin in self.checkin_set.all())

def is_expected_for_round(self, round_number):
return any(expectation.round_number == round_number
for expectation in self.expected_checkins.all())

def __str__(self):
return self.name

Expand Down Expand Up @@ -647,6 +651,26 @@ class Meta:
]


class JudgeExpectedCheckIn(models.Model):
judge = models.ForeignKey(
Judge,
on_delete=models.CASCADE,
related_name="expected_checkins",
)
round_number = models.IntegerField()

def __str__(self):
return f"Judge {self.judge} is expected for round {self.round_number}"

class Meta:
constraints = [
models.UniqueConstraint(
fields=["judge", "round_number"],
name="unique_judge_expectation_per_round",
)
]


class RoomCheckIn(models.Model):
room = models.ForeignKey(Room, on_delete=models.CASCADE)
round_number = models.IntegerField()
Expand Down
21 changes: 21 additions & 0 deletions mittab/apps/tab/templatetags/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,27 @@ def is_checked_in(judge, round_value):
return judge.is_checked_in_for_round(round_value)


@register.filter("is_expected")
def is_expected(judge, round_value):
return judge.is_expected_for_round(round_value)


@register.filter
def lookup(sequence, index):
if sequence is None:
return None
if isinstance(sequence, str):
return None
try:
index = int(index)
except (TypeError, ValueError):
return None
try:
return sequence[index]
except (IndexError, TypeError, KeyError):
return None


@register.simple_tag(takes_context=True)
def judge_team_count(context, judge, pairing):
judge_rejudge_counts = context.get("judge_rejudge_counts", {})
Expand Down
13 changes: 9 additions & 4 deletions mittab/apps/tab/views/public_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,15 @@ 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").prefetch_related("schools", "checkin_set").all(),
"rounds": rounds
})
request,
"public/judges.html",
{
"judges": Judge.objects.order_by("name")
.prefetch_related("schools", "checkin_set", "expected_checkins")
.all(),
"rounds": rounds,
},
)


@cache_public_view(timeout=60)
Expand Down
53 changes: 44 additions & 9 deletions mittab/apps/tab/views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,8 @@ def bulk_check_in(request):

if entity_type == "judge":
checkInObj, id_field = CheckIn, "judge_id"
elif entity_type == "judge_expected":
checkInObj, id_field = JudgeExpectedCheckIn, "judge_id"
else:
checkInObj, id_field = RoomCheckIn, "room_id"

Expand Down Expand Up @@ -576,22 +578,48 @@ def batch_checkin(request):
all_round_numbers = [0] + round_numbers

team_data = [
{"entity": t, "school": t.school, "debaters": t.debaters_display,
"checked_in": t.checked_in}
{
"entity": t,
"school": t.school,
"debaters": t.debaters_display,
"checked_in": t.checked_in,
"expected_checkins": [],
}
for t in Team.objects.prefetch_related("school", "debaters").all()
]

judge_data = [
{"entity": j, "schools": j.schools.all(),
"checkins": [rn in {c.round_number for c in j.checkin_set.all()}
for rn in all_round_numbers]}
for j in Judge.objects.prefetch_related("checkin_set", "schools")
]
judges = Judge.objects.prefetch_related("checkin_set", "expected_checkins", "schools")
judge_data = []
expected_judge_data = []

for j in judges:
checkin_rounds = {c.round_number for c in j.checkin_set.all()}
expected_rounds = {c.round_number for c in j.expected_checkins.all()}
checkins = [rn in checkin_rounds for rn in all_round_numbers]
expected_flags = [rn in expected_rounds for rn in all_round_numbers]

judge_row = {
"entity": j,
"schools": j.schools.all(),
"checkins": checkins,
"expected_checkins": expected_flags,
}
judge_data.append(judge_row)

judge_expected_row = {
"entity": j,
"schools": j.schools.all(),
"checkins": expected_flags,
"expected_checkins": expected_flags,
}
expected_judge_data.append(judge_expected_row)

room_data = [
{"entity": r,
"checkins": [rn in {c.round_number for c in r.roomcheckin_set.all()}
for rn in all_round_numbers]}
for rn in all_round_numbers],
"expected_checkins": [],
}
for r in Room.objects.prefetch_related("roomcheckin_set")
]

Expand All @@ -604,6 +632,7 @@ def column_counts(rows):
return counts

judge_counts = column_counts(judge_data)
expected_judge_counts = column_counts(expected_judge_data)
room_counts = column_counts(room_data)

def round_count_data(counts):
Expand All @@ -614,12 +643,14 @@ def round_count_data(counts):
} for idx in range(len(round_numbers))]

judge_round_counts = round_count_data(judge_counts)
expected_judge_round_counts = round_count_data(expected_judge_counts)
room_round_counts = round_count_data(room_counts)

return render(request, "batch_check_in/check_in.html", {
"team_data": team_data,
"team_headers": ["School", "Team", "Debater Names"],
"judge_data": judge_data,
"judge_expectation_data": expected_judge_data,
"judge_headers": ["School", "Judge"],
"room_data": room_data,
"room_headers": ["Room"],
Expand All @@ -629,8 +660,12 @@ def round_count_data(counts):
"judge_total_count": len(judge_data),
"room_total_count": len(room_data),
"judge_round_counts": judge_round_counts,
"judge_expectation_round_counts": expected_judge_round_counts,
"room_round_counts": room_round_counts,
"judge_outround_count": judge_counts[0] if judge_counts else 0,
"judge_expectation_outround_count": (
expected_judge_counts[0] if expected_judge_counts else 0
),
"room_outround_count": room_counts[0] if room_counts else 0,
})

Expand Down
53 changes: 50 additions & 3 deletions mittab/libs/data_import/import_judges.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from mittab.apps.tab.models import School
from mittab.apps.tab.models import School, JudgeExpectedCheckIn, TabSettings
from mittab.apps.tab.forms import JudgeForm
from mittab.libs.data_import import Workbook, WorkbookImporter, InvalidWorkbookException

Expand All @@ -12,6 +12,45 @@ def import_judges(file_to_import):


class JudgeImporter(WorkbookImporter):
truthy_expectation_values = {
"1",
"true",
"yes",
"y",
"expected",
"in",
"x",
"check",
"checked",
}

def _split_row(self, row):
num_rounds = TabSettings.get("tot_rounds", 5)
if num_rounds and len(row) >= 2 + num_rounds:
expectation_cells = row[-num_rounds:]
school_cells = row[2:-num_rounds]
else:
expectation_cells = []
school_cells = row[2:]
return school_cells, expectation_cells

def _expected_rounds(self, expectation_cells):
expected_rounds = []
for index, cell in enumerate(expectation_cells, start=1):
normalized = str(cell or "").strip().lower()
if normalized and normalized in self.truthy_expectation_values:
expected_rounds.append(index)
return expected_rounds

def _apply_expected_rounds(self, judge, expected_rounds):
for round_number in expected_rounds:
self.create(
JudgeExpectedCheckIn(
judge=judge,
round_number=round_number,
)
)

def import_row(self, row, row_number):
judge_name = row[0]
judge_rank = row[1]
Expand All @@ -21,8 +60,13 @@ def import_row(self, row, row_number):
except ValueError:
self.error("Judge rank is not a number", row)

school_cells, expectation_cells = self._split_row(row)

schools = []
for school_name in row[2:]:
for school_name in school_cells:
school_name = (school_name or "").strip()
if not school_name:
continue
school_query = School.objects.filter(name__iexact=school_name)
if school_query.exists():
school = school_query.first()
Expand All @@ -37,7 +81,10 @@ 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)
expected_rounds = self._expected_rounds(expectation_cells)
if expected_rounds:
self._apply_expected_rounds(judge, expected_rounds)
else:
for _field, error_msgs in form.errors.items():
for error_msg in error_msgs:
Expand Down
Loading
Loading