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.
104 lines
3.6 KiB
Python
104 lines
3.6 KiB
Python
"""Tests for the Plan model: saving, reloading, and editing a masterplan."""
|
|
|
|
import json
|
|
|
|
from tatami.classes import Group
|
|
from tatami.plan import Plan
|
|
from tatami.tatami_masterplan import assign_courses, get_after_party_group
|
|
from conftest import make_participants
|
|
|
|
|
|
def make_plan(n: int = 6) -> Plan:
|
|
groups = [Group(members=[p]) for p in make_participants(n)]
|
|
courses = ["starter", "main", "dessert"] * (n // 3)
|
|
assign_courses(groups, courses)
|
|
after_party = get_after_party_group("party street")
|
|
return Plan(
|
|
groups=groups,
|
|
after_party_group=after_party,
|
|
course_times={"starter": "18:30", "main": "20:00", "dessert": "22:00"},
|
|
organizer_contacts=[("Lars (Organizer)", "0151-23456789")],
|
|
info_text="Welcome!",
|
|
)
|
|
|
|
|
|
class TestPlanRoundTrip:
|
|
def test_save_and_load_preserves_groups_and_hosts(self, tmp_path):
|
|
plan = make_plan()
|
|
path = tmp_path / "masterplan.json"
|
|
plan.save(path)
|
|
|
|
loaded = Plan.load(path)
|
|
|
|
assert [g.uuid for g in loaded.groups] == [g.uuid for g in plan.groups]
|
|
for original, reloaded in zip(plan.groups, loaded.groups):
|
|
assert reloaded.course == original.course
|
|
assert [h.uuid for h in reloaded.hosts] == [h.uuid for h in original.hosts]
|
|
|
|
def test_load_resolves_live_host_objects_not_just_uuids(self, tmp_path):
|
|
plan = make_plan()
|
|
path = tmp_path / "masterplan.json"
|
|
plan.save(path)
|
|
|
|
loaded = Plan.load(path)
|
|
|
|
group = loaded.groups[0]
|
|
assert group.hosts is not None
|
|
# Hosts are live Group objects (so .course etc. is accessible), not just ids.
|
|
assert all(isinstance(h, Group) for h in group.hosts)
|
|
assert {h.course for h in group.hosts} == {"starter", "main", "dessert"}
|
|
|
|
def test_round_trip_is_idempotent(self, tmp_path):
|
|
plan = make_plan()
|
|
path = tmp_path / "masterplan.json"
|
|
plan.save(path)
|
|
first = json.loads(path.read_text())
|
|
|
|
Plan.load(path).save(path)
|
|
second = json.loads(path.read_text())
|
|
|
|
assert first == second
|
|
|
|
def test_carries_course_times_contacts_info_and_spreadsheet_id(self, tmp_path):
|
|
plan = make_plan()
|
|
plan.spreadsheet_id = "abc123"
|
|
path = tmp_path / "masterplan.json"
|
|
plan.save(path)
|
|
|
|
loaded = Plan.load(path)
|
|
|
|
assert loaded.course_times == plan.course_times
|
|
assert loaded.organizer_contacts == plan.organizer_contacts
|
|
assert loaded.info_text == plan.info_text
|
|
assert loaded.spreadsheet_id == "abc123"
|
|
|
|
def test_spreadsheet_id_defaults_to_none(self):
|
|
assert make_plan().spreadsheet_id is None
|
|
|
|
|
|
class TestPlanEditing:
|
|
def test_editing_saved_json_changes_reloaded_plan(self, tmp_path):
|
|
plan = make_plan()
|
|
path = tmp_path / "masterplan.json"
|
|
plan.save(path)
|
|
|
|
data = json.loads(path.read_text())
|
|
data["spreadsheet_id"] = "edited-by-hand"
|
|
data["groups"][0]["members"][0]["address"] = "New Address 1"
|
|
path.write_text(json.dumps(data))
|
|
|
|
loaded = Plan.load(path)
|
|
|
|
assert loaded.spreadsheet_id == "edited-by-hand"
|
|
assert loaded.groups[0].members[0].address == "New Address 1"
|
|
|
|
|
|
class TestPlanParticipants:
|
|
def test_participants_property_collects_everyone_once(self):
|
|
plan = make_plan()
|
|
uuids = [p.uuid for p in plan.participants]
|
|
assert len(uuids) == len(set(uuids))
|
|
member_uuids = {m.uuid for g in plan.groups for m in g.members}
|
|
member_uuids.add(plan.after_party_group.main_member.uuid)
|
|
assert set(uuids) == member_uuids
|