Skip to content

Commit 676d125

Browse files
committed
Handle partially scheduled Pretalx multi-slot sessions
1 parent 6ff209d commit 676d125

6 files changed

Lines changed: 144 additions & 12 deletions

File tree

src/models/europython.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ class EuroPythonSession(BaseModel):
323323
next_session: str | None = None
324324
prev_session: str | None = None
325325
slot_count: int = Field(..., exclude=True)
326+
scheduled_slot_starts: list[datetime] = Field(default_factory=list, exclude=True)
326327
youtube_url: str | None = None
327328

328329
@field_validator("room", mode="before")

src/models/pretalx.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,17 @@ def process_values(cls, values) -> dict:
103103
]
104104
values["resources"] = resources
105105

106-
# Set slot information
106+
# Set slot information from scheduled slots. Pretalx may include empty
107+
# placeholder slots for multi-slot submissions.
107108
if values.get("slots"):
108-
first_slot = PretalxSlot.model_validate(values["slots"][0])
109-
values["room"] = first_slot.room
110-
values["start"] = first_slot.start
109+
slots = [PretalxSlot.model_validate(slot) for slot in values["slots"]]
111110

112-
last_slot = PretalxSlot.model_validate(values["slots"][-1])
113-
values["end"] = last_slot.end
111+
if start_slot := next((slot for slot in slots if slot.start), None):
112+
values["room"] = start_slot.room
113+
values["start"] = start_slot.start
114+
115+
if end_slot := next((slot for slot in reversed(slots) if slot.end), None):
116+
values["end"] = end_slot.end
114117

115118
return values
116119

src/utils/timing_relationships.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,38 @@ class TimingRelationships:
1414
def compute(
1515
cls, all_sessions: ValuesView[PretalxSubmission] | list[PretalxSubmission]
1616
) -> None:
17-
for session in all_sessions:
17+
cls.all_sessions_in_parallel = {}
18+
cls.all_sessions_after = {}
19+
cls.all_sessions_before = {}
20+
cls.all_next_session = {}
21+
cls.all_prev_session = {}
22+
23+
timed_sessions = [
24+
session for session in all_sessions if session.start and session.end
25+
]
26+
27+
for session in timed_sessions:
1828
if not session.start or not session.end:
1929
continue
2030

2131
sessions_in_parallel = cls.compute_sessions_in_parallel(
22-
session, all_sessions
32+
session, timed_sessions
2333
)
2434
sessions_after = cls.compute_sessions_after(
25-
session, all_sessions, sessions_in_parallel
35+
session, timed_sessions, sessions_in_parallel
2636
)
2737
sessions_before = cls.compute_sessions_before(
28-
session, all_sessions, sessions_in_parallel
38+
session, timed_sessions, sessions_in_parallel
2939
)
3040

3141
cls.all_sessions_in_parallel[session.code] = sessions_in_parallel
3242
cls.all_sessions_after[session.code] = sessions_after
3343
cls.all_sessions_before[session.code] = sessions_before
3444
cls.all_next_session[session.code] = cls.compute_prev_or_next_session(
35-
session, sessions_after, all_sessions
45+
session, sessions_after, timed_sessions
3646
)
3747
cls.all_prev_session[session.code] = cls.compute_prev_or_next_session(
38-
session, sessions_before, all_sessions
48+
session, sessions_before, timed_sessions
3949
)
4050

4151
@classmethod
@@ -70,7 +80,9 @@ def compute_sessions_in_parallel(
7080
if (
7181
other_session.code == session.code
7282
or other_session.start is None
83+
or other_session.end is None
7384
or session.start is None
85+
or session.end is None
7486
):
7587
continue
7688

src/utils/transform.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ def pretalx_submissions_to_europython_sessions(
5959
next_session=TimingRelationships.get_next_session(submission.code),
6060
prev_session=TimingRelationships.get_prev_session(submission.code),
6161
slot_count=submission.slot_count,
62+
scheduled_slot_starts=[
63+
slot.start
64+
for slot in submission.slots
65+
if slot.start and slot.end and slot.room
66+
],
6267
youtube_url=youtube_data.get(submission.code),
6368
)
6469
ep_sessions[code] = ep_session

src/utils/utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ def start_times(session: EuroPythonSession) -> list[datetime]:
147147
148148
TODO: We assume a lot of things here, IMHO we should make things more flexible :)
149149
"""
150+
if session.scheduled_slot_starts:
151+
return session.scheduled_slot_starts
152+
150153
if session.slot_count == 2:
151154
# Half day sessions have 2 slots, 90 minutes each, with a 15-minute break in between
152155
return [session.start, session.start + timedelta(minutes=90 + 15)]

tests/test_timing_relationships.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from src.models.pretalx import PretalxSubmission
2+
from src.utils.timing_relationships import TimingRelationships
3+
from src.utils.transform import Transform
4+
from src.utils.utils import Utils
5+
6+
7+
def submission(
8+
code: str,
9+
start: str | None,
10+
end: str | None,
11+
room: str | None = "Room A",
12+
slots: list[dict] | None = None,
13+
) -> PretalxSubmission:
14+
return PretalxSubmission.model_validate(
15+
{
16+
"code": code,
17+
"title": code,
18+
"speakers": [],
19+
"submission_type": "Talk",
20+
"state": "confirmed",
21+
"answers": [],
22+
"slot_count": len(slots) if slots else 1,
23+
"slots": slots
24+
if slots is not None
25+
else [{"room": room, "start": start, "end": end}],
26+
}
27+
)
28+
29+
30+
def test_submission_uses_last_timed_slot_before_blank_placeholders() -> None:
31+
session = submission(
32+
"BTTFFJ",
33+
None,
34+
None,
35+
slots=[
36+
{
37+
"room": "Reception Room F2 (Fishbowl)",
38+
"start": "2026-07-14T09:30:00+02:00",
39+
"end": "2026-07-14T11:00:00+02:00",
40+
},
41+
{
42+
"room": "Reception Room F2 (Fishbowl)",
43+
"start": "2026-07-14T11:15:00+02:00",
44+
"end": "2026-07-14T12:45:00+02:00",
45+
},
46+
{"room": None, "start": None, "end": None},
47+
{"room": None, "start": None, "end": None},
48+
],
49+
)
50+
51+
assert session.room == "Reception Room F2 (Fishbowl)"
52+
assert session.start.isoformat() == "2026-07-14T09:30:00+02:00"
53+
assert session.end.isoformat() == "2026-07-14T12:45:00+02:00"
54+
55+
56+
def test_timing_relationships_ignore_partial_sessions_and_reset_state() -> None:
57+
first = submission(
58+
"FIRST", "2026-07-14T09:30:00+02:00", "2026-07-14T10:30:00+02:00"
59+
)
60+
parallel = submission(
61+
"PARALLEL",
62+
"2026-07-14T10:00:00+02:00",
63+
"2026-07-14T11:00:00+02:00",
64+
room="Room B",
65+
)
66+
partial = submission("PARTIAL", "2026-07-14T10:15:00+02:00", None)
67+
68+
TimingRelationships.compute([first, parallel, partial])
69+
70+
assert TimingRelationships.get_sessions_in_parallel("FIRST") == ["PARALLEL"]
71+
assert TimingRelationships.get_sessions_in_parallel("PARTIAL") is None
72+
73+
TimingRelationships.compute([partial])
74+
75+
assert TimingRelationships.get_sessions_in_parallel("FIRST") is None
76+
77+
78+
def test_schedule_start_times_use_only_scheduled_slots() -> None:
79+
session = submission(
80+
"BTTFFJ",
81+
None,
82+
None,
83+
slots=[
84+
{
85+
"room": "Reception Room F2 (Fishbowl)",
86+
"start": "2026-07-14T09:30:00+02:00",
87+
"end": "2026-07-14T11:00:00+02:00",
88+
},
89+
{
90+
"room": "Reception Room F2 (Fishbowl)",
91+
"start": "2026-07-14T11:15:00+02:00",
92+
"end": "2026-07-14T12:45:00+02:00",
93+
},
94+
{"room": None, "start": None, "end": None},
95+
{"room": None, "start": None, "end": None},
96+
],
97+
)
98+
session.slot_count = 4
99+
100+
TimingRelationships.compute([session])
101+
ep_session = Transform.pretalx_submissions_to_europython_sessions(
102+
{session.code: session}, {}
103+
)[session.code]
104+
105+
assert [dt.isoformat() for dt in Utils.start_times(ep_session)] == [
106+
"2026-07-14T09:30:00+02:00",
107+
"2026-07-14T11:15:00+02:00",
108+
]

0 commit comments

Comments
 (0)