DataTalksClub / DataTalksClub/website
Add target-native course registration and enrollment commands and bulk operations
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 parity epic #59 and preserves #235's rejection of copied CMP APIs.
PM disposition
GROOMED — product contract complete; engineering dispatch remains dependency-blocked. #246 has no independent owner decision and must not inherit decision/needs grooming merely because an upstream contract is unresolved. However, #245 is currently fail-closed on #288: until an authorized owner selects the shared member_label grammar and #245 is PM-reconciled, accepted, integrated, and green, #246 cannot freeze result presentation, start engineering, or enter testing. No masking format is chosen or inferred here.
#28 is owner-approved and CLOSED. Commit 68a1f203a8bd14b483ae381178ee9908b8051e1b on main records the accepted policy: grading repair requires reauthentication plus an explicit “are you sure” confirmation, and there is no dual/second-approver requirement now. Specification 06 separately requires every destructive and bulk action to show scope and expected count and use explicit confirmation.
Applied here: single and bulk learning-in-public repair are grading repair and require reauthentication plus explicit confirmation; Enrollment archive is destructive; every bulk action requires a server-recomputed scope/count/impact preview and explicit confirmation; no #246 action requires dual approval. The still-global mechanism parameters—Studio freshness, admin-API proof shape/TTL/one-use/binding, service-principal eligibility, credential-revocation interaction, and the operational trigger for reconsidering dual approval—remain owned by #32/#33 and their security/operations acceptance. #246 consumes those exact accepted mechanisms and fails closed while they are absent; it does not choose a second domain-specific policy. These are engineering dependencies, not a residual product-grooming decision.
#286 and #287 own the separate blocked CourseInterest and legacy-contract outcomes and add no command here. #246 consumes only the final accepted #242/#244 schemas and must revalidate them before engineering.
Outcome
Authorized operators perform the exact approved registration-to-Enrollment conversion, Cohort-specific Enrollment preference correction, learning-in-public repair, and guarded Enrollment archive/restore commands through Studio and /api/v1/admin/. Both adapters call the same courses-owned services, use UUID resources, revisions, idempotency, function/object/field authorization, redacted audit, and deterministic results. Bounded multi-row work uses one durable operation/job and never bypasses the public registration, identity, Cohort, or educational-history invariants.
Normative authority
_docs/PROCESS.md_docs/specs/04-courses-and-cohorts.md: immutable registration evidence, learner+Cohort Enrollment identity, preserved Cohort-specific preferences, Studio coverage, and target API conventions._docs/specs/06-studio-and-admin-api.md: one capability registry, shared service parity, UUID/revision/idempotency, bulk operations, audit, and guarded archival._docs/specs/07-security-privacy-operations.md: learner/consent protection, high-risk controls, retention, safe failures, observability, and no ghost Enrollment/email._docs/architecture/app-boundaries.mdand_docs/architecture/shared-primitives.md:coursesowns commands; Studio/API/jobs are adapters; commands own authorization/transaction/audit/durable work; leased jobs reload state and fence completion.- Parent #54; target schema #242; public registration and conversion primitive #243; learner preservation #244; management reads/exports #245; authorization foundations #32/#33/#52; high-risk decision #28; delivery boundary #49/#50.
Dependencies and freeze order
Do not engineer until a fresh base contains accepted and integrated:
- #224 then #51 for final Course/Cohort UUID identity, mappings, lifecycle, and object boundaries.
- #248 for the accepted profile/onboarding behavior; its #247 dependency supplies eligible durable-account resolution, survivor aliases, and accounts-owned normalized identity/profile reads.
- #242 for target
CourseRegistrationUUID/revision, immutable Cohort/campaign/profile/privacy/marketing evidence, eligibility/quarantine state, and protected legacy mapping. - #243 for the exact idempotent registration-to-Enrollment conversion primitive and provenance result.
- #244 for final Enrollment UUID/revision, learner+Cohort uniqueness, Cohort-specific preference fields, preserved work/score/certificate behavior, and compatibility fixtures.
- #32 for production role/object/field authorization and high-risk hooks, #33 for bearer principal/scope/operation/OpenAPI behavior, and #52 for final Course/Cohort staff assignments and object scope.
- #231/#234 are transitive through accepted #247/#248; #230 is transitive through #244.
- Accepted #245 supplies the final owner-approved #288 nullable
member_labelgrammar plus the management read/detail/operation field and presentation policy. The label is derived only from an already-authorized immutableMemberProfileUUID, is never a route/lookup/authorization/idempotency input, and does not grant PII access. #246 may be planned in parallel but cannot freeze result presentation, engineer, or enter testing before #288 is approved and #245 is PM-reconciled, accepted, integrated, and green. - Accepted #32/#33 production mechanism evidence for Studio reauthentication freshness, admin-API proof/TTL/one-use/binding, service-principal handling, credential-revocation behavior, and the high-risk policy version consumed by these actions. These are delegated global implementation dependencies, not owner choices inside #246.
#245 is a hard finalization dependency because it owns the accepted read/detail/operation field and masked-presentation policy consumed by these commands. #246 may be inspected or planned in parallel, but engineering must not freeze a candidate until #245 is accepted and integrated. #49 is required only for a future command that creates communication work. None of the allowed commands below creates an EmailDelivery intent, so #49/#50 are coordination boundaries rather than runtime dependencies for this issue.
Record exact prerequisite SHAs, adopted source pin, migration fingerprint, capability-registry digest, OpenAPI digest, accepted #28 decision commit, and consumed #32/#33 policy version in the engineer handoff. Any prerequisite change to resource fields, revisions, operations, roles, scopes, or routes invalidates the frozen plan.
Exact allowed command set
No other registration or Enrollment mutation is authorized by this issue.
1. Convert one registration
courses.registration.convert invokes #243's conversion service for one target-native registration UUID. It requires the registration's current revision, idempotency key, explicit confirmation, and bounded reason. It resolves the immutable registration Cohort and eligible durable learner, never a campaign's current pointer, and creates or returns exactly one Enrollment unique by learner+Cohort with provenance to that registration.
Exact replay or an already-proven identical conversion returns the original Enrollment and is no_change; changed key payload, stale revision, contradictory provenance, foreign Cohort, quarantined/incomplete registration, unavailable identity, archived/cancelled Cohort, or conflicting Enrollment fails closed. Conversion changes no registration snapshot/evidence, account/profile value, preference, work, score, certificate, or email state.
2. Correct one Enrollment's ordinary preferences
courses.enrollment.preferences.correct changes only these Cohort-owned fields, using explicit desired values rather than toggles:
display_name: trimmed escaped plain text, 1–255 characters, or the final accepted #244 explicit generated-alias reset sentinel;display_on_leaderboard: exact boolean;display_public_profile: exact boolean.
At least one declared field is required; undeclared/mass-assignment fields fail. Omitted fields remain byte-for-byte unchanged. The command advances only the Enrollment revision and invalidates only its Cohort projection. It never changes learner/account/profile identity, communication preferences, immutable registration evidence, certificate fields, score/position directly, submissions, reviews, consent, or another Cohort.
3. Repair learning-in-public eligibility
courses.enrollment.learning_in_public.repair accepts the explicit desired disabled=true|false; there is no toggle command.
truesetsdisable_learning_in_public, zeros only that Enrollment's stored homework/project learning-in-public components, recomputes affected totals and that Cohort leaderboard through the accepted courses services, and reports safe affected-row counts.falsere-enables future eligibility but never restores erased scores/links, guesses historical values, or triggers an automatic rescore. The result says that a separately authorized scoring workflow may be needed.
This is a high-risk grading repair. Preview and execution recompute the exact Enrollment/Cohort scope and affected counts. Drift, foreign child relation, ambiguous score state, stale revision, or failed recomputation rolls back the current row transaction.
4. Guardedly archive or restore one Enrollment
courses.enrollment.archive and courses.enrollment.restore change only the accepted target Enrollment lifecycle/archive fields; ordinary hard delete is forbidden.
Archive removes the Enrollment from active learner participation, active recipient selection, active leaderboard/public-profile projection, and new submission/review mutation while retaining its UUID, learner+Cohort identity, immutable provenance, timestamps, work, score, certificate/history, audit, and authorized historical reads. It does not delete or anonymize any learner record. Restore is allowed only when the same learner+Cohort identity remains unique, the durable identity is eligible, the Cohort lifecycle permits participation, and no contradictory replacement Enrollment exists; it restores participation but never reconstructs or rewrites prior preference/score/certificate values.
Legal correction, deletion, anonymization, restriction, and tombstone propagation belong to #64/#23's approved privacy workflow and are not aliases for archive. If the final #244 model does not supply a guarded Enrollment lifecycle/archive representation matching this behavior, PM must split that schema prerequisite rather than encode deletion or invent a boolean in an adapter.
Registration fields and commands deliberately immutable
There is no ordinary registration PATCH, correction, status edit, archive, delete, bulk create, campaign repoint, or consent/profile rewrite. Course/Cohort/campaign target, normalized verified-email snapshot, minimized profile snapshot, comment, privacy acknowledgement, marketing evidence, historical newsletter evidence, account relation, created timestamp, and legacy mapping remain immutable outside #64's approved legal workflow. Quarantine/reconciliation remains owned by #242's reviewed migration/reconciliation services; this issue supplies no operator override that manufactures eligibility.
Bulk command contract
Only these bulk actions are allowed:
convert_registrationsusingcourses.registration.bulk_convert;correct_enrollment_preferencesusingcourses.enrollment.bulk_correct;repair_learning_in_publicusingcourses.enrollment.bulk_learning_in_public_repair;archive_enrollmentsandrestore_enrollmentsusingcourses.enrollment.bulk_archive.
A request contains 2–100 rows. Each row has a client row_key unique within the request, one target UUID, expected target revision, and only the fields for the selected action. The request has one action, one idempotency key, one exact confirmation, one reason code/note, and one server-recomputed scope/count/impact preview digest. Learning-in-public repair additionally requires the accepted #32/#33 reauthentication/API-equivalent evidence because it is grading repair. Mixed actions, duplicate target UUIDs/row keys, more than 100 rows, undocumented fields, arbitrary upload/path/filter/query, or a body over the global bound fails before operation creation; the service never truncates or silently deduplicates.
Every bulk request returns 202 Accepted, one UUID Operation, and Location. There is no synchronous multi-row fallback. The operation uses the shared pending -> running -> succeeded|failed|cancelled model, reports progress_current/progress_total, and stores at most one safe result per input row:
row_key, target UUID, outcomesucceeded|no_change|failed|cancelled;- resulting target UUID/revision only when authorized; and
- one bounded allowlisted reason code, never learner values or exception text.
Rows execute in stable request order in independent transactions. Authorization, target scope, current revision, identity/Cohort invariants, and action preconditions are reloaded for every row. A row failure does not roll back prior successful rows. The terminal operation is succeeded only when every row is succeeded or no_change; any failed row makes it failed with success/no-change/failure/cancel counts while preserving completed work. Exact request replay returns the same operation/results; the same key with any changed row/order/revision/action/value/reason/preview/policy identity returns 409 and creates no second job.
Execution processes at most 25 rows between progress checkpoints. The durable job has a five-minute fenced lease, renews before expiry, carries only operation/action/opaque target identifiers, and reloads database state; it never carries email, profile/comment/consent values, credentials, or request bodies. A lease-lost worker cannot commit later progress/terminal state. Expired leases recover through the normal bounded retry path; maximum attempts remain the shared job-policy value. One operation has a 30-minute elapsed execution deadline; a retry after that boundary records untouched rows as failed: operation_deadline_exceeded rather than running indefinitely.
Cancellation is accepted only for pending or running bulk operations with current operation revision/If-Match. Pending cancellation marks every row cancelled and creates no domain mutation. A running worker checks cancellation before every row and after every committed row; the in-flight row completes or rolls back atomically, completed rows remain, untouched rows become cancelled, and stale worker completion is fenced. Terminal cancellation/retry is a conflict/no-op according to the shared operation contract; cancellation never compensates completed domain changes.
Reason, preview, revision, idempotency, and response contract
Every command requires:
- canonical target UUID(s), never numeric IDs/email/slugs in mutation paths;
- current resource
revision(If-Matchin admin API; hidden exact revision in Studio); Idempotency-Key, scoped to capability plus actor/API principal;- a required allowlisted
reason_code:learner_request,support_correction,policy_enforcement,data_repair,duplicate_resolution, orother; - optional escaped plain-text
reasonof 1–200 characters when present, required forother, with an explicit instruction not to enter email, name, comment, consent, credential, or other learner data; - server-generated preview digest bound to capability, actor/principal, ordered target UUIDs, expected revisions, desired field names/values, exact authorized Course/Cohort scope, affected/count impact, reason code, policy version, and idempotency key; and
- explicit confirmation for every command; learning-in-public repair uses an action-specific “are you sure” confirmation plus accepted #32/#33 reauthentication evidence, while archive and every bulk action must show the exact server-recomputed scope/count/impact before confirmation.
Preview is side-effect free, authorization-scoped before lookup, short-lived under the accepted shared #32/#33 policy, and recomputed at execution. Changed scope/count/revision/impact/digest is 409 preview_stale; stale target revision is 409 revision_conflict; exact replay returns the original result; changed-payload key reuse is 409 idempotency_conflict. Invalid input is 400, authentication 401, missing function capability 403, missing/out-of-scope object after function access generic 404, body size 413, semantic batch limit 422, and rate/cost limit 429. Database/job failure is a safe failed response/operation, never success, empty, or zero.
Capability, role, object, and field policy
Register these exact capabilities/scopes with one registry row per Studio/API action:
| Capability | Default role/scope |
|---|---|
courses.registration.convert |
course_operator function grant and active #52 Course/Cohort assignment whose accepted role allowlist contains the key; site_admin |
courses.registration.bulk_convert |
separate bulk function grant and active in-scope #52 assignment whose accepted role allowlist contains the key; site_admin |
courses.enrollment.preferences.correct |
course_operator function grant plus active in-scope #52 assignment; support_operator with this exact correction capability and exact authorized UUID scope only; site_admin |
courses.enrollment.bulk_correct |
separate bulk function grant and active in-scope #52 assignment whose accepted role allowlist contains the key; site_admin |
courses.enrollment.learning_in_public.repair |
course_operator function grant plus active in-scope #52 assignment whose accepted role allowlist contains the key; site_admin |
courses.enrollment.bulk_learning_in_public_repair |
separate bulk function grant and active in-scope #52 assignment whose accepted role allowlist contains the key; site_admin |
courses.enrollment.archive |
course_operator function grant plus active in-scope #52 assignment whose accepted role allowlist contains the key; site_admin |
courses.enrollment.bulk_archive |
separate bulk function grant and active in-scope #52 assignment whose accepted role allowlist contains the key; site_admin |
Restore uses the corresponding archive capability but remains a distinct registered action/policy row. Bulk is never implied by the single-row capability. support_operator cannot convert, bulk, repair scores, archive, or restore. auditor is read-only. Generic is_staff, superuser, group name, public Person, compatibility token, legacy staff token, and Django-admin status are not authority. API bearer scopes use the exact keys. Service-principal eligibility consumes the explicit #33 per-capability/object policy; an absent or stale mapping fails closed, and no human role name or bearer credential alone grants authority.
Authorization checks function, active #52 Course/Cohort assignment and its accepted per-capability role allowlist, target object, and writable fields before lookup or preview. Except for the explicit global site_admin, neither the course_operator function group nor an assignment grants access alone. Role/assignment creation, mapping, expiry, and revocation remain #32/#52; #246 consumes their current policy and cannot create a grant. A field capability never broadens object scope. API service principals require the exact #33 capability scope and object assignment; a human role name, Bearer credential, or target UUID alone is never sufficient.
Results consume #245's accepted nullable member_label, field allowlist, Hidden/Not provided semantics, and operation-result presentation. The label is derived at authorized presentation time from the already-authorized immutable profile UUID; it is not stored on Registration, Enrollment, job payload, idempotency record, or domain result, and it never substitutes for the target UUID. Full PII is neither required nor returned by these commands. Until #288 and #245 are accepted, all label-bearing result presentation remains unfrozen and engineering is blocked.
#52 view-as and impersonation boundary
Copied impersonation is not an authority path. #52's scoped view-as context remains visibly read-only and bounded to one authorized Enrollment. Every #246 preview or mutation route, unsafe method, service command, bulk-operation creation/cancellation, and API equivalent must fail closed while view-as is active; hiding a button alone is insufficient. The viewed learner never becomes the actor, never contributes permissions, and never supplies idempotency or audit attribution. An operator must exit view-as and execute under their own current function, object, field, reauthentication, and confirmation authority.
Allowed and denied audit events attribute the real staff user or API principal and may record only a bounded view-as-active denial marker, never a learner session, profile value, or substituted identity. View-as start/exit/expiry/revocation and its read-only learner presentation remain owned by #52; #246 adds only command-side denial and regression evidence.
Studio and admin API parity
Studio actions live under the established private Courses shell:
POST /studio/courses/registrations/<uuid>/convert/POST /studio/courses/registrations/bulk-convert/POST /studio/courses/enrollments/<uuid>/preferences/POST /studio/courses/enrollments/<uuid>/learning-in-public/POST /studio/courses/enrollments/<uuid>/archive/POST /studio/courses/enrollments/<uuid>/restore/POST /studio/courses/enrollments/bulk-operations/- shared authorized operation detail/cancel routes supplied by the management foundation.
Admin API parity uses:
POST /api/v1/admin/course-registrations/<uuid>/convertPOST /api/v1/admin/course-registration-conversionsfor bulkPATCH /api/v1/admin/course-enrollments/<uuid>/preferencesPOST /api/v1/admin/course-enrollments/<uuid>/learning-in-public-repairPOST /api/v1/admin/course-enrollments/<uuid>/archivePOST /api/v1/admin/course-enrollments/<uuid>/restorePOST /api/v1/admin/course-enrollment-bulk-operations- shared
GETand revision-guarded cancellation under/api/v1/admin/operations/<uuid>.
Single-row first execution returns the accepted command result; exact replay returns the same result without another mutation. Bulk always returns 202. OpenAPI 3.1 declares exact operation IDs, capabilities, request allowlists, If-Match, idempotency/preview/confirmation and #32/#33 high-risk evidence requirements, result/operation schemas, and every safe error. Studio and API parity tests compare authorization, preview, service request, mutations, revision, audit, job/operation creation, row ordering/results, and errors—not merely route presence.
Every Studio/API/operation/denial/error response is private, no-store; Studio is noindex/nofollow and zero-TTL. Studio uses CSRF; admin API is Bearer-only and never accepts session authority.
Delivery, audit, privacy, and observability boundary
These commands create no EmailDelivery, Relay request, Datamailer/SES call, provider work, template render, callback, or send. If later product scope requires communication, a separately groomed command must atomically create one #49 intent/job after the domain transaction and preserve #50's purpose/sender gate.
Audit allowed and denied preview/request/start, each row outcome, progress, stale/replay/conflict, cancellation, recovery/lease loss, and terminal result. Record only action/capability, target UUID/type, Course/Cohort UUID scope, actor/principal ref, policy/preview/idempotency hashes, expected/result revision, bounded reason code, reason length, safe affected/count summary, outcome/reason code, request/correlation/job/operation IDs, and timestamps. The optional reason text is restricted audit data and passes shared redaction; it never appears in operation JSON, ordinary logs, metrics, screenshots, or error bodies.
Never record or serialize raw email/name/display value/comment/profile/consent/certificate URL, submission/review/link content, account aliases, authorization/cookies/CSRF, request/response bodies, preview proof, provider data, raw SQL/exception, or reversible identity digest. The accepted #288 member_label is also absent from ordinary logs, metrics, traces, job context, idempotency state, and stored operation-row results. A shared append-only audit may include it only as #288's already-authorized immutable target-label snapshot beside target type/UUID; denied, unknown, unavailable, or out-of-scope targets receive no label, and a stricter #32 audit registry stores only the opaque UUID. The label is never authorization or correlation evidence. Metrics use bounded action/outcome/duration/row-count buckets only—never actor, learner, label, email, Course/Cohort, reason text, or target UUID dimensions. Audit follows the approved one-year minimized retention; domain educational history follows #23/#64, not operation cleanup.
Migration and evidence boundary
#246 expects no Registration, Enrollment, account, profile, Course/Cohort, consent, score, certificate, or legacy-source schema/data migration. It does not add a stored member label, identity digest, archive flag, revision field, provenance repair, or backfill. The accepted #242/#244 models must already provide every target UUID, revision, provenance, preference, and guarded lifecycle field this command contract needs. If the integrated prerequisite schema cannot represent guarded archive/restore or another required invariant, engineering stops and PM splits/re-grooms the missing schema owner; an adapter, compatibility column, boolean, or data rewrite is not invented here.
Any migration unexpectedly required by an issue-owned idempotency/audit/operation linkage must return to PM before implementation and be proven strictly necessary rather than duplicating #31/#32/#33 shared primitives. Repository evidence records the exact prerequisite migration leaf set, deterministic migration-plan/schema fingerprint, forward/backward compatibility for any approved issue-owned migration, and makemigrations --check/migration-drift results using synthetic local data only. Production/protected migration, reconciliation, source access, row export, destructive contraction, and cutover remain with #60/#64 and their explicit authority; aggregate counts/checksums in reports contain no row values or source identifiers.
Accepted high-risk policy and delegated mechanism
The authoritative #28 owner record and main commit 68a1f203a8bd14b483ae381178ee9908b8051e1b settle the product decision:
courses.enrollment.learning_in_public.repairand its bulk counterpart are grading repair, so Studio/admin API execution requires reauthentication and an explicit action-specific “are you sure” confirmation.- Enrollment archive is destructive, and every bulk action is bulk work; each requires a server-recomputed preview showing exact authorized Course/Cohort scope, ordered target count, and bounded impact, followed by explicit confirmation.
- Single conversion, preference correction, restore, and every other command retain the explicit-confirmation/revision/idempotency contract in this issue; no action may degrade to a generic click or implicit consent.
- No #246 action requires dual/second-approver approval now. Reconsideration belongs to the shared #32/#33 security/operations policy only if operational evidence warrants it; #246 does not invent a local trigger or second approval state.
- Closed #20 remains authoritative for emergency recovery: management API first and direct database access last resort, with no website break-glass credential.
#32/#33 own the reusable mechanism details: Studio authenticated-at/step-up freshness and failure behavior; the admin-API proof format, TTL, one-use storage/replay and principal/action/scope/count/impact/revision/idempotency/preview binding; exact service-principal grants or denial; immediate credential-revocation effect on outstanding proof; and the accountable operational review trigger. Their absence is fail-closed and blocks engineering/integration, but it does not make #246 product-ambiguous or justify a second domain-specific implementation.
Exact handoff to #59
Only after tester PASS, PM acceptance, focused integration, and on-call-green current main does #246 publish its PII-free command-contract handoff for #59. The handoff records the exact #246 issue/implementation fingerprint; accepted #245/#288 presentation and #32/#33/#52 policy identities; prerequisite merge/source/migration/schema fingerprints; canonical service/result and single/bulk action matrix; Studio route and admin API/OpenAPI operation IDs; capability/function/object/field/service-principal and view-as-denial policy; revision/idempotency/preview/high-risk/operation/cancellation behavior; audit/redaction/retention identity; zero-email/provider/network boundary; fixture/test manifest; and graph-selected verification digests.
#59 consumes those rows exactly once as target_native source/completeness entries. It may verify parity or fill a presentation/registry adapter gap identified by its frozen no-gap matrix, but it may not redefine a #246 command, mask, role, object/field rule, migration, result, high-risk control, impersonation behavior, or delivery boundary. An open/decision-blocked #245/#288, local candidate, partial handoff, stale evidence envelope, or non-green integration is not a #59 input.
Acceptance criteria
- The closed #28 owner record and main commit
68a1f203a8bd14b483ae381178ee9908b8051e1bare recorded and applied; accepted #245/#288 presentation plus exact #32/#33/#52 mechanism/role/object/view-as policy and prerequisite SHAs/pin/migration/capability/OpenAPI fingerprints are frozen. - Only the four command families above exist; registration snapshots/evidence and forbidden Enrollment/account/profile/certificate/work fields cannot be mutated or mass-assigned.
- Single conversion uses #243 and is atomic, cohort-isolated, revision/idempotency safe, replay/concurrency safe, and creates no preference/history/email side effect.
- Preference correction changes only declared Cohort-specific fields; learning-in-public repair implements explicit desired state, exact zero/recompute behavior, and never restores guessed values.
- Archive/restore is guarded, reversible, history-preserving, uniqueness/lifecycle checked, and cannot masquerade as legal deletion/anonymization.
- Every command has UUID, revision/
If-Match, idempotency, reason, preview, confirmation, accepted #32/#33 reauthentication evidence where required, safe conflict semantics, and one redacted audit trail. - Exact capabilities, conjunctive #32 function plus #52 assignment-role/object scope, field allowlists, API scopes, service-principal grants, support/bulk boundaries, and view-as mutation denial are identical in Studio/API; no legacy/staff/superuser/Person/impersonation bypass exists.
- Bulk accepts 2–100 unique ordered rows, always returns one durable
202operation/job, executes per-row transactions, reports deterministic bounded results, fences leases/progress/completion, recovers safely, and honors the exact cancellation/deadline boundary. - Partial failure/replay/stale/denied/cancel/recovery leaves every completed row truthful, never double-applies a row, never hides failure as success, and exposes no out-of-scope existence or learner value.
- Audit/log/metric/operation/OpenAPI/error canary scans prove the exact redaction, #288 target-label-snapshot, and retention boundary; the migration/schema fingerprint proves no unauthorized domain/backfill change; zero network, provider, sender, production-data, or workflow-dispatch side effect occurs.
- Studio uses the private shell with exact scope/count/impact, accessible review/confirm/cancel/status/results, correct safe copy, and responsive/keyboard/no-JS behavior.
- Focused service/API/OpenAPI/capability/job/concurrency/security/adoption/migration-drift tests plus graph-selected full verification pass; independent desktop/mobile/denied/view-as/partial/cancel screenshots are inspected and PM accepts them; the exact PII-free handoff fingerprint is published for #59.
Required Django, API, job, and repository scenarios
- Role matrix:
course_operatorfunction without assignment; assignment without function; every accepted #52 assignment role allowlist in exact Course, parent-authorized Course scope, sibling Cohort, and other Course; support exact preference correction versus forbidden browse/bulk/convert/repair/archive; site admin; auditor; composed roles; removed/expired assignment; inactive/disabled user/principal; explicit/missing service-principal object grant; expired/revoked credential/session; active/expired/forged view-as; legacy token/is_staff/superuser/Person non-authority. - Conversion: eligible/quarantined/incomplete registration; stale target; repointed campaign; same learner in sibling Cohorts; already converted; contradictory provenance; conflicting Enrollment; exact/changed replay; parallel first conversion; rollback at every write; zero changed snapshot/preference/email state.
- Preferences: each allowlisted field alone/together; omitted/unknown/forged fields; blank/generated display alias boundary; true/false combinations; stale revision; sibling/foreign Enrollment; account communication preferences and all historical work unchanged.
- Learning in public: enable/disable/no-change; zero/nonzero homework/project components; exact totals/leaderboard recomputation; malformed foreign child; rollback; re-enable without restoration; concurrent scoring/revision drift; sibling Cohort unchanged.
- Archive/restore: no-work and rich-history Enrollment; active/archive/no-change; new-work/recipient/leaderboard denial after archive; authorized history retained; invalid learner/Cohort; conflicting replacement; cancelled/archived Cohort; legal deletion remains separate.
- Bulk sizes 0/1/2/25/26/100/101; duplicate row key/UUID; mixed action; malformed/overlong body; stable order; all success/no-change; first/middle/last failure; authorization/revision drift between preview and row; exact/changed replay; concurrent same operation.
- Job lifecycle: pending/run/progress/finish; crash before first row, before/after commit, before progress; lease expiry/loss/renewal; retry exhaustion; unknown handler; database failure; 30-minute deadline; stale worker cannot complete; operation/result JSON bounds.
- Cancellation: pending, before first row, between rows, during a row, after last row/before finish, already terminal, stale operation revision, repeated request, lost worker; assert completed rows persist and untouched rows are cancelled exactly once.
- Parity/OpenAPI: every route/operation ID/capability/policy/field/error/result/audit/job side effect matches; unknown parameters rejected; UUID-only route resolution; 400/401/403/404/409/413/422/429 and safe failed-operation behavior.
- Security: CSRF, Bearer-only API, accepted #32/#33 freshness/proof/replay/expiry/binding and credential-revocation behavior, generic out-of-scope 404, private/no-store/noindex, rate/body/time bounds, redaction canaries, no direct model adapter writes, no Datamailer/Relay/SES/network/workflow/production fixture.
Independent browser and artifact evidence
At approximately 1440×900 and 390×844 with synthetic .invalid fixtures, the independent tester captures and reads:
- masked registration detail conversion preview, exact Course/Cohort/count/impact, confirmation, success, replay, stale-revision, denied/out-of-scope state;
- Enrollment preference correction with only allowed fields, validation errors, save/refresh/back behavior, and sibling Cohort unchanged;
- learning-in-public disable preview showing affected counts, Running/complete result, re-enable warning that scores are not restored, and safe failure;
- archive and restore review/confirmation, retained-history state, forbidden new participation, and conflict denial;
- bulk review for mixed success/no-change/failure, Pending/Running/progress/Failed summary, cancellation before and during work, replay, and operation detail;
- signed-out return, stale-auth/API-equivalent denial as representable in Studio, removed capability/assignment, active view-as with visible read-only banner and every command denied, revoked-session browser back, generic missing/out-of-scope page; and
- 320 CSS px/200% zoom, keyboard-only workflow, visible focus, 44 CSS px pointer targets, announced asynchronous status, reduced motion, and JavaScript-disabled review/confirm/status/cancel paths.
Screenshots show masked synthetic identities only—never raw email, display name, comment, consent, certificate URL, submission/review content, reason text, proof/token, provider value, or production data. The tester verifies expected pages rather than debug/error leakage and stores all artifacts under .tmp/.
Explicit non-goals
- Preserve #235: no copied staff-token/email-only bulk create, numeric-ID GET/PATCH, compatibility
/api/mutation, direct model adapter write, broad serializer/model dump, campaign-pointer target, consent/profile rewrite, or request-time Datamailer sync. - No ordinary registration correction/status/removal, anonymous/admin-created registration, target/identity/profile/privacy/marketing/newsletter mutation,
CourseInterest, waitlist/capacity, production import, reconciliation override, or arbitrary upload/filter-driven mass action. - No learner self-service redesign, public registration flow, management read/export implementation (#245), curriculum/scoring/certificate/complaint/peer-review commands, communication audience/send/template/provider/Relay implementation, or unrelated #59 parity.
- No hard deletion of Enrollment/work/history, legal deletion/anonymization implementation, profile/account preference mutation, Person linking, identity merge, view-as/impersonation implementation or mutation-through-view-as, Django-admin ordinary workflow, source-pin advance, or production/protected data.
- No commit, push, merge, deployment, workflow dispatch, provider call, real recipient, secret access, or lifecycle shortcut inside PM grooming/engineering/testing.
Delivery convention
Follow _docs/PROCESS.md. Grooming is complete; engineering remains blocked until the listed accepted foundations, including #32/#33/#52, are integrated. Engineering then leaves one frozen uncommitted candidate; a separate tester recomputes the verification plan, runs focused/full evidence, and inspects screenshots; PM performs final user acceptance. Only then may the engineer commit with Closes #246 and Refs #54; the orchestrator locally merges/pushes without a pull request and dispatches on-call.
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 the dependency issues #242–#245, #32, and #33. Confirm their accepted schemas, policy versions, and digests before inspecting the courses services and Studio/API adapters. Done means all listed single and bulk commands use the shared services, guards, durable operations, audit, and deterministic results without adding unauthorized mutations.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, backend, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 25/100