Skip to content

Commit 1a78196

Browse files
committed
create an api to return lti launch definitions
fixes PLAT-639 test-plan: *the following api endpoints should return lti launch definitions for each tool installed GET api/v1/courses/:course_id/lti_apps/launch_definitions? placements[]=module_item&placements[]=resource_selection GET /api/v1/accounts/:account_id/lti_apps/launch_definitions? placements[]=module_item&placements[]=resource_selection Change-Id: I36ce6176f5cbb83dad7cd8578985acc2a02afdfd Reviewed-on: https://gerrit.instructure.com/41427 QA-Review: Clare Strong <clare@instructure.com> Reviewed-by: Brad Humphrey <brad@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> Product-Review: Nathan Mills <nathanm@instructure.com>
1 parent 0738723 commit 1a78196

9 files changed

Lines changed: 493 additions & 3 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Copyright (C) 2014 Instructure, Inc.
2+
#
3+
# This file is part of Canvas.
4+
#
5+
# Canvas is free software: you can redistribute it and/or modify it under
6+
# the terms of the GNU Affero General Public License as published by the Free
7+
# Software Foundation, version 3 of the License.
8+
#
9+
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
10+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11+
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
12+
# details.
13+
#
14+
# You should have received a copy of the GNU Affero General Public License along
15+
# with this program. If not, see <http://www.gnu.org/licenses/>.
16+
#
17+
module Lti
18+
class LtiAppsController < ApplicationController
19+
before_filter :require_context
20+
before_filter :require_user
21+
include Lti::ApiServiceHelper
22+
23+
def launch_definitions
24+
if authorized_action(@context, @current_user, :update)
25+
placements = params['placements'] || []
26+
collection = AppCollator.bookmarked_collection(@context, placements)
27+
28+
respond_to do |format|
29+
launch_defs = Api.paginate(collection, self, launch_definitions_url)
30+
format.json { render :json => AppCollator.launch_definitions(launch_defs, placements) }
31+
end
32+
end
33+
end
34+
35+
36+
private
37+
38+
def launch_definitions_url
39+
if @context.is_a? Course
40+
api_v1_course_launch_definitions_url(@context)
41+
else
42+
api_v1_account_launch_definitions_url(@context)
43+
end
44+
end
45+
46+
47+
end
48+
end

app/models/context_external_tool.rb

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def #{type}=(hash)
6464
def extension_setting(type, property = nil)
6565
type = type.to_sym
6666
return settings[type] unless property && settings[type]
67-
settings[type][property] || settings[property] || extension_default_value(property)
67+
settings[type][property] || settings[property] || extension_default_value(type, property)
6868
end
6969

7070
def set_extension_setting(type, hash)
@@ -287,14 +287,20 @@ def display_type(extension_type)
287287
extension_setting(extension_type, :display_type) || 'in_context'
288288
end
289289

290-
def extension_default_value(property)
290+
def extension_default_value(type, property)
291291
case property
292292
when :url
293293
url
294294
when :selection_width
295295
800
296296
when :selection_height
297297
400
298+
when :message_type
299+
if type == :resource_selection
300+
'resource_selection'
301+
else
302+
'basic-lti-launch-request'
303+
end
298304
else
299305
nil
300306
end
@@ -448,7 +454,6 @@ def self.all_tools_for(context, options={})
448454
return [] unless (options[:root_account] && options[:root_account].feature_enabled?(:lor_for_account)) ||
449455
(options[:current_user] && options[:current_user].feature_enabled?(:lor_for_user))
450456
end
451-
452457
contexts = []
453458
if options[:user]
454459
contexts << options[:user]
@@ -458,6 +463,7 @@ def self.all_tools_for(context, options={})
458463

459464
scope = ContextExternalTool.shard(context.shard).polymorphic_where(context: contexts).active
460465
scope = scope.having_setting(options[:type]) if options[:type]
466+
scope = scope.selectable if Canvas::Plugin.value_to_boolean(options[:selectable])
461467
scope.order(ContextExternalTool.best_unicode_collation_key('name'))
462468
end
463469

@@ -526,6 +532,8 @@ def self.find_integration_for(context, type)
526532
end
527533
}
528534

535+
scope :selectable, lambda { where("context_external_tools.not_selectable IS NOT TRUE") }
536+
529537
def self.find_for(id, context, type, raise_error=true)
530538
id = id[Api::ID_REGEX] if id.is_a?(String)
531539
unless id.present?

app/models/lti/app_collator.rb

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Copyright (C) 2014 Instructure, Inc.
2+
#
3+
# This file is part of Canvas.
4+
#
5+
# Canvas is free software: you can redistribute it and/or modify it under
6+
# the terms of the GNU Affero General Public License as published by the Free
7+
# Software Foundation, version 3 of the License.
8+
#
9+
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
10+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11+
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
12+
# details.
13+
#
14+
# You should have received a copy of the GNU Affero General Public License along
15+
# with this program. If not, see <http://www.gnu.org/licenses/>.
16+
#
17+
module Lti
18+
class AppCollator
19+
20+
def self.bookmarked_collection(context, placements)
21+
external_tools_scope = ContextExternalTool.all_tools_for(context, selectable: placements.include?('module_item'))
22+
external_tools_collection = BookmarkedCollection.wrap(ExternalToolBookmarker, external_tools_scope)
23+
message_handler_scope = MessageHandler.for_context(context).by_message_types('basic-lti-launch-request')
24+
message_handler_collection = BookmarkedCollection.wrap(MessageHandlerBookmarker, message_handler_scope)
25+
BookmarkedCollection.merge(
26+
['external_tools', external_tools_collection],
27+
['message_handlers', message_handler_collection]
28+
)
29+
end
30+
31+
def self.launch_definitions(collection, placements)
32+
collection.map do |o|
33+
case o
34+
when ContextExternalTool
35+
lti1_launch_definition(o, placements)
36+
when MessageHandler
37+
lti2_launch_definition(o, placements)
38+
end
39+
end
40+
end
41+
42+
43+
private
44+
45+
def self.lti1_launch_definition(tool, placements)
46+
placement = 'resource_selection'
47+
definition = {
48+
definition_type: tool.class.name,
49+
definition_id: tool.id,
50+
name: tool.name,
51+
domain: tool.domain,
52+
placements: {}
53+
}
54+
placements.each do |p|
55+
if tool.has_placement?(p) || p == 'module_item'
56+
definition[:placements][p.to_sym] = {
57+
message_type: tool.extension_setting(placement, :message_type) || tool.extension_default_value(placement, :message_type),
58+
url: tool.extension_setting(placement, :url) || tool.extension_default_value(placement, :url),
59+
title: tool.label_for(placement, I18n.locale || I18n.default_locale.to_s),
60+
}
61+
end
62+
end
63+
definition
64+
end
65+
66+
def self.lti2_launch_definition(message_handler, placements)
67+
{
68+
definition_type: message_handler.class.name,
69+
definition_id: message_handler.id,
70+
name: message_handler.resource_handler.name,
71+
domain: URI(message_handler.launch_path).host,
72+
placements: {
73+
module_item: {
74+
message_type: message_handler.message_type,
75+
url: message_handler.launch_path,
76+
title: message_handler.resource_handler.name,
77+
}
78+
}
79+
}
80+
end
81+
82+
module MessageHandlerBookmarker
83+
def self.bookmark_for(message_handler)
84+
message_handler.resource_handler.name
85+
end
86+
87+
def self.validate(bookmark)
88+
bookmark.is_a?(String)
89+
end
90+
91+
def self.restrict_scope(scope, pager)
92+
if pager.current_bookmark
93+
name = pager.current_bookmark
94+
comparison = (pager.include_bookmark ? ">=" : ">")
95+
scope = scope.where(
96+
"name #{comparison} ?",
97+
name)
98+
end
99+
scope.order('lti_resource_handlers.name')
100+
end
101+
end
102+
103+
module ExternalToolBookmarker
104+
def self.bookmark_for(external_tool)
105+
external_tool.name
106+
end
107+
108+
def self.validate(bookmark)
109+
bookmark.is_a?(String)
110+
end
111+
112+
def self.restrict_scope(scope, pager)
113+
if pager.current_bookmark
114+
name = pager.current_bookmark
115+
comparison = (pager.include_bookmark ? ">=" : ">")
116+
scope = scope.where(
117+
"name #{comparison} ?",
118+
name)
119+
end
120+
scope.order(:name)
121+
end
122+
end
123+
124+
end
125+
end

app/models/lti/message_handler.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
module Lti
2020
class MessageHandler< ActiveRecord::Base
2121
attr_accessible :message_type, :launch_path, :capabilities, :parameters, :resource_handler, :links
22+
attr_readonly :created_at
2223

2324
belongs_to :resource_handler, class_name: "Lti::ResourceHandler", :foreign_key => :resource_handler_id
2425
has_many :links, :class_name => 'Lti::LtiLink'

app/models/lti/resource_handler.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ module Lti
2020
class ResourceHandler < ActiveRecord::Base
2121

2222
attr_accessible :resource_type_code, :placements, :name, :description, :icon_info, :tool_proxy
23+
attr_readonly :created_at
2324

2425
belongs_to :tool_proxy, class_name: 'Lti::ToolProxy'
2526
has_many :message_handlers, class_name: 'Lti::MessageHandler', :foreign_key => :resource_handler_id

config/routes.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,14 @@ def et_routes(context)
10131013
et_routes("account")
10141014
end
10151015

1016+
scope(controller: 'lti/lti_apps') do
1017+
def et_routes(context)
1018+
get "#{context}s/:#{context}_id/lti_apps/launch_definitions", action: :launch_definitions, path_name: "#{context}_launch_definitions"
1019+
end
1020+
et_routes("course")
1021+
et_routes("account")
1022+
end
1023+
10161024
scope(:controller => :external_feeds) do
10171025
def ef_routes(context)
10181026
get "#{context}s/:#{context}_id/external_feeds", :action => :index, :path_name => "#{context}_external_feeds"

spec/apis/lti/lti_app_api_spec.rb

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#
2+
# Copyright (C) 2014 Instructure, Inc.
3+
#
4+
# This file is part of Canvas.
5+
#
6+
# Canvas is free software: you can redistribute it and/or modify it under
7+
# the terms of the GNU Affero General Public License as published by the Free
8+
# Software Foundation, version 3 of the License.
9+
#
10+
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
11+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12+
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13+
# details.
14+
#
15+
# You should have received a copy of the GNU Affero General Public License along
16+
# with this program. If not, see <http://www.gnu.org/licenses/>.
17+
#
18+
19+
require File.expand_path(File.dirname(__FILE__) + '/../api_spec_helper')
20+
21+
module Lti
22+
describe LtiAppsController, type: :request do
23+
24+
let (:account) { Account.create }
25+
let (:product_family) { ProductFamily.create(vendor_code: '123', product_code: 'abc', vendor_name: 'acme', root_account: account) }
26+
27+
describe '#launch_definitions' do
28+
29+
before do
30+
tp = create_tool_proxy
31+
tp.bindings.create(context: account)
32+
rh = create_resource_handler(tp)
33+
@mh = create_message_handler(rh)
34+
@external_tool = new_valid_external_tool(account)
35+
end
36+
37+
it 'returns a list of launch definitions for a context and placements' do
38+
course_with_teacher(active_all: true, user: user_with_pseudonym, account: account)
39+
json = api_call(:get, "/api/v1/courses/#{@course.id}/lti_apps/launch_definitions",
40+
{controller: 'lti/lti_apps', action: 'launch_definitions', format: 'json',
41+
placements: %w(module_item resource_selection), course_id: @course.id.to_s})
42+
json.select {|j| j['definition_type'] == @mh.class.name && j['definition_id'] == @mh.id.to_s}.should_not be_nil
43+
json.select {|j| j['definition_type'] == @external_tool.class.name && j['definition_id'] == @external_tool.id.to_s}.should_not be_nil
44+
end
45+
46+
it 'paginates the launch definitions' do
47+
5.times { |_| new_valid_external_tool(account) }
48+
course_with_teacher(active_all: true, user: user_with_pseudonym, account: account)
49+
json = api_call(:get, "/api/v1/courses/#{@course.id}/lti_apps/launch_definitions?per_page=3",
50+
{controller: 'lti/lti_apps', action: 'launch_definitions', format: 'json',
51+
placements: %w(module_item resource_selection), course_id: @course.id.to_s, per_page: '3'})
52+
53+
json_next = follow_pagination_link('next', :controller => 'lti/lti_apps', :action => 'launch_definitions')
54+
json.count.should == 3
55+
json_next.count.should == 3
56+
json
57+
end
58+
59+
60+
end
61+
62+
63+
def create_tool_proxy(opts = {})
64+
default_opts = {
65+
context: account,
66+
shared_secret: 'shared_secret',
67+
guid: SecureRandom.uuid,
68+
product_version: '1.0beta',
69+
lti_version: 'LTI-2p0',
70+
product_family: product_family,
71+
workflow_state: 'active',
72+
raw_data: 'some raw data'
73+
}
74+
ToolProxy.create(default_opts.merge(opts))
75+
end
76+
77+
def create_resource_handler(tool_proxy, opts = {})
78+
default_opts = {resource_type_code: 'code', name: (0...8).map { (65 + rand(26)).chr }.join, tool_proxy: tool_proxy}
79+
ResourceHandler.create(default_opts.merge(opts))
80+
end
81+
82+
def create_message_handler(resource_handler, opts = {})
83+
default_ops = {message_type: 'basic-lti-launch-request', launch_path: 'https://samplelaunch/blti', resource_handler: resource_handler}
84+
MessageHandler.create(default_ops.merge(opts))
85+
end
86+
87+
def new_valid_external_tool(context, resource_selection = false)
88+
tool = context.context_external_tools.new(:name => (0...8).map { (65 + rand(26)).chr }.join,
89+
:consumer_key => "key",
90+
:shared_secret => "secret")
91+
tool.url = "http://www.example.com/basic_lti"
92+
tool.resource_selection = {:url => "http://example.com/selection_test", :selection_width => 400, :selection_height => 400} if resource_selection
93+
tool.save!
94+
tool
95+
end
96+
97+
end
98+
end

spec/models/context_external_tool_spec.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,16 @@ def url_test(nav_url=nil)
384384
@tools << @account.context_external_tools.create!(:name => "c", :url => "http://www.google.com", :consumer_key => '12345', :shared_secret => 'secret')
385385
ContextExternalTool.all_tools_for(@course).to_a.should eql(@tools.sort_by(&:name))
386386
end
387+
388+
it "returns all tools that are selectable" do
389+
@tools = []
390+
@tools << @root_account.context_external_tools.create!(:name => "f", :domain => "google.com", :consumer_key => '12345', :shared_secret => 'secret')
391+
@tools << @root_account.context_external_tools.create!(:name => "e", :url => "http://www.google.com", :consumer_key => '12345', :shared_secret => 'secret', not_selectable: true)
392+
@tools << @account.context_external_tools.create!(:name => "d", :domain => "google.com", :consumer_key => '12345', :shared_secret => 'secret')
393+
@tools << @course.context_external_tools.create!(:name => "a", :url => "http://www.google.com", :consumer_key => '12345', :shared_secret => 'secret', not_selectable: true)
394+
tools = ContextExternalTool.all_tools_for(@course, selectable: true)
395+
tools.count.should == 2
396+
end
387397
end
388398

389399
describe "placements" do
@@ -623,9 +633,18 @@ def new_external_tool
623633
tool.save!
624634
tool.display_type(:course_navigation).should == 'other_display_type'
625635
end
636+
626637
end
627638
end
628639

640+
describe "#extension_default_value" do
641+
642+
it "returns resource_selection when the type is 'resource_slection'" do
643+
subject.extension_default_value(:resource_selection, :message_type).should == 'resource_selection'
644+
end
645+
646+
end
647+
629648
describe "change_domain" do
630649
let(:prod_base_url) {'http://www.example.com'}
631650
let(:new_host) {'test.example.com'}

0 commit comments

Comments
 (0)