6b0fca0100
Participant and Group are now pydantic BaseModels. Group.hosts can form cycles between groups, so it's kept as a private, non-persisted live list (set via set_hosts()/add_host()) backed by a serializable host_uuids field, re-linked via resolve_hosts() after a reload. A new Plan model (src/tatami/plan.py) bundles groups, the after-party group, and the event config (course_times, organizer_contacts, info_text, spreadsheet_id) and supports save()/load() to/from JSON. tatami_masterplan's __main__ now saves to masterplan.json (PLAN_FILE env var to override) on first run and loads it on later runs instead of recomputing, so the plan can be hand-edited (move a member between groups, change a course, fill in spreadsheet_id) and picked up on rerun without hitting the Routes API again. spreadsheet_id moves out of .env (GOOGLE_SHEETS_SPREADSHEET_ID) onto the plan itself, since it's part of the plan rather than a secret.
154 lines
5.6 KiB
Python
154 lines
5.6 KiB
Python
"""Tests for the core domain model: Participant and Group."""
|
|
|
|
import datetime as dt
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from tatami.classes import Group, Participant
|
|
from conftest import make_participants, make_timedelta_matrix
|
|
|
|
|
|
class TestParticipant:
|
|
def test_uuid_is_unique(self):
|
|
a = Participant(
|
|
name="A", address="addr", phone="", kitchen_size=5, allergies=""
|
|
)
|
|
b = Participant(
|
|
name="A", address="addr", phone="", kitchen_size=5, allergies=""
|
|
)
|
|
assert a.uuid != b.uuid
|
|
|
|
@pytest.mark.parametrize(
|
|
"kitchen_size,minutes",
|
|
[(10, 0), (7, 9), (0, 30), (5, 15)],
|
|
)
|
|
def test_penalty_scales_with_kitchen_size(self, kitchen_size, minutes):
|
|
p = Participant(
|
|
name="A", address="addr", phone="", kitchen_size=kitchen_size, allergies=""
|
|
)
|
|
assert p.get_penalty() == dt.timedelta(minutes=minutes)
|
|
|
|
def test_after_party_time_combines_penalty_and_travel(self):
|
|
people = make_participants(2, kitchen_sizes=[7.0, 10.0])
|
|
# 600s travel between the two participants.
|
|
matrix = make_timedelta_matrix(people, np.array([[0, 600], [600, 0]]))
|
|
after_party = Group(members=[people[1]])
|
|
result = people[0].get_after_party_time(matrix, after_party)
|
|
assert result == dt.timedelta(minutes=9) + dt.timedelta(seconds=600)
|
|
|
|
def test_dict_roundtrip_fields(self):
|
|
p = Participant(
|
|
name="Alice",
|
|
address="addr",
|
|
phone="555",
|
|
kitchen_size=8,
|
|
allergies="peanuts",
|
|
)
|
|
d = p.dict()
|
|
assert d == {
|
|
"uuid": p.uuid,
|
|
"name": "Alice",
|
|
"address": "addr",
|
|
"phone": "555",
|
|
"kitchen_size": 8,
|
|
"allergies": "peanuts",
|
|
}
|
|
|
|
|
|
class TestGroup:
|
|
def test_default_main_member_is_first(self):
|
|
people = make_participants(3)
|
|
group = Group(members=people)
|
|
assert group.main_member is people[0]
|
|
|
|
def test_explicit_main_member_index(self):
|
|
people = make_participants(3)
|
|
group = Group(members=people, main_member=2)
|
|
assert group.main_member is people[2]
|
|
|
|
def test_add_member_appends_without_changing_main(self):
|
|
people = make_participants(2)
|
|
group = Group(members=[people[0]])
|
|
group.add_member(people[1])
|
|
assert people[1] in group.members
|
|
assert group.main_member is people[0]
|
|
|
|
def test_add_member_can_promote_to_main(self):
|
|
people = make_participants(2)
|
|
group = Group(members=[people[0]])
|
|
group.add_member(people[1], main_member=True)
|
|
assert group.main_member is people[1]
|
|
|
|
def test_set_hosts_sorts_by_course(self):
|
|
people = make_participants(3)
|
|
groups = [Group(members=[p]) for p in people]
|
|
groups[0].set_course("dessert")
|
|
groups[1].set_course("starter")
|
|
groups[2].set_course("main")
|
|
host = Group(members=[make_participants(1)[0]])
|
|
host.set_hosts([groups[0], groups[1], groups[2]])
|
|
assert [g.course for g in host.hosts] == ["starter", "main", "dessert"]
|
|
|
|
def test_get_total_time_sums_legs_and_penalties(self):
|
|
# Three host groups (penalties 0) + after party; verify the summed route.
|
|
people = make_participants(4, kitchen_sizes=[10, 10, 10, 10])
|
|
seconds = np.array(
|
|
[
|
|
[0, 100, 0, 0, 50],
|
|
[0, 0, 200, 0, 0],
|
|
[0, 0, 0, 0, 300],
|
|
[0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0],
|
|
]
|
|
)
|
|
# build matrix including an after party participant
|
|
after_party_p = make_participants(1)[0]
|
|
all_people = people + [after_party_p]
|
|
full = make_timedelta_matrix(all_people, seconds)
|
|
|
|
starter = Group(members=[people[0]])
|
|
starter.set_course("starter")
|
|
main = Group(members=[people[1]])
|
|
main.set_course("main")
|
|
dessert = Group(members=[people[2]])
|
|
dessert.set_course("dessert")
|
|
after_party = Group(members=[after_party_p])
|
|
|
|
visitor = Group(members=[people[3]])
|
|
visitor.set_hosts([starter, main, dessert])
|
|
total = visitor.get_total_time(full, after_party)
|
|
# legs: starter->main (100) + main->dessert (200) + dessert->afterparty (300)
|
|
assert total == dt.timedelta(seconds=600)
|
|
|
|
def test_get_total_time_raises_without_hosts(self):
|
|
group = Group(members=make_participants(1))
|
|
after_party = Group(members=make_participants(1))
|
|
with pytest.raises(ValueError):
|
|
group.get_total_time(None, after_party)
|
|
|
|
def test_get_guests_collects_visiting_members(self):
|
|
people = make_participants(4)
|
|
host = Group(members=[people[0]])
|
|
guest_a = Group(members=[people[1]])
|
|
guest_b = Group(members=[people[2], people[3]])
|
|
# get_guests returns early when the host has no hosts of its own, so give it
|
|
# a (self-)host as it would have in a real plan.
|
|
host.set_hosts([host])
|
|
guest_a.set_hosts([host])
|
|
guest_b.set_hosts([host])
|
|
guests = host.get_guests([guest_a, guest_b])
|
|
guest_uuids = {p.uuid for p in guests}
|
|
assert guest_uuids == {people[1].uuid, people[2].uuid, people[3].uuid}
|
|
|
|
def test_dict_serializes_uuids(self):
|
|
people = make_participants(2)
|
|
group = Group(members=people)
|
|
host = Group(members=make_participants(1))
|
|
host.set_course("starter")
|
|
group.set_hosts([host])
|
|
d = group.dict()
|
|
assert d["main_member"] == people[0].uuid
|
|
assert d["members"] == [p.uuid for p in people]
|
|
assert d["hosts"] == [host.uuid]
|