Skip to content

Commit e892fd9

Browse files
committed
feat(invitations): complete external ID bulk invite flow and redesign result dialog
Ext ID is now accepted in bulk invite CSVs. For existing enrolled users and pending invitations, if external ID is provided, it upserts the current external ID. Conflicts due to duplicate emails or external IDs route to Needs Attention, with clear reasons explaining what to be done.
1 parent d6e1808 commit e892fd9

21 files changed

Lines changed: 1203 additions & 533 deletions

File tree

app/controllers/course/user_invitations_controller.rb

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -218,12 +218,16 @@ def invalid_invitations
218218

219219
# Returns the invitation response based on file or entry invitation.
220220
def parse_invitation_result(new_invitations, existing_invitations, new_course_users,
221-
existing_course_users, duplicate_users)
222-
render_to_string(partial: 'invitation_result_data', locals: { new_invitations: new_invitations,
223-
existing_invitations: existing_invitations,
224-
new_course_users: new_course_users,
225-
existing_course_users: existing_course_users,
226-
duplicate_users: duplicate_users })
221+
existing_course_users, duplicate_users,
222+
updated_invitations, updated_course_users)
223+
render_to_string(partial: 'invitation_result_data',
224+
locals: { new_invitations: new_invitations,
225+
existing_invitations: existing_invitations,
226+
new_course_users: new_course_users,
227+
existing_course_users: existing_course_users,
228+
duplicate_users: duplicate_users,
229+
updated_invitations: updated_invitations,
230+
updated_course_users: updated_course_users })
227231
end
228232

229233
# Enables or disables registration codes in the given course.

app/models/concerns/course/unique_external_id_concern.rb

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,20 @@ def normalize_external_id
2727
# @return [void]
2828
def validate_unique_external_id_within_course
2929
return if external_id.blank?
30+
return unless external_id_taken_by_invitation? || external_id_taken_by_course_user?
3031

31-
invitation_exists = Course::UserInvitation.
32-
unconfirmed.
33-
where(course_id: course_id, external_id: external_id).
34-
where.not(id: id).
35-
exists?
36-
37-
course_user_exists = CourseUser.
38-
where(course_id: course_id, external_id: external_id).
39-
where.not(id: id).
40-
exists?
32+
errors.add(:external_id, :taken)
33+
end
4134

42-
return unless invitation_exists || course_user_exists
35+
def external_id_taken_by_invitation?
36+
scope = Course::UserInvitation.unconfirmed.where(course_id: course_id, external_id: external_id)
37+
scope = scope.where.not(id: id) if is_a?(Course::UserInvitation)
38+
scope.exists?
39+
end
4340

44-
errors.add(:external_id, :taken)
41+
def external_id_taken_by_course_user?
42+
scope = CourseUser.where(course_id: course_id, external_id: external_id)
43+
scope = scope.where.not(id: id) if is_a?(CourseUser)
44+
scope.exists?
4545
end
4646
end

app/services/concerns/course/user_invitation_service/process_invitation_concern.rb

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,30 @@ def add_existing_users(users)
6767
new_course_users = []
6868
users.each do |user|
6969
if (course_user = all_course_users[user[:user].id])
70-
existing_course_users << course_user
70+
handle_existing_course_user(user, course_user, existing_course_users)
7171
else
7272
enroll_new_user(user, user[:external_id].presence, new_course_users)
7373
end
7474
end
7575
[new_course_users, existing_course_users]
7676
end
7777

78+
def handle_existing_course_user(user, course_user, existing_course_users)
79+
csv_ext_id = user[:external_id].presence
80+
current_ext_id = course_user.external_id.presence
81+
82+
if csv_ext_id.nil? || csv_ext_id == current_ext_id
83+
existing_course_users << course_user
84+
elsif @taken_external_ids.include?(csv_ext_id)
85+
@duplicate_users.push(user.merge(reason: :external_id_taken))
86+
else
87+
@taken_external_ids.delete(current_ext_id) if current_ext_id
88+
@taken_external_ids.add(csv_ext_id)
89+
course_user.external_id = csv_ext_id
90+
@updated_course_users << { record: course_user, previous_external_id: current_ext_id }
91+
end
92+
end
93+
7894
def enroll_new_user(user, ext_id, new_course_users)
7995
if ext_id && @taken_external_ids.include?(ext_id)
8096
@duplicate_users.push(user.merge(reason: :external_id_taken))
@@ -129,14 +145,30 @@ def invite_new_users(users)
129145
users.each do |user|
130146
invitation = all_invitations[user[:email]]
131147
if invitation
132-
existing_invitations << invitation
148+
handle_existing_invitation(user, invitation, existing_invitations)
133149
else
134150
add_to_new_invitations(user, user[:external_id].presence, new_invitations)
135151
end
136152
end
137153
[new_invitations, existing_invitations]
138154
end
139155

156+
def handle_existing_invitation(user, invitation, existing_invitations)
157+
csv_ext_id = user[:external_id].presence
158+
current_ext_id = invitation.external_id.presence
159+
160+
if csv_ext_id.nil? || csv_ext_id == current_ext_id
161+
existing_invitations << invitation
162+
elsif @taken_external_ids.include?(csv_ext_id)
163+
@duplicate_users.push(user.merge(reason: :external_id_taken))
164+
else
165+
@taken_external_ids.delete(current_ext_id) if current_ext_id
166+
@taken_external_ids.add(csv_ext_id)
167+
invitation.external_id = csv_ext_id
168+
@updated_invitations << { record: invitation, previous_external_id: current_ext_id }
169+
end
170+
end
171+
140172
def add_to_new_invitations(user, ext_id, new_invitations)
141173
if ext_id && @taken_external_ids.include?(ext_id)
142174
@duplicate_users.push(user.merge(reason: :external_id_taken))

app/services/course/user_invitation_service.rb

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,37 @@ def initialize(current_course_user, current_user, current_course)
2424
# because Rails does not handle duplicate nested attribute uniqueness constraints.
2525
#
2626
# @param [Array<Hash>|File|TempFile] users Invites the given users.
27-
# @return [Array<Integer>|nil] An array containing the the size of new_invitations, existing_invitations,
28-
# new_course_users and existing_course_users, duplicate_users respectively if success. nil when fail.
27+
# @return [Array<Integer>|nil] An array containing the size of new_invitations, existing_invitations,
28+
# new_course_users, existing_course_users, duplicate_users, updated_invitations, updated_course_users
29+
# respectively if success. nil when fail.
2930
# @raise [CSV::MalformedCSVError] When the file provided is invalid.
3031
def invite(users)
3132
new_invitations = nil
3233
existing_invitations = nil
3334
new_course_users = nil
3435
existing_course_users = nil
3536
duplicate_users = nil
37+
updated_invitations = nil
38+
updated_course_users = nil
3639

3740
success = Course.transaction do
3841
new_invitations, existing_invitations,
39-
new_course_users, existing_course_users, duplicate_users = invite_users(users)
42+
new_course_users, existing_course_users,
43+
duplicate_users, updated_invitations, updated_course_users = invite_users(users)
44+
raise ActiveRecord::Rollback unless updated_invitations.all? { |u| u[:record].save }
45+
raise ActiveRecord::Rollback unless updated_course_users.all? { |u| u[:record].save }
4046
raise ActiveRecord::Rollback unless new_invitations.all?(&:save)
4147
raise ActiveRecord::Rollback unless new_course_users.all?(&:save)
4248

4349
true
4450
end
4551

46-
send_registered_emails(new_course_users) if success
47-
send_invitation_emails(new_invitations) if success
48-
success ? [new_invitations, existing_invitations, new_course_users, existing_course_users, duplicate_users] : nil
52+
return unless success
53+
54+
send_registered_emails(new_course_users)
55+
send_invitation_emails(new_invitations)
56+
[new_invitations, existing_invitations, new_course_users, existing_course_users,
57+
duplicate_users, updated_invitations, updated_course_users]
4958
end
5059

5160
# Resends invitation emails to CourseUsers to the given course.
@@ -77,6 +86,8 @@ def resend_invitation(invitations)
7786
def invite_users(users)
7887
unique_users, parse_duplicates = parse_invitations(users)
7988
@duplicate_users = parse_duplicates
80-
process_invitations(unique_users) + [@duplicate_users]
89+
@updated_invitations = []
90+
@updated_course_users = []
91+
process_invitations(unique_users) + [@duplicate_users, @updated_invitations, @updated_course_users]
8192
end
8293
end

app/views/course/user_invitations/_invitation_result_data.json.jbuilder

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ json.existingCourseUsers existing_course_users.each do |course_user|
3838
json.phantom course_user.phantom?
3939
end
4040

41-
json.duplicateUsers duplicate_users.each do |duplicate_user, index|
41+
json.duplicateUsers duplicate_users.each.with_index do |duplicate_user, index|
4242
json.id index
4343
json.name duplicate_user[:name]
4444
json.email duplicate_user[:email]
@@ -47,3 +47,25 @@ json.duplicateUsers duplicate_users.each do |duplicate_user, index|
4747
json.phantom duplicate_user[:phantom]
4848
json.reason duplicate_user[:reason]
4949
end
50+
51+
json.updatedInvitations updated_invitations.each do |item|
52+
inv = item[:record]
53+
json.id inv.id
54+
json.name inv.name
55+
json.email inv.email
56+
json.externalId inv.external_id
57+
json.previousExternalId item[:previous_external_id]
58+
json.role inv.role
59+
json.phantom inv.phantom
60+
end
61+
62+
json.updatedCourseUsers updated_course_users.each do |item|
63+
cu = item[:record]
64+
json.id cu.id if cu.id
65+
json.name cu.name.strip
66+
json.email cu.user.email
67+
json.externalId cu.external_id
68+
json.previousExternalId item[:previous_external_id]
69+
json.role cu.role
70+
json.phantom cu.phantom?
71+
end

0 commit comments

Comments
 (0)