Merge remote-tracking branch 'origin/master' into dev/fft

This commit is contained in:
Jon Jensen 2012-06-27 16:40:07 -06:00
commit d45870956f
33 changed files with 992 additions and 504 deletions

View File

@ -340,7 +340,7 @@ class ApplicationController < ActionController::Base
when 'self'
@context = @current_user
else
@context = User.find(params[:user_id])
@context = api_request? ? api_find(User, params[:user_id]) : User.find(params[:user_id])
end
params[:context_id] = params[:user_id]
params[:context_type] = "User"
@ -348,7 +348,7 @@ class ApplicationController < ActionController::Base
elsif params[:course_section_id]
params[:context_id] = params[:course_section_id]
params[:context_type] = "CourseSection"
@context = CourseSection.find(params[:course_section_id])
@context = api_request? ? api_find(CourseSection, params[:course_section_id]) : CourseSection.find(params[:course_section_id])
elsif params[:collection_item_id]
params[:context_id] = params[:collection_item_id]
params[:context_type] = 'CollectionItem'

View File

@ -178,12 +178,12 @@ class ExternalToolsController < ApplicationController
redirect_to named_context_url(@context, :context_url)
return
end
@resource_title = @tool.label_for(selection_type.to_sym)
@resource_url = @tool.settings[selection_type.to_sym][:url]
@opaque_id = @context.opaque_identifier(:asset_string)
@resource_type = selection_type
@return_url ||= url_for(@context)
@launch = BasicLTI::ToolLaunch.new(:url => @resource_url, :tool => @tool, :user => @current_user, :context => @context, :link_code => @opaque_id, :return_url => @return_url, :resource_type => @resource_type)
@launch = @tool.create_launch(@context, @current_user, @return_url, selection_type)
@resource_url = @launch.url
@tool_settings = @launch.generate
render :template => 'external_tools/tool_show'
end
@ -200,20 +200,28 @@ class ExternalToolsController < ApplicationController
# @argument description [string] [optional] A description of the tool
# @argument url [string] [optional] The url to match links against. Either "url" or "domain" should be set, not both.
# @argument domain [string] [optional] The domain to match links against. Either "url" or "domain" should be set, not both.
# @argument icon_url [string] [optional] The url of the icon to show for this tool
# @argument text [string] [optional] The default text to show for this tool
# @argument custom_fields [string] [optional] Custom fields that will be sent to the tool consumer, specified as custom_fields[field_name]
# @argument account_navigation[url] [string] [optional] The url of the external tool for account navigation
# @argument account_navigation[enabled] [boolean] [optional] Set this to enable this feature
# @argument account_navigation[text] [string] [optional] The text that will show on the left-tab in the account navigation
# @argument user_navigation[url] [string] [optional] The url of the external tool for user navigation
# @argument user_navigation[enabled] [boolean] [optional] Set this to enable this feature
# @argument user_navigation[text] [string] [optional] The text that will show on the left-tab in the user navigation
# @argument course_navigation[url] [string] [optional] The url of the external tool for course navigation
# @argument course_navigation[enabled] [boolean] [optional] Set this to enable this feature
# @argument course_navigation[text] [string] [optional] The text that will show on the left-tab in the course navigation
# @argument course_navigation[visibility] [string] [optional] Who will see the navigation tab. "admins" for course admins, "members" for students, null for everyone
# @argument course_navigation[default] [boolean] [optional] Whether the navigation option will show in the course by default or whether the teacher will have to explicitly enable it
# @argument editor_button[url] [string] [optional] The url of the external tool
# @argument editor_button[enabled] [boolean] [optional] Set this to enable this feature
# @argument editor_button[icon_url] [string] [optional] The url of the icon to show in the WYSIWYG editor
# @argument editor_button[selection_width] [string] [optional] The width of the dialog the tool is launched in
# @argument editor_button[selection_height] [string] [optional] The height of the dialog the tool is launched in
# @argument resource_selection[url] [string] [optional] The url of the external tool
# @argument resource_selection[enabled] [boolean] [optional] Set this to enable this feature
# @argument resource_selection[icon_url] [string] [optional] The url of the icon to show in the module external tool list
# @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
@ -228,14 +236,14 @@ class ExternalToolsController < ApplicationController
# -F 'name=LTI Example' \
# -F 'consumer_key=asdfg' \
# -F 'shared_secret=lkjh' \
# -F 'url=http://example.com/ims/lti' \
# -F 'url=https://example.com/ims/lti' \
# -F 'privacy_level=name_only' \
# -F 'custom_fields[key1]=value1' \
# -F 'custom_fields[key2]=value2' \
# -F 'course_navigation[url]=http://example.com/ims/lti/course_endpoint' \
# -F 'course_navigation[text]=Course Materials' \
# -F 'course_navigation[text]=Course Materials' \
# -F 'course_navigation[default]=false'
#
# -F 'course_navigation[enabled]=true'
#
# @example_request
#
# This would create a tool on the account with navigation for the user profile page
@ -244,11 +252,12 @@ class ExternalToolsController < ApplicationController
# -F 'name=LTI Example' \
# -F 'consumer_key=asdfg' \
# -F 'shared_secret=lkjh' \
# -F 'url=http://example.com/ims/lti' \
# -F 'url=https://example.com/ims/lti' \
# -F 'privacy_level=name_only' \
# -F 'user_navigation[url]=http://example.com/ims/lti/user_endpoint' \
# -F 'user_navigation[text]=Soemthing Cool'
#
# -F 'user_navigation[url]=https://example.com/ims/lti/user_endpoint' \
# -F 'user_navigation[text]=Something Cool'
# -F 'user_navigation[enabled]=true'
#
# @example_request
#
# This would create a tool on the account with configuration pulled from an external URL
@ -258,7 +267,7 @@ class ExternalToolsController < ApplicationController
# -F 'consumer_key=asdfg' \
# -F 'shared_secret=lkjh' \
# -F 'config_type=by_url' \
# -F 'config_url=http://example.com/ims/lti/tool_config.xml'
# -F 'config_url=https://example.com/ims/lti/tool_config.xml'
def create
if authorized_action(@context, @current_user, :update)
@tool = @context.context_external_tools.new
@ -327,9 +336,9 @@ class ExternalToolsController < ApplicationController
private
def set_tool_attributes(tool, params)
[:name, :description, :url, :domain, :privacy_level, :consumer_key, :shared_secret,
[:name, :description, :url, :icon_url, :domain, :privacy_level, :consumer_key, :shared_secret,
:custom_fields, :custom_fields_string, :account_navigation, :user_navigation,
:course_navigation, :editor_button, :resource_selection,
:course_navigation, :editor_button, :resource_selection, :text,
:config_type, :config_url, :config_xml].each do |prop|
tool.send("#{prop}=", params[prop]) if params.has_key?(prop)
end

View File

@ -323,7 +323,9 @@ class FilesController < ApplicationController
protected :send_attachment
def send_stored_file(attachment, inline=true, redirect_to_s3=false)
attachment.context_module_action(@current_user, :read) if @current_user && !params[:preview]
user = @current_user
user ||= User.find_by_id(params[:user_id]) if params[:user_id].present?
attachment.context_module_action(user, :read) if user && !params[:preview]
log_asset_access(@attachment, "files", "files") unless params[:preview]
if safer_domain_available?
redirect_to safe_domain_file_url(attachment, @safer_domain_host, params[:verifier], !inline)

View File

@ -113,20 +113,20 @@ class PseudonymSessionsController < ApplicationController
@pseudonym_session.remote_ip = request.remote_ip
found = @pseudonym_session.save
if @pseudonym_session.too_many_attempts?
if !found && params[:pseudonym_session]
pseudonym = Pseudonym.authenticate(params[:pseudonym_session], @domain_root_account.trusted_account_ids, request.remote_ip)
if pseudonym && pseudonym != :too_many_attempts
@pseudonym_session = PseudonymSession.new(pseudonym, params[:pseudonym_session][:remember_me] == "1")
found = @pseudonym_session.save
end
end
if pseudonym == :too_many_attempts || @pseudonym_session.too_many_attempts?
flash[:error] = t 'errors.max_attempts', "Too many failed login attempts. Please try again later or contact your system administrator."
redirect_to login_url
return
end
if !found && params[:pseudonym_session]
if pseudonym = Pseudonym.authenticate(params[:pseudonym_session], @domain_root_account.trusted_account_ids)
@pseudonym_session = PseudonymSession.new(pseudonym, params[:pseudonym_session][:remember_me] == "1")
@pseudonym_session.save
found = true
end
end
@pseudonym = @pseudonym_session && @pseudonym_session.record
# If the user's account has been deleted, feel free to share that information
if @pseudonym && (!@pseudonym.user || @pseudonym.user.unavailable?)

View File

@ -481,8 +481,8 @@ module ApplicationHelper
{
:name => tool.label_for(:editor_button, nil),
:id => tool.id,
:url => tool.settings[:editor_button][:url],
:icon_url => tool.settings[:editor_button][:icon_url],
:url => tool.settings[:editor_button][:url] || tool.url,
:icon_url => tool.settings[:editor_button][:icon_url] || tool.settings[:icon_url],
:width => tool.settings[:editor_button][:selection_width],
:height => tool.settings[:editor_button][:selection_height]
}

View File

@ -1588,6 +1588,11 @@ class Assignment < ActiveRecord::Base
assoc = rubric.associate_with(item, context, :purpose => 'grading')
assoc.use_for_grading = !!hash[:rubric_use_for_grading] if hash.has_key?(:rubric_use_for_grading)
assoc.hide_score_total = !!hash[:rubric_hide_score_total] if hash.has_key?(:rubric_hide_score_total)
if hash[:saved_rubric_comments]
assoc.summary_data ||= {}
assoc.summary_data[:saved_comments] ||= {}
assoc.summary_data[:saved_comments] = hash[:saved_rubric_comments]
end
assoc.save
end
end

View File

@ -26,8 +26,26 @@ class ContextExternalTool < ActiveRecord::Base
state :public
state :deleted
end
set_policy do
def create_launch(context, user, return_url, selection_type=nil)
if selection_type
if self.settings[selection_type.to_sym]
resource_url = self.settings[selection_type.to_sym][:url]
else
raise t('no_selection_type', "This tool has no selection type %{type}", :type => selection_type)
end
end
resource_url ||= self.url
BasicLTI::ToolLaunch.new(:url => resource_url,
:tool => self,
:user => user,
:context => context,
:link_code => context.opaque_identifier(:asset_string),
:return_url => return_url,
:resource_type => selection_type)
end
set_policy do
given { |user, session| self.cached_context_grants_right?(user, session, :update) }
can :read and can :update and can :delete
end
@ -51,9 +69,12 @@ class ContextExternalTool < ActiveRecord::Base
def label_for(key, lang=nil)
labels = settings[key] && settings[key][:labels]
(labels && labels[lang]) ||
labels2 = settings[:labels]
(labels && labels[lang]) ||
(labels && lang && labels[lang.split('-').first]) ||
(settings[key] && settings[key][:text]) ||
(settings[key] && settings[key][:text]) ||
(labels2 && labels2[lang]) ||
(labels2 && lang && labels2[lang.split('-').first]) ||
settings[:text] || name || "External Tool"
end
@ -169,7 +190,7 @@ class ContextExternalTool < ActiveRecord::Base
end
def resource_selection=(hash)
tool_setting(:resource_selection, hash, :selection_width, :selection_height)
tool_setting(:resource_selection, hash, :selection_width, :selection_height, :icon_url)
end
def resource_selection
@ -183,6 +204,22 @@ class ContextExternalTool < ActiveRecord::Base
def editor_button
settings[:editor_button]
end
def icon_url=(i_url)
settings[:icon_url] = i_url
end
def icon_url
settings[:icon_url]
end
def text=(val)
settings[:text] = val
end
def text
settings[:text]
end
def shared_secret=(val)
write_attribute(:shared_secret, val) unless val.blank?
@ -191,19 +228,24 @@ class ContextExternalTool < ActiveRecord::Base
def infer_defaults
self.url = nil if url.blank?
self.domain = nil if domain.blank?
settings.delete(:user_navigation) if settings[:user_navigation] && (!settings[:user_navigation][:url])
settings.delete(:course_navigation) if settings[:course_navigation] && (!settings[:course_navigation][:url])
settings.delete(:account_navigation) if settings[:account_navigation] && (!settings[:account_navigation][:url])
settings.delete(:resource_selection) if settings[:resource_selection] && (!settings[:resource_selection][:url] || !settings[:resource_selection][:selection_width] || !settings[:resource_selection][:selection_height])
settings.delete(:editor_button) if settings[:editor_button] && (!settings[:editor_button][:url] || !settings[:editor_button][:icon_url])
settings[:icon_url] ||= settings[:editor_button][:icon_url] if settings[:editor_button] && settings[:editor_button][:icon_url]
[:resource_selection, :editor_button].each do |type|
if settings[type]
settings[type][:selection_width] = settings[type][:selection_width].to_i
settings[type][:selection_height] = settings[type][:selection_height].to_i
settings[:icon_url] ||= settings[type][:icon_url] if settings[type][:icon_url]
settings[type][:selection_width] = settings[type][:selection_width].to_i if settings[type][:selection_width]
settings[type][:selection_height] = settings[type][:selection_height].to_i if settings[type][:selection_height]
end
end
[:course_navigation, :account_navigation, :user_navigation, :resource_selection, :editor_button].each do |type|
if settings[type]
if !(settings[type][:url] || self.url) || (settings[type].has_key?(:enabled) && !settings[type][:enabled])
settings.delete(type)
end
end
end
settings.delete(:resource_selection) if settings[:resource_selection] && (!settings[:resource_selection][:selection_width] || !settings[:resource_selection][:selection_height])
settings.delete(:editor_button) if settings[:editor_button] && !settings[:icon_url]
self.has_user_navigation = !!settings[:user_navigation]
self.has_course_navigation = !!settings[:course_navigation]
@ -369,19 +411,23 @@ class ContextExternalTool < ActiveRecord::Base
end
end
end
# sets the custom fields from the main tool settings, and any on individual resource type settings
def set_custom_fields(hash, resource_type)
fields = resource_type ? settings[resource_type.to_sym][:custom_fields] : settings[:custom_fields]
(fields || {}).each do |key, val|
key = key.gsub(/[^\w]/, '_').downcase
if key.match(/^custom_/)
hash[key] = val
else
hash["custom_#{key}"] = val
fields = [settings[:custom_fields] || {}]
fields << (settings[resource_type.to_sym][:custom_fields] || {}) if resource_type && settings[resource_type.to_sym]
fields.each do |field_set|
field_set.each do |key, val|
key = key.to_s.gsub(/[^\w]/, '_').downcase
if key.match(/^custom_/)
hash[key] = val
else
hash["custom_#{key}"] = val
end
end
end
end
def clone_for(context, dup=nil, options={})
if !self.cloned_item && !self.new_record?
self.cloned_item = ClonedItem.create(:original_item => self)
@ -453,9 +499,11 @@ class ContextExternalTool < ActiveRecord::Base
settings[setting] = {}
end
settings[setting][:url] = hash[:url]
settings[setting][:url] = hash[:url] if hash[:url]
settings[setting][:text] = hash[:text] if hash[:text]
keys.each { |key| settings[setting][key] = hash[key] }
settings[setting][:custom_fields] = hash[:custom_fields] if hash[:custom_fields]
settings[setting][:enabled] = Canvas::Plugin.value_to_boolean(hash[:enabled]) if hash.has_key?(:enabled)
keys.each { |key| settings[setting][key] = hash[key] if hash.has_key?(key) }
# if the type needs to do some validations for specific keys
yield settings[setting] if block_given?

View File

@ -2712,6 +2712,9 @@ class Course < ActiveRecord::Base
self.attributes.delete_if{|k,v| [:id, :created_at, :updated_at, :syllabus_body, :wiki_id, :default_view, :tab_configuration].include?(k.to_sym) }.each do |key, val|
new_course.write_attribute(key, val)
end
# there's a unique constraint on this, so we need to clear it out
self.self_enrollment_code = nil
self.self_enrollment = false
# The order here is important; we have to set our sis id to nil and save first
# so that the new course can be saved, then we need the new course saved to
# get its id to move over sections and enrollments. Setting this course to

View File

@ -376,22 +376,28 @@ class Pseudonym < ActiveRecord::Base
def self.serialization_excludes; [:crypted_password, :password_salt, :reset_password_token, :persistence_token, :single_access_token, :perishable_token, :sis_ssha]; end
def self.find_all_by_arbitrary_credentials(credentials, account_ids)
def self.find_all_by_arbitrary_credentials(credentials, account_ids, remote_ip)
return [] if credentials[:unique_id].blank? ||
credentials[:password].blank?
Shard.partition_by_shard(account_ids) do |account_ids|
too_many_attempts = false
pseudonyms = Shard.partition_by_shard(account_ids) do |account_ids|
active.
by_unique_id(credentials[:unique_id]).
where(:account_id => account_ids).
all(:include => :user).
select { |p|
p.valid_arbitrary_credentials?(credentials[:password])
valid = p.valid_arbitrary_credentials?(credentials[:password])
too_many_attempts = true if p.audit_login(remote_ip, valid) == :too_many_attempts
valid
}
end
return :too_many_attempts if too_many_attempts
pseudonyms
end
def self.authenticate(credentials, account_ids)
pseudonyms = find_all_by_arbitrary_credentials(credentials, account_ids)
def self.authenticate(credentials, account_ids, remote_ip = nil)
pseudonyms = find_all_by_arbitrary_credentials(credentials, account_ids, remote_ip)
return :too_many_attempts if pseudonyms == :too_many_attempts
site_admin = pseudonyms.find { |p| p.account_id == Account.site_admin.id }
# only log them in if these credentials match a single user OR if it matched site admin
if pseudonyms.map(&:user).uniq.length == 1 || site_admin
@ -399,4 +405,15 @@ class Pseudonym < ActiveRecord::Base
site_admin || pseudonyms.first
end
end
def audit_login(remote_ip, valid_password)
return :too_many_attempts unless Canvas::Security.allow_login_attempt?(self, remote_ip)
if valid_password
Canvas::Security.successful_login!(self, remote_ip)
else
Canvas::Security.failed_login!(self, remote_ip)
end
nil
end
end

View File

@ -99,17 +99,11 @@ class PseudonymSession < Authlogic::Session::Base
super
# have to call super first, as that's what loads attempted_record
if !Canvas::Security.allow_login_attempt?(attempted_record, remote_ip)
if too_many_attempts? || attempted_record.try(:audit_login, remote_ip, !invalid_password?) == :too_many_attempts
self.too_many_attempts = true
errors.add(password_field, I18n.t('errors.max_attempts', 'Too many failed login attempts. Please try again later or contact your system administrator.'))
return
end
if invalid_password?
Canvas::Security.failed_login!(attempted_record, remote_ip)
else
Canvas::Security.successful_login!(attempted_record, remote_ip)
end
end
def too_many_attempts?

View File

@ -53,7 +53,8 @@
<h2><%= @topic.title %></h2>
<% end %>
<% if @locked %>
<%= @locked.is_a?(Hash) ? lock_explanation(@locked, 'topic', @context) : t(:locked_topic, "This topic is currently locked.") %>
<% use_context = (@context.is_a?(Group)) ? @context.context : @context %>
<%= @locked.is_a?(Hash) ? lock_explanation(@locked, 'topic', use_context) : t(:locked_topic, "This topic is currently locked.") %>
<% else %>
<%
js_bundle :discussion

View File

@ -78,7 +78,7 @@
<div class="centered-block-wrap">
<div class="centered-block-inner">
<ul class="ui-listview" style="margin-top: 30px">
<li class="ui-listview-header"><%= mt('file_uploads_for_user', 'File Uploads for **%{user}**', :user => @submission.user.name) %></li>
<li class="ui-listview-header"><%= mt('file_uploads_for_user', 'File Uploads for **%{user}**', :user => params[:hide_student_name] ? t('student', 'Student') : @submission.user.name) %></li>
<% if @submission.attachments.empty? %>
<li>
<div class="ui-listview-text ui-listview-right"></div>

View File

@ -979,22 +979,27 @@ ActiveRecord::ConnectionAdapters::SchemaStatements.class_eval do
# in anticipation of having to re-run migrations due to integrity violations or
# killing stuff that is holding locks too long
def add_foreign_key_if_not_exists(from_table, to_table, options = {})
return if self.adapter_name == 'SQLite'
column = options[:column] || "#{to_table.to_s.singularize}_id"
foreign_key_name = foreign_key_name(from_table, column, options)
return if foreign_keys(from_table).find { |k| k.options[:name] == foreign_key_name }
add_foreign_key(from_table, to_table, options)
case self.adapter_name
when 'SQLite': return
when 'PostgreSQL'
begin
add_foreign_key(from_table, to_table, options)
rescue ActiveRecord::StatementInvalid => e
raise unless e.message =~ /PGError: ERROR:.+already exists/
end
else
column = options[:column] || "#{to_table.to_s.singularize}_id"
foreign_key_name = foreign_key_name(from_table, column, options)
return if foreign_keys(from_table).find { |k| k.options[:name] == foreign_key_name }
add_foreign_key(from_table, to_table, options)
end
end
def remove_foreign_key_if_exists(table, options = {})
return if self.adapter_name == 'SQLite'
if Hash === options
foreign_key_name = foreign_key_name(table, options[:column], options)
else
foreign_key_name = foreign_key_name(table, "#{options.to_s.singularize}_id")
begin
remove_foreign_key(table, options)
rescue ActiveRecord::StatementInvalid => e
raise unless e.message =~ /PGError: ERROR:.+does not exist|Mysql::Error: Error on rename/
end
return unless foreign_keys(table).find { |k| k.options[:name] == foreign_key_name }
remove_foreign_key(table, options)
end
end

View File

@ -185,17 +185,20 @@ If the <code>launch_presentation_return_url</code> were
## Settings
All of these settings are contained under "editor_button" in the tool configuration
- url: &lt;url&gt; (required)
- url: &lt;url&gt; (optional)
This is the URL that will be POSTed to when users click the button in any rich editor. It can be the same as the tool's URL, or something different. Domain and URL matching are not enforced for editor button links. In order to prevent security warnings for users, it is recommended that this URL be over SSL (https).
This is required if a url is not set on the main tool configuration.
- icon_url: &lt;url&gt; (required)
- icon_url: &lt;url&gt; (optional)
This is the URL of the icon that will be shown on the button in the rich editor. Icons should be 16x16 in size, and can be any standard web image format (png, gif, ico, etc.). It is recommended that this URL be over SSL (https).
This is required if an icon_url is not set on the main tool configuration.
- text: &lt;text&gt; (required)
- text: &lt;text&gt; (optional)
This is the default text that will be shown if a user hovers over the editor button. This can be overridden by language-specific settings if desired by using the labels setting. This text will also be shown next to the icon if there are too many buttons and the tool is available in the "more tools" dropdown.
This is required if a text value is not set on the main tool configuration.
- labels: &lt;set of locale-label pairs&gt; (optional)
@ -208,3 +211,7 @@ All of these settings are contained under "editor_button" in the tool configurat
- selection_height: &lt;number&gt; (required)
This value is the explicit height of the dialog that is loaded when a user clicks the icon in the rich editor.
- enabled: &lt;boolean&gt; (required)
Whether to enable this selection feature.

View File

@ -59,13 +59,19 @@ If the <code>launch_presentation_return_url</code> were
## Settings
All of these settings are contained under "resource_selection"
- url: &lt;url&gt; (required)
- url: &lt;url&gt; (optional)
This is the URL that will be POSTed to when users click the button in any rich editor. It can be the same as the tool's URL, or something different. Domain and URL matching are not enforced for editor button links. In order to prevent security warnings for users, it is recommended that this URL be over SSL (https).
This is required if a url is not set on the main tool configuration.
- text: &lt;text&gt; (required)
- icon_url: &lt;url&gt; (optional)
This is the URL of the icon to be shown.
- text: &lt;text&gt; (optional)
This is the default text that will be shown if a user hovers over the editor button. This can be overridden by language-specific settings if desired by using the labels setting. This text will also be shown next to the icon if there are too many buttons and the tool is available in the "more tools" dropdown.
This is required if a text value is not set on the main tool configuration.
- labels: &lt;set of locale-label pairs&gt; (optional)
@ -78,3 +84,7 @@ All of these settings are contained under "resource_selection"
- selection_height: &lt;number&gt; (required)
This value is the explicit height of the dialog that is loaded when a user clicks the icon in the rich editor.
- enabled: &lt;boolean&gt; (required)
Whether to enable this selection feature.

View File

@ -20,9 +20,10 @@ of users.
### Settings
All of these settings are contained under "course_navigation"
- url: &lt;url&gt; (required)
- url: &lt;url&gt; (optional)
This is the URL that will be POSTed to when users click the left tab. It can be the same as the tool's URL, or something different. Domain and URL matching are not enforced for course navigation links. In order to prevent security warnings for users, it is recommended that this URL be over SSL (https).
This is required if a url is not set on the main tool configuration.
- default: ['enabled', 'disabled'] (optional, 'enabled' by default)
@ -32,14 +33,19 @@ All of these settings are contained under "course_navigation"
This specifies what types of users will see the link in the course navigation. "public" means anyone accessing the course, "members" means only users enrolled in the course, and "admins" means only Teachers, TAs, Designers and account admins will see the link.
- text: &lt;text&gt; (required)
- text: &lt;text&gt; (optional)
This is the default text that will be shown in the left hand navigation as the text of the link. This can be overridden by language-specific settings if desired by using the labels setting.
This is required if a text value is not set on the main tool configuration.
- labels: &lt;set of locale-label pairs&gt; (optional)
This can be used to specify different label names for different locales. For example, if an institution supports both English and Spanish interfaces, the text in the link should change depending on the language being displayed. This option lets you support multiple languages for a single tool.
- enabled: &lt;boolean&gt; (required)
Whether to enable this selection feature.
<a name="account_navigation"></a>
## Account navigation links
External tools can also be configured to appear as links in
@ -51,18 +57,24 @@ the link in their account navigation.
### Settings
All of these settings are contained under "account_navigation"
- url: &lt;url&gt; (required)
- url: &lt;url&gt; (optional)
This is the URL that will be POSTed to when users click the left tab. It can be the same as the tool's URL, or something different. Domain and URL matching are not enforced for account navigation links. In order to prevent security warnings for users, it is recommended that this URL be over SSL (https).
This is required if a url is not set on the main tool configuration.
- text: &lt;text&gt; (required)
- text: &lt;text&gt; (optional)
This is the default text that will be shown in the left hand navigation as the text of the link. This can be overridden by language-specific settings if desired by using the labels setting.
This is required if a text value is not set on the main tool configuration.
- labels: &lt;set of locale-label pairs&gt; (optional)
This can be used to specify different label names for different locales. For example, if an institution supports both English and Spanish interfaces, the text in the link should change depending on the language being displayed. This option lets you support multiple languages for a single tool.
- enabled: &lt;boolean&gt; (required)
Whether to enable this selection feature.
<a name="user_navigation"></a>
## User navigation links
External tools can also be configured to appear as links in
@ -76,14 +88,20 @@ profile navigation.
### Settings
All of these settings are contained under "user_navigation"
- url: &lt;url&gt; (required)
- url: &lt;url&gt; (optional)
This is the URL that will be POSTed to when users click the left tab. It can be the same as the tool's URL, or something different. Domain and URL matching are not enforced for user navigation links. In order to prevent security warnings for users, it is recommended that this URL be over SSL (https).
This is required if a url is not set on the main tool configuration.
- text: &lt;text&gt; (required)
- text: &lt;text&gt; (optional)
This is the default text that will be shown in the left hand navigation as the text of the link. This can be overridden by language-specific settings if desired by using the labels setting.
This is required if a text value is not set on the main tool configuration.
- labels: &lt;set of locale-label pairs&gt; (optional)
This can be used to specify different label names for different locales. For example, if an institution supports both English and Spanish interfaces, the text in the link should change depending on the language being displayed. This option lets you support multiple languages for a single tool.
- enabled: &lt;boolean&gt; (required)
Whether to enable this selection feature.

View File

@ -39,9 +39,7 @@ found below.
<blti:extensions platform="canvas.instructure.com">
<lticm:property name="privacy_level">public</lticm:property>
</blti:extensions>
<cartridge_bundle identifierref="BLTI001_Bundle"/>
<cartridge_icon identifierref="BLTI001_Icon"/>
</cartridge_basiclti_link>
</cartridge_basiclti_link>
### Domain matching, "name only" privacy level
<?xml version="1.0" encoding="UTF-8"?>
@ -60,9 +58,7 @@ found below.
<lticm:property name="privacy_level">name_only</lticm:property>
<lticm:property name="domain">example.com</lticm:property>
</blti:extensions>
<cartridge_bundle identifierref="BLTI001_Bundle"/>
<cartridge_icon identifierref="BLTI001_Icon"/>
</cartridge_basiclti_link>
</cartridge_basiclti_link>
## Course Navigation External Tool Examples
@ -78,19 +74,67 @@ found below.
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:launch_url>https://example.com/attendance</blti:launch_url>
<blti:title>Attendance</blti:title>
<blti:description>Provides an interactive seating chart and attendance tool</blti:description>
<blti:extensions platform="canvas.instructure.com">
<lticm:property name="privacy_level">public</lticm:property>
<lticm:property name="domain">example.com</lticm:property>
<lticm:options name="course_navigation">
<lticm:property name="url">https://example.com/attendance</lticm:property>
<lticm:property name="text">Attendance</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>
</cartridge_basiclti_link>
### Minimal configuration with specific launch url for extension
<?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:launch_url>https://example.com/</blti:launch_url>
<blti:title>Attendance</blti:title>
<blti:description>Provides an interactive seating chart and attendance tool</blti:description>
<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</lticm:property>
<lticm:property name="enabled">true</lticm:property>
</lticm:options>
</blti:extensions>
</cartridge_basiclti_link>
### Configuration with specific custom variables for extension
<?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:launch_url>https://example.com/launch</blti:launch_url>
<blti:title>Mind blowing awesomeness</blti:title>
<blti:description>Provides something so awesome you'll just have to launch it to believe it</blti:description>
<blti:extensions platform="canvas.instructure.com">
<lticm:property name="privacy_level">public</lticm:property>
<lticm:options name="course_navigation">
<lticm:property name="enabled">true</lticm:property>
<lticm:options name="custom_fields">
<lticm:property name="key1">value1</lticm:property>
<lticm:property name="key2">value2</lticm:property>
</lticm:options>
</lticm:options>
</blti:extensions>
</cartridge_basiclti_link>
### Teacher/Admin-only navigation
@ -104,20 +148,18 @@ found below.
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:launch_url>https://example.com/attendance</blti:launch_url>
<blti:title>Attendance</blti:title>
<blti:description>Provides an interactive seating chart and attendance tool</blti:description>
<blti:extensions platform="canvas.instructure.com">
<lticm:property name="privacy_level">public</lticm:property>
<lticm:property name="domain">example.com</lticm:property>
<lticm:options name="course_navigation">
<lticm:property name="url">https://example.com/attendance</lticm:property>
<lticm:property name="text">Attendance</lticm:property>
<lticm:property name="visibility">admins</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>
</cartridge_basiclti_link>
### Disabled by default
@ -131,20 +173,18 @@ found below.
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:launch_url>https://example.com/attendance</blti:launch_url>
<blti:title>Attendance</blti:title>
<blti:description>Provides an interactive seating chart and attendance tool</blti:description>
<blti:extensions platform="canvas.instructure.com">
<lticm:property name="privacy_level">public</lticm:property>
<lticm:property name="domain">example.com</lticm:property>
<lticm:options name="course_navigation">
<lticm:property name="url">https://example.com/attendance</lticm:property>
<lticm:property name="text">Attendance</lticm:property>
<lticm:property name="default">disabled</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>
</cartridge_basiclti_link>
### Multiple language support
@ -166,15 +206,14 @@ found below.
<lticm:options name="course_navigation">
<lticm:property name="url">https://example.com/attendance</lticm:property>
<lticm:property name="text">Attendance</lticm:property>
<lticm:property name="enabled">true</lticm:property>
<lticm:options name="labels">
<lticm:property name="en">Attendance</lticm:property>
<lticm:property name="es">Asistencia</lticm:property>
</lticm:options>
</lticm:options>
</blti:extensions>
<cartridge_bundle identifierref="BLTI001_Bundle"/>
<cartridge_icon identifierref="BLTI001_Icon"/>
</cartridge_basiclti_link>
</cartridge_basiclti_link>
## Account Navigation External Tool Examples
@ -190,19 +229,18 @@ found below.
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:launch_url>https://example.com/reports</blti:launch_url>
<blti:title>Custom Reports</blti:title>
<blti:description>Department reports pulled from other campus systems</blti:description>
<blti:extensions platform="canvas.instructure.com">
<lticm:property name="privacy_level">public</lticm:property>
<lticm:property name="domain">example.com</lticm:property>
<lticm:property name="text">Other Reports</lticm:property>
<lticm:options name="account_navigation">
<lticm:property name="url">https://example.com/reports</lticm:property>
<lticm:property name="text">Other Reports</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>
</cartridge_basiclti_link>
## User Navigation External Tool Examples
@ -218,19 +256,17 @@ found below.
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:launch_url>https://example.com/profile</blti:launch_url>
<blti:title>Campus Profile</blti:title>
<blti:description>Access to campus profile from within Canvas</blti:description>
<blti:extensions platform="canvas.instructure.com">
<lticm:property name="privacy_level">public</lticm:property>
<lticm:property name="domain">example.com</lticm:property>
<lticm:options name="user_navigation">
<lticm:property name="url">https://example.com/profile</lticm:property>
<lticm:property name="text">Campus Profile</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>
</cartridge_basiclti_link>
## Rich Editor External Tool Examples
@ -246,22 +282,21 @@ found below.
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:launch_url>https://example.com/image_selector</blti:launch_url>
<blti:title>Image Selector</blti:title>
<blti:description>This connects to the campus image library and allows inserting images into content directly from this library</blti:description>
<blti:extensions platform="canvas.instructure.com">
<lticm:property name="privacy_level">public</lticm:property>
<lticm:property name="domain">example.com</lticm:property>
<lticm:property name="text">Image Library</lticm:property>
<lticm:options name="editor_button">
<lticm:property name="url">https://example.com/image_selector</lticm:property>
<lticm:property name="enabled">true</lticm:property>
<lticm:property name="icon_url">https://example.com/image_selector.png</lticm:property>
<lticm:property name="text">Image Library</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>
</cartridge_basiclti_link>
### Multiple language support
@ -275,14 +310,15 @@ found below.
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:launch_url>https://example.com/image_selector</blti:launch_url>
<blti:title>Image Selector</blti:title>
<blti:description>This connects to the campus image library and allows inserting images into content directly from this library</blti:description>
<blti:extensions platform="canvas.instructure.com">
<lticm:property name="privacy_level">public</lticm:property>
<lticm:property name="domain">example.com</lticm:property>
<lticm:property name="icon_url">https://example.com/image_selector.png</lticm:property>
<lticm:options name="editor_button">
<lticm:property name="url">https://example.com/image_selector</lticm:property>
<lticm:property name="icon_url">https://example.com/image_selector.png</lticm:property>
<lticm:property name="enabled">true</lticm:property>
<lticm:property name="text">Image Library</lticm:property>
<lticm:property name="selection_width">500</lticm:property>
<lticm:property name="selection_height">300</lticm:property>
@ -292,9 +328,7 @@ found below.
</lticm:options>
</lticm:options>
</blti:extensions>
<cartridge_bundle identifierref="BLTI001_Bundle"/>
<cartridge_icon identifierref="BLTI001_Icon"/>
</cartridge_basiclti_link>
</cartridge_basiclti_link>
## Link Selection External Tool Examples
@ -319,15 +353,14 @@ matching, and to only return URLs matching that domain.
<lticm:property name="privacy_level">public</lticm:property>
<lticm:property name="domain">example.com</lticm:property>
<lticm:options name="resource_selection">
<lticm:property name="enabled">true</lticm:property>
<lticm:property name="url">https://example.com/chapter_selector</lticm:property>
<lticm:property name="text">eBook Chapter Selector</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>
</cartridge_basiclti_link>
### Multiple language support
@ -347,6 +380,7 @@ matching, and to only return URLs matching that domain.
<lticm:property name="privacy_level">public</lticm:property>
<lticm:property name="domain">example.com</lticm:property>
<lticm:options name="resource_selection">
<lticm:property name="enabled">true</lticm:property>
<lticm:property name="url">https://example.com/chapter_selector</lticm:property>
<lticm:property name="text">eBook Chapter Selector</lticm:property>
<lticm:property name="selection_width">500</lticm:property>
@ -357,9 +391,7 @@ matching, and to only return URLs matching that domain.
</lticm:options>
</lticm:options>
</blti:extensions>
<cartridge_bundle identifierref="BLTI001_Bundle"/>
<cartridge_icon identifierref="BLTI001_Icon"/>
</cartridge_basiclti_link>
</cartridge_basiclti_link>
## Combined External Tool Configuration Examples
@ -386,19 +418,48 @@ by the service should be scoped to the matching domain.
<lticm:property name="privacy_level">public</lticm:property>
<lticm:property name="domain">example.com</lticm:property>
<lticm:options name="course_navigation">
<lticm:property name="enabled">true</lticm:property>
<lticm:property name="url">https://example.com/attendance</lticm:property>
<lticm:property name="text">Attendance</lticm:property>
<lticm:property name="visibility">admins</lticm:property>
<lticm:property name="default">disabled</lticm:property>
</lticm:options>
<lticm:options name="account_navigation">
<lticm:property name="enabled">true</lticm:property>
<lticm:property name="url">https://example.com/attendance_admin</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>
</cartridge_basiclti_link>
### Course navigation and account navigation with shared url and text
<?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:launch_url>https://example.com/attendance</blti:launch_url>
<blti:title>Attendance</blti:title>
<blti:description>Provides an interactive seating chart and attendance tool</blti:description>
<blti:extensions platform="canvas.instructure.com">
<lticm:property name="privacy_level">public</lticm:property>
<lticm:property name="text">Attendance</lticm:property>
<lticm:options name="course_navigation">
<lticm:property name="enabled">true</lticm:property>
<lticm:property name="visibility">admins</lticm:property>
<lticm:property name="default">disabled</lticm:property>
</lticm:options>
<lticm:options name="account_navigation">
<lticm:property name="enabled">true</lticm:property>
</lticm:options>
</blti:extensions>
</cartridge_basiclti_link>
### Rich editor and link selection with multiple language support
@ -412,33 +473,27 @@ by the service should be scoped to the matching domain.
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:launch_url>https://example.com/wiki</blti:launch_url>
<blti:title>Global Wiki</blti:title>
<blti:description>Institution-wide wiki tool with all the trimmings</blti:description>
<blti:extensions platform="canvas.instructure.com">
<lticm:property name="privacy_level">public</lticm:property>
<lticm:property name="domain">example.com</lticm:property>
<lticm:options name="editor_button">
<lticm:property name="url">https://example.com/wiki</lticm:property>
<lticm:property name="icon_url">https://example.com/wiki.png</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 name="labels">
<lticm:property name="icon_url">https://example.com/wiki.png</lticm:property>
<lticm:property name="text">Build/Link to Wiki Page</lticm:property>
<lticm:options name="labels">
<lticm:property name="en-US">Build/Link to Wiki Page</lticm:property>
<lticm:property name="en-GB">Build/Link to Wiki Page</lticm:property>
</lticm:options>
<lticm:options name="editor_button">
<lticm:property name="enabled">true</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="enabled">true</lticm:property>
<lticm:property name="selection_width">500</lticm:property>
<lticm:property name="selection_height">300</lticm:property>
<lticm:options name="labels">
<lticm:property name="en-US">Build/Link to Wiki Page</lticm:property>
<lticm:property name="en-GB">Build/Link to Wiki Page</lticm:property>
</lticm:options>
</lticm:options>
</blti:extensions>
<cartridge_bundle identifierref="BLTI001_Bundle"/>
<cartridge_icon identifierref="BLTI001_Icon"/>
</cartridge_basiclti_link>
</cartridge_basiclti_link>

View File

@ -91,7 +91,22 @@ module BasicLTI
end
if tool.public?
hash['custom_canvas_user_id'] = user.id
hash['custom_canvas_course_id'] = context.id
if context.respond_to?(:root_account)
pseudo = user.sis_pseudonym_for(context)
elsif tool.context && tool.context.respond_to?(:root_account)
pseudo = user.sis_pseudonym_for(tool.context)
end
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
end
# need to set the locale here (instead of waiting for the first call to
@ -101,7 +116,7 @@ module BasicLTI
hash['context_id'] = context.opaque_identifier(:asset_string)
hash['context_title'] = context.name
hash['context_label'] = context.course_code rescue nil
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'] = 600

View File

@ -92,6 +92,15 @@ module CC
node.rubric_identifierref CCHelper.create_key(assignment.rubric)
node.rubric_use_for_grading assoc.use_for_grading
node.rubric_hide_score_total assoc.hide_score_total
if assoc.summary_data && assoc.summary_data[:saved_comments]
node.saved_rubric_comments do |sc_node|
assoc.summary_data[:saved_comments].each_pair do |key, vals|
vals.each do |val|
sc_node.comment(:criterion_id => key){|a|a << val}
end
end
end
end
end
node.quiz_identifierref CCHelper.create_key(assignment.quiz) if assignment.quiz
node.allowed_extensions assignment.allowed_extensions.join(',') unless assignment.allowed_extensions.blank?

View File

@ -50,7 +50,13 @@ module CC::Importer::Canvas
assignment["grading_standard_migration_id"] = get_node_val(meta_doc, "grading_standard_identifierref")
assignment["rubric_migration_id"] = get_node_val(meta_doc, "rubric_identifierref")
assignment["quiz_migration_id"] = get_node_val(meta_doc, "quiz_identifierref")
if meta_doc.at_css("saved_rubric_comments comment")
assignment[:saved_rubric_comments] = {}
meta_doc.css("saved_rubric_comments comment").each do |comment_node|
assignment[:saved_rubric_comments][comment_node['criterion_id']] ||= []
assignment[:saved_rubric_comments][comment_node['criterion_id']] << comment_node.text.strip
end
end
['title', "allowed_extensions", "grading_type", "submission_types", "external_tool_url"].each do |string_type|
val = get_node_val(meta_doc, string_type)
assignment[string_type] = val unless val.nil?

View File

@ -104,6 +104,17 @@
<xs:element name="external_tool_url" type="xs:string" minOccurs="0"/>
<xs:element name="external_tool_new_tab" type="xs:boolean" minOccurs="0"/>
<xs:element name="freeze_on_copy" type="xs:boolean" minOccurs="0"/>
<xs:element name="saved_rubric_comments" minOccurs="0">
<xs:complexType>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="comment" type="xs:string">
<xs:complexType>
<xs:attribute name="criterion_id" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="identifier" type="xs:ID" use="required"/>
</xs:complexType>

View File

@ -139,12 +139,13 @@ define([
// this is for backwards compatability, we used to store the value as
// strings "true" or "false", but now we store boolean true/false values.
var settingVal = userSettings.get("eg_hide_student_names");
return settingVal === true || settingVal === "true";
return settingVal === true || settingVal === "true" || window.anonymousAssignment;
}
};
function mergeStudentsAndSubmission(){
jsonData.studentsWithSubmissions = jsonData.context.students;
jsonData.studentMap = {};
$.each(jsonData.studentsWithSubmissions, function(i, student){
this.section_ids = $.map($.grep(jsonData.context.enrollments, function(enrollment, i){
return enrollment.user_id === student.id;
@ -157,6 +158,7 @@ define([
this.rubric_assessments = $.grep(visibleRubricAssessments, function(rubricAssessment, i){
return rubricAssessment.user_id === student.id;
});
jsonData.studentMap[student.id] = student;
});
// handle showing students only in a certain section.
@ -227,11 +229,7 @@ define([
}
function initDropdown(){
var hideStudentNames;
if (utils.shouldHideStudentNames() || window.anonymousAssignment) {
hideStudentNames = true;
}
var hideStudentNames = utils.shouldHideStudentNames();
$("#hide_student_names").attr('checked', hideStudentNames);
var options = $.map(jsonData.studentsWithSubmissions, function(s, idx){
var name = htmlEscape(s.name),
@ -930,8 +928,7 @@ define([
//if there is not a valid student_id in the location.hash then force it to be the first student in the class with an assignment to grade.
if (typeof(hash.student_id) != "number" ||
!$.grep(jsonData.studentsWithSubmissions, function(s){
return hash.student_id == s.id;}).length) {
!jsonData.studentMap[hash.student_id]) {
hash.student_id = jsonData.studentsWithSubmissions[0].id;
for (var i = 0, max = jsonData.studentsWithSubmissions.length; i < max; i++){
if (typeof jsonData.studentsWithSubmissions[i].submission !== 'undefined' && jsonData.studentsWithSubmissions[i].submission.workflow_state !== 'graded'){
@ -945,9 +942,8 @@ define([
},
goToStudent: function(student_id){
var student = $.grep(jsonData.studentsWithSubmissions, function(o){
return o.id === student_id;
})[0];
var hideStudentNames = utils.shouldHideStudentNames();
var student = jsonData.studentMap[student_id];
if (student) {
$selectmenu.selectmenu("value", student.id);
@ -955,12 +951,14 @@ define([
if (!this.currentStudent || (this.currentStudent.id != student.id)) {
$selectmenu.change();
}
if (student.avatar_path) {
if (student.avatar_path && !hideStudentNames) {
// If there's any kind of delay in loading the user's avatar, it's
// better to show a blank image than the previous student's image.
$new_image = $avatar_image.clone();
$new_image = $avatar_image.clone().show();
$avatar_image.after($new_image.attr('src', student.avatar_path)).remove();
$avatar_image = $new_image;
} else {
$avatar_image.hide();
}
}
},
@ -971,9 +969,7 @@ define([
handleStudentChanged: function(){
var id = parseInt( $selectmenu.val(), 10 );
this.currentStudent = $.grep(jsonData.studentsWithSubmissions, function(o){
return o.id === id;
})[0];
this.currentStudent = jsonData.studentMap[id];
document.location.hash = "#" + encodeURIComponent(JSON.stringify({
"student_id": this.currentStudent.id
}));
@ -1270,7 +1266,9 @@ define([
this.currentStudent.submission.currentSelectedIndex != null ?
'&version=' + this.currentStudent.submission.currentSelectedIndex :
''
) +'" frameborder="0"></iframe>')
) + (
utils.shouldHideStudentNames() ? "&hide_student_name=1" : ""
) + '" frameborder="0"></iframe>')
.show();
}
}
@ -1320,6 +1318,7 @@ define([
},
showDiscussion: function(){
var hideStudentNames = utils.shouldHideStudentNames();
$comments.html("");
if (this.currentStudent.submission && this.currentStudent.submission.submission_comments) {
$.each(this.currentStudent.submission.submission_comments, function(i, comment){
@ -1327,10 +1326,11 @@ define([
if(comment.submission_comment) { comment = comment.submission_comment; }
comment.posted_at = $.parseFromISO(comment.created_at).datetime_formatted;
// if(comment.anonymous) { comment.author_name = "Anonymous"; }
var hideStudentName = hideStudentNames && jsonData.studentMap[comment.author_id];
if (hideStudentName) { comment.author_name = I18n.t('student', "Student"); }
var $comment = $comment_blank.clone(true).fillTemplateData({ data: comment });
$comment.find('span.comment').html(htmlEscape(comment.comment).replace(/\n/g, "<br />"));
if(comment.avatar_path) {
if (comment.avatar_path && !hideStudentName) {
$comment.find(".avatar").attr('src', comment.avatar_path).show();
}
// this is really poorly decoupled but over in speed_grader.html.erb these rubricAssessment. variables are set.
@ -1426,7 +1426,7 @@ define([
setOrUpdateSubmission: function(submission){
// find the student this submission belongs to and update their submission with this new one, if they dont have a submission, set this as their submission.
var student = $.grep(jsonData.studentsWithSubmissions, function(s){ return s.id === submission.user_id; })[0];
var student = jsonData.studentMap[submission.user_id];
student.submission = student.submission || {};
// stuff that comes back from ajax doesnt have a submission history but handleSubmissionSelectionChange

View File

@ -110,6 +110,31 @@ describe ApplicationController do
end
end
describe "get_context" do
it "should find user with api_find for api requests" do
user_with_pseudonym
@pseudonym.update_attribute(:sis_user_id, 'test1')
@controller.instance_variable_set(:@domain_root_account, Account.default)
@controller.stubs(:named_context_url).with(@user, :context_url).returns('')
@controller.stubs(:params).returns({ :user_id => 'sis_user_id:test1' })
@controller.stubs(:api_request?).returns(true)
@controller.send(:get_context)
@controller.instance_variable_get(:@context).should == @user
end
it "should find course section with api_find for api requests" do
course_model
@section = @course.course_sections.first
@section.update_attribute(:sis_source_id, 'test1')
@controller.instance_variable_set(:@domain_root_account, Account.default)
@controller.stubs(:named_context_url).with(@section, :context_url).returns('')
@controller.stubs(:params).returns({ :course_section_id => 'sis_section_id:test1' })
@controller.stubs(:api_request?).returns(true)
@controller.send(:get_context)
@controller.instance_variable_get(:@context).should == @section
end
end
end

View File

@ -89,7 +89,7 @@ describe "External Tools" do
end
it "should render user navigation tools with a full return url" do
tool = @course.root_account.context_external_tools.build(:shared_secret => 'test_secret', :consumer_key => 'test_key', :name => 'my grade passback test tool', :domain => 'example.com')
tool = @course.root_account.context_external_tools.build(:shared_secret => 'test_secret', :consumer_key => 'test_key', :name => 'my grade passback test tool', :domain => 'example.com', :privacy_level => 'public')
tool.settings[:user_navigation] = {:url => "http://www.example.com", :text => "Example URL"}
tool.save!

View File

@ -147,6 +147,35 @@ describe FilesController do
controller.instance_variable_get(:@current_user).should be_nil
end
it "should update module progressions for html safefiles iframe" do
HostUrl.stubs(:file_host).returns('files-test.host')
course_with_student(:active_all => true, :user => user_with_pseudonym)
login_as
@att = @course.attachments.create(:uploaded_data => stub_file_data("ohai.html", "<html><body>ohai</body></html>", "text/html"))
@module = @course.context_modules.create!(:name => "module")
@tag = @module.add_item({:type => 'attachment', :id => @att.id})
@module.reload
hash = {}
hash[@tag.id.to_s] = {:type => 'must_view'}
@module.completion_requirements = hash
@module.save!
@module.evaluate_for(@user, true, true).state.should eql(:unlocked)
# the response will be on the main domain, with an iframe pointing to the files domain and the actual uploaded html file
get "http://test.host/courses/#{@course.id}/files/#{@att.id}"
response.should be_success
response.content_type.should == 'text/html'
doc = Nokogiri::HTML::DocumentFragment.parse(response.body)
location = doc.at_css('iframe#file_content')['src']
# now reset the user session (simulating accessing via a separate domain), grab the document,
# and verify the module progress was recorded
reset!
get location
response.should be_success
@module.evaluate_for(@user, true, true).state.should eql(:completed)
end
context "should support AssessmentQuestion as a context" do
before do
course_with_teacher(:active_all => true, :user => user_with_pseudonym)

View File

@ -374,6 +374,11 @@ describe "security" do
response.body.should match(/Incorrect username/)
bad_login("5.5.5.5")
response.body.should match(/Too many failed login attempts/)
# should still fail
post_via_redirect "/login",
{ "pseudonym_session[unique_id]" => "nobody@example.com", "pseudonym_session[password]" => "asdfasdf" },
{ "REMOTE_ADDR" => "5.5.5.5" }
response.body.should match(/Too many failed login attempts/)
end
it "should have a higher limit for other ips" do
@ -383,6 +388,11 @@ describe "security" do
response.body.should match(/Incorrect username/)
bad_login("5.5.5.7") # different IP, but too many total failures
response.body.should match(/Too many failed login attempts/)
# should still fail
post_via_redirect "/login",
{ "pseudonym_session[unique_id]" => "nobody@example.com", "pseudonym_session[password]" => "asdfasdf" },
{ "REMOTE_ADDR" => "5.5.5.7" }
response.body.should match(/Too many failed login attempts/)
end
it "should not block other users with the same ip" do
@ -397,6 +407,24 @@ describe "security" do
{ "REMOTE_ADDR" => "5.5.5.5" }
path.should eql("/?login_success=1")
end
it "should apply limitations correctly for cross-account logins" do
account = Account.create!
Account.any_instance.stubs(:trusted_account_ids).returns([account.id])
@pseudonym.account = account
@pseudonym.save!
bad_login("5.5.5.5")
response.body.should match(/Incorrect username/)
bad_login("5.5.5.6") # different IP, so allowed
response.body.should match(/Incorrect username/)
bad_login("5.5.5.7") # different IP, but too many total failures
response.body.should match(/Too many failed login attempts/)
# should still fail
post_via_redirect "/login",
{ "pseudonym_session[unique_id]" => "nobody@example.com", "pseudonym_session[password]" => "asdfasdf" },
{ "REMOTE_ADDR" => "5.5.5.5" }
response.body.should match(/Too many failed login attempts/)
end
end
end

View File

@ -104,6 +104,11 @@ describe BasicLTI do
end
it "should generate correct parameters" do
@user = user_with_managed_pseudonym(:sis_user_id => 'testfun', :name => "A Name")
course_with_teacher_logged_in(:active_all => true, :user => @user, :account => @account)
@course.sis_source_id = 'coursesis'
@course.save!
@tool = @course.context_external_tools.create!(:domain => 'yahoo.com', :consumer_key => '12345', :shared_secret => 'secret', :name => 'tool', :privacy_level => 'public')
hash = BasicLTI.generate(:url => 'http://www.yahoo.com', :tool => @tool, :user => @user, :context => @course, :link_code => '123456', :return_url => 'http://www.google.com')
hash['lti_message_type'].should == 'basic-lti-launch-request'
hash['lti_version'].should == 'LTI-1p0'
@ -114,6 +119,15 @@ describe BasicLTI do
hash['context_id'].should == @course.opaque_identifier(:asset_string)
hash['context_title'].should == @course.name
hash['context_label'].should == @course.course_code
hash['custom_canvas_user_id'].should == @user.id.to_s
hash['custom_canvas_user_login_id'].should == @user.pseudonyms.first.unique_id
hash['custom_canvas_course_id'].should == @course.id.to_s
hash['lis_course_offering_sourcedid'].should == 'coursesis'
hash['lis_person_contact_email_primary'].should == 'nobody@example.com'
hash['lis_person_name_full'].should == 'A Name'
hash['lis_person_name_family'].should == 'Name'
hash['lis_person_name_given'].should == 'A'
hash['lis_person_sourcedid'].should == 'testfun'
hash['launch_presentation_locale'].should == I18n.default_locale.to_s
hash['launch_presentation_document_target'].should == 'iframe'
hash['launch_presentation_width'].should == '600'
@ -134,6 +148,33 @@ describe BasicLTI do
I18n.localizer = lambda { :en }
end
it "should add account info in launch data for account navigation" do
@user = user_with_managed_pseudonym
sub_account = Account.create(:parent_account => @account)
sub_account.sis_source_id = 'accountsis'
sub_account.save!
@tool = sub_account.context_external_tools.create!(:domain => 'yahoo.com', :consumer_key => '12345', :shared_secret => 'secret', :name => 'tool', :privacy_level => 'public')
hash = BasicLTI.generate(:url => 'http://www.yahoo.com', :tool => @tool, :user => @user, :context => sub_account, :link_code => '123456', :return_url => 'http://www.google.com')
hash['custom_canvas_account_id'] = sub_account.id.to_s
hash['custom_canvas_account_sis_id'] = 'accountsis'
hash['custom_canvas_user_login_id'].should == @user.pseudonyms.first.unique_id
end
it "should add account and user info in launch data for user profile launch" do
@user = user_with_managed_pseudonym(:sis_user_id => 'testfun')
sub_account = Account.create(:parent_account => @account)
sub_account.sis_source_id = 'accountsis'
sub_account.save!
@tool = sub_account.context_external_tools.create!(:domain => 'yahoo.com', :consumer_key => '12345', :shared_secret => 'secret', :name => 'tool', :privacy_level => 'public')
hash = BasicLTI.generate(:url => 'http://www.yahoo.com', :tool => @tool, :user => @user, :context => @user, :link_code => '123456', :return_url => 'http://www.google.com')
hash['custom_canvas_account_id'] = sub_account.id.to_s
hash['custom_canvas_account_sis_id'] = 'accountsis'
hash['lis_person_sourcedid'].should == 'testfun'
hash['custom_canvas_user_id'].should == @user.id.to_s
end
it "should include URI query parameters" do
hash = BasicLTI.generate(:url => 'http://www.yahoo.com?a=1&b=2', :tool => @tool, :user => @user, :context => @course, :link_code => '123456', :return_url => 'http://www.google.com')
hash['a'].should == '1'

View File

@ -641,14 +641,17 @@ describe ContentMigration do
@rubric.save!
@assignment = @copy_from.assignments.create!(:title => "some assignment", :points_possible => 12)
assoc = @rubric.associate_with(@assignment, @copy_from, :purpose => 'grading', :use_for_grading => true)
assoc.hide_score_total = true
assoc.use_for_grading = true
assoc.save!
@assoc = @rubric.associate_with(@assignment, @copy_from, :purpose => 'grading', :use_for_grading => true)
@assoc.hide_score_total = true
@assoc.use_for_grading = true
@assoc.save!
end
it "should still associate rubrics and assignments and copy rubric association properties" do
create_rubric_asmnt
@assoc.summary_data = {:saved_comments=>{"309_6312"=>["what the comment", "hey"]}}
@assoc.save!
run_course_copy
rub = @copy_to.rubrics.find_by_migration_id(mig_id(@rubric))
@ -657,6 +660,7 @@ describe ContentMigration do
asmnt2.rubric.id.should == rub.id
asmnt2.rubric_association.use_for_grading.should == true
asmnt2.rubric_association.hide_score_total.should == true
asmnt2.rubric_association.summary_data.should == @assoc.summary_data
end
it "should copy rubrics associated with assignments when rubric isn't selected" do

View File

@ -47,7 +47,7 @@ describe ContextExternalTool do
@tool.settings[:editor_button] = {
"icon_url"=>"http://www.example.com/favicon.ico",
"text"=>"Example",
"url"=>"http://www.example.com",
"url"=>"http://www.example.com",
"selection_height"=>400,
"selection_width"=>600
}
@ -55,7 +55,47 @@ describe ContextExternalTool do
@tool.should_not be_new_record
@tool.errors.should be_empty
end
def url_test(nav_url=nil)
course_with_teacher(:active_all => true)
@tool = @course.context_external_tools.new(:name => "a", :consumer_key => '12345', :shared_secret => 'secret', :url => "http://www.example.com")
[:course_navigation, :account_navigation, :user_navigation, :resource_selection, :editor_button].each do |type|
@tool.send "#{type}=", {
:url => nav_url,
:text => "Example",
:icon_url => "http://www.example.com/image.ico",
:selection_width => 50,
:selection_height => 50
}
@tool.save!
launch = @tool.create_launch(@course, @user, "http://test.com", type)
launch.resource_type.should == type
if nav_url
launch.url.should == nav_url
else
launch.url.should == @tool.url
end
end
end
it "should allow extension to not have a url if the main config has a url" do
url_test
end
it "should prefer the extension url to the main config url" do
url_test(nav_url="https://example.com/special_launch_of_death")
end
it "should not allow extension with no custom url and a domain match" do
@tool = @course.context_external_tools.create!(:name => "a", :domain => "google.com", :consumer_key => '12345', :shared_secret => 'secret')
@tool.course_navigation = {
:text => "Example"
}
@tool.save!
@tool.has_course_navigation.should == false
end
it "should not validate with no domain or url setting" do
@tool = @course.context_external_tools.create(:name => "a", :consumer_key => '12345', :shared_secret => 'secret')
@tool.should be_new_record
@ -63,6 +103,25 @@ describe ContextExternalTool do
@tool.errors['domain'].should == "Either the url or domain should be set."
end
end
it "should allow extension with only 'enabled' key" do
@tool = @course.context_external_tools.create!(:name => "a", :url => "http://google.com", :consumer_key => '12345', :shared_secret => 'secret')
@tool.course_navigation = {
:enabled => "true"
}
@tool.save!
@tool.has_course_navigation.should == true
end
it "should clear disabled extensions" do
@tool = @course.context_external_tools.create!(:name => "a", :url => "http://google.com", :consumer_key => '12345', :shared_secret => 'secret')
@tool.course_navigation = {
:enabled => "false"
}
@tool.save!
@tool.has_course_navigation.should == false
end
describe "find_external_tool" do
it "should match on the same domain" do
@tool = @course.context_external_tools.create!(:name => "a", :domain => "google.com", :consumer_key => '12345', :shared_secret => 'secret')
@ -217,8 +276,34 @@ describe ContextExternalTool do
tool = @course.context_external_tools.create!(:name => "a", :url => "http://www.google.com", :consumer_key => '12345', :shared_secret => 'secret', :custom_fields => {'a' => '123', 'b' => '456'})
tool.custom_fields_string.should == "a=123\nb=456"
end
it "should merge custom fields for extension launches" do
course_with_teacher(:active_all => true)
@tool = @course.context_external_tools.new(:name => "a", :consumer_key => '12345', :shared_secret => 'secret', :custom_fields => {'a' => "1", 'b' => "2"}, :url =>"http://www.example.com")
[:course_navigation, :account_navigation, :user_navigation, :resource_selection, :editor_button].each do |type|
@tool.send "#{type}=", {
:text =>"Example",
:url =>"http://www.example.com",
:icon_url => "http://www.example.com/image.ico",
:custom_fields => {"b" => "5", "c" => "3"},
:selection_width => 50,
:selection_height => 50
}
@tool.save!
hash = @tool.create_launch(@course, @user, "http://test.com", type).generate
hash["custom_a"].should == "1"
hash["custom_b"].should == "5"
hash["custom_c"].should == "3"
@tool.settings[type.to_sym][:custom_fields] = nil
@tool.save!
hash = @tool.create_launch(@course, @user, "http://test.com", type).generate
hash["custom_a"].should == "1"
hash["custom_b"].should == "2"
hash.has_key?("custom_c").should == false
end
end
end
describe "all_tools_for" do
@ -362,53 +447,56 @@ describe ContextExternalTool do
end
describe "label_for" do
append_before(:each) do
@tool = @root_account.context_external_tools.new(:name => 'tool', :consumer_key => '12345', :shared_secret => 'secret', :url => "http://example.com")
end
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'
@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'
@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'
@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'
@tool.settings = {:text => 'tool label', :course_navigation => {:bob => 'asdf'}}
@tool.save!
@tool.label_for(:course_navigation).should == 'tool label'
end
it "should return the tool's locale-specific 'text' value if no 'text' value is set for the sent key" do
@tool.settings = {:text => 'tool label', :labels => {'en' => 'translated tool label'}, :course_navigation => {:bob => 'asdf'}}
@tool.save!
@tool.label_for(:course_navigation, 'en').should == 'translated 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'
@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'
@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'
@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

View File

@ -195,12 +195,15 @@ describe Course do
@course.quizzes.create!
@course.assignments.create!
@course.wiki.wiki_page.save!
@course.self_enrollment = true
@course.sis_source_id = 'sis_id'
@course.stuck_sis_fields = [].to_set
@course.save!
@course.course_sections.should_not be_empty
@course.students.should == [@student]
@course.stuck_sis_fields.should == [].to_set
self_enrollment_code = @course.self_enrollment_code
self_enrollment_code.should_not be_nil
@new_course = @course.reset_content
@ -209,6 +212,7 @@ describe Course do
@course.course_sections.should be_empty
@course.students.should be_empty
@course.sis_source_id.should be_nil
@course.self_enrollment_code.should be_nil
@new_course.reload
@new_course.course_sections.should_not be_empty
@ -219,6 +223,7 @@ describe Course do
@new_course.sis_source_id.should == 'sis_id'
@new_course.syllabus_body.should be_blank
@new_course.stuck_sis_fields.should == [].to_set
@new_course.self_enrollment_code.should == self_enrollment_code
@course.uuid.should_not == @new_course.uuid
@course.wiki_id.should_not == @new_course.wiki_id

View File

@ -18,22 +18,22 @@ describe "assignments" do
#click on assignment in calendar
if due_date.month > current_date.month
driver.find_element(:css, '#content .next_month_link').click
f('#content .next_month_link').click
wait_for_ajax_requests
end
day_id = 'day_' + due_date.year.to_s() + '_' + due_date.strftime('%m') + '_' + due_date.strftime('%d')
day_div = driver.find_element(:id, day_id)
day_id = '#day_' + due_date.year.to_s() + '_' + due_date.strftime('%m') + '_' + due_date.strftime('%d')
day_div = f(day_id)
sleep 1 # this is one of those cases where if we click too early, no subsequent clicks will work
day_div.find_element(:link, assignment_name).click
wait_for_animations
details_dialog = driver.find_element(:id, 'event_details').find_element(:xpath, '..')
details_dialog = f('#event_details').find_element(:xpath, '..')
details_dialog.should include_text(assignment_name)
details_dialog.find_element(:css, '.edit_event_link').click
details_dialog = driver.find_element(:id, 'edit_event').find_element(:xpath, '..')
details_dialog = f('#edit_event').find_element(:xpath, '..')
details_dialog.find_element(:name, 'assignment[title]').should be_displayed
details_dialog.find_element(:css, '#edit_assignment_form .more_options_link').click
#make sure user is taken to assignment details
driver.find_element(:css, 'h2.title').should include_text(assignment_name)
f('h2.title').should include_text(assignment_name)
end
it "should create an assignment" do
@ -44,23 +44,23 @@ describe "assignments" do
#create assignment
click_option('#right-side select.assignment_groups_select', 'second group')
driver.find_element(:css, '.add_assignment_link').click
driver.find_element(:id, 'assignment_title').send_keys(assignment_name)
driver.find_element(:css, '.ui-datepicker-trigger').click
f('.add_assignment_link').click
f('#assignment_title').send_keys(assignment_name)
f('.ui-datepicker-trigger').click
datepicker = datepicker_next
datepicker.find_element(:css, '.ui-datepicker-ok').click
driver.find_element(:id, 'assignment_points_possible').send_keys('5')
f('#assignment_points_possible').send_keys('5')
submit_form('#add_assignment_form')
#make sure assignment was added to correct assignment group
wait_for_animations
first_group = driver.find_element(:css, '#groups .assignment_group:nth-child(2)')
first_group = f('#groups .assignment_group:nth-child(2)')
first_group.should include_text('second group')
first_group.should include_text(assignment_name)
#click on assignment link
driver.find_element(:link, assignment_name).click
driver.find_element(:css, 'h2.title').should include_text(assignment_name)
f("#assignment_#{Assignment.last.id} .title").click
f('h2.title').should include_text(assignment_name)
end
it "should create an assignment with more options" do
@ -71,12 +71,12 @@ describe "assignments" do
group = @course.assignment_groups.first
AssignmentGroup.update_all({:updated_at => 1.hour.ago}, {:id => group.id})
first_stamp = group.reload.updated_at.to_i
driver.find_element(:css, '.add_assignment_link').click
expect_new_page_load { driver.find_element(:css, '.more_options_link').click }
f('.add_assignment_link').click
expect_new_page_load { f('.more_options_link').click }
expect_new_page_load { submit_form('#edit_assignment_form') }
@course.assignments.count.should == 1
driver.find_element(:css, '.no_assignments_message').should_not be_displayed
driver.find_element(:css, '#groups').should include_text(expected_text)
f('.no_assignments_message').should_not be_displayed
f('#groups').should include_text(expected_text)
group.reload
group.updated_at.to_i.should_not == first_stamp
end
@ -84,13 +84,12 @@ describe "assignments" do
it "should verify that self sign-up link works in more options" do
get "/courses/#{@course.id}/assignments"
driver.find_element(:css, '.add_assignment_link').click
expect_new_page_load { driver.find_element(:css, '.more_options_link').click }
driver.find_element(:id, 'assignment_group_assignment').click
f('.add_assignment_link').click
expect_new_page_load { f('.more_options_link').click }
f('#assignment_group_assignment').click
click_option('#assignment_group_category_select', 'new', :value)
ui_dialog = find_with_jquery('.ui-dialog:visible')
ui_dialog.find_element(:css, '.self_signup_help_link img').click
driver.find_element(:id, 'self_signup_help_dialog').should be_displayed
fj('.ui-dialog:visible .self_signup_help_link img').click
f('#self_signup_help_dialog').should be_displayed
end
it "should remove student group option" do
@ -105,11 +104,10 @@ describe "assignments" do
)
@assignment = @course.assignments.last
get "/courses/#{@course.id}/assignments"
expect_new_page_load { driver.find_element(:link, assignment_name).click }
driver.find_element(:css, '.edit_full_assignment_link').click
driver.find_element(:css, '.more_options_link').click
driver.find_element(:id, 'assignment_group_assignment').click
expect_new_page_load { f("#assignment_#{@assignment.id} .title").click }
f('.edit_full_assignment_link').click
f('.more_options_link').click
f('#assignment_group_assignment').click
click_option('#assignment_group_category_select', 'new', :value)
submit_dialog('div.ui-dialog')
wait_for_ajaximations
@ -120,10 +118,10 @@ describe "assignments" do
@assignment.group_category.should_not be_nil
get "/courses/#{@course.id}/assignments"
expect_new_page_load { driver.find_element(:link, assignment_name).click }
driver.find_element(:css, '.edit_full_assignment_link').click
driver.find_element(:css, '.more_options_link').click
driver.find_element(:id, 'assignment_group_assignment').click
expect_new_page_load { f("#assignment_#{@assignment.id} .title").click }
f('.edit_full_assignment_link').click
f('.more_options_link').click
f('#assignment_group_assignment').click
submit_form('#edit_assignment_form')
wait_for_ajaximations
@assignment.reload
@ -135,15 +133,15 @@ describe "assignments" do
skip_if_ie("Out of memory")
get "/courses/#{@course.id}/assignments"
driver.find_element(:css, ".assignment_group .add_assignment_link").click
form = driver.find_element(:css, "#add_assignment_form")
f(".assignment_group .add_assignment_link").click
form = f("#add_assignment_form")
form.find_element(:css, ".assignment_submission_types option[value='online_quiz']").click
expect_new_page_load { form.find_element(:css, ".more_options_link").click }
driver.find_element(:css, ".submission_type_option option[value='none']").should be_selected
driver.find_element(:css, ".assignment_type option[value='assignment']").click
driver.find_element(:css, ".submission_type_option option[value='online']").click
driver.find_element(:css, ".assignment_type option[value='quiz']").click
f(".submission_type_option option[value='none']").should be_selected
f(".assignment_type option[value='assignment']").click
f(".submission_type_option option[value='online']").click
f(".assignment_type option[value='quiz']").click
expect_new_page_load { submit_form('#edit_assignment_form') }
end
@ -154,7 +152,7 @@ describe "assignments" do
due_date = Time.now.utc + 2.days
group = @course.assignment_groups.create!(:name => "default")
second_group = @course.assignment_groups.create!(:name => "second default")
@course.assignments.create!(
@assignment = @course.assignments.create!(
:name => assignment_name,
:due_at => due_date,
:assignment_group => group,
@ -163,36 +161,36 @@ describe "assignments" do
get "/courses/#{@course.id}/assignments"
expect_new_page_load { driver.find_element(:link, assignment_name).click }
driver.find_element(:css, '.edit_full_assignment_link').click
driver.find_element(:css, '.more_options_link').click
driver.find_element(:id, 'assignment_assignment_group_id').should be_displayed
expect_new_page_load { f("#assignment_#{@assignment.id} .title").click }
f('.edit_full_assignment_link').click
f('.more_options_link').click
f('#assignment_assignment_group_id').should be_displayed
click_option('#assignment_assignment_group_id', second_group.name)
click_option('#assignment_grading_type', 'Letter Grade')
#check grading levels dialog
wait_for_animations
keep_trying_until { driver.find_element(:css, 'a.edit_letter_grades_link').should be_displayed }
driver.find_element(:css, 'a.edit_letter_grades_link').click
keep_trying_until { f('a.edit_letter_grades_link').should be_displayed }
f('a.edit_letter_grades_link').click
wait_for_animations
driver.find_element(:id, 'edit_letter_grades_form').should be_displayed
f('#edit_letter_grades_form').should be_displayed
close_visible_dialog
#check peer reviews option
form = f("#edit_assignment_form")
form.find_element(:css, '#assignment_peer_reviews').click
form.find_element(:css, '#auto_peer_reviews').click
driver.find_element(:css, '#assignment_peer_review_count').send_keys('2')
driver.find_element(:css, '#assignment_peer_reviews_assign_at + img').click
f('#assignment_peer_review_count').send_keys('2')
f('#assignment_peer_reviews_assign_at + img').click
datepicker = datepicker_next
datepicker.find_element(:css, '.ui-datepicker-ok').click
driver.find_element(:id, 'assignment_title').send_keys(' edit')
f('#assignment_title').send_keys(' edit')
#save changes
submit_form(form)
wait_for_ajaximations
driver.find_elements(:css, '.loading_image_holder').length.should eql 0
driver.find_element(:css, 'h2.title').should include_text(assignment_name + ' edit')
ff('.loading_image_holder').length.should eql 0
f('h2.title').should include_text(assignment_name + ' edit')
end
context "frozen assignments" do
@ -224,20 +222,20 @@ describe "assignments" do
get "/courses/#{@course.id}/assignments"
expect_new_page_load { driver.find_element(:link, orig_title).click }
driver.find_element(:css, '.edit_full_assignment_link').click
driver.find_element(:css, '.more_options_link').click
expect_new_page_load { f("#assignment_#{@asmnt.id} .title").click }
f('.edit_full_assignment_link').click
f('.more_options_link').click
yield
# title isn't locked, should allow editing
driver.find_element(:id, 'assignment_title').send_keys(' edit')
f('#assignment_title').send_keys(' edit')
#save changes
submit_form('#edit_assignment_form')
wait_for_ajaximations
driver.find_elements(:css, '.loading_image_holder').length.should eql 0
driver.find_element(:css, 'h2.title').should include_text(orig_title + ' edit')
ff('.loading_image_holder').length.should eql 0
f('h2.title').should include_text(orig_title + ' edit')
end
it "should respect frozen attributes for teacher" do
@ -281,7 +279,7 @@ describe "assignments" do
)
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
driver.find_element(:css, "a.edit_full_assignment_link").click
f("a.edit_full_assignment_link").click
submit_form('#edit_assignment_form')
wait_for_animations
@ -291,60 +289,13 @@ describe "assignments" do
errorBoxes.last.text.should eql "There were errors on one or more advanced options"
errorBoxes.last.should be_displayed
driver.find_element(:css, 'a.more_options_link').click
f('a.more_options_link').click
wait_for_animations
errorBoxes = driver.execute_script("return $('.errorBox').filter('[id!=error_box_template]').toArray();")
errorBoxes.size.should eql 1 # the more_options_link one has now been removed from the DOM
errorBoxes.first.text.should eql "The assignment shouldn't be locked again until after the due date"
errorBoxes.first.should be_displayed
end
it "should allow a student view student to view/submit assignments" do
@assignment = @course.assignments.create(
:title => 'Cool Assignment',
:points_possible => 10,
:submission_types => "online_text_entry",
:due_at => Time.now.utc + 2.days)
enter_student_view
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
f('.assignment .title').should include_text @assignment.title
f('.submit_assignment_link').click
assignment_form = f('#submit_online_text_entry_form')
wait_for_tiny(assignment_form)
type_in_tiny('#submission_body', 'my assigment submission')
submit_form(assignment_form)
wait_for_dom_ready
@course.student_view_student.submissions.count.should == 1
f('#sidebar_content .details').should include_text "Turned In!"
end
it "should allow a student view student to submit file upload assignments" do
@assignment = @course.assignments.create(
:title => 'Cool Assignment',
:points_possible => 10,
:submission_types => "online_upload",
:due_at => Time.now.utc + 2.days)
enter_student_view
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
f('.submit_assignment_link').click
filename, fullpath, data = get_file("testfile1.txt")
f('.submission_attachment input').send_keys(fullpath)
f('#submit_file_button').click
wait_for_ajax_requests
wait_for_dom_ready
keep_trying_until {
f('.details .header').should include_text "Turned In!"
f('.details .file-big').should include_text "testfile1"
}
end
end
context "as a student" do
@ -371,48 +322,6 @@ describe "assignments" do
titles[1].text.should == @assignment.title
end
it "should allow you to submit a file" do
@assignment.submission_types = 'online_upload'
@assignment.save!
filename, fullpath, data = get_file("testfile1.txt")
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
f('.submit_assignment_link').click
f('.submission_attachment input').send_keys(fullpath)
f('#submission_comment').send_keys("hello comment")
f('#submit_file_button').click
wait_for_ajax_requests
wait_for_dom_ready
keep_trying_until {
f('#sidebar_content .header').should include_text "Turned In!"
f('.details .file-big').should include_text "testfile1"
}
@submission = @assignment.reload.submissions.find_by_user_id(@student.id)
@submission.submission_type.should == 'online_upload'
@submission.attachments.length.should == 1
@submission.workflow_state.should == 'submitted'
end
it "should not allow a user to submit a file-submission assignment without attaching a file" do
@assignment.submission_types = 'online_upload'
@assignment.save!
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
driver.find_element(:css, '.submit_assignment_link').click
wait_for_ajaximations
driver.find_element(:id, 'submit_file_button').click
wait_for_ajaximations
driver.find_element(:id, 'flash_error_message').should be_displayed
# navigate off the page and dismiss the alert box to avoid problems
# with other selenium tests
driver.find_element(:css, '#section-tabs .home').click
driver.switch_to.alert.accept
driver.switch_to.default_content
end
it "should expand the comments box on click" do
@assignment = @course.assignments.create!(
:name => 'test assignment',
@ -421,17 +330,16 @@ describe "assignments" do
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
driver.find_element(:css, '.submit_assignment_link').click
f('.submit_assignment_link').click
wait_for_ajaximations
driver.execute_script("return $('#submission_comment').height()").should eql 16
driver.execute_script("$('#submission_comment').focus()")
#driver.find_element(:id, 'submission_comment').click
wait_for_ajaximations
driver.execute_script("return $('#submission_comment').height()").should eql 72
# navigate off the page and dismiss the alert box to avoid problems
# with other selenium tests
driver.find_element(:css, '#section-tabs .home').click
f('#section-tabs .home').click
driver.switch_to.alert.accept
driver.switch_to.default_content
end
@ -439,7 +347,7 @@ describe "assignments" do
it "should highlight mini-calendar dates where stuff is due" do
get "/courses/#{@course.id}/assignments/syllabus"
driver.find_element(:css, ".mini_calendar_day.date_#{DUE_DATE.strftime("%m_%d_%Y")}").
f(".mini_calendar_day.date_#{DUE_DATE.strftime("%m_%d_%Y")}").
attribute('class').should match /has_event/
end
@ -457,87 +365,9 @@ describe "assignments" do
@rubric.associate_with @assignment, @course, :purpose => "grading"
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
driver.find_element(:css, ".details").text.should =~ /comment before muting/
driver.find_element(:css, ".details").text.should_not =~ /comment after muting/
end
it "should show as not turned in when submission was auto created in speedgrader" do
# given
@assignment.update_attributes(:submission_types => "online_text_entry")
@assignment.grade_student(@student, :grade => "0")
# when
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
# expect
f('#sidebar_content .details').should include_text "Not Turned In!"
f('#sidebar_content a.submit_assignment_link').text.should == "Submit Assignment"
end
it "should not show as turned in or not turned in when assignment doesn't expect a submission" do
# given
@assignment.update_attributes(:submission_types => "on_paper")
@assignment.grade_student(@student, :grade => "0")
# when
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
# expect
f('#sidebar_content .details').should_not include_text "Turned In!"
f('#sidebar_content .details').should_not include_text "Not Turned In!"
f('#sidebar_content a.submit_assignment_link').should be_nil
end
it "should submit an assignment and validate confirmation information" do
pending "BUG 6783 - Coming Up assignments update error" do
@assignment.update_attributes(:submission_types => 'online_url')
@submission = @assignment.submit_homework(@student)
@submission.submission_type = "online_url"
@submission.save!
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
driver.find_element(:css, '.details .header').should include_text('Turned In!')
get "/courses/#{@course.id}"
driver.execute_script("$('.tooltip_text').css('visibility', 'visible')")
tooltip_text_elements = driver.find_elements(:css, '.tooltip_text > span')
driver.find_element(:css, '.tooltip_text').should be_displayed
tooltip_text_elements[1].text.should == 'submitted'
end
end
it "should not allow blank submissions for text entry" do
@assignment.update_attributes(:submission_types => "online_text_entry")
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
driver.find_element(:css, '.submit_assignment_link').click
assignment_form = driver.find_element(:id, 'submit_online_text_entry_form')
wait_for_tiny(assignment_form)
submit_form(assignment_form)
# it should not actually submit and pop up an error message
driver.find_element(:css, '.error_box').should be_displayed
Submission.count.should == 0
# now make sure it works
lambda {
type_in_tiny('#submission_body', 'now it is not blank')
submit_form(assignment_form)
}.should change { Submission.count }.by(1)
end
it "should not allow a submission with only comments" do
@assignment.update_attributes(:submission_types => "online_text_entry")
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
driver.find_element(:css, '.submit_assignment_link').click
assignment_form = driver.find_element(:id, 'submit_online_text_entry_form')
replace_content(assignment_form.find_element(:id, 'submission_comment'), 'this should not be able to be submitted for grading')
submit_form("#submit_online_text_entry_form")
# it should not actually submit and pop up an error message
driver.find_element(:css, '.error_box').should be_displayed
Submission.count.should == 0
# navigate off the page and dismiss the alert box to avoid problems
# with other selenium tests
driver.find_element(:css, '#section-tabs .home').click
driver.switch_to.alert.accept
driver.switch_to.default_content
details = f(".details")
details.should include_text('comment before muting')
details.should_not include_text('comment after muting')
end
it "should have group comment checkboxes for group assignments" do
@ -551,7 +381,7 @@ describe "assignments" do
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
find_all_with_jquery('table.formtable input[name="submission[group_comment]"]').size.should eql 3
ffj('table.formtable input[name="submission[group_comment]"]').size.should eql 3
end
end
end

View File

@ -232,6 +232,44 @@ describe "speed grader" do
ff("#comments > .comment .avatar")[0]['style'].should match(/display:\s*none/)
end
it "should hide student names and avatar images if \"Hide student names\" is checked" do
# enable avatars
@account = Account.default
@account.enable_service(:avatars)
@account.save!
@account.service_enabled?(:avatars).should be_true
sub = student_submission
sub.add_comment(:comment => "ohai teacher")
get "/courses/#{@course.id}/gradebook/speed_grader?assignment_id=#{@assignment.id}"
wait_for_animations
f("#avatar_image").should be_displayed
f("#settings_link").click
f('#hide_student_names').click
expect_new_page_load {
submit_form('#settings_form')
}
wait_for_animations
f("#avatar_image").should_not be_displayed
f('#combo_box_container .ui-selectmenu .ui-selectmenu-item-header').text.should eql "Student 1"
f('#comments > .comment').should include_text('ohai')
f("#comments > .comment .avatar").should_not be_displayed
f('#comments > .comment .author_name').should include_text('Student')
# add teacher comment
f('#add_a_comment > textarea').send_keys('grader comment')
submit_form('#add_a_comment')
keep_trying_until { ff('#comments > .comment').size == 2 }
# make sure name and avatar show up for teacher comment
ffj("#comments > .comment .avatar:visible").size.should eql 1
ff('#comments > .comment .author_name')[1].should include_text('nobody@example.com')
end
it "should not show students in other sections if visibility is limited" do
@enrollment.update_attribute(:limit_privileges_to_course_section, true)
student_submission

View File

@ -5,9 +5,9 @@ describe "submissions" do
def create_assignment(type = 'online_text_entry')
assignment = @course.assignments.build({
:name => 'media assignment',
:submission_types => type
})
:name => 'media assignment',
:submission_types => type
})
assignment.workflow_state = 'published'
assignment.save!
assignment
@ -20,10 +20,10 @@ describe "submissions" do
end
def open_media_comment_dialog
f('.media_comment_link').click
# swf and stuff loads, give it a sec to do its thing
sleep 0.5
end
f('.media_comment_link').click
# swf and stuff loads, give it a sec to do its thing
sleep 0.5
end
def submit_media_comment_1
open_media_comment_dialog
@ -44,58 +44,243 @@ describe "submissions" do
wait_for_ajax_requests
end
it "should display the grade in grade field" do
course_with_teacher_logged_in
student_in_course
assignment = create_assignment
assignment.submissions.create(:user => @student)
assignment.grade_student @student, :grade => 2
get "/courses/#{@course.id}/assignments/#{assignment.id}/submissions/#{@student.id}"
f('.grading_value')[:value].should == '2'
context 'as a teacher' do
before (:each) do
course_with_teacher_logged_in
end
it "should allow media comments" do
stub_kaltura
student_in_course
assignment = create_assignment
assignment.submissions.create(:user => @student)
get "/courses/#{@course.id}/assignments/#{assignment.id}/submissions/#{@student.id}"
# make sure the JS didn't burn any bridges, and submit two
submit_media_comment_1
submit_media_comment_2
# check that the thumbnails show up on the right sidebar
number_of_comments = driver.execute_script "return $('.comment_list').children().length"
number_of_comments.should == 2
end
it "should display the grade in grade field" do
student_in_course
assignment = create_assignment
assignment.submissions.create(:user => @student)
assignment.grade_student @student, :grade => 2
get "/courses/#{@course.id}/assignments/#{assignment.id}/submissions/#{@student.id}"
f('.grading_value')[:value].should == '2'
end
end
it "should not break when you open and close the media comment dialog" do
stub_kaltura
course_with_student_logged_in
create_assignment_and_go_to_page 'media_recording'
context "student view" do
driver.find_element(:css, ".submit_assignment_link").click
open_button = driver.find_element(:css, ".record_media_comment_link")
before (:each) do
course_with_teacher_logged_in
end
# open it twice
open_button.click
# swf and other stuff load, give it half a second before it starts trying to click
sleep 0.5
close_visible_dialog
open_button.click
sleep 0.5
close_visible_dialog
it "should allow a student view student to view/submit assignments" do
@assignment = @course.assignments.create(
:title => 'Cool Assignment',
:points_possible => 10,
:submission_types => "online_text_entry",
:due_at => Time.now.utc + 2.days)
# fire the callback that the flash object fires
driver.execute_script "window.mediaCommentCallback([{entryId:1, entryType:1}]);"
enter_student_view
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
# see if the confirmation element shows up
f('#media_media_recording_ready').should be_displayed
f('.assignment .title').should include_text @assignment.title
f('.submit_assignment_link').click
assignment_form = f('#submit_online_text_entry_form')
wait_for_tiny(assignment_form)
# submit the assignment so the "are you sure?!" message doesn't freeze up selenium
submit_form('#submit_media_recording_form')
type_in_tiny('#submission_body', 'my assigment submission')
expect_new_page_load { submit_form(assignment_form) }
@course.student_view_student.submissions.count.should == 1
f('#sidebar_content .details').should include_text "Turned In!"
end
it "should allow a student view student to submit file upload assignments" do
@assignment = @course.assignments.create(
:title => 'Cool Assignment',
:points_possible => 10,
:submission_types => "online_upload",
:due_at => Time.now.utc + 2.days)
enter_student_view
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
f('.submit_assignment_link').click
filename, fullpath, data = get_file("testfile1.txt")
f('.submission_attachment input').send_keys(fullpath)
expect_new_page_load { f('#submit_file_button').click }
keep_trying_until {
f('.details .header').should include_text "Turned In!"
f('.details .file-big').should include_text "testfile1"
}
end
end
it "should allow media comments" do
stub_kaltura
course_with_teacher_logged_in
student_in_course
assignment = create_assignment
assignment.submissions.create(:user => @student)
get "/courses/#{@course.id}/assignments/#{assignment.id}/submissions/#{@student.id}"
context 'as a student' do
DUE_DATE = Time.now.utc + 2.days
before (:each) do
course_with_student_logged_in
@assignment = @course.assignments.create!(:title => 'assignment 1', :name => 'assignment 1', :due_at => DUE_DATE)
@second_assignment = @course.assignments.create!(:title => 'assignment 2', :name => 'assignment 2', :due_at => nil)
@third_assignment = @course.assignments.create!(:title => 'assignment 3', :name => 'assignment 3', :due_at => nil)
@fourth_assignment = @course.assignments.create!(:title => 'assignment 4', :name => 'assignment 4', :due_at => DUE_DATE - 1.day)
end
# make sure the JS didn't burn any bridges, and submit two
submit_media_comment_1
submit_media_comment_2
it "should not break when you open and close the media comment dialog" do
stub_kaltura
create_assignment_and_go_to_page 'media_recording'
# check that the thumbnails show up on the right sidebar
number_of_comments = driver.execute_script "return $('.comment_list').children().length"
number_of_comments.should == 2
f(".submit_assignment_link").click
open_button = f(".record_media_comment_link")
# open it twice
open_button.click
# swf and other stuff load, give it half a second before it starts trying to click
sleep 0.5
close_visible_dialog
open_button.click
sleep 0.5
close_visible_dialog
# fire the callback that the flash object fires
driver.execute_script "window.mediaCommentCallback([{entryId:1, entryType:1}]);"
# see if the confirmation element shows up
f('#media_media_recording_ready').should be_displayed
# submit the assignment so the "are you sure?!" message doesn't freeze up selenium
submit_form('#submit_media_recording_form')
end
it "should allow you to submit a file" do
@assignment.submission_types = 'online_upload'
@assignment.save!
filename, fullpath, data = get_file("testfile1.txt")
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
f('.submit_assignment_link').click
f('.submission_attachment input').send_keys(fullpath)
f('#submission_comment').send_keys("hello comment")
expect_new_page_load { f('#submit_file_button').click }
keep_trying_until {
f('#sidebar_content .header').should include_text "Turned In!"
f('.details .file-big').should include_text "testfile1"
}
@submission = @assignment.reload.submissions.find_by_user_id(@student.id)
@submission.submission_type.should == 'online_upload'
@submission.attachments.length.should == 1
@submission.workflow_state.should == 'submitted'
end
it "should not allow a user to submit a file-submission assignment without attaching a file" do
@assignment.submission_types = 'online_upload'
@assignment.save!
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
f('.submit_assignment_link').click
wait_for_ajaximations
f('#submit_file_button').click
wait_for_ajaximations
f('#flash_error_message').should be_displayed
# navigate off the page and dismiss the alert box to avoid problems
# with other selenium tests
f('#section-tabs .home').click
driver.switch_to.alert.accept
driver.switch_to.default_content
end
it "should show as not turned in when submission was auto created in speedgrader" do
# given
@assignment.update_attributes(:submission_types => "online_text_entry")
@assignment.grade_student(@student, :grade => "0")
# when
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
# expect
f('#sidebar_content .details').should include_text "Not Turned In!"
f('#sidebar_content a.submit_assignment_link').text.should == "Submit Assignment"
end
it "should not show as turned in or not turned in when assignment doesn't expect a submission" do
# given
@assignment.update_attributes(:submission_types => "on_paper")
@assignment.grade_student(@student, :grade => "0")
# when
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
# expect
f('#sidebar_content .details').should_not include_text "Turned In!"
f('#sidebar_content .details').should_not include_text "Not Turned In!"
f('#sidebar_content a.submit_assignment_link').should be_nil
end
it "should not allow blank submissions for text entry" do
@assignment.update_attributes(:submission_types => "online_text_entry")
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
f('.submit_assignment_link').click
assignment_form = f('#submit_online_text_entry_form')
wait_for_tiny(assignment_form)
submit_form(assignment_form)
# it should not actually submit and pop up an error message
f('.error_box').should be_displayed
Submission.count.should == 0
# now make sure it works
expect {
type_in_tiny('#submission_body', 'now it is not blank')
submit_form(assignment_form)
}.to change(Submission, :count).by(1)
end
it "should not allow a submission with only comments" do
@assignment.update_attributes(:submission_types => "online_text_entry")
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
f('.submit_assignment_link').click
assignment_form = f('#submit_online_text_entry_form')
replace_content(assignment_form.find_element(:id, 'submission_comment'), 'this should not be able to be submitted for grading')
submit_form("#submit_online_text_entry_form")
# it should not actually submit and pop up an error message
f('.error_box').should be_displayed
Submission.count.should == 0
# navigate off the page and dismiss the alert box to avoid problems
# with other selenium tests
f('#section-tabs .home').click
driver.switch_to.alert.accept
driver.switch_to.default_content
end
it "should submit an assignment and validate confirmation information" do
pending "BUG 6783 - Coming Up assignments update error" do
@assignment.update_attributes(:submission_types => 'online_url')
@submission = @assignment.submit_homework(@student)
@submission.submission_type = "online_url"
@submission.save!
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
f('.details .header').should include_text('Turned In!')
get "/courses/#{@course.id}"
driver.execute_script("$('.tooltip_text').css('visibility', 'visible')")
tooltip_text_elements = ff('.tooltip_text > span')
f('.tooltip_text').should be_displayed
tooltip_text_elements[1].text.should == 'submitted'
end
end
end
end