Skip to content

PR7a — feat(assessment): Attempt base-record split (Phase 1, additive)#8510

Draft
LWS49 wants to merge 11 commits into
lws49/feat-marketplace-pr6d-auditfrom
lws49/feat-marketplace-pr7a-attempt-base-record
Draft

PR7a — feat(assessment): Attempt base-record split (Phase 1, additive)#8510
LWS49 wants to merge 11 commits into
lws49/feat-marketplace-pr6d-auditfrom
lws49/feat-marketplace-pr7a-attempt-base-record

Conversation

@LWS49

@LWS49 LWS49 commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

PR7a — Attempt base-record split (Phase 1)

Stacked on #8507 (pr6d-audit). Phase 1 only: the additive base-record split — no preview feature.

Splits Course::Assessment::Submission into an Attempt base record (keeps the unrenamed course_assessment_submissions table via self.table_name) plus a Submission extension (course_assessment_submission_details), repairs every regression the split touched, and pre-emptively guards base-table reads so no preview row (added in PR7b) can leak into staff-facing counts/stats/reports.

Additive-only — no rename, no data risk. The extension table is new; answers/submission_questions keep submission_id (the base id). Backfill is a gentle 1:1 side-table insert. The unique index (assessment_id, creator_id) stays.

Commits

  • Extension table migration + additive split (Attempt base + Submission extension)
  • Preview-leak guards on base-table reads (statistics counts/grades/times/answers, grade_summary, discussion from_user, get-help reports) — behavior-preserving today (no previews exist yet)
  • Split-regression repairs (data-access surface, authorization rules, grading/ability/live-feedback)

Testing

Full non-feature sweep green at Phase 1 close (runs sequentially — repo can't run parallel rspec).

@LWS49
LWS49 force-pushed the lws49/feat-marketplace-pr6d-audit branch 3 times, most recently from 5230cdc to 86dac50 Compare July 24, 2026 04:44
@LWS49
LWS49 force-pushed the lws49/feat-marketplace-pr7a-attempt-base-record branch from 93216c7 to 68546f0 Compare July 24, 2026 04:46
@LWS49
LWS49 force-pushed the lws49/feat-marketplace-pr6d-audit branch from 86dac50 to 69326c2 Compare July 24, 2026 05:02
@LWS49
LWS49 force-pushed the lws49/feat-marketplace-pr7a-attempt-base-record branch from 68546f0 to b1b31c1 Compare July 24, 2026 05:03
LWS49 added 11 commits July 24, 2026 13:28
Phase 2 preview-leak gate: num_attempted/num_submitted/num_late/latest_submission
read the base course_assessment_submissions directly, so a preview (an Attempt with
no course_assessment_submission_details row) would inflate them. Add an INNER JOIN to
the extension table to restrict to real submissions. No-op on current data (no previews
exist yet); regression test seeds a bare preview attempt and asserts the counts hold.
…istics

Phase 2 preview-leak gate (continued). grade/duration/answer-correctness raw SQL and
the assessment submission_statistics/ancestor/live_feedback list builders read the base
course_assessment_submissions (or Attempt directly), so a preview (Attempt with no
extension row) would leak into staff-facing statistics. Add INNER JOINs to
course_assessment_submission_details (raw SQL) and .joins(:submission) (AR) to restrict
to real submissions. No-op on current data; regression test seeds a bare preview attempt
and asserts submission_statistics reports the student as unstarted (mutation-verified).
…ussion from_user

Phase 2 preview-leak gate (model layer):
- grade_summary raw SQL reads the base directly; add INNER JOIN to the extension so a
  graded preview (Attempt with no extension row) is not summed into gradebook grades.
- SubmissionQuestion.from_user and ProgrammingFileAnnotation.from_user join the Attempt
  base; join through to the extension (submission: :submission) to exclude previews.

Also fixes a Task 2 regression that shipped in ff465f5 and the Phase 1 gate missed
(topic_spec was not in the gate): ProgrammingFileAnnotation.from_user referenced
Course::Assessment::Submission.arel_table[:creator_id], but post-split that arel_table is
the column-less extension table -> PG::UndefinedTable. creator_id lives on Attempt now.
Regression covered by spec/models/course/assessment/submission_spec.rb (grade_summary)
and the now-green spec/models/course/discussion/topic_spec.rb (from_user).
…1 gate

Three sites broke when Submission was split into Attempt + extension but were not in the
12-file acceptance gate, so they shipped red in ff465f5:
- ProgrammingFileAnnotation.from_user referenced Submission.arel_table[:creator_id] (now
  the column-less extension table) -> PG::UndefinedTable (fixed in prior commit).
- answer/comment_notifier/annotated email called submission.course_user on the Attempt
  base; route through attempt.submission (the extension) like its replied sibling.
- ai_generated_post_service_spec called answer.submission.course_user (Attempt); the
  service already uses .submission.submission.course_user — update the spec to match.
assessment_ability.rb was not touched by the split (ff465f5) but its CanCan hash
conditions navigate associations that moved: Course::Assessment::Submission (extension)
no longer has assessment/creator_id/workflow_state (now on the Attempt base). Rules that
did 'Submission, assessment:/creator_id:' raised CanCan::WrongAssociationName, and the
attempting-hash (experience_points_record) applied via Answer.submission (now Attempt)
raised NoMethodError. Route Submission conditions through attempt:, and express the
attempting-owned check against the Attempt (workflow_state + creator_id, the owner per
validate_consistent_user). assessment_ability_spec: 75 examples, 0 failures (was 11 red).
Another gate-missed Task 2 regression (spec not in the 12-file acceptance gate).
More gate-missed Task 2 regressions (download/statistics/gradebook paths). The split left
the extension Submission unable to serve two relation-level access patterns its callers use:
- calculated attributes: register grade/grader_ids/log_count/graded_at on Submission
  (correlated on attempt_id) so @assessment.submissions.calculated(...) works — was raising
  'undefined method call for nil' in submissions_controller#load_submissions and the
  statistics/csv/zip download services.
- eager loading: Submission has no :answers/:assessment AR association (those moved to
  Attempt), so includes(:answers)/(:assessment) raised AssociationNotFound; route them
  through includes(attempt: ...).
Verified green: zip/csv/statistics/ssid download services + jobs, submissions_controller.
Final batch of gate-missed Task 2 regressions:
- base_auto_grading_job: update_exp?/update_exp received answer.submission (now the
  Attempt), whose awarder/awarded_at/points_awarded live on the extension -> the guard was
  always false and re-grade never recomputed EXP (points stale). Route through the extension.
- programming_ability: :create/:destroy_programming_files checked can?(:update, attempt);
  :update rules are on the extension Submission -> check answer.submission.submission.
- live_feedback ThreadConcern: SubmissionQuestion.where(submission_id: @submission) coerced
  the extension to its own id; match @submission.attempt_id (spec builds the SQ on the attempt).
- skills_mastery_preload: plucked Submission#id but belonging_to_submissions matches the
  attempt id -> pluck(:attempt_id).
- auto_grading_service/job specs: stub auto_grade_submission on the attempt, update_column
  :submitted_at on the attempt, reload the separate extension instance before points assertions.
…n questions)

Final Phase 2 preview-leak gate items:
- system-admin and instance-admin get-help usage reports join course_assessment_submissions
  directly; add INNER JOIN course_assessment_submission_details so preview attempts' live
  feedback never inflates get-help analytics.
- submission_questions#all_answers fetched via @assessment.attempts.find(...).submission and
  dereferenced it unguarded; a preview attempt yields a nil submission (previously only the
  incidental authorize!(:read, nil) blocked it). Raise RecordNotFound explicitly; regression
  test covers it.
…wer/SubmissionQuestion#attempt alias

The extension accessor chain read as answer.submission.submission because Answer#submission
returns the Attempt base (FK column is misleadingly submission_id). Add a reader-only #attempt
alias on Answer and SubmissionQuestion and rewrite the 8 chain sites to answer.attempt.submission.
Attempt#submission (the extension has_one) is unchanged. Behavior-preserving.
@LWS49
LWS49 force-pushed the lws49/feat-marketplace-pr6d-audit branch from 69326c2 to 74232c9 Compare July 24, 2026 05:29
@LWS49
LWS49 force-pushed the lws49/feat-marketplace-pr7a-attempt-base-record branch from b1b31c1 to 7afa40f Compare July 24, 2026 05:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant