DataTalksClub / DataTalksClub/website
Convert public course registration to verified account and cohort enrollment
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 0
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
Parent epic: #54.
Outcome
Replace the copied anonymous/required-newsletter campaign submission with one verified-account-owned,
cohort-isolated flow. A visitor resumes through account ownership and the accepted MemberProfile
gate, confirms one course-specific form, and receives a stable confirmation for exactly one immutable
Cohort registration. The same courses-owned application boundary also supplies the single-registration
to single-Enrollment conversion primitive that later Studio/admin adapters may invoke explicitly.
Public registration never creates an anonymous row, never silently enrolls the learner, never rewrites
shared account/profile values, and never requires or infers marketing consent.
Normative authority
_docs/PROCESS.md- Specification 01 — member signup, profile, Slack, and course registration
- Specification 04 — registration, enrollment, and cache boundaries
- Specification 05 — durable delivery model
- Specification 07 — identity, privacy, abuse, and safe errors
- Specification 10 — Course/Cohort and onboarding verification
- Open decisions 6, 10–12, 19, and 21: course registration is verified-account-owned; Relay is the only delivery boundary; registration/privacy/marketing/profile ownership is separated.
_docs/architecture/app-boundaries.md:accountsowns identity/profile writes;coursesowns registration/enrollment commands;email_appowns logical delivery; only a leased durable job may call Relay after commit.- Parent #54; direct source/onboarding/delivery prerequisites #242, #248, and #49; transitive Course/Cohort, identity, and profile foundations are supplied through their accepted handoffs.
Dependencies and delivery order
Do not engineer this issue until accepted and integrated #242, #248, and the ordinary #49 delivery
interface are present on the engineer base. These are the only direct implementation prerequisites:
- #242 supplies the additive immutable Cohort registration identity/evidence, mapping/quarantine,
eventual uniqueness rule, and compatibility-preserving source boundary. It must be consumed as
accepted; this issue does not repeat its schema/backfill or authorize protected-data reconciliation. - #248 supplies verified-account onboarding, server-side same-host path intent/resume,
/accounts/profile/, and currentMemberProfilecompletion. - #49 supplies atomic logical
EmailDeliveryand durable-job creation, idempotency, leases, redacted
status, retry/ambiguity, and the no-network-in-transaction rule. Only its ordinary registration
delivery interface is consumed here; a recovery-only dependency does not become a registration blocker.
The accepted handoffs above carry the exact transitive foundation chain:
#224 -> #51
(#51 + #231 + #234) -> #247
#247 -> (#242 + #248)
(#242 + #248 + ordinary #49 interface) -> #243
#224, #51, #231, #234, and #247 must therefore be present through the accepted prerequisite
handoffs, but are not separately promoted into direct blockers. #108 is a parent epic, not an
implementation interface. #230 is a direct prerequisite only for #244 peer-review preservation.
#286 and #287 own CourseInterest and legacy contraction; neither is implemented here and neither
blocks this public registration slice.
#133 aggregate-only public count remains a preserved coordination boundary, never a source of
registration PII. #50 owns purpose dispatch cutover, Relay sender execution, and Datamailer
retirement; this issue creates business intent/job through #49 but must not enable any sender or
writable fallback. #244 may characterize learner preservation independently, but its final acceptance
consumes the accepted #243 conversion interface; #243 does not wait for #244. #245 owns management
reads/exports. #246 later wraps the conversion service with authorized Studio/admin commands, bulk
operations, and correction surfaces rather than reimplementing it. #46 is event-only.
Source integration is not production authority. Engineering records the exact accepted prerequisite
merge SHAs, source pin and migration leaves inherited from their handoffs, plus the #133 native-boundary
fingerprint. It uses synthetic .invalid evidence only and performs no protected-data reconciliation,
provider/Relay call, purpose activation, production migration, deployment, or legacy contraction.
Public routes and resumable states
Keep one canonical campaign surface:
GET|POST /courses/register/<campaign-slug>/remains the only public course-registration page and
form action. It extendstemplates/core/content_page.html; no second embedded form or document shell
is introduced.- A successful POST uses Post/Redirect/Get back to the same canonical URL. The resulting GET renders
the target registration's confirmation state; refresh, back, repeated POST, a second browser, and an
exact network replay do not create another registration, intent, job, or Enrollment. - There is no public enrollment-conversion URL in this issue. Existing canonical Cohort enrollment,
dashboard, and work routes remain unchanged; #246 later exposes authorized conversion adapters.
The server chooses the first incomplete state on every request; it does not trust a client step number:
- Unknown campaign: normal public 404 with no identity or registration clue.
- Unavailable target: a known campaign with no Cohort, inactive/closed window, or a target Cohort
indraft,grading,completed,archived, orcancelledrenders a non-submittable safe state.
It creates noCourseInterest; the unapproved contract is isolated in decision issue #286. A previously created registration may
still render its own immutable confirmation plus the current closed/cancelled explanation, but cannot
be submitted or converted again through the public page. - Anonymous: render campaign identity/marketing content and one clear account gate. Continue through
the accepted/accounts/signup/or/accounts/login/entry, storing only this same-host campaign path
through #248's accepted accounts-owned server-side intent. Anonymous POST performs no business, profile, delivery, or job mutation. - Authenticated but unavailable/unverified identity: use the accepted #248/accounts generic verification or safe
conflict state. Do not reveal whether another account, alias, registration, or email exists. - Verified but incomplete/unconfirmed profile: resume at
/accounts/profile/; after valid member
confirmation, return to this campaign. Login/settings/history remain available and are not globally
blocked. - Eligible account with complete current profile: render the course-specific form below. Revalidate
identity, target/window, and profile revision on POST; a stale profile/target returns the safe current
step with preserved non-sensitive form input and no partial write. - Already registered for this exact Cohort and eligible normalized identity: render the same private
confirmation outcome. Campaign repointing to a later Cohort does not select the old registration and
does not prevent a new registration for that later Cohort.
All personalized/form/confirmation/error responses are authenticated where required, private, no-store, noindex, absent from sitemap/search/public serializers, and never edge cached. The anonymous
marketing shell may remain public but the form state and response must not become a shared cached object.
Exact course-specific form contract
The eligible form identifies the selected Course, Cohort, campaign, and registration-window state. It may
show the member's own confirmed shared-profile summary and an “Edit profile” link, but it does not post or
re-ask email, name, country, region, organization, work status, professional role, seniority, bio,
ambitions, why-joined, or profile/social URLs.
It posts only:
comment: optional escaped plain text, trimmed, maximum 1,000 characters, blank by default for every
new Cohort registration, never prefilled from profile, another registration, or browser history.privacy_acknowledged: required unchecked checkbox next to the configured course-registration privacy
notice/link and visible notice version. Missing/unknown notice version fails closed before mutation.marketing_consent: separate optional unchecked checkbox only when the current marketing integration
is exposed. Checked/unchecked evidence is recorded separately under #242's contract; when the control
is disabled/absent, no consent is inferred from account defaults, historicalaccepted_newsletter,
Slack, another course/cohort, or participation.
Every control has a programmatic label, help/error association, keyboard-visible focus, an error-summary
link, and a minimum 44 CSS px activation target where the control is intended as a pointer target. Invalid
submission preserves the comment and explicit checkbox state for correction. Shared values remain owned
and writable only through the accepted #247/#248 accounts-owned services.
Registration application service
Add one courses-owned command service (name may follow repository conventions) whose logical request is:
- current authenticated account ID and request context;
- campaign stable ID plus expected immutable Cohort target identity/revision from the rendered form;
- expected profile UUID/revision/completion schema version;
- trimmed comment, privacy notice version/acknowledgement, optional marketing evidence; and
- an opaque idempotency key scoped to the current account and target Cohort.
Within one database transaction it must:
- re-resolve the eligible active durable account through the accounts identity/survivor contract;
- re-read the campaign/window/Cohort and reject a missing, repointed, closed, cancelled, or stale target;
- obtain the current member-confirmed scalar profile snapshot through the accounts service and reject an
incomplete/stale revision rather than using posted profile values; - create or recover the one #242 registration for
(Cohort, normalized verified-email snapshot)with
the exact immutable target, minimized shared-profile snapshot, comment, privacy, and optional marketing
evidence; and - through #49, create exactly one logical delivery intent and exactly one durable job for purpose
course.registration.confirmationand that purpose/template version.
The registration, intent, and job commit together; rollback leaves none. No model signal, request,
transaction.on_commit, callback, or domain service calls Relay, Datamailer, SES, a provider, or another
network endpoint. A worker outage after commit does not roll back registration. #50's one-sender cutover
gate controls whether the leased job may execute; until that gate is integrated it must remain safely
non-dispatching, never fall back to Datamailer, and never create a second intent.
Logical identity is the registration plus purpose/template version. Exact command replay, refresh, and a
unique-constraint race return the original outcome. Reusing the command key with changed target, profile
revision, notice version, comment, marketing evidence, recipient identity, or template version fails as a
safe idempotency conflict and creates no changed work. Backend-portable uniqueness/CAS recovery—not a
process lock, vendor-only row-lock assumption, or “check then insert”—must prove concurrency behavior.
Registration → Enrollment conversion service
Create a separate courses-owned conversion command used later by #246. “Approved” here means the caller
has made an explicit authorized conversion request; public registration does not auto-enroll and this
issue adds no approval UI/state or management permission shortcut.
The command accepts the immutable registration identity, expected registration revision/state, actor
context, and an idempotency key. In one transaction it:
- re-resolves the registration's eligible durable learner and immutable Cohort, never the campaign's
current pointer; - rejects quarantined/incomplete/invalid registration, unavailable identity, cancelled/archived Cohort,
target mismatch, or a conflicting learner/Cohort enrollment; - creates or returns exactly one
Enrollmentunique by learner plus Cohort, linked/provable from that
registration, using the adopted Enrollment defaults without copying registration/profile/marketing
evidence into mutable learner fields; and - leaves the registration and all immutable snapshots/evidence unchanged and creates no email intent,
job, provider call, preference rewrite, submission, score, or cross-Cohort state.
Exact replay and concurrent conversion return the same Enrollment. A different registration cannot claim
an existing Enrollment unless it is the same eligible learner and same immutable Cohort and the service
can prove the same conversion relation; contradictory ownership fails closed for review. A registration
for a later Cohort of the same Course converts independently. #244 remains authoritative for preserving
the wider cohort-specific preference/progress/dashboard/calendar behavior; #246 supplies authorization,
reason/revision confirmation, API/Studio parity, bulk results, and audit around this primitive.
Security, privacy, and failure behavior
- Require CSRF on every browser POST, the accepted normalized-account plus safe-IP application rate limit,
and the existing accessible honeypot/abuse boundary without adding CAPTCHA. - Public errors never disclose another email, account, profile, alias, registration, Enrollment, consent,
or delivery existence. Authenticated confirmation shows only the current synthetic/test member's own
outcome and target; routine logs/audits/metrics contain bounded IDs/reason codes, never raw email,
comment, profile values, consent payload, cookies, tokens, request body, or Relay/provider data. - Unknown/repointed campaign, closed window, cancelled Cohort, stale form/profile/target, malformed or
overlong comment, missing notice, CSRF failure, replay conflict, identity collision/quarantine, database
rollback, delivery-configuration failure, worker outage, and Relay ambiguity have deterministic safe
states with no partial registration/Enrollment/email work. - A delivery-intent configuration failure before commit means registration did not succeed; a worker or
Relay failure after commit preserves registration plus its single intent/job and follows #49 recovery. - Synthetic
.invalididentities only. No production registration/profile data, real recipient, sender
credential, registration export, Relay call, Datamailer call, or provider side effect is acceptance
evidence.
Acceptance criteria
- The accepted direct prerequisite SHAs for #242, #248, and the ordinary #49 interface are recorded
on the engineer handoff together with their verified transitive #224 → #51 and #231/#234 → #247
identities, inherited source pin/migration leaves, and #133 native-boundary fingerprint; no #108
epic state or unrelated #230 behavior is treated as implementation evidence. -
/courses/register/<campaign-slug>/implements the exact anonymous → verification → profile →
course-specific → confirmation state machine, safe first-incomplete-step resume, and PRG replay
behavior without an anonymousCourseRegistration. - Unknown, missing-target, inactive/closed, stale/repointed, draft/grading/completed/archived/cancelled,
unavailable-identity, incomplete-profile, and already-registered states match the contract and make
no forbidden mutation. - The form asks only blank optional ≤1,000-character comment, required versioned privacy acknowledgement,
and separate optional unchecked marketing consent; shared profile/email fields are not posted or
rewritten and legacy newsletter evidence is never current consent. - Successful registration stores exactly #242's immutable target/evidence and minimized confirmed
profile snapshot, unique by Cohort plus accepted normalized verified email; campaign/profile/email
edits do not rewrite it and a later Cohort remains independently registrable. - Registration state, one
course.registration.confirmationlogicalEmailDelivery, and one durable
job commit atomically; exact replay/concurrency return the original, rollback returns none, and no
request/on-commit/model/domain path performs network or Datamailer/SES work. - #50's execution gate is respected: this issue does not enable a sender, writable Datamailer fallback,
automatic resend after ambiguity, or a second canonical renderer/template/transport state. - Explicit registration→Enrollment conversion is atomic, idempotent, concurrency-safe, cohort-isolated,
and unique by learner+Cohort; it uses the immutable registration target, does not auto-enroll on the
public POST, and changes no snapshot/evidence/preference/progress/email state. - CSRF, rate-limit, honeypot, cache/noindex, enumeration, log/audit/metric redaction, and synthetic-data
canary tests pass without leaking account/profile/comment/consent/delivery values. - The complete flow remains server-rendered and usable with JavaScript disabled, keyboard only,
200% zoom/text spacing, reduced motion, and at 320 CSS px without horizontal overflow. - Focused Django/security/integration tests, registration/profile/delivery/adoption characterization,
migration drift, backend portability, graph-selected Playwright, full versioned verification, and
container checks pass with exact counts/digests recorded. - The independent tester captures and inspects every required desktop/mobile/no-JS state below; the PM
accepts the copy, navigation, safe denials, error recovery, confirmation, and later-Cohort isolation.
Required Django and integration scenarios
- Anonymous GET/POST; password signup and supported social return; unverified, expired verification,
inactive/quarantined/absorbed/broken/colliding identities; existing verified complete/incomplete account;
safe server-side campaign intent and second-browser resume. - Campaign not found; no target; inactive; open-boundary instants; target revision/repoint between GET and
POST; each Cohort lifecycle; database clock/timezone behavior; existing registration before/after close.
Treat the configured registration interval as start-inclusive and end-exclusive. - Blank, whitespace, Unicode, exactly 1,000 and 1,001-character comment; privacy unchecked/unknown/stale
notice; marketing absent/unchecked/checked; forged shared-profile/email/campaign fields; mass assignment. - Valid submit; invalid correction; refresh/back/double-click; same-key exact replay; same-key changed
payload; parallel first submits; uniqueness race; rollback at each write boundary; profile edit and
campaign repoint after commit; later Cohort registration for the same Course. - Delivery creation success, missing/changed template configuration, worker down, lease retry, Relay
timeout/response loss/ambiguity/suppression, and restart: one registration/intent/job and zero direct
network or fallback calls. - Conversion exact replay, parallel conversion, stale registration, invalid/quarantined registration,
identity survivor/unavailable/collision, target mismatch, cancelled/archived Cohort, pre-existing same
Enrollment, conflicting Enrollment, and later-Cohort conversion; assert no copied evidence or email work. - Assert zero write to
CustomUser,MemberProfile,content.Person, another Cohort, historical
registration, learner preferences/progress/scores/submissions, or #133 aggregate provenance.
Browser and screenshot acceptance
At approximately 1440×900 and 390×844, using synthetic .invalid accounts, the independent tester must
capture and inspect:
- anonymous campaign account gate and returning verified-complete form;
- unverified resume and incomplete-profile redirect/resume (capture only safe state, no token/address);
- eligible blank form showing target/profile summary, distinct comment/privacy/marketing controls, and no
repeated shared fields; - invalid form with accessible error summary, focused first error, preserved safe values, and 1,001-character
rejection; - successful PRG confirmation, exact replay/refresh, and same campaign repointed to a later Cohort;
- no-target, closed-window, cancelled-Cohort, stale-target, and generic identity-unavailable denials; and
- JavaScript-disabled anonymous, form, invalid, and confirmation states.
For every image, verify the expected page rather than a debug/error page; readable course/Cohort identity;
private/noindex behavior; no horizontal overflow; correct focus/labels/44px targets; and no real email,
profile text, comment, consent payload, token, Slack secret, delivery/provider data, or other learner state
in the screenshot, URL, title, browser history, console, or retained artifacts.
Explicit non-goals
- No accountless course registration,
CourseInterestbehavior, anonymous verification model, capacity,
waitlist, automatic public enrollment, or campaign repointing of history. - No #242 schema/backfill/contract migration, MemberProfile schema/write ownership, Person linking, account
merge, legacy email rewrite, or production-data reconciliation. - No Studio/admin API/list/export/bulk/correction/approval UI, capability design, management command, or
copied numeric-ID/staff-token API; those belong to #245/#246. - No learner preference/progress/dashboard/calendar redesign, curriculum/scoring/certificate change,
event behavior, Slack invite behavior, or management parity beyond the reusable service boundary. - No Relay/provider/template editor, sender cutover, direct SES/Datamailer call, writable compatibility
adapter, production recipient, automatic ambiguous resend, deployment, or production migration.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading _docs/PROCESS.md and Specifications 01, 04, 05, 07, and 10, then verify that prerequisites #242, #248, and #49 are integrated. Trace GET|POST /courses/register// and templates/core/content_page.html. Done means the verified, cohort-scoped flow preserves the stated safe states, idempotency, transaction, privacy, and durable-delivery boundaries without public enrollment conversion.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, authentication, backend, databases
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100