KCTCS has a system admin (user 2093824) who needs College Super Admin access across all 16 KCTCS colleges. Currently, users are tied to ONE college via users.college_id or ONE college system via users.college_system_id, with roles in users.role_mask.
This phase creates organization_roles as the new source of truth for org-level role assignments, removes users.college_id and users.college_system_id, and migrates existing data. No UI changes -- data managed via ActiveAdmin.
College context is already handled by branded_host (subdomain). Each college has a unique subdomain, and branded_host_college in ApplicationController resolves it. Multi-college users just access different subdomains -- no switcher needed.
Create db/migrate/YYYYMMDDHHMMSS_create_organization_roles.rb:
create_table :organization_roles do |t|
t.references :user, null: false, foreign_key: true
t.references :organization, polymorphic: true, null: false
t.string :role, null: false
t.timestamps
end
add_index :organization_roles, [:user_id, :organization_type, :organization_id, :role],
unique: true, name: 'idx_org_roles_unique'
add_index :organization_roles, [:organization_type, :organization_id]
add_index :organization_roles, :roleManually update db/schema.rb per project convention.
Create app/models/organization_role.rb:
belongs_to :user,belongs_to :organization, polymorphic: true- Validate: role in
User::ROLES.keys, organization_type in%w[College CollegeSystem], uniqueness of user+org+role - Scopes:
for_colleges,for_college_systems,with_role,for_organization - Validate role-org consistency (college roles only on College orgs, etc.) using
ROLE_CLASSES
app/models/user.rb:has_many :organization_roles, dependent: :destroyapp/models/college.rb:has_many :organization_roles, as: :organization, dependent: :destroyapp/models/college_system.rb:has_many :organization_roles, as: :organization, dependent: :destroy
Create a migration to populate organization_roles from existing data, then remove old columns:
# Step 1: Populate organization_roles from existing user data
User.where.not(college_id: nil).find_each do |user|
user.role_mask.to_s.split('|').each do |role|
role_class = User::ROLE_CLASSES[role]
if role_class == College
OrganizationRole.find_or_create_by!(user: user, organization_type: 'College',
organization_id: user.college_id, role: role)
end
end
end
User.where.not(college_system_id: nil).find_each do |user|
user.role_mask.to_s.split('|').each do |role|
role_class = User::ROLE_CLASSES[role]
if role_class == CollegeSystem
OrganizationRole.find_or_create_by!(user: user, organization_type: 'CollegeSystem',
organization_id: user.college_system_id, role: role)
end
end
end
# Step 2: Remove columns
remove_column :users, :college_id
remove_column :users, :college_system_idNote: Roles that map to HighSchool, District, Dean, Instructor, etc. in ROLE_CLASSES stay in role_mask for now -- they aren't org-level college/system roles. Only roles mapping to College or CollegeSystem get migrated.
Remove: belongs_to :college, belongs_to :college_system
Remove User#college entirely. The belongs_to :college association goes away with the column. Do NOT add a replacement method -- callers must use branded_host_college (controller context) or user.colleges (list of all). This forces every call site to be explicit about which college they mean.
Remove User#college_system (was belongs_to). Replace with:
def college_system
org = organization_roles.for_college_systems.first
CollegeSystem.find_by(id: org&.organization_id)
endUpdate User#roles (~line 387) -- merge org roles:
def roles
@_cached_roles ||= begin
mask_roles = (role_mask || "").split('|')
org_roles = organization_roles.loaded? ? organization_roles.map(&:role) : organization_roles.pluck(:role)
(mask_roles + org_roles).uniq
end
endClear @_cached_roles on reload.
Update User#college? (~line 549):
def college?
organization_roles.for_colleges.exists?
endUpdate User#college_system? (~line 545):
def college_system?
organization_roles.for_college_systems.exists?
endUpdate User#colleges (~line 699):
def colleges
return [] if admin?
org_college_ids = organization_roles.for_colleges.pluck(:organization_id)
colleges_from_orgs = org_college_ids.any? ? College.where(id: org_college_ids).to_a : []
return colleges_from_orgs if colleges_from_orgs.any?
return [person.college] if instructor? && person.college.present?
return [person.college] if person? && person.college?
(district || high_school || (student || person)&.high_school)&.colleges&.to_a || []
endhas_college_role?, get_college_role, COLLEGE_ROLES -- no changes needed, all derive from roles.
app/controllers/application_controller.rb line 171-175. No current_user.college fallback -- branded_host is the source of truth:
def load_college
@college ||= College.find(params[:college_id]) if params[:college_id]
@college ||= branded_host_college
@college ||= College.find(session[:college_id]) if session[:college_id]
endSimilarly update load_college_system (line 165-168):
def load_college_system
@college_system ||= CollegeSystem.find(params[:college_system_id]) if params[:college_system_id]
@college_system ||= branded_host_college_system
@college_system ||= CollegeSystem.find(session[:college_system_id]) if session[:college_system_id]
endAll 16 references to current_user.college must be replaced. No fallback to a removed method:
| File | Current | Replacement |
|---|---|---|
application_controller.rb:173 |
current_user.college |
Removed (step 6 above) |
application_controller.rb:341 |
current_user.college? |
Still works (updated in step 5) |
colleges/colleges_controller.rb:43 |
current_user.college == @college |
current_user.colleges.include?(@college) |
instructors/instructor_reviews_controller.rb:12 |
current_user.college |
branded_host_college or @college |
students/students_controller.rb:232 |
current_user.college |
branded_host_college |
student_courses_controller.rb:90 |
current_user.college? |
Still works |
high_schools/high_schools_controller.rb:34 |
current_user.college == @college |
current_user.colleges.include?(@college) |
concerns/common_filters.rb:131 |
current_user.college? |
Still works |
concerns/set_system_theme.rb:85-88 |
current_user.college? / current_user.college.raw_branded_host |
college? still works; .college.raw_branded_host -> branded_host_college&.raw_branded_host |
helpers/application_fields_helper.rb:205,211 |
current_user.college? |
Still works |
helpers/registration_active_flows_helper.rb:30,129 |
current_user.college? |
Still works |
helpers/application_helper.rb:170 |
current_user.college? |
Still works |
app/models/ability.rb:
Line 47 -- id computation:
# id now comes from organization_roles
college_org_ids = user.organization_roles.for_colleges.pluck(:organization_id)
system_org_ids = user.organization_roles.for_college_systems.pluck(:organization_id)
id = system_org_ids.first || college_org_ids.first || user.high_school_id || user.district_id || user.student_id
id ||= user.person_ids if user.person?Lines 51-58 -- ids computation:
ids = case
when user.district?
user.district.high_schools.pluck(:id)
when user.college_system?
CollegeSystem.find(system_org_ids.first).college_ids
else
college_org_ids.presence || [id].compact
endAudit all user.college_id references in ability.rb (lines 419, 630, 803, 1225):
- Line 419:
user.college_id->college_org_ids - Line 630:
user.college_id-> checkcollege_org_ids.include?(...) - Line 803: same pattern
- Line 1225:
college_id: user.college_id->college_id: college_org_ids
Audit all college_id: id (singular) in the college branch and update to college_id: ids where the user could have multiple colleges.
12 user.college_id references to update:
| File | Line | Change |
|---|---|---|
app/models/ability.rb |
47, 419, 630, 803, 1225 | Use college_org_ids (see step 7) |
app/models/api_ability.rb |
6-7 | Use org_roles lookup |
app/models/active_flow_step.rb |
833 | user.college_id -> user.colleges.map(&:id) |
app/models/steps/approval_step.rb |
58 | action_user.college_id -> action_user.colleges.map(&:id).include?(target_object.college.id) |
app/models/steps/upload_document_step.rb |
64 | Same pattern |
app/models/steps/review_instructor_and_launch_step.rb |
36 | Same pattern |
app/models/student_de_course_approval_user.rb |
50 | user.college_id -> user.colleges.map(&:id).include?(college_id) |
app/controllers/application_controller.rb |
173, 167 | See step 6 |
New page app/admin/organization_roles.rb:
- Index: user name, org type, org name, role (human-readable), created_at
- Filters: user_id, organization_type, role
- Form: user_id, organization_type, organization_id, role (select)
- Show: all attributes with links
Add panels to existing pages:
- User show page: "Organization Roles" panel showing all org roles for that user
- College show page (if exists in AA): panel showing users with org roles at that college
- CollegeSystem show page (if exists): panel showing users with org roles at that system
spec/models/organization_role_spec.rb: validations, scopes, associationsspec/factories/organization_roles.rb: factory- Add to
spec/models/user_spec.rb:rolesmerges role_mask + org rolescollegesreturns org-role collegescollege?/college_system?based on org roles- Backward compat for users with non-college roles (hs, district, student)
- Ability specs: user with org roles at multiple colleges gets correct permissions
Provide a DEUS script to create organization_roles for the KCTCS user across all 16 colleges with coll_super_admin role.
| File | Change Type |
|---|---|
db/migrate/xxx_create_organization_roles.rb |
New |
db/migrate/xxx_migrate_and_remove_college_columns.rb |
New |
db/schema.rb |
Manual update |
app/models/organization_role.rb |
New |
app/models/user.rb |
Remove belongs_to :college/:college_system, update roles/colleges/college? methods |
app/models/ability.rb |
Update id/ids computation, all user.college_id refs |
app/models/api_ability.rb |
Update user.college_id/college_system_id refs |
app/models/active_flow_step.rb |
Update user.college_id ref |
app/models/steps/approval_step.rb |
Update action_user.college_id ref |
app/models/steps/upload_document_step.rb |
Update action_user.college_id ref |
app/models/steps/review_instructor_and_launch_step.rb |
Update action_user.college_id ref |
app/models/student_de_course_approval_user.rb |
Update user.college_id ref |
app/controllers/application_controller.rb |
Update load_college, load_college_system |
app/models/college.rb |
Add org_roles association |
app/models/college_system.rb |
Add org_roles association |
app/admin/organization_roles.rb |
New |
spec/models/organization_role_spec.rb |
New |
spec/factories/organization_roles.rb |
New |
bundle exec rspec-- all existing specs pass after changes- Rails console: create OrganizationRole entries, verify
user.roles,user.colleges,user.college? - ActiveAdmin: CRUD org roles, verify panels on User/College show pages
- Login as a test user with org roles at 2 colleges, verify access via both branded_host subdomains
- Verify ability grants correct permissions at each college
- Verify workflow steps (approval, upload_document) work for multi-college users