Data model¶
This page is a reference for the database models. It covers each model's purpose, its meaningful fields, its relationships, and the constraints that the database enforces. Field names match the source exactly; the model files are linked from each section.
Everything is scoped to an Event. Talks, rooms, tickets, and pending Pretalx changes all
point back to an event, and access control filters on it (see the
multi-event design).
Entity relationships¶
erDiagram
Event ||--o{ Talk : "has"
Event ||--o{ Room : "has"
Event ||--o{ Ticket : "sold for"
Event ||--o{ PendingPretalxChange : "tracks"
Event }o--o{ CustomUser : "membership"
Room ||--o{ Talk : "hosts"
Room ||--o{ Streaming : "streams"
Talk }o--o{ Speaker : "presented by"
Talk ||--o{ Rating : "rated by"
Talk ||--o{ SavedTalk : "bookmarked as"
Talk ||--o{ Question : "asked about"
CustomUser ||--o{ Talk : "chairs"
Question ||--o{ QuestionVote : "voted on"
Question ||--o{ Answer : "answered by"
CustomUser ||--o{ Rating : "writes"
CustomUser ||--o{ SavedTalk : "saves"
CustomUser ||--o{ Ticket : "holds"
CustomUser ||--o{ Question : "asks"
CustomUser ||--o{ QuestionVote : "casts"
CustomUser ||--o{ Answer : "gives"
events app¶
Source:
events/models.py
Event¶
A single conference event, for example "PyCon DE & PyData 2026". One deployment holds several.
Key fields:
name- unique display name (include the year if relevant).slug- unique slug used in URLs and to organize per-event assets and media. Not necessarily the same as the Pretalx slug.year- optional, validated to the range 2000-2100.is_active- whether the event is visible on the site. Inactive events are filtered out of event resolution and access checks.validation_api_url- external API that confirms a user bought a ticket for this event; blank disables API validation.show_rating_summary- whether regular users see the average rating and count. Staff and superusers always see summaries.- Branding URLs:
main_website_url,imprint_url,code_of_conduct_url,privacy_policy_url,venue_url,transcriptions_url, pluslogo_svg_name,made_by_name,made_by_url. pretalx_url- the Pretalx event base URL. The propertiespretalx_schedule_urlandpretalx_speakers_urlderive the schedule and speaker pages from it.
Relationships: reverse talks, rooms, tickets, pending_pretalx_changes, and users
(many-to-many with CustomUser).
talks app¶
Source: talks/models.py,
models_rating.py,
models_qa.py,
models_pretalx.py
Room¶
A room where talks happen. Rooms are event-scoped, not global.
Key fields: event (required, on_delete=PROTECT), name, pretalx_id (the stable Pretalx
slot.room.id; null for manually created or legacy rooms), description, capacity, and
slido_link.
Room constraints
uniq_room_event_name: room names are unique per event, not globally. The same name can exist under different events.uniq_room_event_pretalx_id: at most one room per Pretalx id per event. The constraint is partial (pretalx_id IS NOT NULL) so legacy and manual rooms with a null id do not collide.
Room.resolve_for_event() is the shared matcher: it looks up (event, pretalx_id) first (the key
that survives a rename on Pretalx), then falls back to (event, name) for legacy rows.
Streaming¶
A live streaming session bound to a room and a time window.
Key fields: room (on_delete=CASCADE), start_time, end_time, video_link, and
transcription_url.
Constraints and behavior: a check constraint (streaming_start_before_end) enforces
start_time < end_time, and clean() rejects overlapping streamings for the same room. A talk is
matched to the streaming that covers its slot (at least half the talk inside the window, allowing a
one-minute start margin), which is how unrecorded talks fall back to the room's live stream.
Speaker¶
A conference speaker.
Key fields: name, biography, avatar (URL), optional gender (text choices) with
gender_self_description, pronouns, and pretalx_id (unique, the match key for imports).
Relationships: many-to-many with Talk (reverse talks).
Talk¶
A conference talk. This is the central model.
Key fields:
presentation_type- one of Keynote, Kids, Lightning, Open Space, Panel, Plenary, Talk, Tutorial. Drives the default duration and track.title,abstract,description.speakers- many-to-many withSpeaker.start_time- defaults to aFAR_FUTUREsentinel (year 2050) for talks not yet scheduled.TalkQuerySet.scheduled()excludes those.duration- aDurationField; defaults from the presentation type when left blank.end_time- derived asstart_time + durationand stored so it can be indexed. Set automatically byapply_derived_defaults()on every save; never written by hand.room- foreign key (on_delete=SET_NULL); a talk's room must belong to the same event.track- defaults to "No track", or "Lightning Talks" for lightning talks.external_image_urlandimage- the uploaded image wins over the external URL; both fall back to a per-event placeholder.pretalx_link- the Pretalx talk page;pretalx_codeparses the submission code out of it.slido_link,video_link(validated, YouTube links getenablejsapi=1appended),transcription_url,video_start_time.event- required (on_delete=CASCADE).session_chair- optional foreign key toCustomUser(on_delete=SET_NULL); the volunteer moderating the session.hide- hides the talk from the public.created_at,updated_at.
Validation: clean() rejects a room from a different event and any talk that overlaps another in
the same room. The model carries a rich set of indexes for the common list and schedule queries (by
start time, room, event, presentation type, and the hide/start_time and end_time
combinations).
TalkQuerySet adds the access and aggregation helpers used across views: accessible_to(user),
scheduled(), with_streamings() (batch-loads the streaming cache to avoid N+1), and
with_rating_stats() (annotates average_rating and rating_count).
Rating¶
A user's 1-to-5 score for a talk, with an optional admin-only comment.
Key fields: talk (on_delete=CASCADE), user (on_delete=CASCADE), score
(PositiveSmallIntegerField), comment (free text, max 2000 characters, visible only to admins),
created_at, updated_at.
Rating constraints
unique_user_talk_rating: one rating per user per talk. Re-rating updates the existing row.rating_score_range: a check constraint enforcing1 <= score <= 5.
SavedTalk¶
A user's bookmark of a talk.
Key fields: user (on_delete=CASCADE), talk (on_delete=CASCADE), created_at. The
unique_user_saved_talk constraint keeps one bookmark per (user, talk).
SavedTalk.talk_ids_for(user) returns the set of saved talk ids for fast membership checks in list
templates.
Question¶
A question asked about a talk. Part of the live Q&A feature.
Key fields: talk (on_delete=CASCADE), content (max 2000 characters), user
(on_delete=SET_NULL, nullable so anonymous questions survive a user deletion), status (one of
approved, answered, rejected; defaults to approved), created_at, updated_at.
QuestionQuerySet provides with_vote_count(), sorted_by_votes(), approved(), answered(),
and not_rejected(). The display_name property obfuscates the author's email for the public
author line.
QuestionVote¶
An upvote on a question.
Key fields: question (on_delete=CASCADE), user (on_delete=CASCADE), created_at. The
unique_question_vote constraint enforces one vote per (question, user).
Answer¶
An answer to a question, typically from a speaker or moderator.
Key fields: question (on_delete=CASCADE), content (max 2000 characters), user
(on_delete=SET_NULL), is_official (flags a speaker or organizer answer), created_at,
updated_at.
Saving an answer flips the parent question to answered unless it was already rejected.
PendingPretalxChange¶
A diff the Pretalx importer detected in --detect-only mode but has not applied to live data.
Admins review these and apply or dismiss them. See the
Pretalx sync manual for the workflow.
Key fields:
event(on_delete=CASCADE) andpretalx_code- the submission this change concerns.talk- the local talk it targets (on_delete=SET_NULL, null for a CREATE).kind-create,update, ordelete.field_diffs- per-field{field: {old, new}}JSON; empty for create/delete.speaker_diffs- speaker many-to-many JSON{added: [...], removed: [...]}.pretalx_payload- a self-contained snapshot of the new values, so applying does not need a fresh Pretalx fetch.first_detected_at,last_detected_at,applied_at/applied_by,dismissed_at/dismissed_by.
Pending-change constraint
unique_open_pending_change_per_submission: at most one open (neither applied nor dismissed)
row per (event, pretalx_code). The constraint is partial
(applied_at IS NULL AND dismissed_at IS NULL). Re-detecting the same diff updates the existing
open row and bumps last_detected_at instead of creating duplicates; a fresh detection after a row
is applied or dismissed creates a new open row.
A row is "pending" while is_pending is true (both timestamps null). mark_applied() and
mark_dismissed() stamp the matching timestamp and author.
users app¶
Source: users/models.py
CustomUser¶
An email-based user. There is no username field. Login is passwordless for regular users (emailed one-time codes); only superusers carry a real password.
Key fields:
email- unique, normalized to lowercase, used asUSERNAME_FIELD.display_name- optional public name shown when asking questions; validated.events- many-to-many withEvent, listing the events the user may access.
save() enforces the password rule (superusers must have one, regular users must not).
label(obfuscate=...) returns the best human-readable name (display name, then full name, then
email), masking the email when used in public contexts. visible_events() returns the active events
the user may see.
Ticket¶
Links a user to an event by a unique ticket id, recording proof that the user holds a ticket for that event.
Key fields: user (on_delete=CASCADE), event (on_delete=CASCADE), ticket_id, created_at.
The unique_ticket_per_event constraint keeps each ticket id unique within an event.