285 lines
12 KiB
Ruby
285 lines
12 KiB
Ruby
#
|
|
# 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,
|
|
|
|
'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) && !@context.account_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 recursively_fetch_previous_lti_context_ids
|
|
recursively_fetch_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
|
|
|
|
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
|
|
|
|
def recursively_fetch_previous_course_ids_and_context_ids
|
|
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_ids", @context.global_id, last_migration_id].cache_key) do
|
|
Course.where(
|
|
"EXISTS (?)", ContentMigration.where(workflow_state: :imported).where("context_id = ? OR context_id IN (
|
|
WITH RECURSIVE t AS (
|
|
SELECT context_id, source_course_id FROM #{ContentMigration.quoted_table_name} WHERE context_id = ?
|
|
UNION
|
|
SELECT content_migrations.context_id, content_migrations.source_course_id FROM #{ContentMigration.quoted_table_name} INNER JOIN t ON content_migrations.context_id=t.source_course_id
|
|
)
|
|
SELECT DISTINCT context_id FROM t
|
|
)", @context.id, @context.id).where("content_migrations.source_course_id = courses.id")
|
|
).pluck(:id, :lti_context_id)
|
|
end
|
|
end
|
|
end
|
|
end
|