DataTalksClub / DataTalksClub/website
Add target-native course registration and enrollment reads and exports
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 0
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
Parent epic: #54. Coordinates with broad course-management parity epic #59.
PM dependency status
BLOCKED — owner decision and integrated foundations required; do not engineer while needs grooming remains. The issue is otherwise specified, but its default masked member identifier is not authorized. #288 owns that shared product decision; this issue must consume it without inventing an email/name mask or reusing the event-provider helper.
Outcome
Authorized operators can inspect target-native course-registration and Cohort-enrollment state, compare bounded campaign/Cohort metrics, and request short-lived CSV exports through private Studio and /api/v1/admin/ adapters over the same read-only courses query/export services. The surfaces use stable UUID resources, apply the authorized queryset before lookup or counting, mask personal data by default, and expose full personal fields only through an additional capability.
This issue adds no registration or Enrollment mutation. Every safe GET is side-effect free; export creation is the sole command and creates only one durable operation/artifact.
Normative authority
_docs/PROCESS.md- Specification 04 — registration, enrollment, Studio coverage, and APIs
- Specification 06 — capability registry, Studio/admin API parity, UUIDs, pagination, audit, and high-risk exports
- Specification 07 — protected learner data, cache isolation, CSV safety, privacy, retention, and observability
_docs/architecture/app-boundaries.md:coursesowns queries/exports; Studio and API are adapters;accountsowns normalized identity/profile reads;coreowns shared operation/audit primitives.- Parent #54; target registration/enrollment contracts #242/#243/#244; accounts-owned identity/profile read contract #247; shared masked-member decision #288; management foundations #32/#33/#52. #246 is the downstream command consumer and #59 is downstream completeness/parity integration.
Dependencies and freeze order
Do not engineer until a fresh base contains these accepted and integrated direct inputs:
- #247 for the accounts-owned normalized-identity, immutable
MemberProfileUUID, and confirmed-profile scalar read contracts. - An authorized owner approval in #288, followed by PM reconciliation here, for the single shared non-PII
member_labelgrammar. The decision alone implements no helper, route, read, export, schema, or permission. - #242 for the target
CourseRegistrationUUID/revision, immutable target/evidence components, eligibility/quarantine state, and protected legacy mapping. #286 and #287 own separate CourseInterest and legacy-contraction outcomes; neither is implemented or exposed here. - #243 for the accepted registration-to-Enrollment provenance relation and idempotent conversion result.
- #244 for final Enrollment UUID/revision, learner+Cohort identity, preserved preference/certificate/score fields, and cross-Cohort read boundaries.
- #32 for the complete production role/function/object/field policy, audit/redaction interface, and high-risk reauthentication/confirmation contract consumed here. Its current bounded uncommitted slices are not an accepted current-main handoff.
- #33 for accepted human/service admin-API principals, exact bearer scopes, revocation, API-equivalent proof, operation, and OpenAPI controls.
- #52 for accepted Course/Cohort assignments, object-scope authorization, compatibility transition, and the assignment-role/capability mapping consumed here. Its support view-as surface must use the same selected #288 grammar, but it does not by itself authorize #245 reads or exports.
#224/#51 are transitive structural inputs through the accepted #242/#244 handoffs; #231/#234 are transitive through #247/#248; #230 is transitive through #244; and #248 plus the ordinary #49 interface are transitive through #243. They remain required provenance but are not duplicate direct blockers. An open dependency or a bounded slice is usable only when its owning tester and PM explicitly accepted the exact named interface, it is integrated and on-call green on the same current-main lineage, and its evidence envelope remains valid.
#64 is a coordination boundary: this issue registers every export artifact and its opaque subject UUIDs with the shared retention/deletion hook, while #64 owns the wider rights/tombstone/restore workflow. #133 aggregate-only registration totals are not a row source and are never joined into these management resources or exports. #46 is event-only and is not a dependency.
#246 is downstream and must consume the exact accepted member_label, field policy, and operation/result presentation from #245; it may not finalize or test a competing mask. #59 consumes accepted #245 and #246 as source/completeness rows and is not a prerequisite for either, so no dependency cycle is introduced.
After #288 owner approval, PM must replace every provisional masked-identifier phrase with the selected literal grammar, record the authorized decision, and remove decision and needs grooming only when this issue is unconditional. The engineer handoff then records exact prerequisite merge SHAs, the selected #288 contract and implementation provenance, adopted-source pin, capability/policy/registry and OpenAPI digests, target migration leaves/fingerprint, and the final field/export matrix digest. Any prerequisite change to fields, identities, authorization hooks, operations, routes, schema, or the selected grammar invalidates the frozen plan.
Courses-owned query boundary
Add courses-owned query/export services usable without importing studio, api, Django request objects, templates, or transport serializers. Studio/API pass an actor/principal context and a validated query object. The service:
- constructs the function-authorized and Course/Cohort-object-scoped queryset before lookup, filtering, counting, metrics, serialization, or export;
- obtains normalized-identity/profile scalar reads only from accepted
accountsservices; derivesmember_labelonly from an already-authorized immutableMemberProfileUUID through the accepted #288 presentation boundary; and never implements another lowercase/email resolver, email/name mask, provider-identifier helper, or fallback identity; - performs no model save, signal, callback, job dispatch, network call, account/profile projection write, registration conversion, score recomputation, or preference update on reads;
- returns transport-neutral typed results with an exact allowlist, masking disposition, page metadata, and stable safe error code;
- derives all metrics from the same authorized filtered target-native rows used by lists; and
- creates export operations through the shared durable-operation/idempotency/audit primitives, with the worker rerunning the same frozen authorized query specification before writing the artifact.
Direct model queries in Studio/API adapters fail repository/parity tests.
Capability, role, and scope contract
Register these exact domain capabilities and matching Django permissions/scopes:
| Capability key | Purpose | Default effective access |
|---|---|---|
courses.registration.read |
masked registration list/detail/exact lookup | course_operator within assigned Course/Cohort; support_operator exact lookup only; auditor global masked read; site_admin |
courses.registration.view_pii |
unmask the separately listed registration fields | additional grant only; never implied by read, staff status, support, auditor, or object assignment |
courses.registration.metrics.read |
authorized campaign/Cohort registration/conversion counts | course_operator in scope, auditor, site_admin; no support browse |
courses.registration.export |
request/download a bounded registration CSV | additional grant plus courses.registration.read; object scope still applies |
courses.enrollment.read |
masked Enrollment list/detail/exact lookup | same role/object rules as registration read |
courses.enrollment.view_pii |
unmask the separately listed Enrollment fields | additional grant only |
courses.enrollment.metrics.read |
authorized Cohort Enrollment counts | course_operator in scope, auditor, site_admin; no support browse |
courses.enrollment.export |
request/download a bounded Enrollment CSV | additional grant plus courses.enrollment.read; object scope still applies |
course_operator above is the specification-06 function group, not a Course/Cohort assignment role. Except for the explicit global site_admin and masked auditor cases, access requires both the #32 function permission and an active in-scope #52 assignment whose accepted role allowlist contains the exact capability; neither the group nor the assignment grants access alone. #245 does not rename or infer #52 roles (owner, instructor, grader, support, communications), and PM must freeze their accepted per-capability mapping before engineering. Removal, expiry, disablement, or scope drift takes effect on the next request and before an export worker or download recheck.
site_admin remains subject to the same service, field declarations, high-risk policy, and audit; generic is_staff, superuser, group-name, compatibility token, public Person, and Django-admin checks are not authorization. Admin API bearer scopes use these exact keys. A service principal receives only explicitly granted keys and object assignments and cannot use a human session or broader role name as evidence.
support_operator may resolve one exact registration/Enrollment UUID or an exact identity lookup authorized by its support scope; it cannot page through a Cohort, run metrics, or export. Exact identity lookup additionally requires the corresponding view_pii capability and uses the accounts identity service. Substring/prefix email search is not offered. member_label is presentation only: it is never a route, lookup/search/filter, authorization, idempotency, correlation, or uniqueness input, and identical labels do not broaden a result.
Routes and adapter parity
Studio
GET /studio/courses/registrations/GET /studio/courses/registrations/<uuid>/GET /studio/courses/registration-metrics/POST /studio/courses/registration-exports/GET /studio/courses/enrollments/GET /studio/courses/enrollments/<uuid>/GET /studio/courses/enrollment-metrics/POST /studio/courses/enrollment-exports/GET /studio/courses/exports/<uuid>/GET /studio/courses/exports/<uuid>/download/
The Courses navigation labels are exactly Registrations and Enrollments. Each list page contains a filter region, exact result count, table, pagination, and an Export CSV action only when allowed. Detail headings are Registration details and Enrollment details. Masked fields show Hidden rather than an empty value; genuine absence shows Not provided. Metrics pages use Registration metrics and Enrollment metrics. Export status uses Pending, Running, Ready, Failed, Expired, and Cancelled; failure copy is safe and actionable, and Ready shows the expiry time and download action.
Admin API
GET /api/v1/admin/course-registrationsGET /api/v1/admin/course-registrations/<uuid>GET /api/v1/admin/course-registration-metricsPOST /api/v1/admin/course-registration-exportsGET /api/v1/admin/course-enrollmentsGET /api/v1/admin/course-enrollments/<uuid>GET /api/v1/admin/course-enrollment-metricsPOST /api/v1/admin/course-enrollment-exports- shared authorized
GET /api/v1/admin/operations/<uuid>for status GET /api/v1/admin/course-data-exports/<uuid>/downloadfor CSV
Every route uses UUID path identifiers. Legacy numeric IDs/slugs may appear only as read-only metadata; no target Studio/admin route resolves by them. POST export returns 202 Accepted with the existing operation envelope and Location for the UUID operation. The generated OpenAPI 3.1 document declares every route, operation ID, exact bearer scope, request/filter/sort/page fields, response schema, CSV media type, and 400/401/403/404/409/413/422/429 states. No undocumented query parameter is accepted.
Studio and API use identical capability entries, services, object/field policy, filters, ordering, counts, masking, export columns, and result/error meaning. CI compares normalized results for the same actor and fixture; HTML presentation differences are not field/result drift.
Resource and field matrix
All timestamps are timezone-aware RFC 3339 in JSON and human-readable with explicit timezone in Studio. All UUIDs serialize canonically. revision is the accepted current resource revision needed by #246; reads do not increment it.
CourseRegistration list/detail
Always present when authorized:
uuid,revision,created_at, accepted lifecycle/eligibility state, quarantine-present boolean and bounded reason code when policy allows it;- Course UUID/slug/title snapshot, Cohort UUID/slug/title snapshot, campaign UUID/slug snapshot;
- profile completion schema version, profile revision, and profile snapshot timestamp;
- conversion status and linked Enrollment UUID when the accepted #243 provenance exists;
- privacy notice version and acknowledgement timestamp;
- marketing-evidence presence only; and
- one nullable
member_label, derived only from the already-authorized immutable profile UUID under the owner-selected #288 grammar.
Present only with courses.registration.view_pii and an in-scope object:
- full accepted MemberProfile UUID, normalized verified-email snapshot, and accepted account UUID;
- certificate/display name, country code, derived region, organization, work status, professional role, and seniority from the immutable minimized snapshot;
- course-specific comment;
- optional marketing granted/not-granted value, notice/policy version, and recorded timestamp; and
- preserved historical
accepted_newsletterevidence as explicitly legacy evidence, never relabelled as current consent.
Never serialized: About/bio, ambitions, why-joined, profile/social URLs, Slack state/secret, account credential/token/session/alias internals, mutable campaign-pointer values presented as historical target, raw reconciliation payloads, reversible identity digests, audit bodies, delivery/provider bodies, or another learner's work.
Enrollment list/detail
Always present when authorized:
uuid,revision,enrollment_date, accepted lifecycle state;- one nullable
member_label, derived only from the already-authorized immutable profile UUID under the owner-selected #288 grammar; Course/Cohort UUID, slug, title; source registration UUID when provenance exists; - generated display alias,
display_on_leaderboard,display_public_profile,disable_learning_in_public, andposition_on_leaderboard; - total score as the accepted stored/derived value without triggering recomputation;
- certificate state/version and booleans for certificate name/URL presence; and
- account-owned communication-preference effective booleans only if #244's accepted query contract exposes them, labelled as account-level rather than Enrollment-owned.
Present only with courses.enrollment.view_pii and an in-scope object:
- eligible MemberProfile/account UUID and normalized email through the accounts read service;
- explicit display name where it is not the generated alias;
- certificate name and certificate URL; and
- the current exact account-level communication-preference values when included by the accepted #244 service.
Never serialized: submissions, answers, review text, GitHub/commit URLs, complaint bodies, provider/email payloads, account/profile free text, another Cohort's state, or mutable fields not in the accepted Enrollment query schema.
A field not available in the final accepted prerequisite schema is removed explicitly during PM revalidation before engineering; engineers do not invent it. Studio/API/export matrices must remain identical after that freeze.
member_label remains provisional until the #288 owner response. Approval selects exactly one literal grammar; it does not authorize a second account/email mask. The selected value is computed at presentation/export time and is not stored on CourseRegistration, Enrollment, MemberProfile, an operation, or an export row source. It serializes as the exact visible string or null in API/CSV; Studio displays Hidden only when the consuming field is unavailable or unauthorized and never substitutes PII. Invalid, absent, quarantined, absorbed/cyclic, inactive, deleted, unauthorized, or nonexistent identities expose no label and preserve the generic result. Collisions remain identical and are distinguished only by separately authorized resource UUIDs. Ordinary logs, metrics, traces, errors, and job context never record the label; any permitted append-only audit target-label snapshot follows the exact #288 boundary and denied/unknown targets receive none.
Filters, lookup, ordering, pagination, and counts
Unknown, repeated scalar, malformed, mixed-scope, or overlong filter values return 400 invalid_query with allowlisted field errors and no partial fallback.
Registration allowlist:
- exact
uuid,course_uuid,cohort_uuid,campaign_uuid, acceptedstate,converted=true|false,quarantined=true|false; created_frominclusive andcreated_beforeexclusive RFC 3339 timestamps; and- exact
identity(account UUID or email normalized/resolved by accounts), permitted only withcourses.registration.view_piiand exact-lookup support scope.
Enrollment allowlist:
- exact
uuid,course_uuid,cohort_uuid,registration_uuid, acceptedstate,certificate_state; display_on_leaderboard,display_public_profile, anddisable_learning_in_publicexact booleans;enrolled_frominclusive andenrolled_beforeexclusive RFC 3339 timestamps; and- exact
identityunder the samecourses.enrollment.view_pii/support rule.
Default stable ordering is -created_at,-uuid for registrations and -enrollment_date,-uuid for Enrollments. The only selectable sort keys are either timestamp direction and uuid in the same direction. Metrics default to stable Course/Cohort/campaign UUID order. Export always uses the corresponding list's selected stable order.
Use bounded page-number pagination: page defaults to 1; page_size defaults to 20 and accepts 1–100. Responses contain items, page, page_size, exact total_count, and nullable next_page/previous_page. Counts and pages are computed after authorization and filters. An authorized out-of-range page is 200 with empty items and the exact count. No count or timing response may reveal out-of-scope objects.
Metrics
Registration metrics accept the registration scope/date filters above plus group_by=cohort|campaign (default cohort) and return, per authorized group:
- total target-native registrations;
- eligible and quarantined counts;
- converted and unconverted counts; and
- invariant-failure count only as a bounded operational count, never raw row values.
Enrollment metrics accept Course/Cohort/date/state filters and return, per authorized Cohort:
- total Enrollments;
- counts by accepted lifecycle state;
- leaderboard-visible, public-profile-visible, learning-in-public-disabled, and certificate issued/revoked/present counts; and
- invariant-failure count only.
Metric groups use the same page contract and maximum size. Zero is returned only for an authorized, complete query over a real scope; unknown/out-of-scope objects do not become zero. #133 baseline overlays never contribute.
Export contract
Registration and Enrollment export requests use the corresponding exact filters/sort plus:
include_pii, defaultfalse;trueadditionally requires the resource'sview_piicapability;- exact boolean
confirmed=true, bounded reason code/text under the accepted #32 high-risk policy; Idempotency-Key; and- the approved fresh-reauthentication/API-equivalent evidence supplied by #32/#33.
The service previews the exact authorized filtered row count and selected column class. If the frozen count is greater than 10,000, return 422 export_too_large and require narrower filters; never truncate or split silently. A request body over the global bound returns 413. Changed-payload key reuse returns 409; exact replay returns the original operation. Export is cancellable only while pending; once row materialization starts it runs to a terminal result and a cancellation request reports the actual boundary.
CSV is UTF-8, comma-delimited, RFC-4180 compatible with one header row and deterministic row/column ordering. Every text cell, including identifiers that begin with =, +, -, @, tab, or carriage return after leading whitespace, passes the shared formula-neutralization helper. CR/LF/control-character, quoting, Unicode, and delimiter canaries round-trip as data, never formulas or extra rows. Masked exports use only the Always-present matrices. PII exports add only the corresponding gated fields; the Never-serialized fields never appear in either form.
The worker writes to the approved encrypted, private operational-artifact storage through a repository abstraction; local/test storage remains under the project-local configured private boundary. No public ACL, stable public URL, source filename, raw filter query, email, or token appears in the object key, operation JSON, logs, metrics, or audit. Store a redacted manifest containing export UUID, operation UUID, capability, policy version, selected column class, safe filter digest, row count, content checksum/size, actor/principal reference, created/completed/expires timestamps, and opaque contained subject UUID links required for #64 deletion propagation—not row values.
A successful artifact expires exactly 24 hours after completion. The authenticated same-origin Studio/API download streams through authorization; there is no bearer token, signed URL, identifier, or filter in a redirect/query string. Every download rechecks active principal, export capability, object scope, and view_pii when applicable. Expired, cancelled, failed, deleted-for-rights, or out-of-scope artifacts return generic 404 and no bytes. Cleanup removes bytes and marks the operation result expired; exact counts/checksum metadata follows the approved one-year minimized audit retention, without preserving row content. Subject deletion immediately revokes and removes any live artifact linked by its opaque subject UUID manifest rather than waiting 24 hours.
Every successful or denied unmasked list, detail, or exact-identity access records a minimized courses.registration.pii_viewed or courses.enrollment.pii_viewed audit event with actor/principal, target type/UUID or safe filtered-scope digest, field-class names, outcome, request/correlation ID, and timestamp. It never records returned values. Masked ordinary reads use bounded access metrics and do not create one audit row per listed object.
Audit separate request, start, completion/failure/cancellation, download allowed/denied, expiry, and privacy deletion. Record action, export/operation UUID, capability, safe filter digest, PII column-class boolean, row count/size/checksum, actor/principal, timestamps, outcome, and bounded reason code. Never record raw rows, field values, email, comment, name, certificate URL, consent value, filter values, filenames/paths, download links, authorization/cookies, request/response bodies, or provider data. Metrics expose only bounded export counts/bytes/duration/outcome by safe operation class—not Course/Cohort/actor/email dimensions.
Migration and evidence boundary
The selected #288 label is derived and requires no alias column, persistent digest, secret, uniqueness constraint, data migration, or backfill. Any #245-owned migration is limited to durable export operation/artifact state strictly required by this contract; it may not rewrite prerequisite registration, Enrollment, account, profile, campaign, consent, score, certificate, or legacy-source rows. Repository verification uses synthetic .invalid data and local/fake storage only. Protected-source reconciliation, production mapping/migration, privacy-right execution, provider/storage administration, deployment, and cutover remain with their owning issues and authorized HUMAN gates.
Forward/backward and migration-drift evidence must cover every #245-owned migration plus compatibility with the exact accepted prerequisite leaves. Counts/checksums/fingerprints are aggregate and non-sensitive; no row payload, email, name, profile value, token, source path, or production identifier enters an issue report or artifact.
Authentication, authorization, cache, and error behavior
- Signed-out Studio requests follow the accepted OIDC/login path with a same-host path-only return; browser back after sign-out/revocation shows no cached page. Missing/invalid/expired/revoked API credentials return
401with the standard safe envelope andWWW-Authenticate; Studio/API never mix session and bearer principals. - An authenticated principal lacking the function capability receives
403. Asking forinclude_piior exact identity lookup without the field capability receives403before query execution. - Once function access exists, missing and out-of-scope registration, Enrollment, export, Course, Cohort, campaign, or operation resources are indistinguishable generic
404after applying the authorized queryset. No response, count, latency assertion, or audit text discloses existence. - Invalid filters/page/sort are
400; stale/idempotency conflict is409; size is413/422; throttle is429; worker/storage/database failure is a redacted error/failed operation, never an empty list or zero metric. - Every Studio, admin API, CSV, denial, redirect target, and error response is
Cache-Control: private, no-store; Studio HTML isnoindex, nofollow,X-Robots-Tag: noindex, nofollow, absent from sitemap/search, and uses zero-TTL edge behavior. CSV sets safe attachment headers,nosniff, and a generic UUID-based filename. - Logs, traces, analytics, OpenAPI examples, fixtures, screenshots, issue reports, operations, and errors use synthetic
.invalididentities and contain no real/reversible learner or registration data.
Studio UX and states
Use the established private Studio shell. Desktop and narrow layouts provide semantic headings, landmarks, labelled filter controls, a captioned responsive table or equivalent reflow, keyboard-complete pagination, visible focus, 44 CSS px pointer targets, announced result/export status, readable timezone dates, and no color-only state.
Required states for both resources:
- empty authorized scope:
No registrations match these filters./No enrollments match these filters.with filters retained and no fake zero/error; - populated masked list, PII-authorized list, detail with real absence versus Hidden;
- invalid filter with linked error summary and preserved safe inputs;
- signed-out, denied capability, out-of-scope/missing generic denial, and revoked-session browser-back state;
- zero/one/many metrics, including a complete authorized zero;
- export confirmation showing exact scope, selected column class, and count; cancel; pending/running; ready with expiry; safe failed; too-large; expired; and denied download.
Do not display raw comment, email, name, organization, certificate URL, or consent value in screenshots. PII-field behavior is proven with synthetic canary values and screenshots may show a visibly redacted/masked fixture, not real data.
Acceptance criteria
- An authorized #288 owner response is recorded; PM substitutes the selected exact
member_labelgrammar, removes the decision/grooming labels, and records its implementation provenance plus the exact accepted prerequisite SHAs/pin/schema/migration/capability/OpenAPI fingerprints. The final field/export matrix is revalidated without an invented prerequisite field. - Courses-owned query/export services apply function, object, and field authorization before lookup/filter/count/serialization/export and have no read side effects; adapters perform no direct model query.
- All eight exact capabilities/scopes, role defaults, Course/Cohort assignments, support exact-lookup restriction, the selected nullable
member_labeldefault, and separately gated full MemberProfile/account/email fields behave identically for Studio/API/CSV; the label is never an input and nois_staff/superuser/Person/legacy-token bypass exists. - Exact UUID routes, resource fields, filters, stable ordering, page contract, authorized counts, metrics, and 400/401/403/404/409/413/422/429 semantics match this issue and have OpenAPI/registry parity.
- Registration and Enrollment lists/details preserve immutable target and cohort isolation; identity resolution uses accounts; no #133 rows, another Cohort, another learner's work, forbidden profile values, or mutable campaign pointer leaks into a result.
- Export preview/request/replay/cancellation/worker/download/expiry/deletion is durable, bounded at 10,000 rows, explicitly confirmed and reauthenticated, formula-safe, encrypted/private, exactly 24-hour-lived, authorization-rechecked, and fully audited without raw values.
- CSV masked/PII column sets exactly match the field matrix; every dangerous formula/control/Unicode/quote/delimiter canary remains one inert cell/row and no Never-serialized value appears.
- Empty, complete zero, invalid, missing/out-of-scope, storage/database failure, too-large, pending/running/ready/failed/expired/cancelled, signed-out/revoked, and denied-field states are deterministic, private/no-store/noindex, accessible, and never misreported as successful empty/zero.
- Studio pages use the private shell, correct copy/navigation, responsive/reflowing tables and forms, keyboard/focus/status behavior, and independent desktop/mobile/denied/export screenshots with synthetic redacted data.
- Focused Django/service/API/OpenAPI/browser/job/storage/security/adoption tests plus the graph-selected full verification pass, with artifact/log/audit canary scans and zero network/email/provider/production-data side effects.
Required Django, API, job, and repository scenarios
- Role matrix: site admin; in-scope/out-of-scope Course operator; support exact lookup versus browse/metrics/export; auditor masked read; read without PII; PII without read; export without PII/read; composed roles; inactive/disabled principal; removed object assignment; expired/revoked token/session; legacy token/
is_staff/Person non-authority. - Registrations: same learner in two Cohorts, two campaigns for one Cohort, campaign repoint, eligible/quarantined/invalid identities, converted/unconverted, privacy/marketing present/absent/legacy evidence, every PII and excluded field canary, and no #133 contribution.
- Enrollments: sibling/unrelated Cohorts, different visibility/learning/certificate/score/preference states, provenance present/absent, generated versus explicit display, historical aliases, unavailable/colliding identity, and no submission/review content or cross-Cohort selection.
- Exact UUID/detail/identity lookup; foreign/missing equality; every filter boundary; inclusive/exclusive timestamps; unknown/repeated/malformed/overlong values; stable tie ordering; page 1/last/out-of-range; page sizes 1/20/100/101; exact authorized total counts under concurrent foreign writes.
- Metrics zero/one/many, each group and status, partial authorization, campaign repoint, quarantine/conversion, Enrollment state/certificate/visibility, invariant-failure count, database error, and aggregate-only #133 baseline exclusion.
- API schema/registry/Studio parity for routes, operation IDs, scopes, object/field policy, filters, sorts, pages, result/error envelopes, CSV media/headers, and undocumented parameter rejection.
- Export 0/1/10,000/10,001 rows; masked/PII; exact replay/changed replay; stale scope/role before worker and before download; pending cancel/running cancel; worker retry/crash; database/storage failure; checksum/size mismatch; formula/control/Unicode/quote/newline canaries; multiple authorized downloads; 24-hour boundary; cleanup retry; subject deletion; no bytes after revocation/expiry.
- Cache/SEO/header tests for every success/redirect/denial/error/download; CSRF on Studio POST; Bearer-only API; rate/body/time limits; generic existence behavior; log/metric/audit/OpenAPI/operation/artifact-name canary scans; no model-write, account/profile projection, conversion, email/job beyond the export operation, network, provider, workflow, or production fixture.
Use uv/Make and the versioned change-selective verification plan. Export worker/storage tests use repository fakes and the approved development boundary; they never create a public object or access production storage/data.
Independent browser and artifact evidence
At approximately 1440×900 and 390×844, with synthetic .invalid fixtures, the independent tester captures and reads:
- Courses navigation plus masked Registration list/detail, filters, pagination, and empty state;
- PII-authorized versus no-PII behavior using visibly synthetic/masked canaries, without retaining unnecessary raw values;
- masked Enrollment list/detail for two sibling Cohorts with distinct preference/certificate state and a direct foreign-Cohort generic denial;
- Registration and Enrollment metrics at zero and populated states;
- invalid-filter error summary, signed-out/login return, denied capability, revoked-session browser back, and out-of-scope/missing safe state;
- export confirmation with exact count/column class, pending/running, Ready with expiry/download, too-large, safe failure, expired, cancelled, and denied download; and
- 320 CSS px reflow/200% zoom, keyboard-only filters/pagination/export/cancel/download, visible focus, status announcement, reduced motion, and JavaScript-disabled list/filter/detail/export-request flows.
CSV is non-HTML evidence: store only a synthetic export under .tmp/, inspect media/disposition/cache headers, exact columns/order/row count, formula neutralization, checksum, expiry behavior, and absence of forbidden fields; then remove it through the tested cleanup path. Every screenshot must show the expected page rather than debug/404 leakage or a broken layout and contain no real or unnecessary synthetic PII, token, URL secret, comment, consent payload, provider value, or production data.
Explicit non-goals
- No registration correction, status change, removal, legal erasure workflow, Enrollment conversion/edit/preferences/learning-in-public repair/removal, bulk create, or any other domain mutation; #246 owns approved management commands and #64 owns the wider rights workflow.
- No public registration/profile/dashboard/course page, learner API, CourseInterest, waitlist/capacity, communication audience/send/template/delivery/provider, scoring/certificate mutation, curriculum, complaint, peer-review, or unrelated #59 surface.
- No numeric-ID or email-in-path compatibility admin API, copied staff token, direct adapter/model query, broad serializer/model dump, mass assignment, arbitrary SQL/filter/sort, Django-admin ordinary workflow, unrestricted view-as, or public/signed download URL.
- No #242 schema/backfill/contract cleanup, #243 public form/conversion implementation, #244 learner preservation changes, #133 aggregate import/reconciliation, account/profile write, Person link, identity merge, privacy-policy selection, or source-pin advance.
- No unbounded/synchronous request-time CSV, arbitrary upload/path/filename, spreadsheet format, permanent export archive, production/protected data, real email/profile/comment/certificate/consent value, network/provider/email call, deployment, workflow dispatch, commit, merge, or push inside this issue's engineering/testing gates.
Delivery convention
Follow _docs/PROCESS.md. Engineering remains dependency-blocked even though grooming is complete. After a frozen uncommitted implementation, an independent tester recomputes the verification plan and inspects all required screenshots/artifacts; PM then performs final acceptance. Only after both pass may the engineer commit with Closes #245 and Refs #54; no pull request.
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 with _docs/specs/04-courses-and-cohorts.md, _docs/specs/06-studio-and-admin-api.md, _docs/specs/07-security-privacy-operations.md, and _docs/architecture/app-boundaries.md. Do not begin implementation until the listed dependencies, especially #247, #288, #242-#244, #32, #33, and #52, are accepted and integrated. Done means parity between the courses-owned services and the specified Studio and admin API routes, including authorized reads, metrics, and exports.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- django, python
- Domain
- api, authorization, backend, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 15/100