Skip to content

Instantly share code, notes, and snippets.

@ckozus
Created April 3, 2026 16:39
Show Gist options
  • Select an option

  • Save ckozus/7b0f4cf1a17a7fb20e8b48e1470b7a47 to your computer and use it in GitHub Desktop.

Select an option

Save ckozus/7b0f4cf1a17a7fb20e8b48e1470b7a47 to your computer and use it in GitHub Desktop.
Unclaimed Students - Implementation Plan (v2, aligned with IDEA-360)

Unclaimed Students - Implementation Plan

Context

This plan supports IDEA-360 (Unclaimed Student — Decouple Student from User Account), the foundational prerequisite for the Student File Importer (IDEA-351) and College Driven Registration. The goal is to make the Student-User association optional, introduce the claim_state model, and ensure all existing platform features work correctly when a student has no user account.

Key constraints:

  • Email and contact information are genuinely optional for unclaimed students
  • Phase 1 is infrastructure only — no creation paths ship in this release
  • Student Accounts page redesign is handled separately by Product
  • Per-college local option gates the feature
  • Unclaimed students will always have a CSA (since they're always associated with a college)
  • Reports will show blank fields for user-sourced data (email, login, cell_phone) — accepted behavior

1. Schema & Model Changes

Migration

Change Detail
claim_state enum on students Values: unclaimed, invitation_pending, claimed. Default: unclaimed.
Backfill claimed Set claim_state = 'claimed' where user_id IS NOT NULL AND user confirmed_at IS NOT NULL
Backfill invitation_pending Set claim_state = 'invitation_pending' where user_id IS NOT NULL AND user confirmed_at IS NULL
belongs_to :user Add optional: true (user_id is already nullable in DB)

Safe Accessor Pattern (app/models/student.rb)

Replace delegate(:cell_phone, :cell_phone=, :email, :email=, :login, to: :user) (line 60) with safe methods. The delegate is removed only on student.rb — the CSA→student delegation (CSA line 54) stays in place and chains through our safe accessors.

def email
  user&.email || read_attribute(:email)
end

def cell_phone
  user&.cell_phone || read_attribute(:cell_phone)
end

def login
  user&.login
end

For claimed students, user&.email returns the user's email (identical to current behavior). For unclaimed students, it falls through to the student's own column (or nil if absent).

Delegation chain: csa.emailstudent.email (CSA delegation stays) → safe accessor → works for both claimed and unclaimed.

cell_phone storage gap: The students table has an email column but NO cell_phone column. Currently cell_phone only exists on the users table. The CSA table has phone (a different field) but not cell_phone. With the safe accessor, student.cell_phone for unclaimed students returns user&.cell_phone (nil) || read_attribute(:cell_phone) (nil — no column). If we ever need to store cell_phone for unclaimed students, we'll need to add the column. (See follow-up question #1.)

Guard before_create callback (lines 50-54)

Currently assumes user is present (self.user.email, self.user.name, self.user.roles). Wrap in if self.user.present?.


2. Impact Inventory

2a. Crash Points (Must Fix)

File Line Code Issue
app/models/student.rb 60 delegate(:email, :cell_phone, :login, to: :user) Crashes if user nil
app/models/student.rb 50-54 self.user.email in before_create Crashes if user nil
app/models/student.rb 810 self.user.branded_host Crashes if user nil
app/models/student.rb 344 first_approver.user.email in get_approver Crashes if approver user nil
app/mailers/active_flow_step_mailer.rb 190 @target_object.student.user appended to array Appends nil, downstream fails
app/mailers/active_flow_step_mailer.rb 396 sent_addresses << user.email Adds nil without .present? check
app/mailers/students_reminder_mailer.rb 29 user = student.user then calls methods Crashes
app/mailers/college_student_application_mailer.rb 7 user = @student.user Crashes
app/mailers/survey_mailer.rb 34 @user = survey.student.user Crashes
app/jobs/background_student_batch_action.rb 13 student.user.destroy Crashes if user nil
app/models/csub_application_api.rb 66 student.user.email Crashes if user nil
app/models/reports/college/all_student_workflows_report.rb 66 student.user.email Crashes

2b. Already Safe (No Changes Needed)

Area Why Safe
All SIS integrations (65+ steps) Use student_number from CSA, never student.user
EthosApi person matching Checks student.email.present? before adding to payload
Denormalized tables LEFT OUTER JOINs produce NULLs
44 of 45 reports Use denormalized columns (NULLs are valid)
Parent/approver email getters Use &. safe navigation
get_parent_email, get_parent_info Check .present? before returning
Mailer address cleanup (line 361) .flatten.compact.reject(&:blank?) removes nils
ability.rb permissions Rules naturally don't match nil user
student_de_course.rb:get_target_user Dead code
Admin pages rec.user.login rescue '' Rescue handles nil
Parent/approver workflows StudentAnonymousParticipant belongs_to the parent/approver's OWN user account, not the student's. Unaffected by student being unclaimed.

2c. Notifications

Principle: Unclaimed students have no user → "Student" notification role is always empty. HS and College roles still work.

Mailer Fix
active_flow_step_mailer.rb (line 190) Guard: only add student.user if present. Fix line 396: check .present? before pushing.
students_reminder_mailer.rb (line 29) Early return if student.user.blank?. Scope reminder queries to exclude unclaimed.
college_student_application_mailer.rb Early return if user blank.
survey_mailer.rb Early return if user blank.

2d. Admin Pages

Page Impact
Admin Students Index rec.user.login rescue ''Safe
Admin Students Show row :user shows nil — Acceptable. Add claim_state indicator.
Admin CSA No direct student.user refs — Safe
Admin "View As" Not possible for unclaimed students — hide the button
Admin "Create Registration" Works if CSA exists

2e. Reports

Report Impact
AllStudentWorkflowsReport (line 66) Crashes — use safe accessor. Will show blank email.
HelperMethods#student_account_status Already handles nil, needs new status for unclaimed
All other reports Safe — use denormalized columns, will show blanks

Blank fields for email/login/cell_phone in reports is accepted behavior.

2f. Workflow Steps (College-Specific)

Low risk — unclaimed students are opt-in per college:

Step Change
steps/ivytech/* (2 files) student.user.emailstudent.email
steps/boisestate/* (1 file) student.user.cell_phonestudent.cell_phone
steps/niacc/* (1 file) student.user.cell_phonestudent.cell_phone

2g. Views / Templates

View Fix
6 reminder email templates Mailer guard prevents rendering; add &. as defense-in-depth
shared/form_parts/_student_signature*.html.erb (3 files) Add &. on user.scanned_signature. Add comment: scanned signatures not supported for unclaimed students.
college_student_applications/_name_and_address_group_ajax_validations.html.erb Add &. on student.user.id
_college_student_applications_page_side.html.erb Already guarded

2h. Controllers & Background Jobs

File Fix
students/students_controller.rb before_action guard on account pages
student_accounts_controller.rb Guard destroy: if no user, destroy student directly
background_student_batch_action.rb Guard student.user before destroy
background_send_student_reminders.rb Filter unclaimed students from query

2i. Other Models

File Fix
form.rb (lines 52, 134, 217) Conditionally add user to liquid_params
csub_application_api.rb (line 66) student.user&.email || ''

3. College-Managed Registrations (Context)

The existing registration infrastructure supports unclaimed students:

  • CreateCourseRegistrationForm validates student_id + course_section_id, does NOT require user. Requires a completed CSA.
  • CreateStudentDeCourseRegistrationService creates StudentDeCourse, triggers active_flows. No user dependency.
  • All SIS integrations use student_number from CSA — verified across 65+ steps.
  • Unclaimed students will always have a CSA since they're always created in the context of a college.

4. Follow-Up Questions

Resolved

Question Resolution
Claim state model claim_state enum (unclaimed/invitation_pending/claimed) per IDEA-360
Future upgrade path Modeled in claim_state transitions
SIS student_number IDEA-361 Connection Records + Entity Resolution
Student Accounts page Product redesigning separately
Admin "View As" Not possible — hide button
Unclaimed student visibility IDEA-359 planned
CSA creation Unclaimed students will always have a CSA
Reports blank fields Accepted — show blanks
Parent/Approver workflows Unaffected — SAP belongs_to parent's own user, not student's user
Scanned signatures Not supported for unclaimed students — guard with comment
Backfill unconfirmed users invitation_pending (not claimed)

Still Open — Needs Input

  1. cell_phone column on students table: The cell_phone column only exists on the users table. The students table has email but not cell_phone. The CSA has phone (different field) but not cell_phone. With the safe accessor, student.cell_phone returns nil for unclaimed students since there's no column to fall through to. The CSA delegation chain (csa.cell_phonestudent.cell_phoneuser.cell_phone) also returns nil. The CSA's get_any_value('cell_phone') can check application_field_values as an alternative source. Do we need to add a cell_phone column to the students table (mirroring how email already works)?

  2. Student merge across claim states: If an unclaimed student is merged with a claimed student, does the merged student keep the user account? Any restrictions?


5. Testing / QA Plan

Setup

Create via console or scripts on the Review App:

  • Unclaimed student (claim_state: 'unclaimed', no user, email may or may not be present)
  • CollegeStudentApplication for the unclaimed student (with student_number, demographics)
  • StudentDeCourse registration (triggers active_flow)
  • Side-by-side with a claimed student for regression comparison

Category A: Admin Pages

# Test Expected
A1 Admin Students Index: unclaimed student in list Listed, login column blank
A2 Admin Students Show: view unclaimed student All panels render, user row blank, claim_state shown
A3 Admin Students Show: sidebars Load without error
A4 Admin Students Show: "View As" button Hidden for unclaimed students
A5 Admin Students Show: "Create Registration" Works if CSA exists
A6 Admin CSA: unclaimed student's CSA Listed and shown normally
A7 Admin Registration Active Flows Listed, user fields NULL

Category B: Reports

# Test Expected
B1 AllStudentWorkflowsReport Email blank, no crash
B2 CourseSectionRosterReport Email blank (denormalized NULL)
B3 RegistrationsAbbreviatedReport Email/Login blank
B4 CSV export of each report Exports without error
B5 HelperMethods#student_account_status Returns correct status for unclaimed

Category C: Workflows / Active Flows

# Test Expected
C1 Create registration for unclaimed student (console) StudentDeCourse created, active_flow launched
C2 Workflow steps execute No crashes
C3 Notification to "Student" role Skipped (no user/email)
C4 Notification to "College" role Sent normally
C5 Notification to "HS" role Sent normally
C6 Form-generating steps Forms render, signature area gracefully empty
C7 SIS integration steps Use student_number from CSA
C8 Parent/approver workflow steps Work normally (parent has own user account)

Category D: Mailers

# Test Expected
D1 ActiveFlowStepMailer for unclaimed student Student role skipped, others notified
D2 StudentsReminderMailer for unclaimed student Early return, no crash
D3 CollegeStudentApplicationMailer Not triggered for unclaimed students
D4 SurveyMailer for unclaimed student Early return, no crash

Category E: Denormalization

# Test Expected
E1 Denormalize unclaimed student's registration User columns NULL
E2 Denormalize unclaimed student's CSA user_email, user_cell_phone NULL
E3 Denormalization validation (admin) No false mismatches

Category F: Edge Cases & Regression

# Test Expected
F1 Unclaimed student with email on student record Email appears in APIs that check it
F2 Unclaimed student without email All blank, no crashes
F3 Unclaimed student with CSA but no student_number SIS steps fail gracefully
F4 Claimed student full self-service flow No regression — identical to pre-change
F5 Liquid templates for unclaimed student {{ student.email }} renders empty
F6 CSUB/JJC API with unclaimed student Handles nil user without crash
F7 Unclaimed student with parent/approver consent steps Parent has own account, workflow works

6. Implementation Phases

Phase Description Risk
0 Migration: claim_state enum + backfill (claimed + invitation_pending) Low
1 Student model: optional user, safe accessors, guarded callbacks Medium
2 Mailer guards (4 mailers + nil email guard) Low
3 Workflow steps: use safe accessors Low
4 Background jobs: nil guards Low
5 Reports: safe accessors, unclaimed status Low
6 Views/templates: safe navigation, signature comments Low
7 Controllers: before_action guards Low
8 Admin pages: claim_state indicator, hide "View As" Low
9 Other models: form.rb, csub_application_api.rb Low

Phase 1 is the foundation. All other phases are incremental.


7. Key Files

Must change:

  • app/models/student.rb — optional user, safe accessors, guarded callbacks, claim_state
  • app/mailers/active_flow_step_mailer.rb — notification guard + nil email guard
  • app/mailers/students_reminder_mailer.rb — early return guard
  • app/mailers/college_student_application_mailer.rb — early return guard
  • app/mailers/survey_mailer.rb — early return guard
  • app/controllers/student_accounts_controller.rb — delete guard
  • app/jobs/background_student_batch_action.rb — batch delete guard
  • app/models/reports/college/helper_methods.rb — unclaimed status
  • app/models/reports/college/all_student_workflows_report.rb — safe accessor
  • app/models/csub_application_api.rb — safe navigation

Should change (safety):

  • app/views/shared/form_parts/_student_signature*.html.erb (3 files) — guard + comment
  • app/views/college_student_applications/_name_and_address_group_ajax_validations.html.erb
  • app/models/steps/ivytech/ (2), steps/boisestate/ (1), steps/niacc/ (1)
  • app/models/form.rb
  • app/controllers/students/students_controller.rb
  • app/models/student.rb:344get_approver nil guard

No changes needed:

  • Denormalization infrastructure
  • SIS integration logic (65+ steps)
  • Most reports (44 of 45)
  • ability.rb
  • Parent/approver workflows (SAP uses own user, not student's)
  • student_de_course.rb:get_target_user (dead code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment