DataTalksClub / DataTalksClub/website

Convert public course registration to verified account and cohort enrollment

Open
#243 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

accessibility auth courses email enhancement frontend integration P0 security testing
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

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:

  1. #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.
  2. #248 supplies verified-account onboarding, server-side same-host path intent/resume,
    /accounts/profile/, and current MemberProfile completion.
  3. #49 supplies atomic logical EmailDelivery and 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 extends templates/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:

  1. Unknown campaign: normal public 404 with no identity or registration clue.
  2. Unavailable target: a known campaign with no Cohort, inactive/closed window, or a target Cohort
    in draft, grading, completed, archived, or cancelled renders a non-submittable safe state.
    It creates no CourseInterest; 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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:

  1. 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.
  2. 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.
  3. 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, historical accepted_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:

  1. re-resolve the eligible active durable account through the accounts identity/survivor contract;
  2. re-read the campaign/window/Cohort and reject a missing, repointed, closed, cancelled, or stale target;
  3. obtain the current member-confirmed scalar profile snapshot through the accounts service and reject an
    incomplete/stale revision rather than using posted profile values;
  4. 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
  5. through #49, create exactly one logical delivery intent and exactly one durable job for purpose
    course.registration.confirmation and 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 Enrollment unique 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 .invalid identities 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 anonymous CourseRegistration.
  • 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.confirmation logical EmailDelivery, 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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:

  1. anonymous campaign account gate and returning verified-complete form;
  2. unverified resume and incomplete-profile redirect/resume (capture only safe state, no token/address);
  3. eligible blank form showing target/profile summary, distinct comment/privacy/marketing controls, and no
    repeated shared fields;
  4. invalid form with accessible error summary, focused first error, preserved safe values, and 1,001-character
    rejection;
  5. successful PRG confirmation, exact replay/refresh, and same campaign repointed to a later Cohort;
  6. no-target, closed-window, cancelled-Cohort, stale-target, and generic identity-unavailable denials; and
  7. 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, CourseInterest behavior, 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.