Skip to content

Commit 609f0c2

Browse files
Dantemssmwvolo
andauthored
Add roles to external-ids, find and create external-ids for specific roles (#1693)
* Add roles to external-ids, find and create external-ids for specific roles * Also handle role in the find user API * Fixed failing tests * Added a routine to infer roles for external ids --------- Co-authored-by: Michael Volo <volo@rice.edu>
1 parent 4e34758 commit 609f0c2

File tree

12 files changed

+80
-9
lines changed

12 files changed

+80
-9
lines changed

app/controllers/api/v1/users_controller.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,9 @@ def find
184184
payload = consume!(Hashie::Mash.new, represent_with: Api::V1::FindUserRepresenter)
185185

186186
user = if payload.external_id.present?
187-
User.joins(:external_ids).find_by!(external_ids: { external_id: payload.external_id })
187+
external_id = ExternalId.find_by_external_id_and_role(payload.external_id, payload.role)
188+
raise ActiveRecord::RecordNotFound unless external_id
189+
external_id.user
188190
elsif payload.uuid.present?
189191
User.find_by!(uuid: payload.uuid)
190192
else

app/models/external_id.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
class ExternalId < ApplicationRecord
22
belongs_to :user, inverse_of: :external_ids
33

4+
enum(role: User::VALID_ROLES)
5+
46
validates :user, presence: true
5-
validates :external_id, presence: true, uniqueness: true
7+
validates :external_id, presence: true, uniqueness: { scope: :role }
8+
9+
# Find an ExternalId by external_id and role, with backwards compatibility
10+
# for unknown_role records when a specific role is provided
11+
def self.find_by_external_id_and_role(external_id, role = nil)
12+
query = { external_id: external_id }
13+
query[:role] = [role, 'unknown_role'] unless role.nil?
14+
15+
external_ids = where(query).to_a
16+
17+
if role.nil?
18+
external_ids.first
19+
else
20+
# First try to find ExternalId with matching role
21+
result = external_ids.find { |ext_id| ext_id.role == role.to_s }
22+
# If no result, try to find ExternalId with unknown_role for backwards compatibility
23+
result || external_ids.find(&:unknown_role?)
24+
end
25+
end
626
end

app/representers/api/v1/external_id_representer.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,10 @@ class ExternalIdRepresenter < Roar::Decorator
1111
type: String,
1212
readable: true,
1313
writeable: true
14+
15+
property :role,
16+
type: String,
17+
readable: true,
18+
writeable: true
1419
end
1520
end

app/representers/api/v1/find_or_create_user_representer.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ class FindOrCreateUserRepresenter < Roar::Decorator
121121
readable: false,
122122
writeable: true,
123123
schema_info: {
124-
description: "The role to assign to the newly created user, one of #{
124+
description: "The role to assign to the newly created user and external_id, one of #{
125125
User.roles.keys.map(&:to_s).inspect
126126
}"
127127
}

app/representers/api/v1/find_user_representer.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ class FindUserRepresenter < Roar::Decorator
2020
description: "External ID to search by"
2121
}
2222

23+
property :role,
24+
type: String,
25+
readable: false,
26+
writeable: true,
27+
schema_info: {
28+
description: "Role to search by (optional), one of #{
29+
User.roles.keys.map(&:to_s).inspect
30+
}"
31+
}
32+
2333
collection :external_ids,
2434
type: String,
2535
readable: true,

app/routines/create_user.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def exec(state:, external_id: nil, username: nil,
2020

2121
outputs[:user] = User.send(create_method) do |user|
2222
user.state = state
23-
user.external_ids << ExternalId.new(external_id: external_id) unless external_id.nil?
23+
user.external_ids << ExternalId.new(external_id: external_id, role: role) unless external_id.nil?
2424
user.username = username
2525
user.first_name = first_name
2626
user.last_name = last_name

app/routines/find_or_create_user.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def exec(options)
2525
# Attempt to find a user by either the external_id, username, or email address
2626
def find_user(options)
2727
if options[:external_id].present?
28-
User.joins(:external_ids).find_by(external_ids: { external_id: options[:external_id] })
28+
ExternalId.find_by_external_id_and_role(options[:external_id], options[:role])&.user
2929
elsif options[:username].present?
3030
User.find_by(username: options[:username])
3131
elsif options[:email].present?
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
class InferExternalIdRoles
2+
def self.call
3+
new.call
4+
end
5+
6+
def call
7+
ExternalId.unknown_role.preload(:user).find_in_batches do |batch|
8+
ActiveRecord::Base.transaction do
9+
batch.each do |external_id|
10+
external_id.role = external_id.user.no_faculty_info? ? :student : :instructor
11+
end
12+
13+
ExternalId.import(
14+
batch,
15+
on_duplicate_key_update: {
16+
conflict_target: [:id],
17+
columns: [:role]
18+
},
19+
validate: false
20+
)
21+
end
22+
end
23+
end
24+
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class AddRoleToExternalIds < ActiveRecord::Migration[6.1]
2+
def change
3+
add_column :external_ids, :role, :integer, null: false, default: 0
4+
5+
add_index :external_ids, [:external_id, :role], unique: true
6+
7+
remove_index :external_ids, :external_id
8+
end
9+
end

db/schema.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#
1111
# It's strongly recommended that you check this file into your version control system.
1212

13-
ActiveRecord::Schema.define(version: 2024_11_07_193019) do
13+
ActiveRecord::Schema.define(version: 2026_01_28_161718) do
1414

1515
# These are extensions that must be enabled in order to support this database
1616
enable_extension "citext"
@@ -175,7 +175,8 @@
175175
t.string "external_id", null: false
176176
t.datetime "created_at", null: false
177177
t.datetime "updated_at", null: false
178-
t.index ["external_id"], name: "index_external_ids_on_external_id", unique: true
178+
t.integer "role", default: 0, null: false
179+
t.index ["external_id", "role"], name: "index_external_ids_on_external_id_and_role", unique: true
179180
t.index ["user_id"], name: "index_external_ids_on_user_id"
180181
end
181182

0 commit comments

Comments
 (0)