Files
lars 6b0fca0100 Move Participant/Group to pydantic and add a saveable/editable Plan model
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.
2026-06-19 16:07:47 +02:00

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