xml config for blti extensions

test plan:
- copy xml from http://lti-examples.heroku.com/config/editor_button
- go to course settings and start creating a new tool
- choose xml paste, and paste xml
- save the tool
- confirm the tool says "Editor button configured"
- load a WYSIWYG in the course, confirm the new editor button appears

- do the same thing but by URL instead of XML paste

- confirm that normal tool configurations still work correctly

Change-Id: I6bb53bde1986e9dda40488018e167bb626907453
Reviewed-on: https://gerrit.instructure.com/7527
Tested-by: Hudson <hudson@instructure.com>
Reviewed-by: Brian Palmer <brianp@instructure.com>
This commit is contained in:
Brian Whitmer 2011-12-10 12:39:42 -07:00
parent e08b0d24a2
commit b02a09c626
15 changed files with 523 additions and 71 deletions

View File

@ -216,13 +216,15 @@ class ExternalToolsController < ApplicationController
# @argument resource_selection[url] [string] [optional] The url of the external tool
# @argument resource_selection[selection_width] [string] [optional] The width of the dialog the tool is launched in
# @argument resource_selection[selection_height] [string] [optional] The height of the dialog the tool is launched in
# @argument config_type [string] [optional] Configuration can be passed in as CC xml instead of using query parameters. If this value is "by_url" or "by_xml" then an xml configuration will be expected in either the "config_xml" or "config_url" parameter. Note that the name parameter overrides the tool name provided in the xml
# @argument config_xml [string] [optional] XML tool configuration, as specified in the CC xml specification. This is required if "config_type" is set to "by_xml"
# @argument config_url [string] [optional] URL where the server can retrieve an XML tool configuration, as specified in the CC xml specification. This is required if "config_type" is set to "by_url"
#
# @example_request
#
# This would create a tool on this course with two custom fields and a course navigation tab
# curl 'http://<canvas>/api/v1/courses/<course_id>/external_tools' \
# -u '<username>:<password>' \
# -F 'api_key=<key>' \
# -F 'access_token=<token>' \
# -F 'name=LTI Example' \
# -F 'consumer_key=asdfg' \
# -F 'shared_secret=lkjh' \
@ -238,8 +240,7 @@ class ExternalToolsController < ApplicationController
#
# This would create a tool on the account with navigation for the user profile page
# curl 'http://<canvas>/api/v1/accounts/<account_id>/external_tools' \
# -u '<username>:<password>' \
# -F 'api_key=<key>' \
# -F 'access_token=<token>' \
# -F 'name=LTI Example' \
# -F 'consumer_key=asdfg' \
# -F 'shared_secret=lkjh' \
@ -247,6 +248,17 @@ class ExternalToolsController < ApplicationController
# -F 'privacy_level=name_only' \
# -F 'user_navigation[url]=http://example.com/ims/lti/user_endpoint' \
# -F 'user_navigation[text]=Soemthing Cool'
#
# @example_request
#
# This would create a tool on the account with configuration pulled from an external URL
# curl 'http://<canvas>/api/v1/accounts/<account_id>/external_tools' \
# -F 'access_token=<token>' \
# -F 'name=LTI Example' \
# -F 'consumer_key=asdfg' \
# -F 'shared_secret=lkjh' \
# -F 'config_type=by_url' \
# -F 'config_url=http://example.com/ims/lti/tool_config.xml'
def create
if authorized_action(@context, @current_user, :update)
@tool = @context.context_external_tools.new
@ -272,8 +284,7 @@ class ExternalToolsController < ApplicationController
#
# This would update the specified keys on this external tool
# curl 'http://<canvas>/api/v1/courses/<course_id>/external_tools/<external_tool_id>' \
# -u '<username>:<password>' \
# -F 'api_key=<key>' \
# -F 'access_token=<token>' \
# -F 'name=Public Example' \
# -F 'privacy_level=public'
def update
@ -318,7 +329,8 @@ class ExternalToolsController < ApplicationController
def set_tool_attributes(tool, params)
[:name, :description, :url, :domain, :privacy_level, :consumer_key, :shared_secret,
:custom_fields, :custom_fields_string, :account_navigation, :user_navigation,
:course_navigation, :editor_button, :resource_selection].each do |prop|
:course_navigation, :editor_button, :resource_selection,
:config_type, :config_url, :config_xml].each do |prop|
tool.send("#{prop}=", params[prop]) if params.has_key?(prop)
end
end

View File

@ -458,7 +458,7 @@ var I18n = I18n || {};
contexts += @context.account_chain if @context.respond_to?(:account_chain)
contexts << @domain_root_account if @domain_root_account
Rails.cache.fetch((['editor_buttons_for'] + contexts.uniq).cache_key) do
tools = ContextExternalTool.having_setting('editor_button').scoped(:conditions => contexts.map{|context| "(context_type='#{context.class.base_class.to_s}' AND context_id=#{context.id})"}.join(" OR "))
tools = ContextExternalTool.active.having_setting('editor_button').scoped(:conditions => contexts.map{|context| "(context_type='#{context.class.base_class.to_s}' AND context_id=#{context.id})"}.join(" OR "))
tools.sort_by(&:id).map do |tool|
{
:name => tool.label_for(:editor_button, nil),

View File

@ -7,14 +7,17 @@ class ContextExternalTool < ActiveRecord::Base
attr_accessible :privacy_level, :domain, :url, :shared_secret, :consumer_key,
:name, :description, :custom_fields, :custom_fields_string,
:course_navigation, :account_navigation, :user_navigation,
:resource_selection, :editor_button
:resource_selection, :editor_button,
:config_type, :config_url, :config_xml
validates_presence_of :name
validates_presence_of :consumer_key
validates_presence_of :shared_secret
validate :url_or_domain_is_set
serialize :settings
attr_accessor :config_type, :config_url, :config_xml
before_save :infer_defaults
validate :check_for_xml_error
workflow do
state :anonymous
@ -44,9 +47,24 @@ class ContextExternalTool < ActiveRecord::Base
def label_for(key, lang=nil)
labels = settings[key] && settings[key][:labels]
(labels && labels[lang]) || (settings[key] && settings[key][:text]) || name || "External Tool"
(labels && labels[lang]) ||
(labels && lang && labels[lang.split('-').first]) ||
(settings[key] && settings[key][:text]) ||
settings[:text] || name || "External Tool"
end
def xml_error(error)
@xml_error = error
end
def check_for_xml_error
if @xml_error
errors.add_to_base(@xml_error)
false
end
end
protected :check_for_xml_error
def readable_state
workflow_state.titleize
end
@ -67,6 +85,43 @@ class ContextExternalTool < ActiveRecord::Base
}.join("\n")
end
def config_type=(val)
@config_type = val
process_extended_configuration
end
def config_xml=(val)
@config_xml = val
process_extended_configuration
end
def config_url=(val)
@config_url = val
process_extended_configuration
end
def process_extended_configuration
return unless (config_type == 'by_url' && config_url) || (config_type == 'by_xml' && config_xml)
tool_hash = nil
begin
converter = CC::Importer::Canvas::Converter.new({:no_archive_file => true})
if config_type == 'by_url'
tool_hash = converter.retrieve_and_convert_blti_url(config_url)
else
tool_hash = converter.convert_blti_xml(config_xml)
end
rescue CC::Importer::BLTIConverter::CCImportError => e
tool_hash = {:error => e.message}
end
real_name = self.name
if tool_hash[:error]
xml_error(tool_hash[:error])
else
ContextExternalTool.import_from_migration(tool_hash, self.context, self)
end
self.name = real_name unless real_name.blank?
end
def custom_fields_string=(str)
hash = {}
str.split(/\n/).each do |line|
@ -354,8 +409,8 @@ class ContextExternalTool < ActiveRecord::Base
item.url = hash[:url] unless hash[:url].blank?
item.domain = hash[:domain] unless hash[:domain].blank?
item.privacy_level = hash[:privacy_level] || 'name_only'
item.consumer_key = 'fake'
item.shared_secret = 'fake'
item.consumer_key ||= 'fake'
item.shared_secret ||= 'fake'
item.settings = hash[:settings].with_indifferent_access if hash[:settings].is_a?(Hash)
if hash[:custom_fields].is_a? Hash
item.settings[:custom_fields] ||= {}

View File

@ -13,7 +13,33 @@
+opacity(0.5)
.content
padding: 0 20px 5px
.extras
display: none
div
font-style: italic
font-size: 0.9em
&:hover
background-color: #eee
.links
+opacity(1.0)
+opacity(1.0)
&.has_editor_button,&.has_resource_selection,&.has_course_navigation,&.has_user_navigation,&.has_account_navigation
.extras
display: table-row
div
display: none
&.has_editor_button
div.editor_button
display: block
&.has_resource_selection
div.resource_selection
display: block
&.has_course_navigation
div.course_nagivation
display: block
&.has_user_navigation
div.user_navigation
display: block
&.has_account_navigation
div.account_navigation
display: block

View File

@ -1,5 +1,16 @@
<% tool ||= external_tool %>
<div class="external_tool <%= 'blank' unless tool %>" id="external_tool_<%= tool ? tool.id : 'blank' %>" data-id="<%= tool && tool.id %>" style="<%= hidden unless tool %>" data-workflow_state="<%= tool.try(:workflow_state) %>">
<%
tool ||= external_tool
classes = []
if tool
classes << "has_editor_button" if tool.try(:has_editor_button)
classes << "has_resource_selection" if tool.try(:has_resource_selection)
classes << "has_course_navigation" if tool.try(:has_course_navigation)
classes << "has_account_navigation" if tool.try(:has_account_navigation)
classes << "has_user_navigation" if tool.try(:has_user_navigation)
end
%>
<div class="external_tool <%= classes.join(" ") %> <%= 'blank' unless tool %>" id="external_tool_<%= tool ? tool.id : 'blank' %>" data-id="<%= tool && tool.id %>" style="<%= hidden unless tool %>" data-workflow_state="<%= tool.try(:workflow_state) %>">
<div class="header">
<div class="name"><%= tool.try(:name) %></div>
<div class="links">
@ -28,6 +39,15 @@
<td class="description" style="font-size: 0.8em;">
<%= tool.try(:description) %>
</td>
</tr><tr class="extras">
<td><%= before_label :extras, "Extras" %></td>
<td>
<div class="editor_button"><%= t :editor_button_configured, "Editor button configured" %></div>
<div class="resource_selection"><%= t :resource_selection_configured, "Resource selection configured" %></div>
<div class="course_navigation"><%= t :course_navigation_configured, "Course navigation configured" %></div>
<div class="user_navigation"><%= t :user_navigation_configured, "User navigation configured" %></div>
<div class="account_navigation"><%= t :account_navigation_configured, "Account navigation configured" %></div>
</td>
</tr>
</table>
</div>

View File

@ -9,55 +9,74 @@
<a href="<%= context_url(@context, :context_external_tools_url) %>" class="external_tools_url" style="display: none;">&nbsp;</a>
<% form_for :external_tool, :url => '.', :html => {:id => 'external_tool_form'} do |f| %>
<table class="formtable">
<tr>
<td><%= f.blabel :name, :en => "Name" %></td>
<td><%= f.text_field :name %></td>
</tr><tr>
<td><%= f.blabel :consumer_key, :en => "Consumer Key" %></td>
<td><%= f.text_field :consumer_key %></td>
</tr><tr>
<td style="vertical-align: top;"><%= f.blabel :shared_secret, :en => "Shared Secret" %></td>
<td>
<%= f.text_field :shared_secret %>
<div style="font-size: 0.8em;" class="shared_secret_note"><%= t :shared_secret_note, "enter a new value to change" %></div>
</td>
</tr><tr>
<td><label for="external_tool_match_by"><%= before_label :match_by, "Match By" %></label></td>
<td>
<select id="external_tool_match_by">
<option value="domain"><%= t :domain, "Domain" %></option>
<option value="url"><%= t :url, "URL" %></option>
</select>
</td>
</tr><tr class='tool_url'>
<td><%= f.blabel :url, :en => "URL" %></td>
<td><%= f.text_field :url %></td>
</tr><tr class='tool_domain'>
<td><%= f.blabel :domain, :en => "Domain" %></td>
<td><%= f.text_field :domain %></td>
</tr><tr>
<td><%= f.blabel :privacy_level, :en => "Privacy" %></td>
<td><%= f.select :privacy_level, [[t(:anonymous, "Anonymous"),'anonymous'],[t(:name_only, "Name Only"),'name_only'],[t(:public, "Public"),'public']] %></td>
</tr><tr>
<td colspan="2">
<%= f.blabel :custom_fields_string, :en => "Custom Fields" %>
<span style="font-size: 0.8em; color: #888;"><%= t('custom_fields_explanation', '(one per line, format: name=value)') %></span>
<br/>
<%= f.text_area :custom_fields_string, :style => "width: 550px; height: 30px;" %>
</td>
</tr><tr>
<td colspan="2">
<%= f.blabel :description, :en => "Description" %><br/>
<%= f.text_area :description, :style => "width: 550px; height: 75px;" %>
</td>
</tr><tr>
<td colspan="2">
<div class="button-container">
<button class="button save_button" type="submit"><%= t "#buttons.save_tool_settings", "Save Tool Settings" %></button>
<button class="button button-secondary cancel_button" type="button"><%= t "#buttons.cancel", "Cancel" %></button>
</div>
</td>
</tr>
<tbody>
<tr>
<td><%= f.blabel :name, :en => "Name" %></td>
<td><%= f.text_field :name %></td>
</tr><tr>
<td><%= f.blabel :consumer_key, :en => "Consumer Key" %></td>
<td><%= f.text_field :consumer_key %></td>
</tr><tr>
<td style="vertical-align: top;"><%= f.blabel :shared_secret, :en => "Shared Secret" %></td>
<td>
<%= f.text_field :shared_secret %>
<div style="font-size: 0.8em;" class="shared_secret_note"><%= t :shared_secret_note, "enter a new value to change" %></div>
</td>
</tr><tr class="config_type_option">
<td><%= f.blabel :config_type, :en => "Configuration Type" %></td>
<td><%= f.select :config_type, [[t(:manual, "Manual Entry"),'manual'],[t(:by_url, "By URL"),'by_url'],[t(:by_xml, "Paste XML"),'by_xml']] %></td>
</tr>
</tbody><tbody class="config_type by_url">
<tr>
<td><%= f.blabel :config_url, :en => "Configuration URL" %></td>
<td><%= f.text_field :config_url %></td>
</tr>
</tbody><tbody class="config_type by_xml">
<tr>
<td><%= f.blabel :config_xml, :en => "Paste XML Here" %></td>
<td><%= f.text_area :config_xml, :style => "width: 300px; height: 60px;" %></td>
</tr>
</tbody><tbody class="config_type manual">
<tr>
<td><label for="external_tool_match_by"><%= before_label :match_by, "Match By" %></label></td>
<td>
<select id="external_tool_match_by">
<option value="domain"><%= t :domain, "Domain" %></option>
<option value="url"><%= t :url, "URL" %></option>
</select>
</td>
</tr><tr class='tool_url'>
<td><%= f.blabel :url, :en => "URL" %></td>
<td><%= f.text_field :url %></td>
</tr><tr class='tool_domain'>
<td><%= f.blabel :domain, :en => "Domain" %></td>
<td><%= f.text_field :domain %></td>
</tr><tr>
<td><%= f.blabel :privacy_level, :en => "Privacy" %></td>
<td><%= f.select :privacy_level, [[t(:anonymous, "Anonymous"),'anonymous'],[t(:name_only, "Name Only"),'name_only'],[t(:public, "Public"),'public']] %></td>
</tr><tr>
<td colspan="2">
<%= f.blabel :custom_fields_string, :en => "Custom Fields" %>
<span style="font-size: 0.8em; color: #888;"><%= t('custom_fields_explanation', '(one per line, format: name=value)') %></span>
<br/>
<%= f.text_area :custom_fields_string, :style => "width: 550px; height: 30px;" %>
</td>
</tr><tr>
<td colspan="2">
<%= f.blabel :description, :en => "Description" %><br/>
<%= f.text_area :description, :style => "width: 550px; height: 75px;" %>
</td>
</tr>
</tbody><tbody>
<tr>
<td colspan="2">
<div class="button-container">
<button class="button save_button" type="submit"><%= t "#buttons.save_tool_settings", "Save Tool Settings" %></button>
<button class="button button-secondary cancel_button" type="button"><%= t "#buttons.cancel", "Cancel" %></button>
</div>
</td>
</tr>
</tbody>
</table>
<% end %>
</div>

View File

@ -34,7 +34,7 @@ class Migrator
@course = {:file_map=>{}, :wikis=>[]}
@course[:name] = @settings[:course_name]
return if settings[:testing]
return if settings[:no_archive_file]
unless settings[:archive_file]
MigratorHelper::download_archive(settings)

View File

@ -86,6 +86,24 @@ module CC::Importer
tool
end
def convert_blti_xml(xml)
doc = Nokogiri::XML(xml)
begin
convert_blti_link(doc)
rescue Nokogiri::XML::XPath::SyntaxError
raise CCImportError.new(I18n.t(:invalid_xml_syntax, "invalid xml syntax"))
end
end
def retrieve_and_convert_blti_url(url)
begin
config_xml = Net::HTTP.get(URI.parse(url))
convert_blti_xml(config_xml)
rescue Timeout::Error
raise CCImportError.new(I18n.t(:retrieve_timeout, "could not retrieve configuration, the server response timed out"))
end
end
def get_custom_properties(node)
props = {}
node.children.each do |property|
@ -107,6 +125,6 @@ module CC::Importer
end
"blti"
end
class CCImportError < Exception; end
end
end

View File

@ -6,6 +6,8 @@ $(document).ready(function() {
var formData = {
domain: "",
url: "",
config_url: "",
config_xml: "",
description: "",
name: "",
custom_fields_string: "",
@ -24,7 +26,9 @@ $(document).ready(function() {
.attr('method', 'POST')
.attr('action', $dialog.find(".external_tools_url").attr('href'));
$dialog.fillFormData(formData, {object_name: 'external_tool'});
$dialog.find(".config_type_option").show();
$("#external_tool_match_by").val('domain').change();
$("#external_tool_config_type").val('manual').change();
});
$dialog.find("form").formSubmit({
beforeSubmit: function(data) {
@ -44,6 +48,12 @@ $(document).ready(function() {
hrefValues: ['id'],
id: 'external_tool_' + tool.id
});
$tool
.toggleClass('has_editor_button', tool.has_editor_button)
.toggleClass('has_resource_selection', tool.has_resource_selection)
.toggleClass('has_course_navigation', tool.has_course_navigation)
.toggleClass('has_user_navigation', tool.has_user_navigation)
.toggleClass('has_account_navigation', tool.has_account_navigation);
$tool.find(".tool_url").showIf(tool.url).end()
.find(".tool_domain").showIf(tool.domain);
$tool.show();
@ -73,6 +83,8 @@ $(document).ready(function() {
width: 600,
height: 420
}).dialog('open');
$dialog.find(".config_type_option").hide();
$("#external_tool_config_type").val('manual').change();
}).delegate('.delete_tool_link', 'click', function(event) {
event.preventDefault();
var $tool = $(this).parents(".external_tool");
@ -96,5 +108,9 @@ $(document).ready(function() {
.find(".tool_domain").show();
}
});
$("#external_tool_config_type").change(function(event) {
$("#external_tool_form .config_type").hide();
$("#external_tool_form .config_type." + $(this).val()).show();
});
});
});

View File

@ -89,7 +89,7 @@
$list.empty();
$.each(options.options, function(optionName, callback){
var $option = $("<div class='option minimal' style='cursor: pointer; padding: 2px 5px; overflow: hidden; white-space: nowrap;'>" +
" <span tabindex='-1'>" + optionName.replace(/_/g, " ") + "</span>" +
" <span tabindex='-1'>" + optionName + "</span>" +
"</div>").appendTo($list);
if($.isFunction(callback)) {

View File

@ -93,4 +93,138 @@ describe ExternalToolsController do
assigns[:tool].should == tool
end
end
describe "POST 'create'" do
it "should require authentication" do
course_with_teacher(:active_all => true)
post 'create', :course_id => @course.id
response.should be_redirect
end
it "should accept basic configurations" do
course_with_teacher_logged_in(:active_all => true)
post 'create', :course_id => @course.id, :external_tool => {:name => "tool name", :url => "http://example.com", :consumer_key => "key", :shared_secret => "secret"}
response.should be_success
assigns[:tool].should_not be_nil
assigns[:tool].name.should == "tool name"
assigns[:tool].url.should == "http://example.com"
assigns[:tool].consumer_key.should == "key"
assigns[:tool].shared_secret.should == "secret"
end
it "should handle advanced xml configurations" do
course_with_teacher_logged_in(:active_all => true)
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</blti:launch_url>
<blti:extensions platform="canvas.instructure.com">
<lticm:property name="privacy_level">public</lticm:property>
<lticm:options name="editor_button">
<lticm:property name="url">http://example.com/editor</lticm:property>
<lticm:property name="icon_url">http://example.com/icon.png</lticm:property>
<lticm:property name="text">Editor Button</lticm:property>
<lticm:property name="selection_width">500</lticm:property>
<lticm:property name="selection_height">300</lticm:property>
</lticm:options>
</blti:extensions>
<cartridge_bundle identifierref="BLTI001_Bundle"/>
<cartridge_icon identifierref="BLTI001_Icon"/>
</cartridge_basiclti_link>
XML
post 'create', :course_id => @course.id, :external_tool => {:name => "tool name", :url => "http://example.com", :consumer_key => "key", :shared_secret => "secret", :config_type => "by_xml", :config_xml => xml}
response.should be_success
assigns[:tool].should_not be_nil
# User-entered name overrides name provided in xml
assigns[:tool].name.should == "tool name"
assigns[:tool].description.should == "Description"
assigns[:tool].url.should == "http://example.com/other_url"
assigns[:tool].consumer_key.should == "key"
assigns[:tool].shared_secret.should == "secret"
assigns[:tool].has_editor_button.should be_true
end
it "should fail gracefully on invalid xml configurations" do
course_with_teacher_logged_in(:active_all => true)
xml = "bob"
post 'create', :course_id => @course.id, :external_tool => {:name => "tool name", :url => "http://example.com", :consumer_key => "key", :shared_secret => "secret", :config_type => "by_xml", :config_xml => xml}
response.should_not be_success
assigns[:tool].should be_new_record
json = json_parse(response.body)
json['errors']['base'][0]['message'].should == I18n.t(:invalid_xml_syntax, 'invalid xml syntax')
course_with_teacher_logged_in(:active_all => true)
xml = "<a><b>c</b></a>"
post 'create', :course_id => @course.id, :external_tool => {:name => "tool name", :url => "http://example.com", :consumer_key => "key", :shared_secret => "secret", :config_type => "by_xml", :config_xml => xml}
response.should_not be_success
assigns[:tool].should be_new_record
json = json_parse(response.body)
json['errors']['base'][0]['message'].should == I18n.t(:invalid_xml_syntax, 'invalid xml syntax')
end
it "should handle advanced xml configurations by URL retrieval" do
course_with_teacher_logged_in(:active_all => true)
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</blti:launch_url>
<blti:extensions platform="canvas.instructure.com">
<lticm:property name="privacy_level">public</lticm:property>
<lticm:options name="editor_button">
<lticm:property name="url">http://example.com/editor</lticm:property>
<lticm:property name="icon_url">http://example.com/icon.png</lticm:property>
<lticm:property name="text">Editor Button</lticm:property>
<lticm:property name="selection_width">500</lticm:property>
<lticm:property name="selection_height">300</lticm:property>
</lticm:options>
</blti:extensions>
<cartridge_bundle identifierref="BLTI001_Bundle"/>
<cartridge_icon identifierref="BLTI001_Icon"/>
</cartridge_basiclti_link>
XML
Net::HTTP.expects(:get).with(URI.parse("http://config.example.com")).returns(xml)
post 'create', :course_id => @course.id, :external_tool => {:name => "tool name", :url => "http://example.com", :consumer_key => "key", :shared_secret => "secret", :config_type => "by_url", :config_url => "http://config.example.com"}
response.should be_success
assigns[:tool].should_not be_nil
# User-entered name overrides name provided in xml
assigns[:tool].name.should == "tool name"
assigns[:tool].description.should == "Description"
assigns[:tool].url.should == "http://example.com/other_url"
assigns[:tool].consumer_key.should == "key"
assigns[:tool].shared_secret.should == "secret"
assigns[:tool].has_editor_button.should be_true
end
it "should fail gracefully on invalid URL retrieval or timeouts" do
Net::HTTP.expects(:get).with(URI.parse("http://config.example.com")).raises(Timeout::Error)
course_with_teacher_logged_in(:active_all => true)
xml = "bob"
post 'create', :course_id => @course.id, :external_tool => {:name => "tool name", :url => "http://example.com", :consumer_key => "key", :shared_secret => "secret", :config_type => "by_url", :config_url => "http://config.example.com"}
response.should_not be_success
assigns[:tool].should be_new_record
json = json_parse(response.body)
json['errors']['base'][0]['message'].should == I18n.t(:retrieve_timeout, 'could not retrieve configuration, the server response timed out')
end
end
end

View File

@ -21,11 +21,11 @@ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
CC_XML_EXPORT_DIR = File.dirname(__FILE__) + '/../../fixtures/cc/cc_export'
def get_cc_converter
CC::Importer::Canvas::Converter.new({:testing=>true})
CC::Importer::Canvas::Converter.new({:no_archive_file=>true})
end
def get_standard_converter
CC::Importer::Standard::Converter.new({:testing=>true})
CC::Importer::Standard::Converter.new({:no_archive_file=>true})
end
def get_cc_export_file(rel_path)

View File

@ -132,7 +132,7 @@ describe "Canvas Cartridge importing" do
a_2 = ag2_2.assignments.first
ag2_2.rules.should == "drop_lowest:2\ndrop_highest:5\nnever_drop:%s\n" % a_2.id
end
it "should import external tools" do
tool1 = @copy_from.context_external_tools.new
tool1.url = 'http://instructure.com'

View File

@ -252,6 +252,57 @@ describe ContextExternalTool do
end
end
describe "label_for" do
it "should return the tool name if nothing else is configured and no key is sent" do
tool = @root_account.context_external_tools.new(:name => 'tool', :consumer_key => '12345', :shared_secret => 'secret', :url => "http://example.com")
tool.save!
tool.label_for(nil).should == 'tool'
end
it "should return the tool name if nothing is configured on the sent key" do
tool = @root_account.context_external_tools.new(:name => 'tool', :consumer_key => '12345', :shared_secret => 'secret', :url => "http://example.com")
tool.settings = {:course_navigation => {:bob => 'asfd'}}
tool.save!
tool.label_for(:course_navigation).should == 'tool'
end
it "should return the tool's 'text' value if no key is sent" do
tool = @root_account.context_external_tools.new(:name => 'tool', :consumer_key => '12345', :shared_secret => 'secret', :url => "http://example.com")
tool.settings = {:text => 'tool label', :course_navigation => {:url => "http://example.com", :text => 'course nav'}}
tool.save!
tool.label_for(nil).should == 'tool label'
end
it "should return the tool's 'text' value if no 'text' value is set for the sent key" do
tool = @root_account.context_external_tools.new(:name => 'tool', :consumer_key => '12345', :shared_secret => 'secret', :url => "http://example.com")
tool.settings = {:text => 'tool label', :course_navigation => {:bob => 'asdf'}}
tool.save!
tool.label_for(:course_navigation).should == 'tool label'
end
it "should return the setting's 'text' value for the sent key if available" do
tool = @root_account.context_external_tools.new(:name => 'tool', :consumer_key => '12345', :shared_secret => 'secret', :url => "http://example.com")
tool.settings = {:text => 'tool label', :course_navigation => {:url => "http://example.com", :text => 'course nav'}}
tool.save!
tool.label_for(:course_navigation).should == 'course nav'
end
it "should return the locale-specific label if specified and matching exactly" do
tool = @root_account.context_external_tools.new(:name => 'tool', :consumer_key => '12345', :shared_secret => 'secret', :url => "http://example.com")
tool.settings = {:text => 'tool label', :course_navigation => {:url => "http://example.com", :text => 'course nav', :labels => {'en-US' => 'english nav'}}}
tool.save!
tool.label_for(:course_navigation, 'en-US').should == 'english nav'
tool.label_for(:course_navigation, 'es').should == 'course nav'
end
it "should return the locale-specific label if specified and matching based on general locale" do
tool = @root_account.context_external_tools.new(:name => 'tool', :consumer_key => '12345', :shared_secret => 'secret', :url => "http://example.com")
tool.settings = {:text => 'tool label', :course_navigation => {:url => "http://example.com", :text => 'course nav', :labels => {'en' => 'english nav'}}}
tool.save!
tool.label_for(:course_navigation, 'en-US').should == 'english nav'
end
end
describe "find_for" do
def new_external_tool(context)
context.context_external_tools.new(:name => "bob", :consumer_key => "bob", :shared_secret => "bob", :domain => "google.com")

View File

@ -14,6 +14,14 @@ describe "editing external tools" do
driver.find_element(:css, "#external_tool_name").send_keys "Tool"
driver.find_element(:css, "#external_tool_consumer_key").send_keys "Key"
driver.find_element(:css, "#external_tool_shared_secret").send_keys "Secret"
driver.find_element(:css, "#external_tool_match_by option[value='url']").click
driver.find_element(:css, "#external_tool_domain").should_not be_displayed
driver.find_element(:css, "#external_tool_url").should be_displayed
driver.find_element(:css, "#external_tool_match_by option[value='domain']").click
driver.find_element(:css, "#external_tool_domain").should be_displayed
driver.find_element(:css, "#external_tool_url").should_not be_displayed
driver.find_element(:css, "#external_tool_domain").send_keys "example.com"
driver.find_element(:css, "#external_tool_custom_fields_string").send_keys "a=1\nb=123"
driver.find_element(:css, "#external_tools_dialog .save_button").click
@ -21,15 +29,108 @@ describe "editing external tools" do
keep_trying_until { !driver.find_element(:css, "#external_tools_dialog").displayed? }
tool = ContextExternalTool.last
driver.find_element(:css, "#external_tool_#{tool.id}").should be_displayed
tool_elem = driver.find_element(:css, "#external_tool_#{tool.id}")
tool_elem.should be_displayed
tool.should_not be_nil
tool.name.should == "Tool"
tool.consumer_key.should == "Key"
tool.shared_secret.should == "Secret"
tool.domain.should == "example.com"
tool_elem.attribute('class').should_not match(/has_editor_button|has_resource_selection|has_course_navigation|has_account_navigation|has_user_navigation/)
tool.settings[:custom_fields].should == {'a' => '1', 'b' => '123'}
end
it "should allow creating a new course external tool with extensions" do
course_with_teacher_logged_in
get "/courses/#{@course.id}/settings"
keep_trying_until { driver.find_element(:css, "#tab-tools-link").displayed? }
driver.find_element(:css, "#tab-tools-link").click
driver.find_element(:css, ".add_tool_link").click
driver.find_element(:css, "#external_tools_dialog").should be_displayed
driver.find_element(:css, "#external_tool_name").send_keys "Tool"
driver.find_element(:css, "#external_tool_consumer_key").send_keys "Key"
driver.find_element(:css, "#external_tool_shared_secret").send_keys "Secret"
driver.find_element(:css, "#external_tool_config_type option[value='by_url']").click
driver.find_element(:css, "#external_tool_form .config_type.manual").should_not be_displayed
driver.find_element(:css, "#external_tool_config_url").should be_displayed
driver.find_element(:css, "#external_tool_config_xml").should_not be_displayed
driver.find_element(:css, "#external_tool_config_type option[value='by_xml']").click
driver.find_element(:css, "#external_tool_form .config_type.manual").should_not be_displayed
driver.find_element(:css, "#external_tool_config_url").should_not be_displayed
driver.find_element(:css, "#external_tool_config_xml").should be_displayed
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</blti:launch_url>
<blti:extensions platform="canvas.instructure.com">
<lticm:property name="privacy_level">public</lticm:property>
<lticm:options name="editor_button">
<lticm:property name="url">http://example.com/editor</lticm:property>
<lticm:property name="icon_url">http://example.com/icon.png</lticm:property>
<lticm:property name="text">Editor Button</lticm:property>
<lticm:property name="selection_width">500</lticm:property>
<lticm:property name="selection_height">300</lticm:property>
</lticm:options>
<lticm:options name="resource_selection">
<lticm:property name="url">https://example.com/wiki</lticm:property>
<lticm:property name="text">Build/Link to Wiki Page</lticm:property>
<lticm:property name="selection_width">500</lticm:property>
<lticm:property name="selection_height">300</lticm:property>
</lticm:options>
<lticm:options name="course_navigation">
<lticm:property name="url">https://example.com/attendance</lticm:property>
<lticm:property name="text">Attendance</lticm:property>
</lticm:options>
<lticm:options name="user_navigation">
<lticm:property name="url">https://example.com/attendance</lticm:property>
<lticm:property name="text">Attendance</lticm:property>
</lticm:options>
<lticm:options name="account_navigation">
<lticm:property name="url">https://example.com/attendance</lticm:property>
<lticm:property name="text">Attendance</lticm:property>
</lticm:options>
</blti:extensions>
<cartridge_bundle identifierref="BLTI001_Bundle"/>
<cartridge_icon identifierref="BLTI001_Icon"/>
</cartridge_basiclti_link>
XML
driver.find_element(:css, "#external_tool_config_xml").send_keys xml
driver.find_element(:css, "#external_tools_dialog .save_button").click
keep_trying_until { !driver.find_element(:css, "#external_tools_dialog").displayed? }
tool = ContextExternalTool.last
tool_elem = driver.find_element(:css, "#external_tool_#{tool.id}")
tool_elem.should be_displayed
tool.has_editor_button.should be_true
tool.has_resource_selection.should be_true
tool.has_course_navigation.should be_true
tool.has_account_navigation.should be_true
tool.has_user_navigation.should be_true
tool_elem.attribute('class').should match(/has_editor_button/)
tool_elem.attribute('class').should match(/has_resource_selection/)
tool_elem.attribute('class').should match(/has_course_navigation/)
tool_elem.attribute('class').should match(/has_account_navigation/)
tool_elem.attribute('class').should match(/has_user_navigation/)
tool.name.should == "Tool"
tool.consumer_key.should == "Key"
tool.shared_secret.should == "Secret"
tool.url.should == "http://example.com/other_url"
end
it "should allow editing an existing external tool with custom fields" do
course_with_teacher_logged_in
tool = @course.context_external_tools.create!(:name => "new tool", :consumer_key => "key", :shared_secret => "secret", :domain => 'example.com', :custom_fields => {'a' => '1', 'b' => '2'})