-
Notifications
You must be signed in to change notification settings - Fork 78
Expand file tree
/
Copy pathuser_invitations_controller.rb
More file actions
300 lines (267 loc) · 9.92 KB
/
user_invitations_controller.rb
File metadata and controls
300 lines (267 loc) · 9.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# frozen_string_literal: true
class Course::UserInvitationsController < Course::ComponentController
before_action :authorize_invitation!
load_resource :invitation, through: :course, class: 'Course::UserInvitation', parent: false,
only: :destroy
def index
respond_to do |format|
format.json do
@invitations = current_course.invitations.order(name: :asc)
@without_invitations = params[:without_invitations]
end
end
end
def create
result = invite
if result
create_invitation_success(result)
else
propagate_errors
errors = current_course.errors[:base]
render json: { errors: errors }, status: :bad_request
end
end
def destroy
if @invitation.destroy
destroy_invitation_success
else
destroy_invitation_failure
end
end
def resend_invitation
@invitation = load_invitations.first
@serial_number = params[:serial_number]
if @invitation && invitation_service.resend_invitation(load_invitations)
resend_invitation_success
else
resend_invitation_failure
end
end
def resend_invitations
if invitation_service.resend_invitation(load_invitations)
resend_invitations_success
else
resend_invitations_failure
end
end
def toggle_registration
render 'new' if enable_registration_code(registration_params)
end
private
def course_user_invitation_params
@course_user_invitation_params ||= begin
params[:course] = { invitations_attributes: {} } unless params.key?(:course)
params.require(:course).permit(:invitations_file, :registration_key,
invitations_attributes: [:name, :email, :role, :phantom,
:timeline_algorithm, :external_id])
end
end
# Determines the parameters to be passed to the invitation service object.
#
# @return [Tempfile]
# @return [Hash]
def invitation_params
@invitation_params ||= course_user_invitation_params[:invitations_file]&.tempfile ||
course_user_invitation_params[:invitations_attributes].to_h
end
# Returns the param on whether to enable or disable registration via registration code.
#
# @return [Boolean]
def registration_params
@registration_params ||= course_user_invitation_params[:registration_key] == 'checked'
end
# Strong params for resending of invitations.
#
# @return [String|nil] Returns invitation.id. If none were found, nil is returned.
def resend_invitation_params
@resend_invitation_params ||=
(params.permit(:user_invitation_id)[:user_invitation_id] unless params[:user_invitation_id].blank?)
end
# Loads existing invitations for the resending of invitations. Method handles the following cases:
# 1) Single invitation - specified with the user_invitation_id param
# 2) All un-confirmed invitation - if user_invitation_id param was not found
def load_invitations
@invitations ||= begin
ids = resend_invitation_params
ids ||= current_course.invitations.retryable.unconfirmed.select(:id)
if ids.blank?
[]
else
current_course.invitations.retryable.unconfirmed.where('course_user_invitations.id IN (?)', ids)
end
end
end
# Prevents access to this set of pages unless the user is a staff of the course.
def authorize_invitation!
authorize!(:manage_users, current_course)
end
# Determines if the user uploaded a file.
#
# @return [Boolean]
def invite_by_file?
invitation_params.is_a?(Tempfile)
end
# Invites the users via the service object.
#
# @return [Boolean] True if the invitation was successful.
def invite
invitation_service.invite(invitation_params)
rescue CSV::MalformedCSVError => e
current_course.errors.add(:base, e.message)
false
end
# Creates a user invitation service object for this object.
#
# @return [Course::UserInvitationService]
def invitation_service
@invitation_service ||= Course::UserInvitationService.new(current_course_user, current_user, current_course)
end
# Propagate errors from the parameters depending on the type of the parameters.
#
# @return [void]
def propagate_errors
aggregate_errors.each { |msg| current_course.errors.add(:base, msg) }
end
# Aggregates errors from all the known sources of failure.
#
# @return [Array<String>] An array of failure messages;
def aggregate_errors
invalid_course_user_errors + invalid_invitation_email_errors
end
# Aggregates Course User objects which have errors.
#
# @return [Array<String>]
def invalid_course_user_errors
invalid_course_users.flat_map do |course_user|
email = course_user.user&.email || course_user.name
errors = []
if course_user.errors.of_kind?(:external_id, :taken)
errors << t('errors.course.user_invitations.duplicate_external_id',
email: email, external_id: course_user.external_id)
end
if course_user.errors.of_kind?(:user, :taken) || course_user.errors.of_kind?(:course, :taken)
errors << t('errors.course.user_invitations.already_enrolled', email: email)
end
other_errors = course_user.errors.reject { |e| %w[external_id user course].include?(e.attribute.to_s) }
# .any? is intentional: this is a catch-all for unrecognised errors; all messages are forwarded verbatim
if other_errors.any?
message = other_errors.map { |e| course_user.errors.full_message(e.attribute, e.message) }.
to_sentence
errors << t('errors.course.user_invitations.other_error', email: email, message: message)
end
errors
end
end
# Finds all the invalid Course User objects in the current course.
#
# @return [Array<CourseUser>]
def invalid_course_users
current_course.course_users.reject(&:valid?)
end
# Aggregates errors in invitations.
#
# @return [Array<String>]
def invalid_invitation_email_errors
invalid_invitations.flat_map do |invitation|
email = invitation.email
errors = []
if invitation.errors.of_kind?(:external_id, :taken)
errors << t('errors.course.user_invitations.duplicate_external_id',
email: email, external_id: invitation.external_id)
end
# .any? is intentional: invalid_email is a broad wrapper; the actual messages are forwarded in `message`
if invitation.errors[:email].any?
message = invitation.errors[:email].to_sentence
errors << t('errors.course.user_invitations.invalid_email', email: email, message: message)
end
if invitation.errors.of_kind?(:base, :existing_invitation)
errors << t('errors.course.user_invitations.duplicate_invitation', email: email)
end
other_errors = invitation.errors.reject { |e| %w[external_id email base].include?(e.attribute.to_s) }
# .any? is intentional: this is a catch-all for unrecognised errors; all messages are forwarded verbatim
if other_errors.any?
message = other_errors.map { |e| invitation.errors.full_message(e.attribute, e.message) }.
to_sentence
errors << t('errors.course.user_invitations.other_error', email: email, message: message)
end
errors
end
end
# Finds all the invalid invitation objects in the current course.
#
# @return [Array<Course::UserInvitation>]
def invalid_invitations
current_course.invitations.reject(&:valid?)
end
# Returns the invitation response based on file or entry invitation.
def parse_invitation_result(new_invitations, existing_invitations, new_course_users,
existing_course_users, duplicate_users)
render_to_string(partial: 'invitation_result_data', locals: { new_invitations: new_invitations,
existing_invitations: existing_invitations,
new_course_users: new_course_users,
existing_course_users: existing_course_users,
duplicate_users: duplicate_users })
end
# Enables or disables registration codes in the given course.
#
# @param [Boolean] enable True if registration codes should be enabled.
# @return [Boolean]
def enable_registration_code(enable)
if enable
return true if current_course.registration_key
current_course.generate_registration_key
else
current_course.registration_key = nil
end
current_course.save
end
# @return [Course::UsersComponent]
# @return [nil] If component is disabled.
def component
current_component_host[:course_users_component]
end
def resend_invitation_success
respond_to do |format|
format.json do
render partial: 'course_user_invitation_list_data', locals: { invitation: @invitation.reload }, status: :ok
end
end
end
def resend_invitation_failure
respond_to do |format|
format.json { head :bad_request }
end
end
def resend_invitations_success
respond_to do |format|
format.json do
render partial: 'course_user_invitation_list', locals: { invitations: @invitations.reload }, status: :ok
end
end
end
def resend_invitations_failure
respond_to do |format|
format.json { head :bad_request }
end
end
def destroy_invitation_success
respond_to do |format|
format.json { render json: { id: @invitation.id }, status: :ok }
end
end
def destroy_invitation_failure
respond_to do |format|
format.json { render json: { errors: @invitation.errors.full_messages }, status: :bad_request }
end
end
def create_invitation_success(result)
respond_to do |format|
format.json do
render json: {
newInvitations: result[0].length,
invitationResult: parse_invitation_result(*result)
}, status: :ok
end
end
end
end