show validation errors when configuring external tools

fixes #9725, fixes #9776, fixes #7311,
Test plan:
* go to the 'external tools' tab on the 'course settings' page
* create and edit some external tools
- make sure to omit fields or input invalid data and make sure you
get sensible error messages back

Change-Id: I88a7d5e439a27a599cbcfcfbd576434386949d35
Reviewed-on: https://gerrit.instructure.com/13332
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Jeremy Putnam <jeremyp@instructure.com>
Reviewed-by: Bracken Mosbacker <bracken@instructure.com>
Product-Review: Bracken Mosbacker <bracken@instructure.com>
This commit is contained in:
Brad Humphrey 2013-04-09 16:17:30 -06:00
parent 0cb619ce64
commit 5e109f4434
22 changed files with 498 additions and 314 deletions

View File

@ -1 +1 @@
require ['account_settings', 'external_tools']
require ['account_settings']

View File

@ -5,7 +5,6 @@ require [
'compiled/views/course_settings/tabs/tabUsers'
'vendor/jquery.cookie'
'course_settings'
'external_tools'
'grading_standards'
], (NavigationView, UserCollectionView, UserCollection) ->

View File

@ -0,0 +1,10 @@
require [
'compiled/collections/ExternalToolCollection',
'compiled/views/ExternalTools/IndexView'
], (ExternalToolCollection, ExternalToolsIndexView) ->
collection = new ExternalToolCollection()
collection.fetch()
view = new ExternalToolsIndexView
el: '#external_tools'
collection: collection
view.render()

View File

@ -0,0 +1,7 @@
define [
'compiled/collections/PaginatedCollection'
'compiled/models/ExternalTool'
], (PaginatedCollection, ExternalTool) ->
class ExternalToolCollection extends PaginatedCollection
model: ExternalTool

View File

@ -301,7 +301,11 @@ define [
truncate: ( string, max ) ->
return textHelper.truncateText( string, { max: max } )
enrollmentName: enrollmentName
titleize: (str) ->
words = str.split(/[ _]+/)
titleizedWords = _(words).map (w) -> w[0].toUpperCase() + w.slice(1)
titleizedWords.join(' ')
}
return Handlebars

View File

@ -0,0 +1,14 @@
define ['Backbone'], ({Model}) ->
class ExternalTool extends Model
resourceName: 'external_tools'
computedAttributes: [
{
name: 'custom_field_string'
deps: ['custom_fields']
}
]
custom_field_string: ->
("#{k}=#{v}" for k,v of @get('custom_fields')).join("\n")

View File

@ -0,0 +1,74 @@
define [
'i18n!external_tools'
'jst/ExternalTools/EditView'
'compiled/views/ValidatedFormView'
'compiled/jquery/fixDialogButtons'
], (I18n, template, ValidatedFormView) ->
class EditView extends ValidatedFormView
template: template
id: 'external_tool_form'
className: 'validated-form-view form-horizontal bootstrap-form'
events:
'change #external_tool_config_type': 'onConfigTypeChange'
render: ->
super
@$el.dialog
title: I18n.t 'dialog_title', 'Edit External Tool'
width: 520
height: "auto"
resizable: true
close: => @$el.remove()
buttons: [
class: "btn-primary"
text: I18n.t 'submit', 'Submit'
'data-text-while-loading': I18n.t 'saving', 'Saving...'
click: => @submit()
]
@onConfigTypeChange()
@$el.submit (e) =>
@submit()
return false
this
submit: ->
this.$el.parent().find('.btn-primary').removeClass('ui-state-hover')
super
onSaveSuccess: ->
@$el.dialog 'close'
onConfigTypeChange: ->
configType = @$('#external_tool_config_type').val()
@$('.config_type').hide().attr('aria-expanded', false)
@$(".config_type.#{configType}").show().attr('aria-expanded', true)
showErrors: (errors) ->
@removeErrors()
for fieldName, field of errors
$input = @findField fieldName
html = (@translations[message] or message for {message} in field).join('</p><p>')
@addError($input, html)
removeErrors: ->
@$('.error .help-inline').remove()
@$('.control-group').removeClass('error')
@$('.alert.alert-error').remove()
addError: (input, message) ->
input = $(input)
input.parents('.control-group').addClass('error')
input.after("<span class='help-inline'>#{message}</span>")
input.one 'keypress', ->
$(this).parents('.control-group').removeClass('error')
$(this).parents('.control-group').find('.help-inline').remove()
onSaveFail: (xhr) =>
super
message = I18n.t 'generic_error', 'There was an error in processing your request'
@$el.prepend("<div class='alert alert-error'>#{message}</span>")

View File

@ -0,0 +1,70 @@
define [
'underscore'
'jquery'
'jst/ExternalTools/IndexView'
'compiled/views/ExternalTools/EditView'
'compiled/views/PaginatedView'
'i18n!external_tools'
], (_, $, template, EditView, PaginatedView, I18n) ->
class IndexView extends PaginatedView
template: template
events:
'click [data-delete-external-tool]': 'deleteExternalToolHandler'
'click [data-edit-external-tool]': 'editExternalToolHandler'
'click .add_tool_link': 'addTool'
initialize: ->
super
@collection.on 'sync', @render, this
@collection.on 'reset', @render, this
@collection.on 'destroy', @render, this
@render()
deleteExternalToolHandler: (e) =>
id = @$(e.target).closest('a').data('delete-external-tool')
@confirmDelete =>
@collection.get(id).destroy()
confirmDelete: (deleteFunc) ->
msg = I18n.t 'remove_tool',
"Are you sure you want to remove this tool?
Any courses using this tool will no longer work."
dialog = $("<div>#{msg}</div>").dialog
modal: true,
resizable: false
title: I18n.t 'are_you_sure', 'Are you sure?'
buttons: [
text: I18n.t 'buttons.cancel', 'Cancel'
click: => dialog.dialog 'close'
,
text: I18n.t 'buttons.delete', 'Delete'
click: =>
deleteFunc()
dialog.dialog 'close'
]
editExternalToolHandler: (e) =>
id = @$(e.target).closest('a').data('edit-external-tool')
new EditView(model: @collection.get(id)).render()
addTool: =>
@collection.add({}, silent: true)
new EditView(model: @collection.last()).render()
toJSON: ->
extras = [
{extension_type: 'editor_button', text: I18n.t 'editor_button_configured', 'Editor button configured'}
{extension_type: 'resource_selection', text: I18n.t 'resource_selection_configured', 'Resource selection configured'}
{extension_type: 'course_navigation', text: I18n.t 'course_navigation_configured', 'Course navigation configured'}
{extension_type: 'account_navigation', text: I18n.t 'account_navigation_configured', 'Account navigation configured'}
{extension_type: 'user_navigation', text: I18n.t 'user_navigation_configured', 'User navigation configured'}
{extension_type: 'homework_submission', text: I18n.t 'homework_submission_configured', 'Homework submission configured'}
]
json = super
for tool in json
tool.extras = (extra for extra in extras when tool[extra.extension_type]?)
json

View File

@ -4,10 +4,11 @@ define [
'jquery'
'underscore'
'compiled/fn/preventDefault'
'i18n!errors'
'jquery.toJSON'
'jquery.disableWhileLoading'
'jquery.instructure_forms'
], (Backbone, ValidatedMixin, $, _) ->
], (Backbone, ValidatedMixin, $, _, preventDefault, I18n) ->
##
# Sets model data from a form, saves it, and displays errors returned in a
@ -121,3 +122,33 @@ define [
$.parseJSON(response.responseText).errors
catch error
{}
translations:
required: I18n.t "required", "Required"
blank: I18n.t "blank", "Required"
##
# Errors are displayed relative to the field to which they belong. If
# the key of the error in the response doesn't match the name attribute
# of the form input element, configure a selector here.
#
# For example, given a form field like this:
#
# <input name="user[first_name]">
#
# and an error response like this:
#
# {errors: { first_name: {...} }}
#
# you would do this:
#
# fieldSelectors:
# first_name: '[name=user[first_name]]'
fieldSelectors: null
findField: (field) ->
selector = @fieldSelectors?[field] or "[name='#{field}']"
$el = @$(selector)
if $el.data('rich_text')
$el = $el.next('.mceEditor').find(".mceIframeContainer")
$el

View File

@ -12,6 +12,9 @@ class ContextExternalTool < ActiveRecord::Base
validates_length_of :name, :maximum => maximum_string_length
validates_presence_of :consumer_key
validates_presence_of :shared_secret
validates_presence_of :config_url, :if => lambda { |t| t.config_type == "by_url" }
validates_presence_of :config_xml, :if => lambda { |t| t.config_type == "by_xml" }
validates_length_of :domain, :maximum => 253, :allow_blank => true
validate :url_or_domain_is_set
serialize :settings
attr_accessor :config_type, :config_url, :config_xml
@ -76,15 +79,10 @@ class ContextExternalTool < ActiveRecord::Base
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
(@config_errors || []).each { |attr,msg|
errors.add attr, msg
}
end
protected :check_for_xml_error
@ -154,18 +152,37 @@ class ContextExternalTool < ActiveRecord::Base
rescue CC::Importer::BLTIConverter::CCImportError => e
tool_hash = {:error => e.message}
end
@config_errors = []
error_field = config_type == 'by_xml' ? 'config_xml' : 'config_url'
converter = CC::Importer::BLTIConverter.new
tool_hash = if config_type == 'by_url'
uri = URI.parse(config_url)
raise URI::InvalidURIError unless uri.host && uri.port
converter.retrieve_and_convert_blti_url(config_url)
else
converter.convert_blti_xml(config_xml)
end
real_name = self.name
if tool_hash[:error]
xml_error(tool_hash[:error])
@config_errors << [error_field, tool_hash[:error]]
else
ContextExternalTool.import_from_migration(tool_hash, self.context, self)
ContextExternalTool.import_from_migration(tool_hash, context, self)
end
self.name = real_name unless real_name.blank?
rescue CC::Importer::BLTIConverter::CCImportError => e
@config_errors << [error_field, e.message]
rescue URI::InvalidURIError
@config_errors << [:config_url, "Invalid URL"]
rescue ActiveRecord::RecordInvalid => e
@config_errors += Array(e.record.errors)
end
def custom_fields_string=(str)
hash = {}
str.split(/\n/).each do |line|
str.split(/[\r\n]+/).each do |line|
key, val = line.split(/=/)
hash[key] = val if key.present? && val.present?
end

View File

@ -21,7 +21,6 @@
.content
padding: 0 20px 5px
.extras
display: none
div
font-style: italic
font-size: 0.9em
@ -52,4 +51,9 @@
&.has_account_navigation
div.account_navigation
display: block
table
td:first-child
width: 100px

View File

@ -1,94 +1,3 @@
<% js_bundle :external_tools %>
<div id="external_tools">
<p><%= mt :external_tools_note, <<-HEREDOC, :lti_index_url => "https://lti-examples.heroku.com/index.html", :lti_examples_url => "http://help.instructure.com/entries/20878626-lti-tools-and-examples"
External (LTI) Tools are an easy way to add new features to Canvas.
They can be added to individual courses, or to all courses in an account.
Once configured, you can link to them through course modules and create assignments for
assessment tools.
Click [here](%{lti_index_url}) to see some LTI tools that work great with Canvas. You can also check out the Canvas Community topics about LTI tools [here](%{lti_examples_url}).
HEREDOC
%>
</p>
<% @context.context_external_tools.active.each do |tool| %>
<%= render :partial => 'external_tools/external_tool', :object => tool %>
<% end %>
<%= render :partial => 'external_tools/external_tool' %>
<div id="external_tools_dialog" style="display: none;">
<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">
<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 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(:email_only, "Email Only"),'email_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="btn cancel_button" type="button"><%= t "#buttons.cancel", "Cancel" %></button>
<button class="btn btn-primary save_button" type="submit"><%= t "#buttons.save_tool_settings", "Save Tool Settings" %></button>
</div>
</td>
</tr>
</tbody>
</table>
<% end %>
</div>
</div>
<div class="button-container">
<button class="btn btn-primary add_tool_link"><i class="icon-add"></i> <%= t "#buttons.add_external_tool", "Add External Tool" %></button>
</div>

View File

@ -0,0 +1,116 @@
<fieldset>
<div class="control-group">
<label class="control-label" for="external_tool_name">Name</label>
<div class="controls">
<input type="text" id="external_tool_name" value="{{name}}" name="name">
</div>
</div>
<div class="control-group">
<label class="control-label" for="external_tool_consumer_key">Consumer Key</label>
<div class="controls">
<input type="text" id="external_tool_consumer_key" value="{{consumer_key}}" name="consumer_key">
</div>
</div>
<div class="control-group">
<label class="control-label" for="external_tool_shared_secret">Shared Secret</label>
<div class="controls">
<input type="text" id="external_tool_shared_secret" value="{{shared_secret}}" name="shared_secret">
{{#if id}}
<p class="help-block">{{#t "shared_secret_note"}}Enter a new value
to change{{/t}}</p>
{{/if}}
</div>
</div>
<div class="control-group {{#if id}}hide{{/if}}">
<label class="control-label" for="external_tool_config_type">Configuration Type</label>
<div class="controls">
<select id="external_tool_config_type" name="config_type">
<option value="manual">{{#t "manual"}}Manual Entry{{/t}}</option>
<option value="by_url">{{#t "by_url"}}By URL{{/t}}</option>
<option value="by_xml">{{#t "by_xml"}}Paste XML{{/t}}</option>
</select>
</div>
</div>
<div class="control-group config_type by_url">
<label class="control-label" for="external_tool_config_url">
{{#t "config_url"}}Configuration URL{{/t}}
</label>
<div class="controls">
<input type="text" id="external_tool_config_url" value="{{config_url}}" name="config_url">
</div>
</div>
<div class="control-group config_type by_xml">
<label class="control-label" for="external_tool_config_xml">
{{#t "paste_xml"}}Paste XML Here{{/t}}
</label>
<div class="controls">
<textarea id="external_tool_config_xml" name="config_xml"
></textarea>
</div>
</div>
<div class="control-group config_type manual">
<label class="control-label" for="external_tool_url">
{{#t "tool_url"}}URL{{/t}}
</label>
<div class="controls">
<input type="text" id="external_tool_url" value="{{url}}" name="url">
</div>
</div>
<div class="control-group config_type manual">
<label class="control-label" for="external_tool_domain">
{{#t "tool_domain"}}Domain{{/t}}
</label>
<div class="controls">
<input type="text" id="external_tool_domain" value="{{domain}}" name="domain">
</div>
</div>
<div class="control-group config_type manual">
<label class="control-label" for="external_tool_privacy_level">
{{#t "privacy"}}Privacy{{/t}}
</label>
<div class="controls">
<select id="external_tool_privacy_level" name="privacy_level">
<option
value="anonymous"
{{#ifEqual privacy_level "anonymous"}}selected{{/ifEqual}}
>{{#t "anonymous"}}Anonymous{{/t}}</option>
<option
value="email_only"
{{#ifEqual privacy_level "email_only"}}selected{{/ifEqual}}
>{{#t "email_only"}}E-Mail Only{{/t}}</option>
<option
value="name_only"
{{#ifEqual privacy_level "name_only"}}selected{{/ifEqual}}
>{{#t "name_only"}}Name Only{{/t}}</option>
<option
value="public"
{{#ifEqual privacy_level "public"}}selected{{/ifEqual}}
>{{#t "public"}}Public{{/t}}</option>
</select>
</div>
</div>
<div class="control-group config_type manual">
<label class="control-label" for="external_tool_custom_fields_string">
{{#t "custom_fields"}}Custom Fields{{/t}}
</label>
<div class="controls">
<textarea
id="external_tool_custom_fields_string"
name="custom_fields_string"
>{{custom_fields_string}}</textarea>
<div class="help-block"> {{#t "custom_feilds_explanation"}}One per
line. Format: name=value{{/t}}</div>
</div>
</div>
<div class="control-group config_type manual">
<label class="control-label" for="external_tool_description">
{{#t "description"}}Description{{/t}}
</label>
<div class="controls">
<textarea
id="external_tool_description"
name="description"
>{{description}}</textarea>
</div>
</div>
</fieldset>

View File

@ -0,0 +1,83 @@
<p>{{#t "external_tools_note"}}
External (LTI) Tools are an easy way to add new features to Canvas.
They can be added to individual courses, or to all courses in an account.
Once configured, you can link to them through course modules and create assignments for
assessment tools.{{/t}}</p>
<p>{{#t "external_tools_references"}}Click <a href="https://lti-examples.heroku.com/index.html">here</a> to see some
LTI tools that work great with Canvas. You can also check out the Canvas
Community topics about LTI tools <a href="http://help.instructure.com/entries/20878626-lti-tools-and-examples">
here</a>
{{/t}}</p>
{{#each this}}
<div id=external_tool_{{id}}
class=external_tool
data-id={{id}}
data-workflow-state={{workflow_state}}>
<div class="header clearfix">
<div class="name">{{name}}</div>
<div class="links">
<a href="#"
class="edit_tool_link"
title="{{#t "edit_tool"}}Edit Tool{{/t}}"
data-edit-external-tool={{id}}
><i class="icon-edit btn"></i></a>
<a href="#"
class="delete_tool_link"
title="{{#t "delete_tool"}}Delete Tool{{/t}}"
data-delete-external-tool={{id}}
><i class="icon-trash btn"></i></a>
</div>
<div class="clear"></div>
<div class="content">
<table class="table-condensed">
<tr>
<td>{{#t "privacy"}}Privacy{{/t}}:</td>
<td class="readable_state">{{titleize privacy_level}}</td>
</tr><tr>
<td>{{#t "consumer_key"}}Consumer Key{{/t}}:</td>
<td class="consumer_key">{{consumer_key}}</td>
</tr>
{{#if url}}
<tr class="tool_url">
<td>{{#t "url"}}URL{{/t}}:</td>
<td class="url">{{url}}</td>
</tr>
{{/if}}
{{#if domain}}
<tr class="tool_domain">
<td>{{#t "domain"}}Domain{{/t}}:</td>
<td class="domain">{{domain}}</td>
</tr>
{{/if}}
<tr>
<td>{{#t "description"}}Description{{/t}}:</td>
<td class="description" style="font-size: 0.8em;">{{description}}</td>
</tr>
{{#if vendor_help_link}}
<tr class="tool_vendor_help_link">
<td>{{#t "tool_vendor_help_link"}}Help Link{{/t}}:</td>
<td class="vendor_help_link"><a href="{{vendor_help_link}}"">{{vendor_help_link}}</a></td>
</tr>
{{/if}}
{{#if extras}}
<tr class="extras">
<td>{{#t "extras"}}Extras{{/t}}:</td>
<td>
{{#each extras}}<div class="{{this.extension_type}}">{{this.text}}</div>{{/each}}
</td>
</tr>
{{/if}}
</table>
</div>
</div>
</div>
{{/each}}
<div class="button-container">
<button class="btn btn-primary add_tool_link">
<i class="icon-add"></i>
{{#t "add_external_tool"}}Add External Tool{{/t}}
</button>
</div>

View File

@ -26,10 +26,11 @@ module Api::V1::ExternalTools
end
def external_tool_json(tool, context, user, session, extension_types = ContextExternalTool::EXTENSION_TYPES)
methods = %w[privacy_level custom_fields]
methods = %w[privacy_level custom_fields workflow_state vendor_help_link]
methods += extension_types
json = api_json(tool, user, session,
:only => %w(id name description url domain consumer_key created_at updated_at),
:only => %w(id name description url domain consumer_key
created_at updated_at description),
:methods => methods
)

View File

@ -1,171 +0,0 @@
define([
'i18n!external_tools',
'jquery' /* $ */,
'jquery.instructure_forms' /* formSubmit, fillFormData */,
'jqueryui/dialog',
'compiled/jquery/fixDialogButtons' /* fix dialog formatting */,
'jquery.instructure_misc_plugins' /* confirmDelete, showIf */,
'jquery.templateData' /* fillTemplateData, getTemplateData */
], function(I18n, $) {
$(document).ready(function() {
var $dialog = $("#external_tools_dialog");
$(".add_tool_link").click(function(event) {
event.preventDefault();
var formData = {
domain: "",
url: "",
config_url: "",
config_xml: "",
description: "",
name: "",
custom_fields_string: "",
privacy: "anonymous",
consumer_key: "",
shared_secret: ""
}
$dialog.dialog({
title: I18n.t('titles.edit_external_tool', "Edit External Tool"),
width: 600
}).fixDialogButtons();
$dialog.find(".shared_secret_note").hide();
$dialog.find("form")
.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();
});
/* Form Submit Summary */
$dialog.find("form").formSubmit({
beforeSubmit: function(data) {
$(this).find("button")
.attr('disabled', true)
.filter('.save_button')
.text(I18n.t('messages.saving_tool_settings', "Saving Tool Settings..."));
},
success: function(tool) {
var $this = $(this);
var $tool = $("#external_tool_" + tool.id);
$this.find("button")
.attr('disabled', false)
.filter('.save_button')
.text(I18n.t('buttons.save_tool_settings', "Save Tool Settings"));
$dialog.dialog('close');
var tool_div_doesnt_exist = $tool.length == 0;
if(tool_div_doesnt_exist) {
$tool = $("#external_tool_blank").clone(true)
.removeAttr('id');
$("#external_tools").append($tool);
}
$tool.fillTemplateData({
data: tool,
dataValues: ['id', 'workflow_state'],
hrefValues: ['id', 'vendor_help_link'],
id: 'external_tool_' + tool.id
});
/* Clear the shared seceret input field because it never gets updated and
* should no longer be visible after you update *
*/
$('#external_tool_shared_secret').val('');
$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_homework_submission', tool.has_homework_submission)
.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.find(".tool_vendor_help_link")
.showIf(tool.vendor_help_link);
$tool.show();
},
error: function(data) {
$(this).find("button")
.attr('disabled', false)
.filter('.save_button')
.text(I18n.t('errors.save_tool_settings_failed', "Save Tool Settings Failed"));
}
});
$dialog.find(".cancel_button").click(function() {
$dialog.dialog('close');
$('#external_tool_shared_secret').val('');
});
$("#external_tools").delegate('.edit_tool_link', 'click', function(event) {
event.preventDefault();
var $tool = $(this).parents(".external_tool");
var data = $tool.getTemplateData({textValues: ['name', 'description', 'domain', 'url', 'consumer_key', 'custom_fields_string'], dataValues: ['id', 'workflow_state']});
data.privacy_level = data.workflow_state;
$("#external_tool_match_by").val(data.url ? 'url' : 'domain').change();
$dialog.find(".shared_secret_note").show();
$dialog.find("form")
.attr('method', 'PUT')
.attr('action', $tool.find(".update_tool_url").attr('rel'));
$dialog.fillFormData(data, {object_name: 'external_tool'});
$dialog.dialog({
title: I18n.t('titles.edit_external_tool', "Edit External Tool"),
width: 600
});
$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");
var url = $tool.find(".update_tool_url").attr('rel');
$tool.confirmDelete({
url: url,
message: I18n.t('prompts.remove_tool', "Are you sure you want to remove this tool? Any courses using this tool will no longer work."),
success: function() {
$(this).slideUp(function() {
$(this).remove();
});
}
});
});
$("#external_tool_match_by").change(function() {
if($(this).val() == 'url') {
$(this).parents("form").find(".tool_domain").hide().find(":text").val("").end().end()
.find(".tool_url").show();
} else {
$(this).parents("form").find(".tool_url").hide().find(":text").val("").end().end()
.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

@ -111,7 +111,7 @@ describe ExternalToolsController, :type => :integration do
{:controller => 'external_tools', :action => 'show', :format => 'json',
:"#{type}_id" => context.id.to_s, :external_tool_id => et.id.to_s})
json.should == example_json(et)
json.diff(example_json(et)).should == {}
end
def not_found_call(context, type="course")
@ -128,7 +128,8 @@ describe ExternalToolsController, :type => :integration do
{:controller => 'external_tools', :action => 'index', :format => 'json',
:"#{type}_id" => context.id.to_s})
json.should == [example_json(et)]
json.size.should == 1
json.first.diff(example_json(et)).should == {}
end
def create_call(context, type="course")
@ -138,7 +139,7 @@ describe ExternalToolsController, :type => :integration do
context.context_external_tools.count.should == 1
et = context.context_external_tools.last
json.should == example_json(et)
json.diff(example_json(et)).should == {}
end
def update_call(context, type="course")
@ -148,7 +149,7 @@ describe ExternalToolsController, :type => :integration do
{:controller => 'external_tools', :action => 'update', :format => 'json',
:"#{type}_id" => context.id.to_s, :external_tool_id => et.id.to_s}, post_hash)
et.reload
json.should == example_json(et)
json.diff(example_json(et)).should == {}
end
def destroy_call(context, type="course")
@ -251,6 +252,7 @@ describe ExternalToolsController, :type => :integration do
"domain"=>nil,
"url"=>"http://www.example.com/ims/lti",
"id"=>et ? et.id : nil,
"workflow_state"=>"public",
"resource_selection"=>
{"text"=>"",
"url"=>"http://www.example.com/ims/lti/resource",

View File

@ -0,0 +1,29 @@
define [
'jquery'
'compiled/views/ExternalTools/EditView'
], ($, EditView) ->
module 'ExternalTools',
setup: ->
@view = new EditView()
@view.render()
teardown: ->
@view.$el.dialog 'close'
test 'adds errors', 6, ->
@view.addError(@view.$('input').first(), 'Wrong!')
equal $('.help-inline').size(), 1
equal $('.error').size(), 1
ok $(".help-inline:contains('Wrong!')").is ':visible'
@view.addError(@view.$('input').last(), 'Also Wrong...')
equal $('.help-inline').size(), 2
equal $('.error').size(), 2
ok $(".help-inline:contains('Also Wrong...')").is ':visible'
test 'removes all errors', 2, ->
@view.addError(@view.$('input').first(), 'Wrong!')
@view.addError(@view.$('input').last(), 'Also Wrong...')
equal $('.help-inline').size(), 2
@view.removeErrors()
equal $('.help-inline').size(), 0

View File

@ -285,7 +285,7 @@ describe ExternalToolsController do
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')
json['errors']['config_xml'][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>"
@ -293,7 +293,7 @@ describe ExternalToolsController do
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')
json['errors']['config_xml'][0]['message'].should == I18n.t(:invalid_xml_syntax, 'Invalid xml syntax')
end
it "should handle advanced xml configurations by URL retrieval" do
@ -348,7 +348,7 @@ describe ExternalToolsController do
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')
json['errors']['config_url'][0]['message'].should == I18n.t(:retrieve_timeout, 'could not retrieve configuration, the server response timed out')
end
end

View File

@ -55,7 +55,7 @@ describe "admin settings tabs" do
it "should delete an external tool" do
add_external_tool
hover_and_click(".delete_tool_link:visible")
driver.switch_to.alert.accept
fj('.ui-dialog button:contains(Delete):visible').click
wait_for_ajax_requests
tool = ContextExternalTool.last
tool.workflow_state.should == "deleted"
@ -67,7 +67,7 @@ describe "admin settings tabs" do
new_description = "a different description"
hover_and_click(".edit_tool_link:visible")
replace_content(f("#external_tool_description"), new_description)
fj(".save_button:visible").click
fj('.ui-dialog button:contains(Submit):visible').click
wait_for_ajax_requests
tool = ContextExternalTool.last
tool.description.should == new_description

View File

@ -16,7 +16,7 @@ describe "editing external tools" do
f('#external_tool_consumer_key').send_keys('fdjaklfjdaklfdjaslfjajfkljsalkjflas')
f('#external_tool_shared_secret').send_keys('r08132ufio1jfj1iofj3j1kf3ljl1')
f('#external_tool_domain').send_keys('instructure.com')
submit_form('#external_tool_form')
fj('.ui-dialog:visible .btn-primary').click()
wait_for_ajaximations
f("#external_tool_#{ContextExternalTool.find_by_name(tool_name).id} .edit_tool_link").click
f('#external_tool_name').should have_attribute(:value, tool_name)
@ -27,7 +27,6 @@ describe "editing external tools" do
get "/courses/#{@course.id}/settings"
f("#tab-tools-link").click
add_external_tool
f("#external_tools_dialog").should_not be_displayed
tool = ContextExternalTool.last
tool_elem = f("#external_tool_#{tool.id}")
tool_elem.should be_displayed
@ -44,18 +43,15 @@ describe "editing external tools" do
get "/courses/#{@course.id}/settings"
keep_trying_until { f("#tab-tools-link").should be_displayed }
f("#tab-tools-link").click
tool_elem = f("#external_tool_#{tool.id}")
tool_elem.find_element(:css, ".edit_tool_link").click
f("#external_tools_dialog").should be_displayed
f("#external_tool_#{tool.id} .edit_tool_link").click
replace_content(f("#external_tool_name"), "new tool (updated)")
replace_content(f("#external_tool_consumer_key"), "key (updated)")
replace_content(f("#external_tool_shared_secret"), "secret (updated)")
replace_content(f("#external_tool_domain"), "example2.com")
replace_content(f("#external_tool_custom_fields_string"), "a=9\nb=8")
f("#external_tools_dialog .save_button").click
fj('.ui-dialog:visible .btn-primary').click()
wait_for_ajax_requests
f("#external_tools_dialog").should_not be_displayed
tool_elem = fj("#external_tools .external_tool:visible").should be_displayed
tool_elem = fj("#external_tools .external_tool").should be_displayed
tool_elem.should_not be_nil
tool.reload
tool.name.should == "new tool (updated)"

View File

@ -9,7 +9,6 @@ shared_examples_for "external tools tests" do
secret = "secret"
f("#tab-tools .add_tool_link").click
f("#external_tools_dialog").should be_displayed
f("#external_tool_name").send_keys(name)
f("#external_tool_consumer_key").send_keys(key)
@ -21,7 +20,7 @@ shared_examples_for "external tools tests" do
else
add_manual opts
end
submit_form("#external_tools_dialog")
fj('.ui-dialog:visible .btn-primary').click()
wait_for_ajax_requests
# ContextExternalTool.count.should != 0
tool = ContextExternalTool.last
@ -38,24 +37,16 @@ shared_examples_for "external tools tests" do
tool.url.should == url
tool.workflow_state.should == "public"
tool.description.should == "Description"
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
f("#external_tool_#{tool.id} .url").text.should == url
f("#external_tool_#{tool.id} .editor_button").should be_displayed
f("#external_tool_#{tool.id} .resource_selection").should be_displayed
f("#external_tool_#{tool.id} .course_navigation").should be_displayed
f("#external_tool_#{tool.id} .user_navigation").should be_displayed
f("#external_tool_#{tool.id} .account_navigation").should be_displayed
f("#external_tool_#{tool.id} .readable_state").text.should == "Public"
f("#external_tool_#{tool.id} .description").text.should == "Description"
f("#external_tool_#{tool.id} .vendor_help_link").should be_displayed
f("#external_tool_#{tool.id} .vendor_help_link").text.should == tool.vendor_help_link
ContextExternalTool::EXTENSION_TYPES.each do |type|
tool.extension_setting(type).should_not be_empty
f("#external_tool_#{tool.id} .#{type}").should be_displayed
end
f("#external_tool_#{tool.id} .url").text.should eql url
elsif opts.include? :url
url = "https://lti-examples.heroku.com/tool_redirect"
kitten_text = "pictures of kittens to your site"
@ -100,7 +91,7 @@ shared_examples_for "external tools tests" do
def add_manual (opts)
f("#external_tool_config_type option[value='manual']").click
f("#external_tool_form .config_type.manual").should be_displayed
f(".config_type.manual").should be_displayed
f("#external_tool_config_url").should_not be_displayed
f("#external_tool_config_xml").should_not be_displayed
@custom_key = "value"
@ -111,10 +102,8 @@ shared_examples_for "external tools tests" do
f("#external_tool_description").send_keys(@description)
if opts.include? :manual_url
@manual_url = @domain+":80"
f("#external_tool_match_by option[value='url']").click
f("#external_tool_url").send_keys(@manual_url)
else
f("#external_tool_match_by option[value='domain']").click
f("#external_tool_domain").send_keys(@domain)
end
@ -139,7 +128,7 @@ shared_examples_for "external tools tests" do
def add_xml
f("#external_tool_config_type option[value='by_xml']").click
f("#external_tool_form .config_type.manual").should_not be_displayed
f(".config_type.manual").should_not be_displayed
f("#external_tool_config_url").should_not be_displayed
f("#external_tool_config_xml").should be_displayed