add lti variable substitution support
Added a basic variable substitution framework and implemented a few variables as examples: $Person.name.full, $Person.name.given, $Person.name.family Added a concluded enrollment roles variable so that a tool can know what a users concluded roles were. The variable is $Canvas.membership.concludedRoles Variable names are case sensitive. Test Plan: * Create an lti tool launch with a custom key like this: * custom_my_var=$Person.name.full * With a concluded user do a tool launch that has the $Canvas.membership.concludedRoles variable * it should list the concluded roles * Configure tools through an XML file with a custom param and make sure it works Change-Id: Iefb85c441680c7ab3623ce85e405e1c48cda837c Reviewed-on: https://gerrit.instructure.com/21193 QA-Review: Clare Strong <clare@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Brad Humphrey <brad@instructure.com> Product-Review: Bracken Mosbacker <bracken@instructure.com>
This commit is contained in:
parent
d01c5fddf6
commit
5a689faa6d
229
lib/basic_lti.rb
229
lib/basic_lti.rb
|
@ -1,209 +1,24 @@
|
|||
#
|
||||
# Copyright (C) 2013 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/>.
|
||||
#
|
||||
module BasicLTI
|
||||
def self.explicit_signature_settings(timestamp, nonce)
|
||||
@timestamp = timestamp
|
||||
@nonce = nonce
|
||||
end
|
||||
|
||||
def self.generate_params(params, url, key, secret)
|
||||
require 'uri'
|
||||
require 'oauth'
|
||||
require 'oauth/consumer'
|
||||
uri = URI.parse(url)
|
||||
|
||||
if uri.port == uri.default_port
|
||||
host = uri.host
|
||||
else
|
||||
host = "#{uri.host}:#{uri.port}"
|
||||
end
|
||||
|
||||
consumer = OAuth::Consumer.new(key, secret, {
|
||||
:site => "#{uri.scheme}://#{host}",
|
||||
:signature_method => "HMAC-SHA1"
|
||||
})
|
||||
|
||||
path = uri.path
|
||||
path = '/' if path.empty?
|
||||
if !uri.query.blank?
|
||||
CGI.parse(uri.query).each do |query_key, query_values|
|
||||
unless params[query_key]
|
||||
params[query_key] = query_values.first
|
||||
end
|
||||
end
|
||||
end
|
||||
options = {
|
||||
:scheme => 'body',
|
||||
:timestamp => @timestamp,
|
||||
:nonce => @nonce
|
||||
}
|
||||
request = consumer.create_signed_request(:post, path, nil, options, params.stringify_keys)
|
||||
|
||||
# the request is made by a html form in the user's browser, so we
|
||||
# want to revert the escapage and return the hash of post parameters ready
|
||||
# for embedding in a html view
|
||||
hash = {}
|
||||
request.body.split(/&/).each do |param|
|
||||
key, val = param.split(/=/).map{|v| CGI.unescape(v) }
|
||||
hash[key] = val
|
||||
end
|
||||
hash
|
||||
end
|
||||
|
||||
def self.generate(*args)
|
||||
BasicLTI::ToolLaunch.new(*args).generate
|
||||
end
|
||||
|
||||
# Returns the LTI membership based on the LTI specs here: http://www.imsglobal.org/LTI/v1p1pd/ltiIMGv1p1pd.html#_Toc309649701
|
||||
def self.user_lti_data(user, context=nil)
|
||||
data = {}
|
||||
memberships = []
|
||||
if context.is_a?(Course)
|
||||
memberships += user.current_enrollments.find_all_by_course_id(context.id).uniq
|
||||
data['enrollment_state'] = memberships.any?{|membership| membership.state_based_on_date == :active} ? 'active' : 'inactive'
|
||||
end
|
||||
if context.respond_to?(:account_chain) && !context.account_chain_ids.empty?
|
||||
memberships += user.account_users.find_all_by_account_id(context.account_chain_ids).uniq
|
||||
end
|
||||
data['role_types'] = memberships.map{|membership|
|
||||
case membership
|
||||
when StudentEnrollment, StudentViewEnrollment
|
||||
'Learner'
|
||||
when TeacherEnrollment
|
||||
'Instructor'
|
||||
when TaEnrollment
|
||||
'urn:lti:role:ims/lis/TeachingAssistant'
|
||||
when DesignerEnrollment
|
||||
'ContentDeveloper'
|
||||
when ObserverEnrollment
|
||||
'urn:lti:instrole:ims/lis/Observer'
|
||||
when AccountUser
|
||||
'urn:lti:instrole:ims/lis/Administrator'
|
||||
else
|
||||
'urn:lti:instrole:ims/lis/Observer'
|
||||
end
|
||||
}.uniq
|
||||
data['role_types'] = ["urn:lti:sysrole:ims/lis/None"] if memberships.empty?
|
||||
data
|
||||
end
|
||||
|
||||
class ToolLaunch < Struct.new(:url, :tool, :user, :context, :link_code, :return_url, :resource_type, :root_account, :hash)
|
||||
|
||||
def initialize(options)
|
||||
self.url = options[:url] || raise("URL required for generating Basic LTI content")
|
||||
self.tool = options[:tool] || raise("Tool required for generating Basic LTI content")
|
||||
self.user = options[:user] || raise("User required for generating Basic LTI content")
|
||||
self.context = options[:context] || raise("Context required for generating Basic LTI content")
|
||||
self.link_code = options[:link_code] || raise("Link Code required for generating Basic LTI content")
|
||||
self.return_url = options[:return_url] || raise("Return URL required for generating Basic LTI content")
|
||||
self.resource_type = options[:resource_type]
|
||||
if self.context.respond_to? :root_account
|
||||
self.root_account = context.root_account
|
||||
elsif self.tool.context.respond_to? :root_account
|
||||
self.root_account = tool.context.root_account
|
||||
end
|
||||
root_account || raise("Root account required for generating Basic LTI content")
|
||||
|
||||
self.hash = {}
|
||||
end
|
||||
|
||||
def for_assignment!(assignment, outcome_service_url, legacy_outcome_service_url)
|
||||
hash['lis_result_sourcedid'] = BasicLTI::BasicOutcomes.encode_source_id(tool, context, assignment, user)
|
||||
hash['lis_outcome_service_url'] = outcome_service_url
|
||||
hash['ext_ims_lis_basic_outcome_url'] = legacy_outcome_service_url
|
||||
hash['ext_outcome_data_values_accepted'] = ['url', 'text'].join(',')
|
||||
hash['custom_canvas_assignment_title'] = assignment.title
|
||||
hash['custom_canvas_assignment_points_possible'] = assignment.points_possible
|
||||
if tool.public?
|
||||
hash['custom_canvas_assignment_id'] = assignment.id
|
||||
end
|
||||
end
|
||||
|
||||
def for_homework_submission!(assignment)
|
||||
self.resource_type = 'homework_submission'
|
||||
|
||||
return_types_map = {'online_upload' => 'file', 'online_url' => 'url'}
|
||||
return_types = []
|
||||
assignment.submission_types.split(',').each do |submission_type|
|
||||
submission_type.strip!
|
||||
return_types << return_types_map[submission_type.strip] if return_types_map.has_key? submission_type
|
||||
end
|
||||
hash['ext_content_return_types'] = return_types.join(',') unless return_types.blank?
|
||||
hash['ext_content_file_extensions'] = assignment.allowed_extensions.join(',') unless assignment.allowed_extensions.blank?
|
||||
|
||||
hash['custom_canvas_assignment_id'] = assignment.id if tool.public?
|
||||
end
|
||||
|
||||
def generate
|
||||
hash['lti_message_type'] = 'basic-lti-launch-request'
|
||||
hash['lti_version'] = 'LTI-1p0'
|
||||
hash['resource_link_id'] = link_code
|
||||
hash['resource_link_title'] = tool.name
|
||||
hash['user_id'] = user.opaque_identifier(:asset_string)
|
||||
hash['user_image'] = user.avatar_url
|
||||
user_data = BasicLTI.user_lti_data(user, context)
|
||||
hash['roles'] = user_data['role_types'].join(',') # AccountAdmin, Student, Faculty or Observer
|
||||
hash['custom_canvas_enrollment_state'] = user_data['enrollment_state'] if user_data['enrollment_state']
|
||||
|
||||
if tool.include_name?
|
||||
hash['lis_person_name_given'] = user.first_name
|
||||
hash['lis_person_name_family'] = user.last_name
|
||||
hash['lis_person_name_full'] = user.name
|
||||
end
|
||||
if tool.include_email?
|
||||
hash['lis_person_contact_email_primary'] = user.email
|
||||
end
|
||||
if tool.public?
|
||||
hash['custom_canvas_user_id'] = user.id
|
||||
pseudo = user.find_pseudonym_for_account(self.root_account)
|
||||
if pseudo
|
||||
hash['lis_person_sourcedid'] = pseudo.sis_user_id if pseudo.sis_user_id
|
||||
hash['custom_canvas_user_login_id'] = pseudo.unique_id
|
||||
end
|
||||
if context.is_a?(Course)
|
||||
hash['custom_canvas_course_id'] = context.id
|
||||
hash['lis_course_offering_sourcedid'] = context.sis_source_id if context.sis_source_id
|
||||
elsif context.is_a?(Account)
|
||||
hash['custom_canvas_account_id'] = context.id
|
||||
hash['custom_canvas_account_sis_id'] = context.sis_source_id if context.sis_source_id
|
||||
end
|
||||
hash['custom_canvas_api_domain'] = root_account.domain
|
||||
end
|
||||
|
||||
# need to set the locale here (instead of waiting for the first call to
|
||||
# I18n.t like we usually do), because otherwise we'll have the wrong code
|
||||
# for the launch_presentation_locale.
|
||||
I18n.set_locale_with_localizer
|
||||
|
||||
hash['context_id'] = context.opaque_identifier(:asset_string)
|
||||
hash['context_title'] = context.name
|
||||
hash['context_label'] = context.course_code if context.respond_to?(:course_code)
|
||||
hash['launch_presentation_locale'] = I18n.locale || I18n.default_locale.to_s
|
||||
hash['launch_presentation_document_target'] = 'iframe'
|
||||
hash['launch_presentation_width'] = tool.extension_setting(resource_type, :selection_width) if resource_type
|
||||
hash['launch_presentation_height'] = tool.extension_setting(resource_type, :selection_height) if resource_type
|
||||
hash['launch_presentation_return_url'] = return_url
|
||||
hash['tool_consumer_instance_guid'] = root_account.lti_guid
|
||||
hash['tool_consumer_instance_name'] = root_account.name
|
||||
hash['tool_consumer_instance_contact_email'] = HostUrl.outgoing_email_address # TODO: find a better email address to use here
|
||||
hash['tool_consumer_info_product_family_code'] = 'canvas'
|
||||
hash['tool_consumer_info_version'] = 'cloud'
|
||||
tool.set_custom_fields(hash, resource_type)
|
||||
if resource_type == 'editor_button'
|
||||
hash['selection_directive'] = 'embed_content' #backwards compatibility
|
||||
hash['ext_content_intended_use'] = 'embed'
|
||||
hash['ext_content_return_types'] = 'oembed,lti_launch_url,url,image_url,iframe'
|
||||
hash['ext_content_return_url'] = return_url
|
||||
elsif resource_type == 'resource_selection'
|
||||
hash['selection_directive'] = 'select_link' #backwards compatibility
|
||||
hash['ext_content_intended_use'] = 'navigation'
|
||||
hash['ext_content_return_types'] = 'lti_launch_url'
|
||||
hash['ext_content_return_url'] = return_url
|
||||
elsif resource_type == 'homework_submission'
|
||||
hash['ext_content_intended_use'] = 'homework'
|
||||
hash['ext_content_return_url'] = return_url
|
||||
end
|
||||
hash['oauth_callback'] = 'about:blank'
|
||||
BasicLTI.generate_params(hash, url, tool.consumer_key, tool.shared_secret)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
require_dependency 'basic_lti/basic_lti'
|
||||
require_dependency 'basic_lti/tool_launch'
|
||||
require_dependency 'basic_lti/basic_outcomes'
|
||||
require_dependency 'basic_lti/variable_substitutor'
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
#
|
||||
# Copyright (C) 2013 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/>.
|
||||
#
|
||||
module BasicLTI
|
||||
def self.explicit_signature_settings(timestamp, nonce)
|
||||
@timestamp = timestamp
|
||||
@nonce = nonce
|
||||
end
|
||||
|
||||
def self.generate_params(params, url, key, secret)
|
||||
require 'uri'
|
||||
require 'oauth'
|
||||
require 'oauth/consumer'
|
||||
uri = URI.parse(url)
|
||||
|
||||
if uri.port == uri.default_port
|
||||
host = uri.host
|
||||
else
|
||||
host = "#{uri.host}:#{uri.port}"
|
||||
end
|
||||
|
||||
consumer = OAuth::Consumer.new(key, secret, {
|
||||
:site => "#{uri.scheme}://#{host}",
|
||||
:signature_method => "HMAC-SHA1"
|
||||
})
|
||||
|
||||
path = uri.path
|
||||
path = '/' if path.empty?
|
||||
if !uri.query.blank?
|
||||
CGI.parse(uri.query).each do |query_key, query_values|
|
||||
unless params[query_key]
|
||||
params[query_key] = query_values.first
|
||||
end
|
||||
end
|
||||
end
|
||||
options = {
|
||||
:scheme => 'body',
|
||||
:timestamp => @timestamp,
|
||||
:nonce => @nonce
|
||||
}
|
||||
request = consumer.create_signed_request(:post, path, nil, options, params.stringify_keys)
|
||||
|
||||
# the request is made by a html form in the user's browser, so we
|
||||
# want to revert the escapage and return the hash of post parameters ready
|
||||
# for embedding in a html view
|
||||
hash = {}
|
||||
request.body.split(/&/).each do |param|
|
||||
key, val = param.split(/=/).map{|v| CGI.unescape(v) }
|
||||
hash[key] = val
|
||||
end
|
||||
hash
|
||||
end
|
||||
|
||||
def self.generate(*args)
|
||||
BasicLTI::ToolLaunch.new(*args).generate
|
||||
end
|
||||
|
||||
# Returns the LTI membership based on the LTI specs here: http://www.imsglobal.org/LTI/v1p1pd/ltiIMGv1p1pd.html#_Toc309649701
|
||||
def self.user_lti_data(user, context=nil)
|
||||
data = {}
|
||||
memberships = []
|
||||
concluded_memberships = []
|
||||
|
||||
# collect canvas course/account enrollments
|
||||
if context.is_a?(Course)
|
||||
memberships += user.current_enrollments.find_all_by_course_id(context.id).uniq
|
||||
data['enrollment_state'] = memberships.any?{|membership| membership.state_based_on_date == :active} ? 'active' : 'inactive'
|
||||
concluded_memberships = user.concluded_enrollments.find_all_by_course_id(context.id).uniq
|
||||
end
|
||||
if context.respond_to?(:account_chain) && !context.account_chain_ids.empty?
|
||||
memberships += user.account_users.find_all_by_account_id(context.account_chain_ids).uniq
|
||||
end
|
||||
|
||||
# convert canvas enrollments to LIS roles
|
||||
data['role_types'] = memberships.map{|membership|
|
||||
enrollment_to_membership(membership)
|
||||
}.uniq
|
||||
data['role_types'] = ["urn:lti:sysrole:ims/lis/None"] if memberships.empty?
|
||||
|
||||
data['concluded_role_types'] = concluded_memberships.map{|membership|
|
||||
enrollment_to_membership(membership)
|
||||
}.uniq
|
||||
data['concluded_role_types'] = ["urn:lti:sysrole:ims/lis/None"] if concluded_memberships.empty?
|
||||
|
||||
data
|
||||
end
|
||||
|
||||
def self.enrollment_to_membership(membership)
|
||||
case membership
|
||||
when StudentEnrollment, StudentViewEnrollment
|
||||
'Learner'
|
||||
when TeacherEnrollment
|
||||
'Instructor'
|
||||
when TaEnrollment
|
||||
'urn:lti:role:ims/lis/TeachingAssistant'
|
||||
when DesignerEnrollment
|
||||
'ContentDeveloper'
|
||||
when ObserverEnrollment
|
||||
'urn:lti:instrole:ims/lis/Observer'
|
||||
when AccountUser
|
||||
'urn:lti:instrole:ims/lis/Administrator'
|
||||
else
|
||||
'urn:lti:instrole:ims/lis/Observer'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,3 +1,20 @@
|
|||
#
|
||||
# Copyright (C) 2013 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/>.
|
||||
#
|
||||
module BasicLTI::BasicOutcomes
|
||||
class Unauthorized < Exception; end
|
||||
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
#
|
||||
# Copyright (C) 2013 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/>.
|
||||
#
|
||||
module BasicLTI
|
||||
class ToolLaunch < Struct.new(:url, :tool, :user, :context, :link_code, :return_url, :resource_type, :root_account, :hash, :user_data)
|
||||
|
||||
def initialize(options)
|
||||
self.url = options[:url] || raise("URL required for generating Basic LTI content")
|
||||
self.tool = options[:tool] || raise("Tool required for generating Basic LTI content")
|
||||
self.user = options[:user] || raise("User required for generating Basic LTI content")
|
||||
self.context = options[:context] || raise("Context required for generating Basic LTI content")
|
||||
self.link_code = options[:link_code] || raise("Link Code required for generating Basic LTI content")
|
||||
self.return_url = options[:return_url] || raise("Return URL required for generating Basic LTI content")
|
||||
self.resource_type = options[:resource_type]
|
||||
if self.context.respond_to? :root_account
|
||||
self.root_account = context.root_account
|
||||
elsif self.tool.context.respond_to? :root_account
|
||||
self.root_account = tool.context.root_account
|
||||
end
|
||||
root_account || raise("Root account required for generating Basic LTI content")
|
||||
|
||||
self.hash = {}
|
||||
end
|
||||
|
||||
def for_assignment!(assignment, outcome_service_url, legacy_outcome_service_url)
|
||||
hash['lis_result_sourcedid'] = BasicLTI::BasicOutcomes.encode_source_id(tool, context, assignment, user)
|
||||
hash['lis_outcome_service_url'] = outcome_service_url
|
||||
hash['ext_ims_lis_basic_outcome_url'] = legacy_outcome_service_url
|
||||
hash['ext_outcome_data_values_accepted'] = ['url', 'text'].join(',')
|
||||
hash['custom_canvas_assignment_title'] = assignment.title
|
||||
hash['custom_canvas_assignment_points_possible'] = assignment.points_possible
|
||||
if tool.public?
|
||||
hash['custom_canvas_assignment_id'] = assignment.id
|
||||
end
|
||||
end
|
||||
|
||||
def for_homework_submission!(assignment)
|
||||
self.resource_type = 'homework_submission'
|
||||
|
||||
return_types_map = {'online_upload' => 'file', 'online_url' => 'url'}
|
||||
return_types = []
|
||||
assignment.submission_types.split(',').each do |submission_type|
|
||||
submission_type.strip!
|
||||
return_types << return_types_map[submission_type.strip] if return_types_map.has_key? submission_type
|
||||
end
|
||||
hash['ext_content_return_types'] = return_types.join(',') unless return_types.blank?
|
||||
hash['ext_content_file_extensions'] = assignment.allowed_extensions.join(',') unless assignment.allowed_extensions.blank?
|
||||
|
||||
hash['custom_canvas_assignment_id'] = assignment.id if tool.public?
|
||||
end
|
||||
|
||||
def generate
|
||||
hash['lti_message_type'] = 'basic-lti-launch-request'
|
||||
hash['lti_version'] = 'LTI-1p0'
|
||||
hash['resource_link_id'] = link_code
|
||||
hash['resource_link_title'] = tool.name
|
||||
hash['user_id'] = user.opaque_identifier(:asset_string)
|
||||
hash['user_image'] = user.avatar_url
|
||||
self.user_data = BasicLTI.user_lti_data(user, context)
|
||||
hash['roles'] = self.user_data['role_types'].join(',') # AccountAdmin, Student, Faculty or Observer
|
||||
hash['custom_canvas_enrollment_state'] = self.user_data['enrollment_state'] if self.user_data['enrollment_state']
|
||||
|
||||
if tool.include_name?
|
||||
hash['lis_person_name_given'] = user.first_name
|
||||
hash['lis_person_name_family'] = user.last_name
|
||||
hash['lis_person_name_full'] = user.name
|
||||
end
|
||||
if tool.include_email?
|
||||
hash['lis_person_contact_email_primary'] = user.email
|
||||
end
|
||||
if tool.public?
|
||||
hash['custom_canvas_user_id'] = user.id
|
||||
pseudo = user.find_pseudonym_for_account(self.root_account)
|
||||
if pseudo
|
||||
hash['lis_person_sourcedid'] = pseudo.sis_user_id if pseudo.sis_user_id
|
||||
hash['custom_canvas_user_login_id'] = pseudo.unique_id
|
||||
end
|
||||
if context.is_a?(Course)
|
||||
hash['custom_canvas_course_id'] = context.id
|
||||
hash['lis_course_offering_sourcedid'] = context.sis_source_id if context.sis_source_id
|
||||
elsif context.is_a?(Account)
|
||||
hash['custom_canvas_account_id'] = context.id
|
||||
hash['custom_canvas_account_sis_id'] = context.sis_source_id if context.sis_source_id
|
||||
end
|
||||
hash['custom_canvas_api_domain'] = root_account.domain
|
||||
end
|
||||
|
||||
# need to set the locale here (instead of waiting for the first call to
|
||||
# I18n.t like we usually do), because otherwise we'll have the wrong code
|
||||
# for the launch_presentation_locale.
|
||||
I18n.set_locale_with_localizer
|
||||
|
||||
hash['context_id'] = context.opaque_identifier(:asset_string)
|
||||
hash['context_title'] = context.name
|
||||
hash['context_label'] = context.course_code if context.respond_to?(:course_code)
|
||||
hash['launch_presentation_locale'] = I18n.locale || I18n.default_locale.to_s
|
||||
hash['launch_presentation_document_target'] = 'iframe'
|
||||
hash['launch_presentation_width'] = tool.extension_setting(resource_type, :selection_width) if resource_type
|
||||
hash['launch_presentation_height'] = tool.extension_setting(resource_type, :selection_height) if resource_type
|
||||
hash['launch_presentation_return_url'] = return_url
|
||||
hash['tool_consumer_instance_guid'] = root_account.lti_guid
|
||||
hash['tool_consumer_instance_name'] = root_account.name
|
||||
hash['tool_consumer_instance_contact_email'] = HostUrl.outgoing_email_address # TODO: find a better email address to use here
|
||||
hash['tool_consumer_info_product_family_code'] = 'canvas'
|
||||
hash['tool_consumer_info_version'] = 'cloud'
|
||||
tool.set_custom_fields(hash, resource_type)
|
||||
if resource_type == 'editor_button'
|
||||
hash['selection_directive'] = 'embed_content' #backwards compatibility
|
||||
hash['ext_content_intended_use'] = 'embed'
|
||||
hash['ext_content_return_types'] = 'oembed,lti_launch_url,url,image_url,iframe'
|
||||
hash['ext_content_return_url'] = return_url
|
||||
elsif resource_type == 'resource_selection'
|
||||
hash['selection_directive'] = 'select_link' #backwards compatibility
|
||||
hash['ext_content_intended_use'] = 'navigation'
|
||||
hash['ext_content_return_types'] = 'lti_launch_url'
|
||||
hash['ext_content_return_url'] = return_url
|
||||
elsif resource_type == 'homework_submission'
|
||||
hash['ext_content_intended_use'] = 'homework'
|
||||
hash['ext_content_return_url'] = return_url
|
||||
end
|
||||
hash['oauth_callback'] = 'about:blank'
|
||||
|
||||
VariableSubstitutor.new(self).substitute!
|
||||
BasicLTI.generate_params(hash, url, tool.consumer_key, tool.shared_secret)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,76 @@
|
|||
#
|
||||
# Copyright (C) 2013 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/>.
|
||||
#
|
||||
module BasicLTI
|
||||
class VariableSubstitutor
|
||||
|
||||
def initialize(tool_launch)
|
||||
@launch = tool_launch
|
||||
end
|
||||
|
||||
|
||||
# modifies the launch hash by substituting all the known variables
|
||||
# if a variable is not supported or not allowed the value will not change
|
||||
def substitute!
|
||||
@launch.hash.each do |key, val|
|
||||
if val.to_s.starts_with? '$'
|
||||
method_name = "sub_#{var_to_method(val)}"
|
||||
if self.respond_to?(method_name, true)
|
||||
if new_val = self.send(method_name)
|
||||
@launch.hash[key] = new_val
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def var_to_method(var_name)
|
||||
var_name.gsub('$', '').gsub('.', '_')
|
||||
end
|
||||
|
||||
|
||||
### These should return the value of substituting the variable the method is named for
|
||||
### The method name should be prefixed with 'sub_' and have the same name as the variable except change all . to _
|
||||
### For Example, to support substituting $Person.name.full, create a method called sub_Person_name_full
|
||||
### If appropriate, check permissions by using the @launch object to reference the user/course
|
||||
|
||||
# $Person.name.full
|
||||
def sub_Person_name_full
|
||||
@launch.tool.include_name? ? @launch.user.name : nil
|
||||
end
|
||||
|
||||
# $Person.name.family
|
||||
def sub_Person_name_family
|
||||
@launch.tool.include_name? ? @launch.user.last_name : nil
|
||||
end
|
||||
|
||||
# $Person.name.given
|
||||
def sub_Person_name_given
|
||||
@launch.tool.include_name? ? @launch.user.first_name : nil
|
||||
end
|
||||
|
||||
# returns the same LIS Role values as the default 'roles' parameter,
|
||||
# but for concluded enrollments
|
||||
# $Canvas.membership.concludedRoles
|
||||
def sub_Canvas_membership_concludedRoles
|
||||
@launch.user_data['concluded_role_types'] ? @launch.user_data['concluded_role_types'].join(',') : nil
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -0,0 +1,73 @@
|
|||
#
|
||||
# Copyright (C) 2011 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/>.
|
||||
#
|
||||
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../../sharding_spec_helper.rb')
|
||||
|
||||
|
||||
describe BasicLTI::VariableSubstitutor do
|
||||
|
||||
before do
|
||||
@launch = mock()
|
||||
@subber = BasicLTI::VariableSubstitutor.new(@launch)
|
||||
end
|
||||
|
||||
it "should substitute user info if allowed" do
|
||||
@launch.stubs(:user).returns(@launch)
|
||||
@launch.stubs(:tool).returns(@launch)
|
||||
@launch.stubs("include_name?").returns(true)
|
||||
@launch.stubs(:name).returns("full name")
|
||||
@launch.stubs(:first_name).returns("full")
|
||||
@launch.stubs(:last_name).returns("name")
|
||||
@hash = {'full' => '$Person.name.full', 'last' => '$Person.name.family', 'first' => '$Person.name.given'}
|
||||
@launch.stubs(:hash).returns(@hash)
|
||||
|
||||
@subber.substitute!
|
||||
@hash.should == {'full' => 'full name', 'last' => 'name', 'first' => 'full'}
|
||||
end
|
||||
|
||||
it "should leave variable if not allowed" do
|
||||
@launch.stubs(:user).returns(@launch)
|
||||
@launch.stubs(:tool).returns(@launch)
|
||||
@launch.stubs("include_name?").returns(false)
|
||||
@launch.stubs(:name).returns("full name")
|
||||
@hash = {'full' => '$Person.name.full'}
|
||||
@launch.stubs(:hash).returns(@hash)
|
||||
|
||||
@subber.substitute!
|
||||
@hash.should == {'full' => '$Person.name.full'}
|
||||
end
|
||||
|
||||
it "should leave variable if not supported" do
|
||||
@hash = {'something_crazy' => '$Person.social_security_number'}
|
||||
@launch.stubs(:hash).returns(@hash)
|
||||
|
||||
@subber.substitute!
|
||||
@hash.should == {'something_crazy' => '$Person.social_security_number'}
|
||||
end
|
||||
|
||||
it "should add concluded enrollments" do
|
||||
@hash = {'concluded_roles' => '$Canvas.membership.concludedRoles'}
|
||||
@launch.stubs(:hash).returns(@hash)
|
||||
@launch.stubs(:user_data).returns({'concluded_role_types' => ['hey']})
|
||||
|
||||
@subber.substitute!
|
||||
@hash.should == {'concluded_roles' => 'hey'}
|
||||
end
|
||||
|
||||
|
||||
end
|
|
@ -367,57 +367,66 @@ describe BasicLTI do
|
|||
BasicLTI.user_lti_data(student, @course2)['role_types'].should == ['urn:lti:sysrole:ims/lis/None']
|
||||
end
|
||||
|
||||
it "xml converter should use raise an error when unescaped ampersands are used in launch url" do
|
||||
xml = <<-XML
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<cartridge_basiclti_link xmlns="http://www.imsglobal.org/xsd/imslticc_v1p0"
|
||||
xmlns:blti = "http://www.imsglobal.org/xsd/imsbasiclti_v1p0"
|
||||
xmlns:lticm ="http://www.imsglobal.org/xsd/imslticm_v1p0"
|
||||
xmlns:lticp ="http://www.imsglobal.org/xsd/imslticp_v1p0"
|
||||
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation = "http://www.imsglobal.org/xsd/imslticc_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticc_v1p0.xsd
|
||||
http://www.imsglobal.org/xsd/imsbasiclti_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imsbasiclti_v1p0.xsd
|
||||
http://www.imsglobal.org/xsd/imslticm_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticm_v1p0.xsd
|
||||
http://www.imsglobal.org/xsd/imslticp_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticp_v1p0.xsd">
|
||||
<blti:title>Other Name</blti:title>
|
||||
<blti:description>Description</blti:description>
|
||||
<blti:launch_url>http://example.com/other_url?unescapedampersands=1&arebadnews=2</blti:launch_url>
|
||||
<cartridge_bundle identifierref="BLTI001_Bundle"/>
|
||||
<cartridge_icon identifierref="BLTI001_Icon"/>
|
||||
</cartridge_basiclti_link>
|
||||
XML
|
||||
lti = CC::Importer::BLTIConverter.new
|
||||
lambda {lti.convert_blti_xml(xml)}.should raise_error
|
||||
it "should list concluded roles" do
|
||||
course_with_student(:active_all => true)
|
||||
course_with_teacher(:course => @course, :active_all => true)
|
||||
@course.complete
|
||||
BasicLTI.user_lti_data(@student, @course)['concluded_role_types'].should == ['Learner']
|
||||
BasicLTI.user_lti_data(@teacher, @course)['concluded_role_types'].should == ['Instructor']
|
||||
end
|
||||
|
||||
it "xml converter should use raise an error when unescaped ampersands are used in custom url properties" do
|
||||
xml = <<-XML
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<cartridge_basiclti_link xmlns="http://www.imsglobal.org/xsd/imslticc_v1p0"
|
||||
xmlns:blti = "http://www.imsglobal.org/xsd/imsbasiclti_v1p0"
|
||||
xmlns:lticm ="http://www.imsglobal.org/xsd/imslticm_v1p0"
|
||||
xmlns:lticp ="http://www.imsglobal.org/xsd/imslticp_v1p0"
|
||||
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation = "http://www.imsglobal.org/xsd/imslticc_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticc_v1p0.xsd
|
||||
http://www.imsglobal.org/xsd/imsbasiclti_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imsbasiclti_v1p0.xsd
|
||||
http://www.imsglobal.org/xsd/imslticm_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticm_v1p0.xsd
|
||||
http://www.imsglobal.org/xsd/imslticp_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticp_v1p0.xsd">
|
||||
<blti:title>Other Name</blti:title>
|
||||
<blti:description>Description</blti:description>
|
||||
<blti:launch_url>http://example.com</blti:launch_url>
|
||||
<blti:extensions platform="canvas.instructure.com">
|
||||
<lticm:property name="privacy_level">public</lticm:property>
|
||||
<lticm:options name="course_navigation">
|
||||
<lticm:property name="url">https://example.com/attendance?param1=1¶m2=2</lticm:property>
|
||||
<lticm:property name="enabled">true</lticm:property>
|
||||
</lticm:options>
|
||||
</blti:extensions>
|
||||
<cartridge_bundle identifierref="BLTI001_Bundle"/>
|
||||
<cartridge_icon identifierref="BLTI001_Icon"/>
|
||||
</cartridge_basiclti_link>
|
||||
XML
|
||||
lti = CC::Importer::BLTIConverter.new
|
||||
lambda {lti.convert_blti_xml(xml)}.should raise_error
|
||||
end
|
||||
end
|
||||
|
||||
it "xml converter should use raise an error when unescaped ampersands are used in launch url" do
|
||||
xml = <<-XML
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<cartridge_basiclti_link xmlns="http://www.imsglobal.org/xsd/imslticc_v1p0"
|
||||
xmlns:blti = "http://www.imsglobal.org/xsd/imsbasiclti_v1p0"
|
||||
xmlns:lticm ="http://www.imsglobal.org/xsd/imslticm_v1p0"
|
||||
xmlns:lticp ="http://www.imsglobal.org/xsd/imslticp_v1p0"
|
||||
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation = "http://www.imsglobal.org/xsd/imslticc_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticc_v1p0.xsd
|
||||
http://www.imsglobal.org/xsd/imsbasiclti_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imsbasiclti_v1p0.xsd
|
||||
http://www.imsglobal.org/xsd/imslticm_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticm_v1p0.xsd
|
||||
http://www.imsglobal.org/xsd/imslticp_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticp_v1p0.xsd">
|
||||
<blti:title>Other Name</blti:title>
|
||||
<blti:description>Description</blti:description>
|
||||
<blti:launch_url>http://example.com/other_url?unescapedampersands=1&arebadnews=2</blti:launch_url>
|
||||
<cartridge_bundle identifierref="BLTI001_Bundle"/>
|
||||
<cartridge_icon identifierref="BLTI001_Icon"/>
|
||||
</cartridge_basiclti_link>
|
||||
XML
|
||||
lti = CC::Importer::BLTIConverter.new
|
||||
lambda {lti.convert_blti_xml(xml)}.should raise_error
|
||||
end
|
||||
|
||||
it "xml converter should use raise an error when unescaped ampersands are used in custom url properties" do
|
||||
xml = <<-XML
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<cartridge_basiclti_link xmlns="http://www.imsglobal.org/xsd/imslticc_v1p0"
|
||||
xmlns:blti = "http://www.imsglobal.org/xsd/imsbasiclti_v1p0"
|
||||
xmlns:lticm ="http://www.imsglobal.org/xsd/imslticm_v1p0"
|
||||
xmlns:lticp ="http://www.imsglobal.org/xsd/imslticp_v1p0"
|
||||
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation = "http://www.imsglobal.org/xsd/imslticc_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticc_v1p0.xsd
|
||||
http://www.imsglobal.org/xsd/imsbasiclti_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imsbasiclti_v1p0.xsd
|
||||
http://www.imsglobal.org/xsd/imslticm_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticm_v1p0.xsd
|
||||
http://www.imsglobal.org/xsd/imslticp_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticp_v1p0.xsd">
|
||||
<blti:title>Other Name</blti:title>
|
||||
<blti:description>Description</blti:description>
|
||||
<blti:launch_url>http://example.com</blti:launch_url>
|
||||
<blti:extensions platform="canvas.instructure.com">
|
||||
<lticm:property name="privacy_level">public</lticm:property>
|
||||
<lticm:options name="course_navigation">
|
||||
<lticm:property name="url">https://example.com/attendance?param1=1¶m2=2</lticm:property>
|
||||
<lticm:property name="enabled">true</lticm:property>
|
||||
</lticm:options>
|
||||
</blti:extensions>
|
||||
<cartridge_bundle identifierref="BLTI001_Bundle"/>
|
||||
<cartridge_icon identifierref="BLTI001_Icon"/>
|
||||
</cartridge_basiclti_link>
|
||||
XML
|
||||
lti = CC::Importer::BLTIConverter.new
|
||||
lambda {lti.convert_blti_xml(xml)}.should raise_error
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue