Colleges need to create and manage students without those students having user accounts. The goal:
- Students without users - structural changes to support
student.userbeing nil - College-managed registrations - colleges create
student_de_courses(triggeringactive_flows) andcollege_student_applicationsfor these students - Platform stability - all existing features must work with userless students
Key constraint: Email is optional for managed students. Colleges may not have email at creation time. This means student.email can return nil - not just "read from a different source" but genuinely absent.
Gated by a per-college local option. Creation UI/flows are a future phase.
| Line | Current | Change |
|---|---|---|
| 3 | belongs_to :user, autosave: true |
Add optional: true |
| 50-54 | before_create assumes user present |
Wrap in if self.user.present? |
| 60 | delegate(:cell_phone, :cell_phone=, :email, :email=, :login, to: :user) |
Replace with safe accessors |
| 810 | self.user.branded_host |
self.user&.branded_host |
Safe Accessor Pattern (replaces delegate on line 60):
def email
user&.email || read_attribute(:email)
end
def cell_phone
user&.cell_phone || read_attribute(:cell_phone)
end
def login
user&.login
endThese return nil when there's no user AND no value on the student record. Callers must handle nil. The existing email= (line 64) already guards with return unless user.present?.
Delegation chain effect: CollegeStudentApplication (line 54) delegates :email, :cell_phone to :student. After this change, csa.email → student.email → safe accessor → may return nil. This is correct behavior.
managedboolean on students (default: false, null: false) + indexcell_phonestring on students (currently only on users)- College local option (e.g.,
MANAGED_STUDENTS_ENABLED)
| Feature | Behavior |
|---|---|
student.email |
Returns nil |
student.cell_phone |
Returns nil |
student.login |
Returns nil |
| Notifications to student | Not sent (no user, no email) |
| SIS integrations (EthosApi etc.) | Email omitted from payload (already checks .present?) |
| Reports email columns | Blank/NULL |
Liquid templates {{ student.email }} |
Renders empty string |
| Workflow steps accessing email | Must handle nil (most already do) |
-
CreateCourseRegistrationForm(app/forms/create_course_registration_form.rb) - validatesstudent_idandcourse_section_id, does NOT require user. But requiresstudent.college_application_for_college_or_system_present?(college)— student MUST have a completed CollegeStudentApplication. -
CreateStudentDeCourseRegistrationService— createsStudentDeCourse, triggersBackgroundRegisterCoursewhich launchesActiveFlowDefinition.instantiate_and_launch_active_flow. No user dependency. -
CollegeStudentApplication— stores all demographic/contact data. Storesstudent_number(primary SIS identifier). Required for registration. Does NOT reference User directly.
All SIS integrations use student_number from CSA, not student.user:
| Integration | Files | Accesses email? | Handles nil email? |
|---|---|---|---|
| EthosApi | 37 | Yes, via student.email |
Yes - checks .present? before adding to payload |
| Banner | 17 | No | N/A |
| Colleague | 6 | No | N/A |
| Jenzabar | 3 | No | N/A |
| PeopleSoft | 1 | No | N/A |
| IvyTech (custom) | 3 | Yes, student.user.email |
No - needs safe accessor |
| CSUB (custom) | 1 | Yes, student.user.email |
No - crashes, needs fix |
Managed students must have a CollegeStudentApplication to be registered. This is the natural data container — it stores student_number, SSN, demographics, all data that SIS integrations need.
HIGH - Direct crashes with nil user or nil email:
| 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 methods fail |
app/mailers/active_flow_step_mailer.rb |
396 | sent_addresses << user.email |
Adds nil to array without checking .present? |
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 |
MEDIUM - Could produce unexpected behavior with nil email:
| File | Line | Code | Issue |
|---|---|---|---|
app/models/reports/college/all_student_workflows_report.rb |
66 | student.user.email |
Crashes (after safe accessor: returns nil, report shows blank) |
| Workflow init steps (SAC, DBU, etc.) | various | fields['approver_email'] = student.get_approver_email(college) |
Stores nil in fields, passed to mailers |
app/models/form.rb |
52,134,217 | liquid_params['user'] = student.user |
Liquid renders empty for nil — cosmetic only |
| Area | Why Safe |
|---|---|
| EthosApi person matching | Checks student.email.present? before adding to payload |
Parent/approver email getters (first_parent_email, first_approver_email) |
Use &. safe navigation |
get_parent_email, get_parent_info |
Check .present? before returning |
| Denormalized tables (registration_active_flows, report_*) | LEFT OUTER JOINs produce NULLs |
| Most reports | Use denormalized columns (NULLs are valid) |
Mailer all_email_addresses cleanup |
Line 361: .flatten.compact.reject(&:blank?) removes nils |
admission_application_hash |
Uses self.user&.email |
ability.rb permissions |
Rules naturally don't match for nil user |
student_de_course.rb:get_target_user |
Dead code — not called anywhere meaningful |
Admin pages rec.user.login rescue '' |
Rescue handles nil |
| Mailer | Fix |
|---|---|
active_flow_step_mailer.rb (line 190) |
Guard: only add student.user if present. Also fix line 396: check user.email.present? before pushing. |
students_reminder_mailer.rb (line 29) |
Early return if student.user.blank?. Also scope reminder queries to exclude managed students. |
college_student_application_mailer.rb |
Early return if user blank. (Managed students won't complete apps, so this won't be triggered, but guard anyway.) |
survey_mailer.rb |
Early return if user blank. |
Notification principle: For managed students, the "Student" notification role is always empty. HS and College role notifications still work (they use their own user records).
| Feature | Impact | Action Needed |
|---|---|---|
| Student listing | Works (student.user&. already used in some places) |
Minor guards |
| Status column | Managed students fall into "account_not_yet_confirmed" (misleading) | Add "Managed" status |
| Send Reminders batch action | Must skip managed students | Filter out or skip gracefully |
| Batch Delete | Calls user.destroy — crashes if nil |
Guard: destroy student directly if no user |
| Individual Delete | Same as batch | Same guard |
| Reminder email templates | Reference <student-login> |
Not sent for managed students (mailer guard) |
| Page | Impact |
|---|---|
| Admin Students Index | rec.user.login rescue '' — Safe |
| Admin Students Show | row :user shows nil — Acceptable |
| Admin CSA | No direct student.user refs — Safe |
| Admin "View As" | Redirects to student portal — Needs guard for managed students |
| Admin "Create Registration" | Works if CSA exists |
| Report | Email Source | Impact |
|---|---|---|
| AllStudentWorkflowsReport | student.user.email direct |
Crashes — use safe accessor |
| CourseSectionRosterReport | Denormalized column | Safe (NULL) |
| RegistrationsAbbreviatedReport | Denormalized columns | Safe (NULL) |
| StudentApplicationStatusReport | Denormalized user_email |
Safe (NULL) |
| HelperMethods#student_account_status | student.user.nil? |
Already handles nil but returns misleading status |
| BoisestateOnCampusApplicationsReport | Eager loads :user |
Needs audit |
Implication: Email columns in reports will be blank for managed students. This is the expected behavior since email is genuinely optional.
College-specific steps are low risk (managed students are opt-in per college). But safe navigation is good practice:
| Step | Change |
|---|---|
steps/ivytech/* (2 files) |
student.user.email → student.email |
steps/boisestate/* (1 file) |
student.user.cell_phone → student.cell_phone |
steps/niacc/* (1 file) |
student.user.cell_phone → student.cell_phone |
| View | Protection |
|---|---|
| 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 |
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 with @student.user && |
| Controller | Change |
|---|---|
students/students_controller.rb |
before_action guard on account pages (edit_user_account, etc.) |
student_accounts_controller.rb |
Guard destroy: if no user, destroy student directly |
| Job | Change |
|---|---|
background_student_batch_action.rb |
Guard student.user before destroy; destroy student directly if no user |
background_send_student_reminders.rb |
Filter managed students from query; mailer guard is backup |
-
CSA for managed students: Managed students MUST have a CSA for registrations (enforced by
CreateCourseRegistrationForm). Should CSA be created with the student, or separately? What fields are mandatory? (student_number? SSN? address?) -
Status on All Students page: Managed students currently show "Account Not Yet Confirmed" (misleading). Add new status "Managed (No Account)"? Or should managed students appear on a separate listing entirely?
-
Batch operations: When "Send Reminders" or "Delete" is used, should managed students be silently skipped, filtered out of selection, or shown with a warning?
-
Reports with blank email: Reports will show blank email/login for managed students. Is this acceptable? Should we pull email from a different source, or add an indicator?
-
SIS student_number: For SIS-integrated colleges, managed students need
student_number. Will colleges assign this manually, or should the system create the student in the SIS and get back a number? -
Parent/Approver workflows: Will managed students have parent consent workflows? Parents are
StudentAnonymousParticipantrecords with their own User accounts. If managed students need parent consent, do parents still need accounts? -
Future upgrade path: Can a managed student later get a full User account? Does the system need to support this transition?
-
CSA visualization: Will admins view/edit CSA data for managed students through existing admin pages, or through a new interface?
-
Scanned signatures: Stored on User record. Will managed students need signatures? If so, where stored?
-
Student merge: If a managed student is merged with a non-managed student, which record wins?
-
"View As" admin feature: Hide the button for managed students, or show explanatory message?
Create via console or scripts (no creation UI in this phase):
- Managed student (
managed: true, no user, email may or may not be present) - CollegeStudentApplication for the managed student (with student_number, demographics)
- StudentDeCourse registration (triggers active_flow)
- Compare side-by-side with a normal student through all scenarios
| # | Test | Expected |
|---|---|---|
| A1 | Admin Students Index: managed student in list | Listed, login column blank |
| A2 | Admin Students Show: view managed student | All panels render, user row shows blank/nil |
| A3 | Admin Students Show: sidebars (CSA, registrations, terms) | Load without error |
| A4 | Admin Students Show: "View As" button | Hidden or shows message |
| A5 | Admin Students Show: "Create Registration" | Works if CSA exists |
| A6 | Admin CSA Index: managed student's CSA | Listed normally |
| A7 | Admin CSA Show: all panels | No errors |
| A8 | Admin Registration Active Flows | Listed, user fields NULL |
| # | Test | Expected |
|---|---|---|
| B1 | Managed student in listing | Shown with correct status |
| B2 | Status column | Shows "Managed" or similar, NOT "Account Not Yet Confirmed" |
| B3 | Filter by status | Managed status filter works |
| B4 | Filter by name / high school | Works |
| B5 | Send Reminders with managed students selected | Skipped gracefully, no error |
| B6 | Batch Delete with managed students | Student destroyed directly, no crash |
| B7 | Individual Delete for managed student | Same as B6 |
| # | Test | Expected |
|---|---|---|
| C1 | AllStudentWorkflowsReport | Email blank, no crash |
| C2 | CourseSectionRosterReport | Email blank (denormalized NULL) |
| C3 | RegistrationsAbbreviatedReport | Email/Login blank |
| C4 | StudentApplicationStatusReport | user_email blank |
| C5 | CSV export of each report | Exports without error |
| C6 | HelperMethods#student_account_status | Correct status for managed students |
| # | Test | Expected |
|---|---|---|
| D1 | Create registration for managed student (console) | StudentDeCourse created, active_flow launched |
| D2 | Workflow steps execute | No crashes |
| D3 | Notification to "Student" role | Skipped (no user/email) |
| D4 | Notification to "College" role | Sent normally |
| D5 | Notification to "HS" role | Sent normally |
| D6 | Form-generating steps | Forms render (_student_signature handles nil) |
| D7 | SIS integration steps (if applicable) | Use student_number from CSA |
| D8 | Steps that set approver_email/parent_email fields | Handle nil gracefully |
| # | Test | Expected |
|---|---|---|
| E1 | ActiveFlowStepMailer for managed student | Student role skipped, others notified |
| E2 | StudentsReminderMailer for managed student | Early return, no email, no crash |
| E3 | CollegeStudentApplicationMailer | Not triggered (managed students don't complete apps) |
| E4 | SurveyMailer for managed student | Early return, no crash |
| # | Test | Expected |
|---|---|---|
| F1 | Denormalize managed student's registration | User columns NULL, email columns NULL |
| F2 | Denormalize managed student's CSA | user_email, user_cell_phone NULL |
| F3 | Denormalization validation (admin) | No false mismatches |
| # | Test | Expected |
|---|---|---|
| G1 | Managed student with email on student record | Email shows in reports/APIs that check it |
| G2 | Managed student without email | All blank, no crashes |
| G3 | Managed student with CSA but no student_number | SIS steps fail gracefully (existing behavior) |
| G4 | Normal student (non-managed) full flow | No regression - works exactly as before |
| G5 | Student merge: managed + non-managed | TBD (product question) |
| G6 | Upgrade managed student (add user later) | TBD (product question) |
| Phase | Description | Risk |
|---|---|---|
| 0 | DB migration: managed, cell_phone columns + local option |
Low |
| 1 | Student model: optional user, safe accessors, guarded callbacks | Medium |
| 2 | Mailer guards (4 mailers + get_addresses_from_users nil check) |
Low |
| 3 | Student Accounts page: status, batch actions, delete guards | Low |
| 4 | Workflow steps: use safe accessors | Low |
| 5 | Background jobs: nil guards | Low |
| 6 | Reports: safe accessors, managed status | Low |
| 7 | Views/templates: safe navigation | Low |
| 8 | Controllers: before_action guards | Low |
| 9 | Admin pages: managed indicator, guard "View As" | Low |
| 10 | Other models: form.rb, csub_application_api.rb | Low |
| Future | Creation UI, CSA creation flow, batch import | Medium |
Phase 1 is the foundation. Once deployed, the system won't crash on nil users. All other phases can be done incrementally.
Must change:
app/models/student.rb— optional user, safe accessors, guarded callbacksapp/mailers/active_flow_step_mailer.rb— notification guard + nil email guard (line 396)app/mailers/students_reminder_mailer.rb— early return guardapp/mailers/college_student_application_mailer.rb— early return guardapp/mailers/survey_mailer.rb— early return guardapp/controllers/student_accounts_controller.rb— delete guardapp/jobs/background_student_batch_action.rb— batch delete guardapp/models/reports/college/helper_methods.rb— managed statusapp/models/reports/college/all_student_workflows_report.rb— safe accessorapp/models/csub_application_api.rb—student.user&.email || ''
Should change (safety):
app/views/shared/form_parts/_student_signature*.html.erb(3 files)app/views/college_student_applications/_name_and_address_group_ajax_validations.html.erbapp/models/steps/ivytech/(2),steps/boisestate/(1),steps/niacc/(1)app/models/form.rb— conditional liquid user paramapp/controllers/students/students_controller.rb— account page guardsapp/models/student.rb:344—get_approvernil guard
No changes needed:
- Denormalization infrastructure (LEFT OUTER JOINs → NULLs)
- SIS integration logic (EthosApi, Banner, Colleague etc. — use student_number)
- Most reports (use denormalized columns)
ability.rb(rules naturally don't match nil user)student_de_course.rb:get_target_user(dead code)