Skip to content

Establish the non-activating Event lifecycle and calendar core #289

Description

@alexeygrigorev

Parent: #45

Normative authority:

Accepted inputs are closed #18, #19, #136, and #173. This issue expands the one existing events.Event; it never creates another Event identity or changes the accepted /events/<positive-public-id>/<current-title-slug> canonical.

Outcome

Create a deliberately non-activating Event domain foundation that full #45 can consume:

  1. forward-only schema on the existing Event identity for the checked source's type/schedule, registration window, lifecycle timestamps, revision, stable calendar UID, and calendar sequence;
  2. an exact, atomic, network-free import of the checked 421-Event source envelope;
  3. revision-guarded lifecycle, schedule, and registration-availability services owned by events; and
  4. deterministic pure RFC 5545 serialization with no HTTP route, template, attachment, delivery, or provider action.

The accepted public pages continue to read content/public_projection/events.json. Landing this issue must therefore produce no public-page, route, sitemap, feed, cache, Studio, admin API, registration, email, Q&A, job, or provider behavior change.

Frozen source envelope

The groomed envelope on origin/main face8e4808d65afbf0374d1ced7a88079950d663 is:

  • content/public_projection/events.json: 421 records, SHA-256 333c53c88445d9cbc6d91c26f9043a6998645346a84fdd3b2782ec0a989d7fc6;
  • events/event_identity_manifest.json: schema 2, 421 UUID/public-ID bindings and 1,684 aliases, SHA-256 1fd4ff474199cae9ce97a75ac8061aa4c8388edf66a1cb960377e0494559b6f5;
  • legacy source: DataTalksClub/datatalksclub.github.io@ee43d3fa0929faf691178d79f19528e6f15a83e5, _data/events.yaml;
  • accepted content overlay: DataTalksClub/content@e29f56ce70bd997171a78a9f0facc9354797f421, green source run 31365358459;
  • exact present types: conference 6, podcast 214, webinar 132, workshop 69;
  • all 421 starts are timezone-aware Berlin instants (+01:00 or +02:00), 2 have an end, and every imported timezone name is Europe/Berlin.

Engineering must recompute this envelope before work. Any base/source/artifact/digest/count difference returns the issue to PM instead of silently widening or repairing source content here. The repository's current broader release/source recovery remains an operational start gate: implementation begins only from a clean current origin/main whose required base CI and source provenance gates are green.

Schema contract

Extend events.Event in place. Final state must contain:

  • event_type: required allowlisted value conference, podcast, webinar, or workshop for the checked import;
  • starts_at: required timezone-aware instant stored by Django in UTC;
  • ends_at: optional aware instant, strictly after starts_at when present;
  • timezone_name: required valid IANA zone; every checked legacy record imports as Europe/Berlin without reinterpreting its already-aware instant;
  • optional registration_opens_at and registration_closes_at; when both exist open is before close, and any close is no later than start;
  • positive monotonic revision, initialized to 1 for the imported unchanged row;
  • nullable published_at, completed_at, cancelled_at, and archived_at; imported legacy rows remain published and retain null where the historical timestamp is unknown rather than fabricating evidence;
  • immutable unique calendar_uid exactly event-<lowercase-event-uuid>@datatalks.club; and
  • non-negative calendar_sequence, initialized to RFC 5545 sequence 0.

Changing title/date/source data never changes UUID, public ID, calendar UID, source attachment, or aliases. New Events created through the identity service default to draft; the migration preserves all existing checked rows as published. No capacity or waitlist column, placeholder, serializer field, or availability branch is added.

Use forward migrations with historical models and deterministic data. Fresh migration and upgrade from the current schema must converge to the same 421 core rows on SQLite and PostgreSQL. Do not query a network, production system, current wall clock, request Host, or runtime model from a migration.

Deterministic import contract

Add a versioned Event-core manifest or equivalent immutable checked artifact derived only from the frozen source envelope. Its parser/importer must:

  • join each record by the exact existing UUID plus public ID and exact repository/revision/source key/checksum; title/date/slug/provider guessing is forbidden;
  • carry only UUID/public ID, source provenance/checksum, allowed event type, exact start/end instant, and retained timezone name needed by this child;
  • validate the complete set before a write: exact counts, unique identities, all 421 identity-manifest records present once, no extra record, allowed keys/types, aware timestamps, end ordering, timezone validity, and digest binding;
  • default to dry-run and require explicit apply; apply is one transaction and updates no alias, public projection, description, speaker, link, Q&A, historical aggregate, registration, audit, job, or provider row;
  • be byte/dataset deterministic across two clean builds, idempotent on exact replay, and report bounded counts/digests only; and
  • reject missing, duplicate, changed, ambiguous, renumbered, invalid-zone, naive-time, unsupported-type, checksum, or partial input before any mutation.

A later source revision is not accepted implicitly. It requires a reviewed manifest/envelope update under #45 or another groomed source issue.

Lifecycle and availability service contract

Application services in events are the only supported new mutation boundary. They use the existing immutable Event UUID, require an expected revision, lock/recheck the row, and either commit one result or make no change.

Exact transitions are:

draft -> published -> completed -> archived
  |          |
  +----------+-> cancelled -> archived

cancelled is reachable only from draft or published. No reverse, skip, self-transition, completion-after-cancellation, or physical delete behavior is introduced. A successful transition increments revision once and records only the matching known timestamp. Cancellation also increments calendar sequence once in the same transaction. Invalid/stale/concurrent transitions leave revision, timestamps, and sequence unchanged.

A schedule service accepts aware start/end values plus a valid IANA timezone and optional registration window, revalidates all ordering inside the lock, and increments revision once. A genuine start/end/timezone change increments calendar sequence once; an exact no-op does neither. This child does not own title, body, visibility, location, Person, relationship, notification-decision, or public rendering edits.

The pure availability query returns a typed result plus bounded reason from current persisted state and an injected aware now:

  • available only when lifecycle is published, now < starts_at, registration_opens_at is absent or now >= opens_at, and registration_closes_at is absent or now < closes_at;
  • unavailable reasons are exactly lifecycle-not-published, not-yet-open, registration-closed, or event-started, selected in that order after validating the record;
  • there is no capacity, waitlist, email preference, identity, cookie, or provider check.

Pure ICS contract

Implement a standard-library, side-effect-free serializer below events. It receives an already-resolved Event plus an explicit canonical detail URL from #173's builder and emits UTF-8 RFC 5545 bytes with CRLF endings, 75-octet UTF-8-safe folding, and correct escaping of backslash, comma, semicolon, CR/LF.

The stable ordered calendar/event fields are VERSION:2.0, PRODID:-//DataTalks.Club//Events//EN, CALSCALE:GREGORIAN, an explicit method enum (PUBLISH, REQUEST, or CANCEL), UID, SEQUENCE, UTC DTSTAMP, UTC DTSTART, optional UTC DTEND, escaped SUMMARY, optional escaped public description/location supplied through an explicitly public data object, canonical numeric Event URL, and STATUS:CANCELLED only for cancellation. Dates serialize as UTC YYYYMMDDTHHMMSSZ; sequence is the stored non-negative integer. Serialization is deterministic for identical inputs.

Protected join information is not a serializer input and must be absent from output, exceptions, logs, fixtures, and reports. This issue does not decide whether a future public download uses PUBLISH, nor does it attach REQUEST/CANCEL to email; those adapters remain #45/#46/#49 work.

Public ICS route authority decision

No authoritative source currently defines an Event ICS endpoint. Spec 02 requires future registration/calendar builders to emit the numeric/current-slug Event identity, but it does not choose among possible suffix/subpath routes. The existing course /<course>/calendar.ics compatibility route is course-owned and is not authority for Events.

Therefore this child adds no URL pattern, view, response headers, filename, cache class, template action, sitemap/feed entry, redirect, or browser link. Full #45 must separately record and groom the exact public Event ICS path/method/cache/alias contract before any route is implemented. The serializer's URL property is the already-approved Event detail canonical, not a new ICS endpoint.

Acceptance criteria

  • Forward migrations expand the one existing Event model with the exact fields/constraints/defaults above; 421 checked rows retain UUID/public ID/source/aliases, import as published with exact schedule/type/timezone, sequence 0, stable unique UID, revision 1, and no fabricated publication timestamp.
  • Fresh and upgrade migrations converge on SQLite/PostgreSQL; migration drift is empty; malformed persisted combinations are rejected by database constraints where portable and by service/import validation otherwise.
  • Dry-run/apply/replay of the frozen import is atomic, network-free, deterministic, complete, and idempotent; every named malformed/mismatched/partial case fails before writes with bounded non-sensitive diagnostics.
  • Revision-guarded lifecycle and schedule services implement exactly the transition/window/sequence rules, including concurrent stale writers, rollback, no-op, DST, ambiguous fold=0, leap day, already-aware source values, and timezone validation.
  • Availability boundary tests prove every exact state/window/start boundary and reason ordering with an injected clock and no capacity/waitlist behavior.
  • Pure ICS fixtures prove exact byte order, CRLF, UTF-8 folding, escaping, UTC conversion, optional end/description/location, stable UID, monotonic sequence, cancellation method/status, canonical numeric detail URL, deterministic replay, and protected-value canary absence.
  • Route inventories, public URLConf, templates, public projection/checksums, sitemap/feed output, Studio/admin OpenAPI/parity, Q&A, historical totals, durable jobs, and network/provider call inventories are byte-identical or explicitly proven untouched; no Event ICS route resolves.
  • A current versioned verification plan, uv-backed lint/format/type/migration checks, focused SQLite/PostgreSQL migration/service/concurrency/import/ICS tests, graph-selected broader Django/container checks, independent tester PASS, and PM acceptance exist before commit.

Required test scenarios

  1. Migrate empty/current databases; verify 421 exact core rows, 1,684 unchanged aliases, stable UUID/public ID/source attachments, rerun determinism, and rollback on final-record failure.
  2. Exercise every allowed/forbidden lifecycle edge, stale revisions, two-writer publish/cancel and reschedule/cancel races, timestamp/sequence outcomes, exact no-op, and transaction rollback on SQLite plus the applicable separate-connection PostgreSQL cases.
  3. Exercise availability immediately before/at/after open, close, and start for draft/published/completed/cancelled/archived records, including absent one/both window bounds.
  4. Import ordinary winter/summer Berlin instants, DST ambiguity under the accepted fold=0 convention, leap day, explicit aware offsets, naive/malformed times, invalid zones, unsupported types, identity/checksum mismatch, duplicate/missing/extra rows, and changed source revision.
  5. Serialize ordinary, Unicode/folded, escaped, optional-end, and cancelled Events twice byte-for-byte. Parse fixtures with an independent test parser or strict assertions and prove the output contains the numeric detail URL but no UUID route, protected join canary, registration address, provider identifier, or new endpoint.
  6. Prove current public Event pages and all route/link/schema inventories are unchanged, no public ICS URL resolves, no worker/job/network seam is invoked, and importing does not create or modify Q&A or historical-total rows.

Verification and screenshots

This is a database/domain/internal-serialization change with no product-page render impact. Screenshot evidence is not_applicable unless the versioned verification graph finds an actual render path; unrelated public screenshots are not evidence. The independent tester still runs the graph-selected Playwright tier (normally smoke for a backend-only change), validates the frozen plan/envelope, and reports route non-activation.

Dependencies and ordering

Explicit non-goals

No public/template/feed/sitemap/calendar-download route; no public-reader cutover; no Event description, summary, image, visibility, location, protected join data, recording/recap/course/cohort/external-event relationship, Person/speaker/host row, Studio/admin adapter or capability, registration/token/preference/attendance/export, notification decision, EmailDelivery, durable job, Q&A change, historical-total change, provider/network call, production/source access, capacity/waitlist, new package, commit, push, deployment, or issue closure outside the normal lifecycle.

Delivery convention

Follow _docs/PROCESS.md: engineer leaves the focused implementation uncommitted; a separate tester validates the frozen plan, focused SQLite/PostgreSQL tests, graph-selected broader gates, and route non-activation; PM accepts afterward. Only then may the engineer commit with body Closes #289, the orchestrator locally merge with --no-ff and push, and on-call alone observe CI. No pull request or production action belongs to this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Must-have or release-blockingdata-migrationArea: data-migrationenhancementNew feature or requesteventsArea: eventsfoundationArea: foundationintegrationArea: integrationtestingArea: testing

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions