forked from capability-boosters-dev/canvas-lms
-
Notifications
You must be signed in to change notification settings - Fork 0
/
substitutions_helper.rb
304 lines (262 loc) · 13.4 KB
/
substitutions_helper.rb
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
301
302
303
304
# frozen_string_literal: true
#
# Copyright (C) 2014 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Filters added to this controller apply to all controllers in the application.
# Likewise, all the methods added will be available for all controllers.
module Lti
class SubstitutionsHelper
LIS_ROLE_MAP = {
'user' => LtiOutbound::LTIRoles::System::USER,
'siteadmin' => LtiOutbound::LTIRoles::System::SYS_ADMIN,
'teacher' => LtiOutbound::LTIRoles::Institution::INSTRUCTOR,
'student' => LtiOutbound::LTIRoles::Institution::STUDENT,
'admin' => LtiOutbound::LTIRoles::Institution::ADMIN,
'observer' => LtiOutbound::LTIRoles::Context::OBSERVER,
AccountUser => LtiOutbound::LTIRoles::Institution::ADMIN,
StudentEnrollment => LtiOutbound::LTIRoles::Context::LEARNER,
TeacherEnrollment => LtiOutbound::LTIRoles::Context::INSTRUCTOR,
TaEnrollment => LtiOutbound::LTIRoles::Context::TEACHING_ASSISTANT,
DesignerEnrollment => LtiOutbound::LTIRoles::Context::CONTENT_DEVELOPER,
ObserverEnrollment => LtiOutbound::LTIRoles::Context::OBSERVER,
StudentViewEnrollment => LtiOutbound::LTIRoles::Context::LEARNER
}
LIS_V2_ROLE_MAP = {
'user' => 'http://purl.imsglobal.org/vocab/lis/v2/system/person#User',
'siteadmin' => 'http://purl.imsglobal.org/vocab/lis/v2/system/person#SysAdmin',
'teacher' => 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Instructor',
'student' => 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Student',
'admin' => 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator',
AccountUser => 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator',
TaEnrollment => ['http://purl.imsglobal.org/vocab/lis/v2/membership/instructor#TeachingAssistant', 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor'],
StudentEnrollment => 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner',
TeacherEnrollment => 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor',
DesignerEnrollment => 'http://purl.imsglobal.org/vocab/lis/v2/membership#ContentDeveloper',
ObserverEnrollment => 'http://purl.imsglobal.org/vocab/lis/v2/membership#Mentor',
StudentViewEnrollment => 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner',
Course => 'http://purl.imsglobal.org/vocab/lis/v2/course#CourseOffering'
}
LIS_V2_ROLE_NONE = 'http://purl.imsglobal.org/vocab/lis/v2/person#None'
# Nearly identical to LIS_V2_ROLE_MAP except:
# 1. Corrects typo in first TaEnrollment URI ('instructor'->'Instructor')
# 2. Values uniformly (frozen) Arrays
# 3. Has Group roles
# 4. Has no Course role
LIS_V2_LTI_ADVANTAGE_ROLE_MAP = {
'user' => [ 'http://purl.imsglobal.org/vocab/lis/v2/system/person#User' ].freeze,
'siteadmin' => [ 'http://purl.imsglobal.org/vocab/lis/v2/system/person#SysAdmin' ].freeze,
'fake_student' => [ 'http://purl.imsglobal.org/vocab/lti/system/person#TestUser' ].freeze,
'teacher' => [ 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Instructor' ].freeze,
'student' => [ 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Student' ].freeze,
'admin' => [ 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator' ].freeze,
AccountUser => [ 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator' ].freeze,
TaEnrollment => [
'http://purl.imsglobal.org/vocab/lis/v2/membership/Instructor#TeachingAssistant',
'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor'
].freeze,
StudentEnrollment => [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' ].freeze,
TeacherEnrollment => [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor' ].freeze,
DesignerEnrollment => [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#ContentDeveloper' ].freeze,
ObserverEnrollment => [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Mentor' ].freeze,
StudentViewEnrollment => [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' ].freeze,
:group_member => [ 'http://purl.imsglobal.org/vocab/lis/v2/membership#Member' ].freeze,
:group_leader => [
'http://purl.imsglobal.org/vocab/lis/v2/membership#Member',
'http://purl.imsglobal.org/vocab/lis/v2/membership#Manager'
].freeze
}.freeze
# Inversion of LIS_V2_LTI_ADVANTAGE_ROLE_MAP, i.e.:
#
# {
# '<lis-url>' => [<enrollment-class>, <logical-sys-or-insitution-role-name-string>, <enrollment-class>],
# '<lis-url>' => [<group-membership-type-symbol>, <group-membership-type-symbol>],
# ...
# }
#
# (Extra copy at the end is to undo the default value ([]))
INVERTED_LIS_V2_LTI_ADVANTAGE_ROLE_MAP = LIS_V2_LTI_ADVANTAGE_ROLE_MAP.each_with_object(Hash.new([])) do |(key,values), memo|
values.each { |value| memo[value] += [key] }
end.reverse_merge({}).freeze
LIS_V2_LTI_ADVANTAGE_ROLE_NONE = 'http://purl.imsglobal.org/vocab/lis/v2/system/person#None'.freeze
def initialize(context, root_account, user, tool = nil)
@context = context
@root_account = root_account
@user = user
@tool = tool
end
def account
@account ||=
case @context
when Account
@context
when Course
@context.account
else
@root_account
end
end
def enrollments_to_lis_roles(enrollments)
enrollments.map { |enrollment| Lti::LtiUserCreator::ENROLLMENT_MAP[enrollment.class] }.uniq
end
def all_roles(version = 'lis1')
case version
when 'lis2'
role_map = LIS_V2_ROLE_MAP
role_none = LIS_V2_ROLE_NONE
when 'lti1_3'
role_map = LIS_V2_LTI_ADVANTAGE_ROLE_MAP
role_none = LIS_V2_LTI_ADVANTAGE_ROLE_NONE
else
role_map = LIS_ROLE_MAP
role_none = LtiOutbound::LTIRoles::System::NONE
end
if @user
context_roles = course_enrollments.each_with_object(Set.new) { |role, set| set.add([*role_map[role.class]].join(",")) }
institution_roles = @user.roles(@root_account, true).flat_map { |role| role_map[role] }
if Account.site_admin.account_users_for(@user).present?
institution_roles.push(*role_map['siteadmin'])
end
(context_roles + institution_roles).to_a.compact.uniq.sort.join(',')
else
role_none
end
end
def course_enrollments
return [] unless @context.is_a?(Course) && @user
@current_course_enrollments ||= @context.current_enrollments.where(user_id: @user.id)
end
def course_sections
return [] unless @context.is_a?(Course) && @user
@current_course_sections ||= @context.course_sections.where(id: course_enrollments.map(&:course_section_id)).select("id, sis_source_id")
end
def account_enrollments
unless @current_account_enrollments
@current_account_enrollments = []
if @user && @context.respond_to?(:account_chain) && [email protected]_chain.empty?
@current_account_enrollments = AccountUser.active.where(user_id: @user, account_id: @context.account_chain).shard(@context.shard)
end
end
@current_account_enrollments
end
def current_lis_roles
enrollments = course_enrollments + account_enrollments
enrollments.size > 0 ? enrollments_to_lis_roles(enrollments).join(',') : LtiOutbound::LTIRoles::System::NONE
end
def concluded_course_enrollments
@concluded_course_enrollments ||=
@context.is_a?(Course) && @user ? @user.enrollments.concluded.where(course_id: @context.id).shard(@context.shard) : []
end
def concluded_lis_roles
concluded_course_enrollments.size > 0 ? enrollments_to_lis_roles(concluded_course_enrollments).join(',') : LtiOutbound::LTIRoles::System::NONE
end
def granted_permissions(permissions_to_check)
permissions_to_check.select{|p| @context.grants_right?(@user, p.to_sym) }.join(",")
end
def current_canvas_roles
roles = (course_enrollments + account_enrollments).map(&:role).map(&:name).uniq
roles = roles.map{|role| role == "AccountAdmin" ? "Account Admin" : role} # to maintain backwards compatibility
roles.join(',')
end
def current_canvas_roles_lis_v2(version = 'lis2')
roles = (course_enrollments + account_enrollments).map(&:class).uniq
role_map = version == 'lti1_3' ? LIS_V2_LTI_ADVANTAGE_ROLE_MAP : LIS_V2_ROLE_MAP
roles.map { |r| role_map[r] }.join(',')
end
def enrollment_state
enrollments = @user ? @context.enrollments.where(user_id: @user.id).preload(:enrollment_state) : []
return '' if enrollments.size == 0
enrollments.any? { |membership| membership.state_based_on_date == :active } ? LtiOutbound::LTIUser::ACTIVE_STATE : LtiOutbound::LTIUser::INACTIVE_STATE
end
def previous_lti_context_ids
previous_course_ids_and_context_ids.map(&:last).compact.join(',')
end
def previous_course_ids
previous_course_ids_and_context_ids.map(&:first).sort.join(',')
end
def section_ids
course_enrollments.map(&:course_section_id).uniq.sort.join(',')
end
def section_restricted
@context.is_a?(Course) && @user && @context.visibility_limited_to_course_sections?(@user)
end
def section_sis_ids
course_sections.map(&:sis_source_id).compact.uniq.sort.join(',')
end
def sis_email
sis_ps = SisPseudonym.for(@user, @context, type: :trusted, require_sis: true)
sis_ps.sis_communication_channel&.path || sis_ps.communication_channels.order(:position).active.first&.path if sis_ps
end
def email
# we are using sis_email for lti2 tools, or if the 'prefer_sis_email' extension is set for LTI 1
e = if !lti1? || (@tool&.extension_setting(nil, :prefer_sis_email)&.downcase ||
@tool&.extension_setting(:tool_configuration, :prefer_sis_email)&.downcase) == "true"
sis_email
end
e || @user.email
end
def recursively_fetch_previous_lti_context_ids(limit: 1000)
return '' unless @context.is_a?(Course)
# now find all parents for locked folders
last_migration_id = @context.content_migrations.where(workflow_state: :imported).order(id: :desc).limit(1).pluck(:id).first
return '' unless last_migration_id
# we can cache on the last migration because even if copies are done elsewhere they won't affect anything
# until a new copy is made to _this_ course
Rails.cache.fetch(["recursive_copied_course_lti_context_ids", @context.global_id, last_migration_id].cache_key) do
# Finds content migrations for this course and recursively, all content
# migrations for the source course of the migration -- that is, all
# content migrations that directly or indirectly provided content to
# this course. From there we get the unique list of courses, ordering by
# which has the migration with the latest timestamp.
results = Course.from("(WITH RECURSIVE all_contexts AS (
SELECT context_id, source_course_id
FROM #{ContentMigration.quoted_table_name}
WHERE context_id=#{@context.id}
UNION
SELECT content_migrations.context_id, content_migrations.source_course_id
FROM #{ContentMigration.quoted_table_name}
INNER JOIN all_contexts t ON content_migrations.context_id = t.source_course_id
)
SELECT DISTINCT ON (courses.lti_context_id) courses.id, ct.finished_at, courses.lti_context_id
FROM #{Course.quoted_table_name}
INNER JOIN #{ContentMigration.quoted_table_name} ct
ON ct.source_course_id = courses.id
AND ct.workflow_state = 'imported'
AND (ct.context_id IN (
SELECT x.context_id
FROM all_contexts x))
ORDER BY courses.lti_context_id, ct.finished_at DESC
) as courses").
where.not(lti_context_id: nil).order(finished_at: :desc).limit(limit + 1).pluck(:lti_context_id)
# We discovered that at around 3000 lti_context_ids, the form data gets too
# big and breaks the LTI launch. We decided to truncate after 1000 and note
# it in the launch as "truncated"
results = results.first(limit) << 'truncated' if results.length > limit
results.join(',')
end
end
private
def lti1?
@tool&.respond_to?(:extension_setting)
end
def previous_course_ids_and_context_ids
return [] unless @context.is_a?(Course)
@previous_ids ||= Course.where(
"EXISTS (?)", ContentMigration.where(context_id: @context.id, workflow_state: :imported).where("content_migrations.source_course_id = courses.id")
).pluck(:id, :lti_context_id)
end
end
end