Speaker Portal
The design of the speaker module (the speakers app): how presenters are
invited, what they and the organizers see, the two-sided checklists, the
media pipeline, scheduling, and the public embeds. Written as a proposal on
15 September 2026 for PyLadiesCon 2026 (first weekend of December, online on
Discord) and kept here as the reference for the module as it is built.
Where the built module answers a question differently from the proposal,
this page says what was built: the session types and presenter roles that
became rows (§8.1), readiness (§9.3a), and where a person's own tasks live
(§9.6). Smaller divergences, and the reasons for them, are listed in
speakers/README.md, which is the place to look when this page and the code
disagree.
Stack: Django, PostgreSQL, Digital Ocean Spaces.
1. Overview
PyLadiesCon 2026 is a special edition built around workshops, panels, and PyJam performances, with speakers invited directly by the organizing team rather than selected through a call for proposals. The speaker portal is the single place where that program is assembled and run:
- Organizers add sessions and the people presenting them, and send invitations.
- Speakers accept, fill in their bio and session details, and see exactly what they need to do and by when.
- Organizers see exactly what they owe each speaker — emails, promo materials, scheduling — and speakers can see that too.
- The schedule is built in the portal, assigned to Discord channels, and shown to everyone in their own timezone.
- The conference website shows the schedule and speakers by embedding a widget from the portal, so there is one source of truth.
- PyJam performers upload their videos to the portal and the team tracks each video through post-production to YouTube.
The portal is the system of record for the program. There is no external CFP tool to synchronize with; everything the team currently keeps in spreadsheets about speakers moves here.
1.1 Goals
| Goal | How the design meets it |
|---|---|
| No spreadsheet | Every speaker fact and every status lives in the portal; CSV is an export, not a working document |
| Two-way transparency | Speakers see the team's checklist for them, not just their own |
| Timezone safety | All times stored in UTC, rendered in the viewer's zone; organizers can preview any speaker's local time before confirming a slot |
| Lightweight for speakers | Every content field is optional markdown; a title and two sentences is a complete listing |
| Customizable process | Checklists are templates organizers edit in the portal, not code |
| One source for the website | The conference site embeds a widget; publishing in the portal updates the site |
| Reusable next year | Everything is scoped to a conference (edition); templates and settings clone forward |
1.2 Scope
In: session and presenter management, invitations, speaker self-service profile and session editing, two-sided checklists with reminders, scheduling with Discord channel assignment, public schedule and speaker embeds with calendar feeds, promo asset storage, video upload and post-production tracking for pre-recorded sessions, CSV/JSON export, pretix registration sync.
Out: call for proposals and review, automated transcription/translation/video rendering (uploads and tracking are in; processing is manual), speaker certificates, translation of the portal UI, Discord bot automation (channel and role management stay manual; an estimate is in §12.2), payments or honoraria.
Part 1 — How it works
2. The speaker's journey
A speaker never fills in a form they didn't ask for. The whole journey is: get invited, accept, tell us about your session, do a short list of things, show up.
2.1 Invitation
An organizer adds the session and the person, writes a personal note, and sends the invitation. The email carries a link that is just for that speaker; clicking it creates their account. From then on they sign in with a one-time code emailed to them, or with a password if they set one: the welcome page offers to set one and the dashboard asks until they have or dismiss it.
2.2 The dashboard
The first thing a speaker sees after accepting, and the page they come back to. Two lists side by side:
- Your to-dos — update bio, read the guide, register, join Discord, confirm the slot, share materials, tech check. Items the portal can verify tick themselves (the bio is there; the pretix order was found).
- What we're doing for you — the team's list for this speaker, with who is on each item. A speaker can see "promo materials — in progress, Lena" without emailing anyone.
Reminders go out by email a week, three days, and a day before anything is due, in the speaker's own timezone.
2.3 Their session
Every content field is optional markdown: summary, outline, prerequisites, audience. A title and two sentences is a complete listing; a full outline is welcome. Co-presenters and panelists are listed with their roles. Nothing here is visible to the public until the team publishes the session.
2.4 Their schedule
The schedule in the speaker's own timezone, their sessions highlighted, visible before the program is public with a "not yet public" badge. Each session has an add-to-calendar link, and there is a personal calendar feed that stays correct if a slot moves.
3. Running the program as an organizer
3.1 Sessions
One list for everything that will be on the schedule — workshops, panels, PyJam performances, and also the opening, breaks, and closing. Status moves from draft, to invited, to confirmed, to scheduled, to published, and any of them can be cancelled. A speaker liaison sees only the rows assigned to them.
3.2 A presenter, in one place
Everything about one person: their to-dos, the team's to-dos for them (assignable and tickable here), sessions, invitation history, promo assets, and an activity log showing what the portal did automatically — "pretix order paid → registration marked done".
3.3 The checklist board
Presenters down the side, every checklist item across the top, one colour per cell. Sort by most overdue and the people who need a nudge float to the top. Tabs switch between the speaker side, the organizer side, and post-production. This replaces the spreadsheet's status columns.
Checklists are templates the team edits in the portal — items can be renamed, reordered, added, or removed per role, and the templates carry forward to next year.
3.4 Building the schedule
A grid with Discord channels as columns and 15-minute steps as rows. Unscheduled sessions wait on the side and are dragged in; a "+ program item" button drops an opening or break straight onto the grid, spanning all channels. Conflicts — two sessions in one channel, a presenter double-booked — are highlighted where they happen.
The timezone switcher shows the grid as a specific presenter would see it, which is how "we scheduled her at 3 a.m." gets caught before the confirmation email.
3.5 Publishing
The program is internal until the team flips it to published. Each session is published individually and only once it is scheduled; a presenter's bio becomes public only through a published session. A preview link lets the conference website be built against the draft program.
4. PyJam performances
PyJam sessions are pre-recorded. The performer uploads the video; the team post-produces it and publishes to YouTube for a scheduled premiere or watch party.
4.1 The performer
Not built (M3b). The upload panel and the post-production lists below are the proposal; today a performer sees their checklist and nothing uploads.
Same dashboard as any speaker, plus an upload panel: the video goes straight to storage in chunks and resumes if the connection drops, and the panel shows the duration against the length limit. The performer's second list is "what we're doing with your video", so they can watch it move through transcription, translation, and the final cut. When the final cut is ready, an "approve the final cut" item opens for them.
4.2 Post-production
The third tab on the checklist board: sessions down the side, pipeline items across — MC intro and outro, quality review, length check, transcribe, review transcript, translate (one column per language), title card and final cut, publish to YouTube. Items backed by a file complete themselves when the file is uploaded. The length check turns red and blocks the row when a video is over the limit. Every item is assignable, and the template is editable like all the others.
5. The conference website
The website embeds a widget served by the portal. When the team publishes in the portal, the site updates; there is no rebuild.
- The schedule shows in the visitor's timezone, with breaks as bands and sessions as cards.
- Every session has a subscribe link — a calendar feed for that one session, so a reschedule updates the attendee's calendar automatically.
- Visitors can star sessions and subscribe to just those, with no account.
- Unpublished sessions simply aren't there; no "TBA" rows.
The website already depends on the portal for stats.json, so this adds no new dependency. The widget is long-cached behind the CDN and shows a link to the portal's schedule page if the API is unreachable.
6. The workflows side by side
Organizer Speaker / performer Portal (automatic)
───────── ─────────────────── ──────────────────
add session + presenter
send invitation ──────────────► accept, account created ────────► checklists instantiated
update bio, session details ──────► "bio updated" ticks
read speaker guide ─────────────► "read guide" ticks
register on pretix ─────────────► webhook → "registered" ticks
send onboarding, registration info
prepare promo materials ────────► sees "in progress, Lena"
schedule on the grid ───────────► sees slot in own timezone ─────► "scheduled" ticks; conflicts checked
send schedule confirmation ─────► confirms slot
(PyJam) ◄────────────────────── uploads video ─────────────────► duration checked; "uploaded" ticks
post-produce, assign items ──► sees pipeline progress
publish to YouTube
publish program ────────────────► card goes public ──────────────► website widget + calendar feeds update
Part 2 — How it is built
7. Roles and access
| Role | Sees | Does |
|---|---|---|
| Organizer | everything in the edition | add sessions and presenters, invite, schedule, publish, edit checklist templates, complete organizer tasks |
| Speaker liaison (volunteer) | only the presenters and sessions assigned to them | edit those presenters' details on their behalf, complete organizer tasks for them, send reminders |
| Presenter (speaker, panelist, moderator, host, performer) | their own profile and sessions, co-presenters' names and bios, the schedule, their checklist, and the organizer checklist for them | edit own profile and session content, tick own checklist items, upload their video |
| Public | published sessions and presenters through the widget and API | read |
Liaisons seeing only their assigned speakers is a deliberate change from a shared spreadsheet where every volunteer sees every speaker's email and status. It is enforced in database queries and covered by the portal's tenant-isolation test suite.
Speakers sign in with a one-time code emailed to them. A password is optional: the welcome page offers to set one, the dashboard asks until they do or dismiss the reminder, and the usual reset flow works for those who have one.
7.1 Permissions and teams (M2b)
Not built. Access today is
is_staffandis_superuserplus team membership and item assignment, which is what this section is meant to replace.
The table above says what each audience does. This section says how the portal decides, and replaces the mechanism the portal grew up with.
Where access comes from today. Four mechanisms coexist and only one is a permission. The Django is_staff / is_superuser flags mean "organizer" everywhere: the admin mixins, the is_organizer context flag, the speaker-side is_speaker_organizer, sponsorship management, queryset scoping, the "who gets internal emails" queries and a dozen template checks; is_superuser alone gates conferences and Start next year. The volunteer Role model (Admin, Staff, Vendor, Volunteer) is assigned on the review form and used for two things only: routing internal notification emails to Admin and Staff, and picking the admin variant of the onboarding email; it grants no access. Object relations (team leads, Presenter.liaison, item assignee / team, "approved volunteer of this edition") are data-model checks and stay that way. The one real permission is portal_account.view_maintenance, granted by the "Infra maintainers" group and read through has_perm; it is the pattern to generalise.
Principle. Every "may this user do this" check goes through user.has_perm("app.codename") in Python and {% if perms.app.codename %} in templates, wrapped in a named predicate in each app's permissions.py. Teams are the grant vehicle; permissions are how access is read. is_superuser keeps Django's built-in bypass as break-glass; is_staff survives only for signing in to the Django admin.
Teams grant, an auth backend derives. Team gains permissions and lead_permissions (many-to-many to auth.Permission), set by the portal admin when creating a team, editable afterwards, cloned by Start next year. Rather than syncing users into global auth.Group rows (teams are per edition, groups are not, so last year's Sponsorship team would keep this year's rights until something removed them), a small authentication backend implements get_all_permissions by reading the user's approved memberships and leaderships in the active edition's teams. Leads get the lead set plus the team set. Nothing is ever stale and the edition switch revokes by itself. The backend also implements with_perm so recipient queries (User.objects.with_perm(...)) find team members. Baseline grants, computed the same way and tied to no team: an approved volunteer of the active edition may read sponsors and open their own volunteering tasks; a presenter gets the speaker side.
The catalogue. Built-in add / change / delete / view are reused where they fit; custom ones are declared in Meta.permissions on the model that owns the feature.
| Area | Permission | Replaces |
|---|---|---|
| Conferences | portal.view_organizer_dashboard |
is_organizer for Organize and its rail |
portal.change_conference, add_conference, delete_conference, portal.start_next_year |
superuser | |
| Volunteers | volunteer.view_volunteerprofile |
is_staff on the list and other people's profiles |
volunteer.review_volunteerprofile |
staff on the review form (leads keep their own team via the object check) | |
volunteer.add_team, change_team, delete_team |
VolunteerAdminRequiredMixin |
|
volunteer.receive_volunteer_notifications |
the Admin / Staff roles, i.e. the whole reason Role exists |
|
| Sponsorship | sponsorship.view_sponsorshipprofile |
approved volunteer of the edition (kept as the baseline grant) |
sponsorship.add/change/delete_sponsorshipprofile, sponsorship.send_invoice |
organizer | |
sponsorship.view_unconfirmed_sponsors |
superuser in the list filter | |
sponsorship.add/change/delete_sponsorshiptier |
organizer | |
sponsorship.receive_sponsorship_notifications |
role-based routing in the sponsorship tasks | |
| Speakers | speakers.manage_program (sessions, invitations, presenters, scheduling, publishing, pretix link) |
is_speaker_organizer |
speakers.view_program (read every session and presenter; liaisons stay scoped through Presenter.liaison) |
organizer | |
speakers.change_checklisttemplate, speakers.change_handbook |
organizer; split out because it is content-team work | |
speakers.change_speakersettings |
organizer; separate because the row holds pretix secrets | |
speakers.assign_checklistitem |
"organizer or the presenter's liaison" (the liaison half stays relational) | |
speakers.liaise_presenters (may be picked as a liaison) |
"staff or approved volunteer" | |
| Media (M3b) | speakers.view_mediaasset, add_mediaasset, change_mediaasset, delete_mediaasset |
"organizers any kind" in §8.8; performers keep raw video on their own sessions relationally |
speakers.publish_session_video (YouTube URL, publish time, premiere location) |
organizer; narrower than manage_program so Design can close the pipeline |
|
| Promo (M5) | speakers.view/add/change_promoasset |
organizer |
| Maintenance | portal_account.view_maintenance |
already a permission |
Ticking checklist items is never a permission: the assignee, an approved member of the owning team, the presenter's liaison, or the presenter themselves, as today.
Default permission sets (presets the admin picks from when creating a team; every team's set stays editable):
| Team | Members | Leads add |
|---|---|---|
| Organizers (core) | everything above except delete_conference and start_next_year |
those two |
| Volunteer coordination | view and review volunteer profiles, manage teams, volunteer notifications | |
| Sponsorship | sponsor add / change / delete, send invoice, unconfirmed statuses, tiers, sponsorship notifications | |
| Program | manage program, view program, assign items, liaise, templates, handbook | change_speakersettings |
| Speaker liaisons | liaise presenters, assign items | |
| Design | view program, view / add / change media assets, publish session video; promo assets when they exist | delete media assets, assign items |
| Website, Content, Social media | none; work reaches them through task assignment, which is relational | |
| Infra maintainers | view maintenance (unchanged) |
What goes away. Role, RoleTypes and VolunteerProfile.roles, with a data migration mapping Admin and Staff to membership in that edition's Organizers team (Vendor and Volunteer granted nothing). Every is_staff / is_superuser check in portal logic: the mixins in common/mixins.py, the context-processor flags, the speaker predicates, the sponsorship viewer mixin and filter, the recipient queries, the liaison dropdown and the template checks. Rosters, profiles and emails show team names and a Lead badge instead of roles. Sample data and test factories grant through teams.
Risks. Today every staff account is an organizer everywhere; after the switch, people must actually be on a team or they lose access on day one, which is why the migration builds an Organizers team from today's staff users. Liaison and team-member checks stay object-level by design, so this does not simplify them. The superuser bypass must stay or the first admin locks themselves out.
8. Data model
Every model belongs to a portal.Conference — the portal's existing per-edition tenant — so PyLadiesCon 2027 is a new conference row with the same code.
portal.Conference
├── SpeakerSettings (one row: the module's switch and the edition's defaults)
├── SessionType ──── PresenterRole (which roles a type allows)
├── Session ──────────────── SessionPresenter ──── Presenter ──── User (optional)
│ │ (role, order) │
│ ├── ScheduleSlot ──── DiscordChannel └── ChecklistItem (owner=SPEAKER)
│ ├── MediaAsset (pre-recorded sessions)
│ ├── PromoAsset (M5, not built)
│ └── ChecklistItem (owner=ORGANIZER; per presenter or per session)
├── ChecklistTemplate ──── ChecklistTemplateItem
├── ReadinessGate (what a checklist line can wait for, §9.3a)
├── Invitation
├── Handbook (versioned) ──── HandbookReadReceipt
├── ReminderLog
└── ActivityLog
A presenter's pretix order is reached through Presenter.pretix_order; the
PretixOrder model itself belongs to the attendee app, not to this one.
8.1 Session
One row per anything that appears on the schedule: workshops, panels, PyJam performances, and also the opening, closing, breaks, and social slots. There is no separate "program item" type; a break is a session with no presenters.
| Field | Notes |
|---|---|
kind |
A SessionType row per edition, not an enumeration: the list above became the seeded defaults (WORKSHOP · PANEL · PYJAM · TALK · LIGHTNING, and the program types OPENING · CLOSING · KEYNOTE · ANNOUNCEMENT · BREAK · SOCIAL · OTHER), and organizers add their own on the "Types and roles" page without a deploy. The row carries what the code used to switch on: is_content, the default duration and delivery, whether it spans all channels, and which PresenterRole rows it allows. |
delivery |
LIVE (default) or PRE_RECORDED. PyJam defaults to pre-recorded; any kind can be switched. Pre-recorded sessions get the media pipeline (§8.8) and a post-production checklist (§9.7). |
is_content |
read from the type row: a content type needs at least one presenter to be confirmed; a program type can be confirmed with none (a break) or with hosts (the opening) |
title |
required — the only required field |
summary_md, outline_md, prerequisites_md, audience_md, notes_md |
Markdown, all optional |
level, language, duration_minutes |
optional; duration defaults from kind (workshop 90, panel 60) |
video_length_limit_minutes, youtube_url, youtube_publish_at, premiere_location |
pre-recorded only. premiere_location is DISCORD (watch party in the slot's channel) or YOUTUBE (YouTube Premiere at the slot time), defaulting from the edition's speaker settings; it changes what the public card links to and nothing else, so the team can decide per session or late. |
status |
DRAFT → INVITED → CONFIRMED → SCHEDULED → PUBLISHED, plus CANCELLED. Program kinds skip INVITED. |
is_public |
explicit publish switch; nothing reaches the website without it (§11.5) |
slug |
stable public identifier used in URLs and calendar feeds |
Markdown is rendered server-side and sanitized. The source is stored, never the HTML.
8.2 Presenter
The person, independent of any session. One presenter can run a workshop and sit on a panel.
| Field | Notes |
|---|---|
user |
nullable. Linked when the invitation is accepted; a presenter can be added and scheduled before they ever log in |
display_name, pronouns, bio_md, headshot, location, timezone |
timezone drives reminder timing and the speaker's own schedule view |
email |
invitation and reminder target; unique per conference |
website_url, github_username, mastodon_url, linkedin_url, bluesky_username |
the links shown on a public presenter card |
is_public |
presenter-controlled: opt out of a public bio page while still being named on the schedule |
8.3 SessionPresenter
Links a presenter to a session with a role (a PresenterRole row; the seeded ones are PRESENTER, PANELIST, MODERATOR, HOST and PERFORMER, and a type only allows the roles it lists), an order for display, and confirmed_at. Panels are sessions of kind PANEL with panelists and a moderator; nothing special is needed for them. Panels are assembled by organizers.
8.4 Invitation
An organizer invites a presenter to a specific session (or, for panelists, to the conference generally) with a personal note. The email carries a single-use, expiring token. Accepting it creates or links the account, confirms the presenter on the session, and instantiates their checklist. sent_at, opened_at, accepted_at, declined_at are recorded so the sessions list can show "invited 9 days ago, not yet accepted".
8.5 Scheduling
DiscordChannel — name, channel_id, url, kind (STAGE, VOICE, TEXT, FORUM), is_active. Channels are created on Discord by hand and recorded here.
ScheduleSlot — one per session: channel (nullable — null means all channels, so the opening or a break spans the whole grid), start_utc, end_utc. end_utc defaults from the session duration.
Validation: no two slots overlap on one channel (an all-channel slot conflicts with everything in its window, except other program-kind bands); a presenter in two overlapping slots is flagged as a warning, not blocked — a moderator moving between rooms is legitimate.
All times are stored in UTC. SpeakerSettings.conference_timezone (one row per conference) is only the organizer's default display; the conference itself has no timezone.
8.6 PromoAsset
Not built (M5). No such model exists yet.
Speaker cards, posters, and social images, attached to a session or a presenter, stored privately, downloadable by the speaker from their dashboard. Organizers upload them (the team's existing Canva workflow produces them; the presenter list exports directly to Canva's bulk-create CSV). Generating cards in the portal is a possible later addition.
8.7 Handbook
The speaker guide, versioned. Reading it records a HandbookReadReceipt; publishing a new version re-opens the "read the speaker guide" item for everyone who read the old one.
8.8 MediaAsset
Partly built (M3b). The model exists and the video-length rule reads it; nothing uploads or probes a file yet.
For pre-recorded sessions, one row per file that moves through post-production:
| Field | Notes |
|---|---|
kind |
RAW_VIDEO (performer upload) · INTRO · OUTRO (MC recordings) · PROCESSED_VIDEO (final cut) · TRANSCRIPT · TRANSLATION · TITLE_CARD · THUMBNAIL · OTHER |
file |
Digital Ocean Spaces, private; presigned upload and download |
language |
for transcripts and translations, one row per language |
version |
increments on re-upload; old versions kept until deleted |
duration_seconds |
probed server-side after upload; drives the length-limit check |
status, notes_md |
UPLOADING · READY · FAILED · SUPERSEDED; reviewer notes ("audio clips at 4:10") |
Performance videos are routinely several gigabytes, so the browser uploads directly to object storage in chunks using presigned multipart URLs, with per-part retry and resume. The portal finalizes the upload and records the asset. Performers can upload raw video for their own sessions; organizers upload any kind.
9. Checklists
Checklists are the core of the module: what each speaker must do, what the team must do for each speaker, and — for pre-recorded sessions — what the team must do to each video. All three are the same mechanism.
9.1 Templates are organizer-defined
Checklists are data, not code. Organizers create and edit templates in the portal; the defaults listed below are seed data loaded when an edition is set up, and every item can be renamed, reordered, removed, or added to. The only fixed part is a small registry of auto-completion rules (§9.3) that a template item may reference — organizers pick from a list, they do not write code. Templates clone across editions, so next year starts from this year's checklists.
A ChecklistTemplate has a scope:
- Presenter scope — keyed by session kind and role ("Workshop presenter", "Panelist", "Moderator", "PyJam performer"). Instantiated once per presenter per session.
- Session scope — keyed by session kind and delivery ("PyJam post-production"). Instantiated once per session, for work on the session itself.
Each ChecklistTemplateItem has:
| Field | Notes |
|---|---|
owner |
SPEAKER or ORGANIZER |
title, description_md |
|
due_offset |
days relative to an anchor: invitation accepted, conference start, or session start |
auto_complete_rule |
optional, from the registry in §9.3 |
requires_asset_kind |
optional: the item is satisfied when a ready MediaAsset of that kind (and language) exists on the session |
is_required |
required items gate the session reaching CONFIRMED. None of the default lines below is required: accepting the invitation is the presenter's confirmation (decided in the review round of 16 September 2026), so a seeded edition confirms a session as soon as its required presenters accept. An organizer may mark a line required, and from then on it holds the session at INVITED until it is done or skipped; the session page shows what it is waiting on. |
assignee_default |
organizer items only: the presenter's liaison, or unassigned |
Templates exist for content kinds and for hosted program kinds (opening, closing, keynote get a two-item host template). Breaks and socials have no checklist.
Default speaker items (workshop presenter): update bio and headshot · confirm session title and summary · read the speaker guide · register for the conference · join Discord · confirm scheduled slot · share a link to workshop materials · tech check.
Default organizer items (per presenter): invitation sent · presenter in portal · onboarding email sent · registration info sent · promo materials prepared · promo materials shared with presenter · session scheduled · schedule confirmation sent · Discord channel and speaker role assigned · day-of reminder sent.
Panelist and moderator templates are lighter (no materials, no outline). The PyJam templates are in §9.7.
9.2 Instances
When an invitation is accepted, every template item becomes a ChecklistItem for that presenter and session with a status (TODO · DONE · SKIPPED · BLOCKED), a computed due_date that organizers can edit per item, an assignee for organizer items, completed_by/completed_at, and a note. Organizers can add one-off items to any presenter or session, and can back-fill an item added to a template after some checklists were created.
9.3 Auto-completion
Items complete themselves when the portal can tell:
| Item | Completes when |
|---|---|
| Bio and headshot updated | bio non-empty and headshot set |
| Read the speaker guide | read receipt exists for the current version |
| Invitation sent / presenter in portal | invitation sent / accepted |
| Session scheduled | a slot exists |
| Registered for the conference | a pretix order exists for the presenter's email (§12.1) |
| Upload your video, transcribe, translate, final cut, … | a ready asset of the required kind exists |
| Video length within limit | latest video duration ≤ the session's limit; otherwise the item becomes BLOCKED with the overage shown |
| Joined Discord | self-attested in 2026 (checkbox); becomes automatic once Discord account linking exists (§12.2) |
Rules re-run when the relevant record changes and nightly as a safety net.
9.3a Readiness: items nobody can start yet
An item exists long before it can be done: confirming a slot before the schedule is built, reading a guide nobody has published, a tech check the team has not opened booking for, an organizer item that waits on a portal feature. Such an item waits. It keeps its place in the list, muted, with one line saying what it waits for, and a manual tick is refused, so the disabled box is not the only guard.
A template line waits on any of three sources, and an organizer override outranks all of them:
- A rule, for what the database can answer: the session has a slot, the guide it points at is published, registration is configured.
- A gate, a named switch organizers flip on the "Readiness gates" page, for work the portal cannot see. The item carries the gate's code, so a gate created later attaches to the items that already named it, deleting one puts them back to waiting, and a code with no gate row waits too. Gates fail shut in every direction, and clone into next year shut.
- Another item, for the one piece of work that unblocks this one, which is how a speaker line waits on the organizer line behind it.
- The override (organizer-only): open this item whatever it waits for, or hold it shut whatever it does not, recorded in the activity log.
A finished item never waits, whatever its sources say. A waiting item is counted but never chased: it is in "3 of 12 done, 2 waiting" and out of the overdue count, the digests and the reminder emails.
9.4 Reminders
A daily job emails each presenter one digest of their open items due within 7, 3, and 1 days, in their own timezone. Organizer items go to the assignee, or to the organizers list if unassigned. Every send is logged so the same reminder never goes out twice.
9.5 What the speaker sees
The dashboard became a summary and the checklist got a page of its own ("My speaker checklist"), with two views: everything by due date, or grouped by session. Both carry the same two lists:
- Your to-dos — their items, tickable in place, due dates in their timezone, coloured by urgency.
- What we're doing for you — the team's items for them, read-only, with status and who is on it. A speaker sees "promo materials — in progress, Lena" instead of emailing to ask.
Lines that are not about one session (bio, guide, registration, Discord, the tech check) are instantiated once per presenter and grouped as "For you as a speaker".
9.6 What the organizer sees
- Checklist board — presenters down the side, every checklist item across the top, one colour per cell, sortable by most overdue. Tabs for the speaker side, the organizer side, and post-production. This is the replacement for the spreadsheet's status columns.
- My volunteering tasks — organizer items assigned to me or to a team I am on, soonest first or grouped by presenter, with what I have finished under them. It sits in the personal rail rather than the Organize one, for organizers too, because it is a person's own work rather than a view of the edition.
- Presenter page — both checklists, invitation history, sessions, assets, activity.
9.7 PyJam: pre-recorded performances and post-production
PyJam sessions are performances recorded by the performer, post-produced by the team, and published to YouTube around a scheduled slot. The flow end to end:
- Organizer adds the session (kind PyJam, pre-recorded) and invites the performer.
- Performer accepts, fills in title and description, and uploads the video from their dashboard. The upload panel shows the file, its duration against the length limit, and the version history.
- The post-production checklist for the session is instantiated. Each item is assignable to a team member.
- The performer's dashboard shows those items in "What we're doing with your video", so they can see "transcript — done, translation — in progress".
- When the final cut is ready, the performer's "approve the final cut" item opens.
- The organizer publishes to YouTube by hand, pastes the URL and publish time into the session, and the last item completes.
Performer checklist (seed): update bio and headshot · confirm title and description · read the performer guide · upload your performance video · approve the final cut · register · join Discord.
Post-production checklist (seed; session-scoped, organizer-owned, fully editable):
| Item | Completes when |
|---|---|
| Record intro and outro video (MC) | intro and outro assets exist |
| Review audio and video quality | manual, with notes on the asset |
| Check video length is within limit | automatic; blocks with the overage |
| Transcribe | transcript asset exists for the session language |
| Review transcript | manual |
| Translate | translation asset exists — one item per target language configured in the edition's speaker settings |
| Add title card and assemble final video | processed video asset exists |
| Publish to YouTube with schedule and transcript | YouTube URL and publish time set on the session |
Items are ordered but not gated on each other; transcription can start before the outro is recorded. Because items key on asset kind, automating a step later (say, machine transcription) is a background job that creates the asset — no checklist change.
A pre-recorded session still takes a schedule slot — the premiere or watch-party time — and appears in the public schedule and calendar feeds like any other session.
10. Scheduling UI
Not built (M3).
ScheduleSlotrows exist and the sample data writes them, but there is no editor and the presenter's schedule page is a placeholder.
Organizer editor — a day-by-time grid: columns are Discord channels, rows are 15-minute steps across the conference days. Unscheduled sessions wait in a sidebar and are dragged onto the grid; dragging moves a session, resizing changes its duration. A "+ program item" button on any cell creates an opening, break, or social inline, so the skeleton of a day is built without leaving the grid. All-channel slots render as full-width bands. Conflicts — channel overlap, a presenter double-booked — are highlighted in place.
A timezone switcher on the grid shows the whole schedule as a specific presenter would see it. That is how the team catches "we scheduled her at 3 a.m." before sending the confirmation.
Presenter view — the same schedule, read-only, in the presenter's own timezone with their sessions highlighted, visible before publication with a "not yet public" badge, with an add-to-calendar link per session and a personal calendar feed.
Timezone handling — UTC everywhere in storage and the API; rendering uses the browser's timezone (overridable, remembered). No timezone arithmetic in templates.
11. Public embeds and export
Not built (M4). None of the URLs below are routed yet; they are the proposal's shape for when they are.
The conference website is static. The portal exposes read-only data and a drop-in widget so the site never needs a rebuild when the program changes.
11.1 JSON API
GET /api/v1/<conference>/sessions/ published sessions with presenters and slot
GET /api/v1/<conference>/presenters/ public presenters with their sessions
GET /api/v1/<conference>/schedule/ slots by day, plus channels
GET /api/v1/<conference>/sessions/<slug>/ one session
Only published sessions and public presenters (others appear by name only on their sessions), never emails. Responses are cached for five minutes per conference and invalidated on save. Program-kind sessions carry is_content: false so the widget can draw breaks as bands rather than cards.
11.2 Embeddable widget
<div data-pyladiescon-widget="schedule" data-conference="pyladiescon-2026"></div>
<script src="https://portal.pyladies.com/static/widget/v1.js" defer></script>
A small self-contained script (no framework) that renders schedule, speakers, or a single session into the host element, in the visitor's timezone, with minimal CSS the conference site can restyle through CSS variables. An iframe version exists for pages that cannot add scripts.
The widget is served by the portal. The conference site already depends on the portal for stats.json, so this does not add a new failure mode; what it adds is mitigation: the script and other static assets are long-cached behind the CDN so a stalled application keeps serving the last good copy, the widget shows a link to the portal's own schedule page instead of a blank box if the API is unreachable, and no portal deploys happen during the conference weekend except hotfixes.
11.3 Calendar feeds
Attendees can put a single workshop or panel in their calendar and have it stay correct if the slot moves:
GET /api/v1/<conference>/schedule.ics everything (subscribable)
GET /api/v1/<conference>/schedule.ics?sessions=a,b,c a personal selection (subscribable)
GET /api/v1/<conference>/schedule.ics?kind=WORKSHOP&channel=… filtered
GET /api/v1/<conference>/sessions/<slug>.ics one session
GET /api/v1/<conference>/presenters/<slug>.ics everything one presenter is on
- Every feed is subscribable (
webcal://), not just downloadable. Subscribing to a session means a reschedule updates the attendee's calendar. - The widget lets a visitor star sessions; the stars are kept in the browser and joined into one feed URL. No account needed.
- Feeds use stable UIDs per session, bump the sequence on every change, and emit cancelled events rather than dropping them, so subscribed calendars stay correct.
- Logged-in presenters get their own feed including unpublished sessions, so their calendar is right before the program is public.
11.4 Export
- JSON or Markdown data file of sessions and presenters for a static-site build, for anyone who prefers to bake the program in rather than fetch it.
- CSV of presenters with sessions and checklist completion — the old spreadsheet, exported from the source of truth instead of being it.
- Canva bulk-create CSV for speaker cards, straight from the presenter list.
11.5 Draft vs. published
The program is internal until the team publishes it, and a presenter's details stay hidden until their session is scheduled and published. Three switches, evaluated together:
| Level | Switch | Effect |
|---|---|---|
| Edition | program_visibility = internal / published |
Master switch, planned for SpeakerSettings and not built yet. While internal, the API returns an empty program and the widget shows "Program coming soon". |
| Session | scheduled and is_public |
A session appears only once it has a slot and an organizer has published it. Scheduling alone publishes nothing. |
| Presenter | is_public |
Opt-out hides bio, headshot, and links; the name still appears on their sessions. |
A presenter's bio is public only if the program is published, they are on at least one published session, and they have not opted out. A confirmed presenter whose session is not yet scheduled is simply absent from the public API — no "TBA" rows leak names.
Preview for the website build: every public endpoint and the widget accept a signed, expiring preview token that bypasses the visibility rules, so the conference site can be developed against the draft program and switched to live by removing the token. Preview responses are never cached.
Cancelling a session or a presenter withdrawing un-publishes it, invalidates the cache, and the widget drops it on next load — no site rebuild.
12. Integrations
12.1 Pretix (registration)
Registration is through pretix, which offers an API and webhooks. The portal keeps a PretixOrder record per order and uses it to auto-complete "registered for the conference":
- Webhooks for order placed, paid, cancelled, expired, and changed. Since pretix webhooks carry an order reference rather than a signed payload, the receiver re-fetches the order from the API before recording it.
- Nightly reconciliation pages through orders modified since the last run, catching missed webhooks and edits made in the pretix backend.
- Matching is by email, case-insensitive, against both the order email and attendee emails. Organizers can link an order to a presenter by hand when someone registered under a different address; a manual link wins.
- Organizer controls: a "registered" column on the board and a "look up in pretix" button on the presenter page.
- Optional: if speakers register free, voucher codes can be created through the pretix API and included in the registration-info email.
Pretix settings (URL, organizer, event, token, webhook secret) are per conference.
12.2 Discord (deferred)
Channel and role setup is manual in 2026: organizers create channels on Discord and record them in the portal. If the team wants automation later, it would add Discord account linking for presenters (which also turns "joined Discord" into an automatic check), a bot that creates channels from the schedule and assigns a speaker role, and a schedule announcement post. Estimated effort is around 56 hours hand-coded or 28 hours AI-assisted with review, plus a Discord admin to create the bot application. Account linking alone is about 10 hours and is the most valuable piece.
13. Storage, email, background jobs
- Files — Digital Ocean Spaces, private bucket, presigned URLs for upload and download; multipart presigned upload for video;
ffprobeon the worker for duration; a bucket lifecycle rule expires abandoned multipart uploads. Public headshots are copied to a public prefix on publish. - Email — the portal's existing backend. Templates: invitation, invitation reminder, onboarding, registration info, schedule confirmation, daily checklist digest. Every send is logged.
- Jobs — nightly auto-completion re-check, daily reminder digest, nightly pretix reconciliation, cache warm after publish. Uses the portal's existing job runner; no new infrastructure.
14. Build order
The conference is about eleven weeks away and invitations need to go out well before that, so the order lets organizers start inviting after the first milestone while the rest is built.
The milestones below are called stages in the code and in
speakers/README.md. Translating between the two:
| Here | In the code | Where it is cited |
|---|---|---|
| M1 | Stage 1 | — |
| M2 | Stage 2 | speakers/README.md on where checklists are instantiated |
| M2b | no stage number yet | — |
| M3 | Stage 3 | ScheduleSlot ("shell for Stage 3") and the presenter schedule placeholder |
| M3b | Stage 3b | MediaAsset |
| M4 | Stage 5 | speakers/README.md on the public schedule, and the addresses page |
| M5 | no stage number yet | — |
A number with a decimal is a task, not a stage: Stage 1.5 and 4.1 are the storage work (bucket and presigned URLs, then the upload tasks 4.1 to 4.3 inside Stage 3b), and Stage 2.5 is the round that brings htmx in.
| Milestone | Delivers | Target | Status |
|---|---|---|---|
| M1 — Presenters and invitations | Session, Presenter, SessionPresenter, Invitation; invitation email and accept flow; speaker profile and session editing; admin; isolation tests | late September | Built |
| M2 — Checklists | template editor and seed defaults; instances; auto-completion; speaker dashboard; organizer board and queue; pretix sync; reminder digests | early–mid October | Built, plus readiness (§9.3a) which the proposal did not have |
| M2b — Permissions | permission catalogue and team-derived grants (§7.1); every app on has_perm; team permission pickers and presets; Role dropped; media permissions land with M3b |
mid October, before M3 | Not built. Access is still is_staff/is_superuser plus team membership |
| M3 — Scheduling | Discord channels, slots, program items, conflict checks, drag-and-drop editor, presenter schedule view, calendar feeds | mid–late October | Not built. ScheduleSlot exists and is written by the sample data; there is no editor, and the presenter's schedule page is a placeholder |
| M3b — Media | video upload, duration check, performer upload panel, session-scoped templates, post-production board | late October, alongside M3 | Not built. MediaAsset exists as a shell and the video-length rule reads it; nothing uploads |
| M4 — Public | JSON API, widget, iframe, draft/preview/publish, data export; wired into the conference site | early November | Not built. None of the URLs in §11 exist yet |
| M5 — Hardening | promo assets, reminder tuning, load test on the public endpoints, documentation | mid November | Not built, except this page |
Each milestone ships behind the edition's feature flag, so the 2026 conference can use one while the next is in review.
Sections 4.1, 8.6, 8.8, 10 and 11 describe work in the unbuilt milestones and are written in the present tense as proposals. The status column above is the place to check before reading any of them as a description of the running portal.
15. Decisions and open questions
Decided
- No CFP; the portal is the system of record for the program.
- Registration through pretix, synced by webhook and nightly reconciliation.
- Discord channel and role setup stays manual in 2026.
- Panels are assembled by organizers.
- The whole schedule, including opening and breaks, is built in the portal.
- Checklists are organizer-defined templates with shipped defaults.
- The widget is served by the portal.
- PyJam performances are pre-recorded, uploaded by performers, post-produced by the team, and published to YouTube by hand.
- Premiere on YouTube or watch party on Discord is supported per session and can be decided later.
- The program and presenter details are internal until published; a presenter's bio is public only through a published, scheduled session.
Open
- PyJam length limit and translation languages — edition settings, but the seed template needs the values.
- Do speakers register through the normal ticket flow or with a voucher? Decides whether voucher generation goes into M2.
Appendix A — If a future edition uses pretalx
A future PyLadiesCon may run a call for proposals again. Nothing in this design assumes it never will, and the right way to bring pretalx in is import, not integration:
- Pretalx owns the call for proposals, review, and acceptance. The portal owns everything after acceptance — checklists, scheduling, media, publishing — exactly as it does now.
- When submissions are accepted, an organizer runs an import (a management command or a button on the sessions page) that reads accepted submissions and their speakers from the pretalx API and creates
SessionandPresenterrows: title, abstract, description, duration, language, track, and speaker name, bio, and email. Sessions arrive asCONFIRMEDwith the presenter linked, and the invitation step is replaced by a "welcome, your account is ready" email pointing at the sign-in code. - Each imported row keeps a
pretalx_code, so the import can be re-run to pick up late acceptances or edited abstracts without creating duplicates. Organizers choose per field whether pretalx or the portal wins on re-import; the sensible default is pretalx for abstracts until the portal's copy has been edited, portal thereafter. - Scheduling stays in the portal. Pretalx's schedule features are not used, which avoids two schedules that can disagree.
- Speakers still edit their bio and session content in the portal. If the team wants those edits reflected in pretalx (for its public pages), that is a one-way push that can be added later; it is not required.
- No webhooks, no nightly sync, no cross-checking of accounts — a deliberately smaller surface than a live two-way integration, because after acceptance the portal is the source of truth and pretalx becomes an archive.
The import is a self-contained module of roughly a week's work and touches no existing model beyond adding the pretalx_code fields. Everything else in this document — checklists, scheduling, widget, media, publishing — works unchanged with imported sessions.