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

This commit is contained in:
Jon Jensen 2012-05-27 23:18:12 -06:00
commit ef9be1e374
55 changed files with 953 additions and 152 deletions

View File

@ -175,6 +175,15 @@ class AssignmentGroupsController < ApplicationController
def destroy
@assignment_group = AssignmentGroup.find(params[:id])
if authorized_action(@assignment_group, @current_user, :delete)
if @assignment_group.has_frozen_assignments?(@current_user)
@assignment_group.errors.add('workflow_state', t('errors.cannot_delete_group', "You can not delete a group with a locked assignment.", :att_name => 'workflow_state'))
respond_to do |format|
format.html { redirect_to named_context_url(@context, :context_assignments_url) }
format.json { render :json => @assignment_group.errors.to_json, :status => :bad_request }
end
return
end
if params[:move_assignments_to]
@new_group = @context.assignment_groups.active.find(params[:move_assignments_to])
order = @new_group.assignments.active.map(&:id)

View File

@ -22,7 +22,7 @@ class EnrollmentsApiController < ApplicationController
before_filter :get_course_from_section, :require_context
@@errors = {
:missing_parameters => "No parameters given",
:missing_parameters => 'No parameters given',
:missing_user_id => "Can't create an enrollment without a user. Include enrollment[user_id] to create an enrollment",
:bad_type => 'Invalid type'
}
@ -132,15 +132,21 @@ class EnrollmentsApiController < ApplicationController
c[:workflow_state] = params[:state] if params[:state].present?
c[:course_section_id] = @section.id if @section.present?
}
endpoint_scope = (@context.is_a?(Course) ? (@section.present? ? "section" : "course") : "user")
scope_arguments = { :conditions => @conditions, :order => 'enrollments.type ASC, users.sortable_name ASC' }
scope_arguments = { :conditions => @conditions,
:order => 'enrollments.type ASC, users.sortable_name ASC',
:include => [:user, :course, :course_section] }
return unless enrollments = @context.is_a?(Course) ?
course_index_enrollments(scope_arguments) :
user_index_enrollments(scope_arguments)
enrollments = Api.paginate(
enrollments,
self, send("api_v1_#{endpoint_scope}_enrollments_path"))
includes = [:user] + Array(params[:include])
render :json => enrollments.map { |e| enrollment_json(e, @current_user, session, includes) }
end
@ -250,9 +256,19 @@ class EnrollmentsApiController < ApplicationController
# Returns an ActiveRecord scope of enrollments on success, false on failure.
def user_index_enrollments(scope_arguments)
user = api_find(User, params[:user_id])
# if user is requesting for themselves, just return all of their
# enrollments without any extra checking.
return user.current_enrollments.scoped(scope_arguments) if user == @current_user
if user == @current_user
enrollments = if params[:state].present?
user.enrollments.scoped(scope_arguments.merge(
:conditions => conditions_for_self))
else
user.current_and_invited_enrollments.scoped(scope_arguments)
end
return enrollments
end
# otherwise check for read_roster rights on all of the requested
# user's accounts
@ -266,10 +282,39 @@ class EnrollmentsApiController < ApplicationController
render_unauthorized_action(@user) and return false if approved_accounts.empty?
scope_arguments[:conditions].merge!({ 'enrollments.root_account_id' => approved_accounts })
enrollments = scope_arguments[:conditions].include?(:workflow_state) ?
user.enrollments.scoped(scope_arguments) :
user.current_and_invited_enrollments.scoped(scope_arguments)
# by default, return active and invited courses. don't use the existing
# current_and_invited_enrollments scope because it won't return enrollments
# on unpublished courses.
scope_arguments[:conditions][:workflow_state] ||= %w{active invited}
enrollments
user.enrollments.scoped(scope_arguments)
end
# Internal: Collect type, section, and state info from params and format them
# for use in a request for the requester's own enrollments.
#
# Returns a hash or array.
def conditions_for_self
type, state = params.values_at(:type, :state)
conditions = [[], {}]
if type.present?
conditions[0] << 'enrollments.type IN (:type)'
conditions[1][:type] = type
end
if state.present?
state.map(&:to_sym).each do |s|
conditions[0] << User::ENROLLMENT_CONDITIONS[s]
end
end
if @section.present?
conditions[0] << 'enrollments.course_section_id = :course_section_id'
conditions[1][:course_section_id] = @section.id
end
conditions[0] = conditions[0].join(' AND ')
conditions
end
end

View File

@ -131,10 +131,11 @@ class PseudonymSessionsController < ApplicationController
p.valid_arbitrary_credentials?(params[:pseudonym_session][:password])
}
end
# only log them in if these credentials match a single user
if valid_alternatives.map(&:user).uniq.length == 1
site_admin_alternative = valid_alternatives.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 valid_alternatives.map(&:user).uniq.length == 1 || site_admin_alternative
# prefer a pseudonym from Site Admin if possible, otherwise just choose one
valid_alternative = valid_alternatives.find {|p| p.account_id == Account.site_admin.id } || valid_alternatives.first
valid_alternative = site_admin_alternative || valid_alternatives.first
@pseudonym_session = PseudonymSession.new(valid_alternative, params[:pseudonym_session][:remember_me] == "1")
@pseudonym_session.save
found = true

View File

@ -37,7 +37,7 @@ class QuizzesController < ApplicationController
@surveys = @quizzes.select{|q| q.quiz_type == 'survey' || q.quiz_type == 'graded_survey' }
@submissions_hash = {}
@submissions_hash
@current_user && @current_user.quiz_submissions.each do |s|
@current_user && @current_user.quiz_submissions.scoped(:conditions => ['quizzes.context_id=? AND quizzes.context_type=?', @context.id, @context.class.to_s], :include => :quiz).each do |s|
if s.needs_grading?
s.grade_submission(:finished_at => s.end_at)
s.reload

View File

@ -898,8 +898,29 @@ class UsersController < ApplicationController
end
end
#@API Delete a user.
# Delete a user record from Canvas.
#
# WARNING: This API will allow a user to delete themselves. If you do this,
# you won't be able to make API calls or log into Canvas.
#
# @example_request
#
# curl https://<canvas>/api/v1/users/5 \
# -H 'Authorization: Bearer <ACCESS_TOKEN>' \
# -X DELETE
#
# @example_response
#
# {
# "id":133,
# "login_id":"bieber@example.com",
# "name":"Justin Bieber",
# "short_name":"The Biebs",
# "sortable_name":"Bieber, Justin"
# }
def destroy
@user = User.find(params[:id])
@user = api_request? ? api_find(User, params[:id]) : User.find(params[:id])
if authorized_action(@user, @current_user, [:manage, :manage_logins])
@user.destroy(@user.grants_right?(@current_user, session, :manage_logins))
if @user == @current_user
@ -908,13 +929,15 @@ class UsersController < ApplicationController
end
respond_to do |format|
flash[:notice] = t('user_is_deleted', "%{user_name} has been deleted", :user_name => @user.name)
if @user == @current_user
format.html { redirect_to root_url }
else
format.html { redirect_to(users_url) }
format.html do
flash[:notice] = t('user_is_deleted', "%{user_name} has been deleted", :user_name => @user.name)
redirect_to(@user == @current_user ? root_url : users_url)
end
format.json do
get_context # need the context for user_json
render :json => user_json(@user, @current_user, session)
end
format.json { render :json => @user.to_json }
end
end
end

View File

@ -1526,8 +1526,12 @@ class Assignment < ActiveRecord::Base
description += hash[:instructions_in_html] == false ? ImportedHtmlConverter.convert_text(hash[:instructions] || "", context) : ImportedHtmlConverter.convert(hash[:instructions] || "", context)
description += Attachment.attachment_list_from_migration(context, hash[:attachment_ids])
item.description = description
item.copied = true
item.copying = true
if hash[:freeze_on_copy]
item.freeze_on_copy = true
item.copied = true
item.copying = true
end
if !hash[:submission_types].blank?
item.submission_types = hash[:submission_types]
elsif ['discussion_topic'].include?(hash[:submission_format])
@ -1608,7 +1612,7 @@ class Assignment < ActiveRecord::Base
[:all_day, :turnitin_enabled, :peer_reviews_assigned, :peer_reviews,
:automatic_peer_reviews, :anonymous_peer_reviews,
:grade_group_students_individually, :allowed_extensions, :min_score,
:max_score, :mastery_score, :position, :peer_review_count, :freeze_on_copy
:max_score, :mastery_score, :position, :peer_review_count
].each do |prop|
item.send("#{prop}=", hash[prop]) unless hash[prop].nil?
end
@ -1729,6 +1733,10 @@ class Assignment < ActiveRecord::Base
false
end
def can_copy?(user)
!att_frozen?("no_copying", user)
end
def frozen_atts_not_altered
return if self.copying
FREEZABLE_ATTRIBUTES.each do |att|

View File

@ -235,4 +235,15 @@ class AssignmentGroup < ActiveRecord::Base
group.save
end
def has_frozen_assignments?(user)
return false unless PluginSetting.settings_for_plugin(:assignment_freezer)
return false unless self.active_assignments.length > 0
self.active_assignments.each do |asmnt|
return true if asmnt.frozen_for_user?(user)
end
false
end
end

View File

@ -131,7 +131,7 @@ class ContentExport < ActiveRecord::Base
selected_content[asset_type][CC::CCHelper.create_key(obj)] = true
end
def add_error(user_message, exception_or_info)
def add_error(user_message, exception_or_info=nil)
self.settings[:errors] ||= []
if exception_or_info.is_a?(Exception)
er = ErrorReport.log_exception(:course_export, exception_or_info)

View File

@ -7,7 +7,7 @@ class ContextExternalTool < ActiveRecord::Base
:name, :description, :custom_fields, :custom_fields_string,
:course_navigation, :account_navigation, :user_navigation,
:resource_selection, :editor_button,
:config_type, :config_url, :config_xml
:config_type, :config_url, :config_xml, :tool_id
validates_presence_of :name
validates_presence_of :consumer_key
validates_presence_of :shared_secret
@ -33,10 +33,13 @@ class ContextExternalTool < ActiveRecord::Base
end
def url_or_domain_is_set
setting_types = [:user_navigation, :course_navigation, :account_navigation, :resource_selection, :editor_button]
# both url and domain should not be set
if url.present? && domain.present?
errors.add(:url, t('url_or_domain_not_both', "Either the url or domain should be set, not both."))
errors.add(:domain, t('url_or_domain_not_both', "Either the url or domain should be set, not both."))
elsif url.blank? && domain.blank?
# url or domain (or url on canvas lti extension) is required
elsif url.blank? && domain.blank? && setting_types.all?{|k| !settings[k] || settings[k]['url'].blank? }
errors.add(:url, t('url_or_domain_required', "Either the url or domain should be set."))
errors.add(:domain, t('url_or_domain_required', "Either the url or domain should be set."))
end
@ -194,6 +197,7 @@ class ContextExternalTool < ActiveRecord::Base
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
@ -305,7 +309,14 @@ class ContextExternalTool < ActiveRecord::Base
context = nil
end
end
return nil if contexts.empty?
# Always use the preferred tool if it's valid and has a resource_selection configuration.
# If it didn't have resource_selection then a change in the URL would have been done manually,
# and there's no reason to assume a different URL was intended. With a resource_selection
# insertion, there's a stronger chance that a different URL was intended.
preferred_tool = ContextExternalTool.active.find_by_id(preferred_tool_id)
return preferred_tool if preferred_tool && preferred_tool.settings[:resource_selection]
contexts.each do |context|
res = context.context_external_tools.active.sort_by{|t| [t.precedence, t.id == preferred_tool_id ? 0 : 1] }.detect{|tool| tool.url && tool.matches_url?(url) }
return res if res
@ -403,6 +414,7 @@ class ContextExternalTool < ActiveRecord::Base
item.migration_id = hash[:migration_id]
item.name = hash[:title]
item.description = hash[:description]
item.tool_id = hash[:tool_id]
item.url = hash[:url] unless hash[:url].blank?
item.domain = hash[:domain] unless hash[:domain].blank?
item.privacy_level = hash[:privacy_level] || 'name_only'

View File

@ -21,6 +21,7 @@ class DeveloperKey < ActiveRecord::Base
belongs_to :account
has_many :page_views
has_many :access_tokens
has_many :context_external_tools, :primary_key => 'tool_id', :foreign_key => 'tool_id'
attr_accessible :api_key, :name, :user, :account

View File

@ -1292,4 +1292,18 @@ class Quiz < ActiveRecord::Base
end
end
end
def self.lockdown_browser_plugin_enabled?
Canvas::Plugin.all_for_tag(:lockdown_browser).any? { |p| p.settings[:enabled] }
end
def require_lockdown_browser
self[:require_lockdown_browser] && Quiz.lockdown_browser_plugin_enabled?
end
alias :require_lockdown_browser? :require_lockdown_browser
def require_lockdown_browser_for_results
self[:require_lockdown_browser_for_results] && Quiz.lockdown_browser_plugin_enabled?
end
alias :require_lockdown_browser_for_results? :require_lockdown_browser_for_results
end

View File

@ -32,18 +32,28 @@ class User < ActiveRecord::Base
serialize :preferences
include Workflow
# Internal: SQL fragments used to return enrollments in their respective workflow
# states. Where needed, these consider the state of the course to ensure that
# students do not see their enrollments on unpublished courses.
ENROLLMENT_CONDITIONS = {
:active => "( enrollments.workflow_state = 'active' and ((courses.workflow_state = 'claimed' and (enrollments.type IN ('TeacherEnrollment', 'TaEnrollment', 'DesignerEnrollment', 'StudentViewEnrollment'))) or (enrollments.workflow_state = 'active' and courses.workflow_state = 'available')) )",
:invited => "( enrollments.workflow_state = 'invited' and ((courses.workflow_state = 'available' and (enrollments.type = 'StudentEnrollment' or enrollments.type = 'ObserverEnrollment')) or (courses.workflow_state != 'deleted' and (enrollments.type IN ('TeacherEnrollment', 'TaEnrollment', 'DesignerEnrollment', 'StudentViewEnrollment')))))",
:deleted => "enrollments.workflow_state = 'deleted'",
:rejected => "enrollments.workflow_state = 'rejected'",
:completed => "enrollments.workflow_state = 'completed'",
:creation_pending => "enrollments.workflow_state = 'creation_pending'",
:inactive => "enrollments.workflow_state = 'inactive'" }
has_many :communication_channels, :order => 'position', :dependent => :destroy
has_one :communication_channel, :order => 'position'
has_many :enrollments, :dependent => :destroy
has_many :current_enrollments, :class_name => 'Enrollment', :include => [:course, :course_section], :conditions => "enrollments.workflow_state = 'active' and ((courses.workflow_state = 'claimed' and (enrollments.type IN ('TeacherEnrollment', 'TaEnrollment', 'DesignerEnrollment', 'StudentViewEnrollment'))) or (enrollments.workflow_state = 'active' and courses.workflow_state = 'available'))", :order => 'enrollments.created_at'
has_many :invited_enrollments, :class_name => 'Enrollment', :include => [:course, :course_section], :conditions => "enrollments.workflow_state = 'invited' and ((courses.workflow_state = 'available' and (enrollments.type = 'StudentEnrollment' or enrollments.type = 'ObserverEnrollment')) or (courses.workflow_state != 'deleted' and (enrollments.type = 'TeacherEnrollment' or enrollments.type = 'TaEnrollment' or enrollments.type = 'DesignerEnrollment')))", :order => 'enrollments.created_at'
has_many :current_enrollments, :class_name => 'Enrollment', :include => [:course, :course_section], :conditions => ENROLLMENT_CONDITIONS[:active], :order => 'enrollments.created_at'
has_many :invited_enrollments, :class_name => 'Enrollment', :include => [:course, :course_section], :conditions => ENROLLMENT_CONDITIONS[:invited], :order => 'enrollments.created_at'
has_many :current_and_invited_enrollments, :class_name => 'Enrollment', :include => [:course], :order => 'enrollments.created_at',
:conditions => "( enrollments.workflow_state = 'active' and ((courses.workflow_state = 'claimed' and (enrollments.type IN ('TeacherEnrollment', 'TaEnrollment', 'DesignerEnrollment', 'StudentViewEnrollment'))) or (enrollments.workflow_state = 'active' and courses.workflow_state = 'available')) )
OR
( enrollments.workflow_state = 'invited' and ((courses.workflow_state = 'available' and (enrollments.type = 'StudentEnrollment' or enrollments.type = 'ObserverEnrollment')) or (courses.workflow_state != 'deleted' and (enrollments.type IN ('TeacherEnrollment', 'TaEnrollment', 'DesignerEnrollment', 'StudentViewEnrollment')))) )"
:conditions => [ENROLLMENT_CONDITIONS[:active], ENROLLMENT_CONDITIONS[:invited]].join(' OR ')
has_many :not_ended_enrollments, :class_name => 'Enrollment', :conditions => ["enrollments.workflow_state NOT IN (?)", ['rejected', 'completed', 'deleted']]
has_many :concluded_enrollments, :class_name => 'Enrollment', :include => [:course, :course_section], :conditions => "enrollments.workflow_state = 'completed'", :order => 'enrollments.created_at'
has_many :concluded_enrollments, :class_name => 'Enrollment', :include => [:course, :course_section], :conditions => ENROLLMENT_CONDITIONS[:completed], :order => 'enrollments.created_at'
has_many :observer_enrollments
has_many :observee_enrollments, :foreign_key => :associated_user_id, :class_name => 'ObserverEnrollment'
has_many :user_observers, :dependent => :delete_all

View File

@ -1,3 +1,5 @@
<p>consumer key: <span id="consumer_key"><%= params[:oauth_consumer_key] %></span></p>
<% if params[:selection_directive] == 'embed_content' %>
<a id="image_link" class="link" href="<%= @return_url %>?embed_type=image&url=/images/delete.png&width=16&height=16&alt=delete">image embed</a>
<br/>

View File

@ -16,7 +16,7 @@
</script>
<% end %>
<form action="<%= @resource_url %>" method="POST" <%= raw("target='#{@target || 'tool_content'}'") unless @self_target %> id="tool_form" class="<%= 'new_tab' if @tag.try(:new_tab) %>">
<form action="<%= @resource_url %>" method="POST" <%= raw("target='#{@target || 'tool_content'}'") unless @self_target %> id="tool_form" class="<%= 'new_tab' if @tag.try(:new_tab) %>" data-tool-id="<%= @tool.tool_id || 'unknown' %>">
<% @tool_settings.each do |key, value| %>
<%= hidden_field_tag key, value %>
<% end %>

View File

@ -246,8 +246,9 @@
</td>
<% end %>
</tr>
<% if !assignment.special_class && (has_comments || has_scoring_details) %>
<tr class="comments <%= 'assignment_graded' if submission && submission.grade && !assignment.muted? %>" style="display: none;">
<%# always add row (even if empty) so javascript references work %>
<tr class="comments <%= 'assignment_graded' if submission && submission.grade && !assignment.muted? %>" style="display: none;">
<% if !assignment.special_class && (has_comments || has_scoring_details) %>
<td colspan="5" style="padding-bottom: 20px;">
<% if assignment && assignment.points_possible && assignment.points_possible > 0 && !assignment.muted? %>
<% high, low, mean = assignment.grade_distribution(@all_submissions.select { |s| s.assignment_id == assignment.id }) %>
@ -328,18 +329,18 @@
<% end %>
<% end %>
</td>
</tr>
<% unless visible_rubric_assessments.empty? %>
<tr class="rubric_assessments <%= 'assignment_graded' if submission && submission.grade %>" style="display: none;">
<td colspan="5">
<% visible_rubric_assessments.each do |assessment| %>
<div id="assessor" style="text-align: right; margin-bottom: 5px"><%= t(:assessment_by, "Assessment by %{name}", :name => assessment.assessor_name) %></span>
<%= render :partial => "shared/rubric", :object => assessment.rubric, :locals => { :assessment => assessment } %>
<% end %>
</td>
</tr>
<% end %>
<% end %>
</tr>
<tr class="rubric_assessments <%= 'assignment_graded' if submission && submission.grade %>" style="display: none;">
<% unless visible_rubric_assessments.empty? %>
<td colspan="5">
<% visible_rubric_assessments.each do |assessment| %>
<div class="assessor" style="text-align: right; margin-bottom: 5px"><%= t(:assessment_by, "Assessment by %{name}", :name => assessment.assessor_name) %></div>
<%= render :partial => "shared/rubric", :object => assessment.rubric, :locals => { :assessment => assessment } %>
<% end %>
</td>
<% end %>
</tr>
<% end %>
</table>
<% muted_assignment_count = @assignments.inject(0) { |i, a| if a.muted? then i + 1 else i end } %>

View File

@ -9,6 +9,12 @@ TEXT
%></p>
</td>
</tr>
<tr>
<td colspan="2">
<%= f.check_box "no_copying", {}, 'yes', 'no' %>
<%= f.label "no_copying", t('no_copying_frozen', "Don't allow frozen assignments to be copied.") %>
</td>
</tr>
<% Assignment::FREEZABLE_ATTRIBUTES.sort.each do |att| %>
<tr>
<td colspan=2><%= f.check_box att, {}, 'yes', 'no' %>

View File

@ -30,9 +30,11 @@
<a href="#" class="edit_group_link no-hover">
<%= image_tag 'edit.png', :alt => t('alts.edit_group_details', "Edit"), :title => t('links.edit_group_details', "Edit Group Details") %>
</a>
<a href="<%= context_url(@context, :controller => :assignment_groups, :action => :destroy, :id => (assignment_group ? assignment_group.id : "{{ id }}")) %>" class="delete_group_link no-hover">
<%= image_tag 'delete.png', :alt => t('alts.delete_assignment_group', "Delete"), :title => t('titles.delete_assignment_group', "Delete Assignment Group") %>
</a>
<% if !group || !group.has_frozen_assignments?(@current_user) %>
<a href="<%= context_url(@context, :controller => :assignment_groups, :action => :destroy, :id => (assignment_group ? assignment_group.id : "{{ id }}")) %>" class="delete_group_link no-hover">
<%= image_tag 'delete.png', :alt => t('alts.delete_assignment_group', "Delete"), :title => t('titles.delete_assignment_group', "Delete Assignment Group") %>
</a>
<% end %>
</div>
<div class="clear"></div>
<div class="more_info" style="min-height: 30px; display: none; padding-left: 30px; font-size: 0.8em;">

View File

@ -125,6 +125,7 @@ stylesheets:
- public/stylesheets/compiled/g_instructure.css
- public/stylesheets/compiled/ellipsis.css
- public/stylesheets/compiled/g_util_misc.css
- public/stylesheets/compiled/g_util_fancy_links.css
- public/stylesheets/compiled/g_bootstrap_parts.css
- public/stylesheets/compiled/g_util_buttons.css
- public/stylesheets/compiled/g_util_inst_tree.css

View File

@ -756,6 +756,7 @@ ActionController::Routing::Routes.draw do |map|
users.delete 'users/self/todo/:asset_string/:purpose', :action => :ignore_item, :path_name => 'users_todo_ignore'
users.post 'accounts/:account_id/users', :action => :create
users.get 'accounts/:account_id/users', :action => :index, :path_name => 'account_users'
users.delete 'accounts/:account_id/users/:id', :action => :destroy
users.put 'users/:id', :action => :update
users.post 'users/:user_id/files', :action => :create_file

View File

@ -0,0 +1,20 @@
class AddToolIdToExternalTools < ActiveRecord::Migration
tag :predeploy
def self.up
# using tool_id instead of developer_key.id lets us
# use the same keys as lti-examples.heroku.com for
# tying multiple context_external_tools to the
# same third-party tool
add_column :context_external_tools, :tool_id, :string
add_index :context_external_tools, [:tool_id]
add_column :developer_keys, :tool_id, :string
add_index :developer_keys, [:tool_id], :unique => true
end
def self.down
remove_column :context_external_tools, :tool_id
remove_index :context_external_tools, [:tool_id]
remove_column :developer_keys, :tool_id
remove_index :developer_keys, [:tool_id]
end
end

View File

@ -21,10 +21,17 @@ module CC
def add_assignments
@course.assignments.active.no_graded_quizzes_or_topics.each do |assignment|
next unless export_object?(assignment)
title = assignment.title rescue I18n.t('course_exports.unknown_titles.assignment', "Unknown assignment")
if !assignment.can_copy?(@user)
add_error(I18n.t('course_exports.errors.assignment_is_locked', "The assignment \"%{title}\" could not be copied because it is locked.", :title => title))
next
end
begin
add_assignment(assignment)
rescue
title = assignment.title rescue I18n.t('course_exports.unknown_titles.assignment', "Unknown assignment")
add_error(I18n.t('course_exports.errors.assignment', "The assignment \"%{title}\" failed to export", :title => title), $!)
end
end

View File

@ -61,6 +61,7 @@ module CC
elsif tool.url =~ %r{https://}
blti_node.blti :secure_launch_url, tool.url
end
blti_node.blti(:icon, tool.settings[:icon_url]) if tool.settings[:icon_url]
blti_node.blti :vendor do |v_node|
v_node.lticp :code, 'unknown'
v_node.lticp :name, 'unknown'
@ -75,6 +76,7 @@ module CC
end
blti_node.blti(:extensions, :platform => CC::CCHelper::CANVAS_PLATFORM) do |ext_node|
ext_node.lticm(:property, tool.tool_id, 'name' => 'tool_id') if tool.tool_id
ext_node.lticm :property, tool.workflow_state, 'name' => 'privacy_level'
ext_node.lticm(:property, tool.domain, 'name' => 'domain') unless tool.domain.blank?
if for_course_copy

View File

@ -79,12 +79,16 @@ module CC::Importer
tool[:domain] = ext[:custom_fields].delete 'domain'
tool[:consumer_key] = ext[:custom_fields].delete 'consumer_key'
tool[:shared_secret] = ext[:custom_fields].delete 'shared_secret'
tool[:tool_id] = ext[:custom_fields].delete 'tool_id'
tool[:settings] = ext[:custom_fields]
else
tool[:extensions] << ext
end
end
if icon = get_node_val(doc, "#{blti}|icon")
tool[:settings] ||= {}
tool[:settings][:icon_url] = icon
end
tool
end

View File

@ -20,7 +20,7 @@ module CC
include CCHelper
attr_accessor :exporter, :weblinks, :basic_ltis
delegate :add_error, :set_progress, :export_object?, :for_course_copy, :add_item_to_export, :to => :exporter
delegate :add_error, :set_progress, :export_object?, :for_course_copy, :add_item_to_export, :user, :to => :exporter
def initialize(exporter)
@exporter = exporter

View File

@ -24,6 +24,7 @@ module CC
def initialize(manifest, resources_node, html_exporter)
@manifest = manifest
@user = manifest.user
@resources_node = resources_node
@course = manifest.course
@export_dir = @manifest.export_dir
@ -55,10 +56,17 @@ module CC
@course.quizzes.active.each do |quiz|
next unless export_object?(quiz) || export_object?(quiz.assignment)
title = quiz.title rescue I18n.t('unknown_quiz', "Unknown quiz")
if quiz.assignment && !quiz.assignment.can_copy?(@user)
add_error(I18n.t('course_exports.errors.quiz_is_locked', "The quiz \"%{title}\" could not be copied because it is locked.", :title => title))
next
end
begin
generate_quiz(quiz)
rescue
title = quiz.title rescue I18n.t('unknown_quiz', "Unknown quiz")
add_error(I18n.t('course_exports.errors.quiz', "The quiz \"%{title}\" failed to export", :title => title), $!)
end
end

View File

@ -21,7 +21,7 @@ module QTI
include CC::CCHelper
attr_accessor :exporter
delegate :add_error, :set_progress, :export_object?, :qti_export?, :course, :to => :exporter
delegate :add_error, :set_progress, :export_object?, :qti_export?, :course, :user, :to => :exporter
delegate :referenced_files, :to => :@html_exporter
def initialize(exporter)

View File

@ -35,6 +35,7 @@ module CC
@manifest = manifest
@manifest_node = manifest_node
@course = @manifest.course
@user = @manifest.user
@export_dir = @manifest.export_dir
@resources = nil
@zip_file = manifest.zip_file

View File

@ -21,10 +21,16 @@ module CC
def add_topics
@course.discussion_topics.active.each do |topic|
next unless export_object?(topic) || export_object?(topic.assignment)
title = topic.title rescue I18n.t('course_exports.unknown_titles.topic', "Unknown topic")
if topic.assignment && !topic.assignment.can_copy?(@user)
add_error(I18n.t('course_exports.errors.topic_is_locked', "The topic \"%{title}\" could not be copied because it is locked.", :title => title))
next
end
begin
add_topic(topic)
rescue
title = topic.title rescue I18n.t('course_exports.unknown_titles.topic', "Unknown topic")
add_error(I18n.t('course_exports.errors.topic', "The discussion topic \"%{title}\" failed to export", :title => title), $!)
end
end

View File

@ -293,6 +293,6 @@ module GoogleDocs
end
def self.config
Canvas::Plugin.find(:google_docs).try(:settings) || (YAML.load_file(RAILS_ROOT + "/config/google_docs.yml")[RAILS_ENV] rescue nil)
Canvas::Plugin.find(:google_docs).try(:settings) || Setting.from_config('google_docs')
end
end

View File

@ -158,10 +158,7 @@ module LinkedIn
end
def self.config
# Return existing value, even if nil, as long as it's defined
return @config if defined?(@config)
@config ||= Canvas::Plugin.find(:linked_in).try(:settings)
@config ||= (YAML.load_file(RAILS_ROOT + "/config/linked_in.yml")[RAILS_ENV] rescue nil)
Canvas::Plugin.find(:linked_in).try(:settings) || Setting.from_config('linked_in')
end
end

View File

@ -151,10 +151,7 @@ module Twitter
end
def self.config
# Return existing value, even if nil, as long as it's defined
return @twitter_config if defined?(@twitter_config)
@twitter_config ||= Canvas::Plugin.find(:twitter).try(:settings)
@twitter_config ||= YAML.load_file(RAILS_ROOT + "/config/twitter.yml")[RAILS_ENV] rescue nil
Canvas::Plugin.find(:twitter).try(:settings) || Setting.from_config('twitter')
end
end

View File

@ -91,6 +91,7 @@ define([
// puts the little red box when something bad happens in ajax.
$(document).ready(function() {
$("#instructure_ajax_error_result").defaultAjaxError(function(event, request, settings, error, debugOnly) {
if (error === 'abort') return;
var status = "0";
var text = I18n.t('no_text', "No text");
var json_data = {};

View File

@ -257,14 +257,16 @@ $(document).ready(function() {
$select.find(".tools").empty();
for(var idx in data) {
var tool = data[idx];
var $tool = $tool_template.clone(true);
$tool.toggleClass('resource_selection', !!tool.resource_selection_settings);
$tool.fillTemplateData({
data: tool,
dataValues: ['id', 'url', 'domain', 'name']
});
$tool.data('tool', tool);
$select.find(".tools").append($tool.show());
if(tool.url || tool.domain || tool.resource_selection_settings) {
var $tool = $tool_template.clone(true);
$tool.toggleClass('resource_selection', !!tool.resource_selection_settings);
$tool.fillTemplateData({
data: tool,
dataValues: ['id', 'url', 'domain', 'name']
});
$tool.data('tool', tool);
$select.find(".tools").append($tool.show());
}
}
}, function(data) {
$select.find(".message").text(I18n.t('errors.loading_failed', "Loading Failed"));

View File

@ -134,6 +134,12 @@ define([
match = (window.location.pathname.match(pathRegex) || window.location.search.match(searchRegex));
if (!match) return false;
return match[1];
},
shouldHideStudentNames: function() {
// 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";
}
};
@ -171,10 +177,7 @@ define([
//by defaut the list is sorted alphbetically by student last name so we dont have to do any more work here,
// if the cookie to sort it by submitted_at is set we need to sort by submitted_at.
var hideStudentNames;
if (userSettings.get("eg_hide_student_names")) {
hideStudentNames = true;
}
var hideStudentNames = utils.shouldHideStudentNames();
if(hideStudentNames) {
jsonData.studentsWithSubmissions.sort(function(a,b){
return ((a && a.submission && a.submission.id) || Number.MAX_VALUE) -
@ -226,7 +229,7 @@ define([
function initDropdown(){
var hideStudentNames;
if (userSettings.get("eg_hide_student_names") || window.anonymousAssignment) {
if (utils.shouldHideStudentNames() || window.anonymousAssignment) {
hideStudentNames = true;
}
$("#hide_student_names").attr('checked', hideStudentNames);
@ -382,7 +385,7 @@ define([
submitForm: function(e){
userSettings.set('eg_sort_by', $('#eg_sort_by').val());
userSettings.set('eg_hide_student_names', $("#hide_student_names").prop('checked').toString());
userSettings.set('eg_hide_student_names', $("#hide_student_names").prop('checked'));
$(e.target).find(".submit_button").attr('disabled', true).text(I18n.t('buttons.saving_settings', "Saving Settings..."));
window.location.reload();
return false;

View File

@ -15,7 +15,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define(['jquery'], function($) {
define(['jquery', 'jquery.google-analytics'], function($) {
if(!$("#tool_form").hasClass('new_tab')) {
$("#content").addClass('padless');
@ -32,6 +32,10 @@ if(!$("#tool_form").hasClass('new_tab')) {
$(this).find(".load_tab,.tab_loaded").toggle();
});
}
var toolName = $("#tool_form").attr('data-tool-id') || "unknown";
$.trackEvent('tool_launch', toolName);
$("#tool_form:not(.new_tab)").submit().hide();
$(document).ready(function() {
if($("#tool_content").length) {

View File

@ -1,17 +1,15 @@
/** vim: et:ts=4:sw=4:sts=4
* @license RequireJS 1.0.1 Copyright (c) 2010-2011, The Dojo Foundation All Rights Reserved.
* @license RequireJS 1.0.8 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
* Available via the MIT or new BSD license.
* see: http://github.com/jrburke/requirejs for details
*/
/*jslint strict: false, plusplus: false, sub: true */
/*global window: false, navigator: false, document: false, importScripts: false,
jQuery: false, clearInterval: false, setInterval: false, self: false,
setTimeout: false, opera: false */
/*global window, navigator, document, importScripts, jQuery, setTimeout, opera */
var requirejs, require, define;
(function () {
(function (undefined) {
//Change this version number for each release.
var version = "1.0.1",
var version = "1.0.8",
commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,
cjsRequireRegExp = /require\(\s*["']([^'"\s]+)["']\s*\)/g,
currDirRegExp = /^\.\//,
@ -37,8 +35,13 @@ var requirejs, require, define;
interactiveScript = null,
checkLoadedDepth = 0,
useInteractive = false,
reservedDependencies = {
require: true,
module: true,
exports: true
},
req, cfg = {}, currentlyAddingScript, s, head, baseElement, scripts, script,
src, subPath, mainScript, dataMain, i, ctx, jQueryCheck, checkLoadedTimeoutId;
src, subPath, mainScript, dataMain, globalI, ctx, jQueryCheck, checkLoadedTimeoutId;
function isFunction(it) {
return ostring.call(it) === "[object Function]";
@ -268,6 +271,10 @@ var requirejs, require, define;
if (pkgConfig && name === pkgName + '/' + pkgConfig.main) {
name = pkgName;
}
} else if (name.indexOf("./") === 0) {
// No baseName, so this is ID is resolved relative
// to baseUrl, pull off the leading dot.
name = name.substring(2);
}
}
return name;
@ -319,7 +326,15 @@ var requirejs, require, define;
url = urlMap[normalizedName];
if (!url) {
//Calculate url for the module, if it has a name.
url = context.nameToUrl(normalizedName, null, parentModuleMap);
//Use name here since nameToUrl also calls normalize,
//and for relative names that are outside the baseUrl
//this causes havoc. Was thinking of just removing
//parentModuleMap to avoid extra normalization, but
//normalize() still does a dot removal because of
//issue #142, so just pass in name here and redo
//the normalization. Paths outside baseUrl are just
//messy to support.
url = context.nameToUrl(name, null, parentModuleMap);
//Store the URL mapping for later.
urlMap[normalizedName] = url;
@ -379,8 +394,8 @@ var requirejs, require, define;
* per module because of the implication of path mappings that may
* need to be relative to the module name.
*/
function makeRequire(relModuleMap, enableBuildCallback) {
var modRequire = makeContextModuleFunc(context.require, relModuleMap, enableBuildCallback);
function makeRequire(relModuleMap, enableBuildCallback, altRequire) {
var modRequire = makeContextModuleFunc(altRequire || context.require, relModuleMap, enableBuildCallback);
mixin(modRequire, {
nameToUrl: makeContextModuleFunc(context.nameToUrl, relModuleMap),
@ -407,25 +422,31 @@ var requirejs, require, define;
map = manager.map,
fullName = map.fullName,
args = manager.deps,
listeners = manager.listeners;
listeners = manager.listeners,
execCb = config.requireExecCb || req.execCb,
cjsModule;
//Call the callback to define the module, if necessary.
if (cb && isFunction(cb)) {
if (config.catchError.define) {
try {
ret = req.execCb(fullName, manager.callback, args, defined[fullName]);
ret = execCb(fullName, manager.callback, args, defined[fullName]);
} catch (e) {
err = e;
}
} else {
ret = req.execCb(fullName, manager.callback, args, defined[fullName]);
ret = execCb(fullName, manager.callback, args, defined[fullName]);
}
if (fullName) {
//If setting exports via "module" is in play,
//favor that over return value and exports. After that,
//favor a non-undefined return value over exports use.
if (manager.cjsModule && manager.cjsModule.exports !== undefined) {
cjsModule = manager.cjsModule;
if (cjsModule &&
cjsModule.exports !== undefined &&
//Make sure it is not already the exports value
cjsModule.exports !== defined[fullName]) {
ret = defined[fullName] = manager.cjsModule.exports;
} else if (ret === undefined && manager.usingExports) {
//exports already set the defined value.
@ -590,7 +611,22 @@ var requirejs, require, define;
//Use parentName here since the plugin's name is not reliable,
//could be some weird string with no path that actually wants to
//reference the parentName's path.
plugin.load(name, makeRequire(map.parentMap, true), load, config);
plugin.load(name, makeRequire(map.parentMap, true, function (deps, cb) {
var moduleDeps = [],
i, dep, depMap;
//Convert deps to full names and hold on to them
//for reference later, when figuring out if they
//are blocked by a circular dependency.
for (i = 0; (dep = deps[i]); i++) {
depMap = makeModuleMap(dep, map.parentMap);
deps[i] = depMap.fullName;
if (!depMap.prefix) {
moduleDeps.push(deps[i]);
}
}
depManager.moduleDeps = (depManager.moduleDeps || []).concat(moduleDeps);
return context.require(deps, cb);
}), load, config);
}
}
@ -619,7 +655,7 @@ var requirejs, require, define;
prefix = map.prefix,
plugin = prefix ? plugins[prefix] ||
(plugins[prefix] = defined[prefix]) : null,
manager, created, pluginManager;
manager, created, pluginManager, prefixMap;
if (fullName) {
manager = managerCallbacks[fullName];
@ -657,7 +693,18 @@ var requirejs, require, define;
//If there is a plugin needed, but it is not loaded,
//first load the plugin, then continue on.
if (prefix && !plugin) {
pluginManager = getManager(makeModuleMap(prefix), true);
prefixMap = makeModuleMap(prefix);
//Clear out defined and urlFetched if the plugin was previously
//loaded/defined, but not as full module (as in a build
//situation). However, only do this work if the plugin is in
//defined but does not have a module export value.
if (prefix in defined && !defined[prefix]) {
delete defined[prefix];
delete urlFetched[prefixMap.url];
}
pluginManager = getManager(prefixMap, true);
pluginManager.add(function (plugin) {
//Create a new manager for the normalized
//resource ID and have it call this manager when
@ -846,15 +893,62 @@ var requirejs, require, define;
}
};
function forceExec(manager, traced) {
if (manager.isDone) {
return undefined;
function findCycle(manager, traced) {
var fullName = manager.map.fullName,
depArray = manager.depArray,
fullyLoaded = true,
i, depName, depManager, result;
if (manager.isDone || !fullName || !loaded[fullName]) {
return result;
}
//Found the cycle.
if (traced[fullName]) {
return manager;
}
traced[fullName] = true;
//Trace through the dependencies.
if (depArray) {
for (i = 0; i < depArray.length; i++) {
//Some array members may be null, like if a trailing comma
//IE, so do the explicit [i] access and check if it has a value.
depName = depArray[i];
if (!loaded[depName] && !reservedDependencies[depName]) {
fullyLoaded = false;
break;
}
depManager = waiting[depName];
if (depManager && !depManager.isDone && loaded[depName]) {
result = findCycle(depManager, traced);
if (result) {
break;
}
}
}
if (!fullyLoaded) {
//Discard the cycle that was found, since it cannot
//be forced yet. Also clear this module from traced.
result = undefined;
delete traced[fullName];
}
}
return result;
}
function forceExec(manager, traced) {
var fullName = manager.map.fullName,
depArray = manager.depArray,
i, depName, depManager, prefix, prefixManager, value;
if (manager.isDone || !fullName || !loaded[fullName]) {
return undefined;
}
if (fullName) {
if (traced[fullName]) {
return defined[fullName];
@ -885,7 +979,7 @@ var requirejs, require, define;
}
}
return fullName ? defined[fullName] : undefined;
return defined[fullName];
}
/**
@ -898,8 +992,9 @@ var requirejs, require, define;
var waitInterval = config.waitSeconds * 1000,
//It is possible to disable the wait interval by using waitSeconds of 0.
expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(),
noLoads = "", hasLoadedProp = false, stillLoading = false, prop,
err, manager;
noLoads = "", hasLoadedProp = false, stillLoading = false,
cycleDeps = [],
i, prop, err, manager, cycleManager, moduleDeps;
//If there are items still in the paused queue processing wait.
//This is particularly important in the sync case where each paused
@ -929,7 +1024,20 @@ var requirejs, require, define;
noLoads += prop + " ";
} else {
stillLoading = true;
break;
if (prop.indexOf('!') === -1) {
//No reason to keep looking for unfinished
//loading. If the only stillLoading is a
//plugin resource though, keep going,
//because it may be that a plugin resource
//is waiting on a non-plugin cycle.
cycleDeps = [];
break;
} else {
moduleDeps = managerCallbacks[prop] && managerCallbacks[prop].moduleDeps;
if (moduleDeps) {
cycleDeps.push.apply(cycleDeps, moduleDeps);
}
}
}
}
}
@ -946,9 +1054,26 @@ var requirejs, require, define;
err = makeError("timeout", "Load timeout for modules: " + noLoads);
err.requireType = "timeout";
err.requireModules = noLoads;
err.contextName = context.contextName;
return req.onError(err);
}
if (stillLoading || context.scriptCount) {
//If still loading but a plugin is waiting on a regular module cycle
//break the cycle.
if (stillLoading && cycleDeps.length) {
for (i = 0; (manager = waiting[cycleDeps[i]]); i++) {
if ((cycleManager = findCycle(manager, {}))) {
forceExec(cycleManager, {});
break;
}
}
}
//If still waiting on loads, and the waiting load is something
//other than a plugin resource, or there are still outstanding
//scripts, then just try back later.
if (!expired && (stillLoading || context.scriptCount)) {
//Something is still waiting to load. Wait for it, but only
//if a timeout is not already in effect.
if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) {
@ -1006,6 +1131,9 @@ var requirejs, require, define;
resume = function () {
var manager, map, url, i, p, args, fullName;
//Any defined modules in the global queue, intake them now.
context.takeGlobalQueue();
resumeDepth += 1;
if (context.scriptCount <= 0) {
@ -1047,8 +1175,17 @@ var requirejs, require, define;
} else {
//Regular dependency.
if (!urlFetched[url] && !loaded[fullName]) {
req.load(context, fullName, url);
urlFetched[url] = true;
(config.requireLoad || req.load)(context, fullName, url);
//Mark the URL as fetched, but only if it is
//not an empty: URL, used by the optimizer.
//In that case we need to be sure to call
//load() for each module that is mapped to
//empty: so that dependencies are satisfied
//correctly.
if (url.indexOf('empty:') !== 0) {
urlFetched[url] = true;
}
}
}
}
@ -1160,8 +1297,7 @@ var requirejs, require, define;
context.requireWait = false;
//But first, call resume to register any defined modules that may
//be in a data-main built file before the priority config
//call. Also grab any waiting define calls for this context.
context.takeGlobalQueue();
//call.
resume();
context.require(cfg.priority);
@ -1238,10 +1374,6 @@ var requirejs, require, define;
//then resume the dependency processing.
if (!context.requireWait) {
while (!context.scriptCount && context.paused.length) {
//For built layers, there can be some defined
//modules waiting for intake into the context,
//in particular module plugins. Take them.
context.takeGlobalQueue();
resume();
}
}
@ -1303,11 +1435,6 @@ var requirejs, require, define;
} : null]);
}
//If a global jQuery is defined, check for it. Need to do it here
//instead of main() since stock jQuery does not register as
//a module via define.
jQueryCheck();
//Doing this scriptCount decrement branching because sync envs
//need to decrement after resume, otherwise it looks like
//loading is complete after the first dependency is fetched.
@ -1352,7 +1479,8 @@ var requirejs, require, define;
moduleName = normalize(moduleName, relModuleMap && relModuleMap.fullName);
//If a colon is in the URL, it indicates a protocol is used and it is just
//an URL to a file, or if it starts with a slash or ends with .js, it is just a plain file.
//an URL to a file, or if it starts with a slash, contains a query arg (i.e. ?)
//or ends with .js, then assume the user meant to use an url and not a module id.
//The slash is important for protocol-less URLs as well as full paths.
if (req.jsExtRegExp.test(moduleName)) {
//Just a plain path, not module name lookup, so just return it.
@ -1388,7 +1516,7 @@ var requirejs, require, define;
//Join the path parts together, then figure out if baseUrl is needed.
url = syms.join("/") + (ext || ".js");
url = (url.charAt(0) === '/' || url.match(/^\w+:/) ? "" : config.baseUrl) + url;
url = (url.charAt(0) === '/' || url.match(/^[\w\+\.\-]+:/) ? "" : config.baseUrl) + url;
}
return config.urlArgs ? url +
@ -1724,7 +1852,8 @@ var requirejs, require, define;
node = context && context.config && context.config.xhtml ?
document.createElementNS("http://www.w3.org/1999/xhtml", "html:script") :
document.createElement("script");
node.type = type || "text/javascript";
node.type = type || (context && context.config.scriptType) ||
"text/javascript";
node.charset = "utf-8";
//Use async so Gecko does not block on executing the script if something
//like a long-polling comet tag is being run first. Gecko likes
@ -1751,7 +1880,15 @@ var requirejs, require, define;
//https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution
//UNFORTUNATELY Opera implements attachEvent but does not follow the script
//script execution mode.
if (node.attachEvent && !isOpera) {
if (node.attachEvent &&
// check if node.attachEvent is artificially added by custom script or
// natively supported by browser
// read https://github.com/jrburke/requirejs/issues/187
// if we can NOT find [native code] then it must NOT natively supported.
// in IE8, node.attachEvent does not have toString()
// TODO: a better way to check interactive mode
!(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code]') < 0) &&
!isOpera) {
//Probably IE. IE (at least 6-8) do not fire
//script onload right after executing the script, so
//we cannot tie the anonymous define call to a name.
@ -1809,7 +1946,7 @@ var requirejs, require, define;
//Figure out baseUrl. Get it from the script tag with require.js in it.
scripts = document.getElementsByTagName("script");
for (i = scripts.length - 1; i > -1 && (script = scripts[i]); i--) {
for (globalI = scripts.length - 1; globalI > -1 && (script = scripts[globalI]); globalI--) {
//Set the "head" where we can append children by
//using the script's parent.
if (!head) {
@ -1917,18 +2054,10 @@ var requirejs, require, define;
setTimeout(function () {
ctx.requireWait = false;
//Any modules included with the require.js file will be in the
//global queue, assign them to this context.
ctx.takeGlobalQueue();
//Allow for jQuery to be loaded/already in the page, and if jQuery 1.4.3,
//make sure to hold onto it for readyWait triggering.
ctx.jQueryCheck();
if (!ctx.scriptCount) {
ctx.resume();
}
req.checkReadyState();
}, 0);
}
}());
}());

View File

@ -290,6 +290,22 @@ describe EnrollmentsApiController, :type => :integration do
}
end
it "should return enrollments for unpublished courses" do
course
@course.claim
enrollment = course.enroll_student(@student)
enrollment.update_attribute(:workflow_state, 'active')
# without a state[] filter
json = api_call(:get, @user_path, @user_params)
json.map { |e| e['id'] }.should include enrollment.id
# with a state[] filter
json = api_call(:get, "#{@user_path}?state[]=active",
@user_params.merge(:state => %w{active}))
json.map { |e| e['id'] }.should include enrollment.id
end
it "should not return enrollments from other accounts" do
# enroll the user in a course in another account
account = Account.create!(:name => 'Account Two')
@ -383,6 +399,24 @@ describe EnrollmentsApiController, :type => :integration do
}
end
it "should not show enrollments for courses that aren't published" do
# Setup test with an unpublished course and an active enrollment in
# that course.
course
@course.claim
enrollment = course.enroll_student(@user)
enrollment.update_attribute(:workflow_state, 'active')
# Request w/o a state[] filter.
json = api_call(:get, @user_path, @user_params)
json.map { |e| e['id'] }.should_not include enrollment.id
# Request w/ a state[] filter.
json = api_call(:get, "#{@user_path}?state[]=active&type[]=StudentEnrollment",
@user_params.merge(:state => %w{active}, :type => %w{StudentEnrollment}))
json.map { |e| e['id'] }.should_not include enrollment.id
end
it "should not include the users' sis and login ids" do
json = api_call(:get, @path, @params)
json.each do |res|

View File

@ -560,6 +560,64 @@ describe "Users API", :type => :integration do
end
end
describe "user deletion" do
before do
@admin = account_admin_user
course_with_student(:user => user_with_pseudonym(:name => 'Student', :username => 'student@example.com'))
@student = @user
@user = @admin
@path = "/api/v1/accounts/#{Account.default.id}/users/#{@student.id}"
@path_options = { :controller => 'users', :action => 'destroy',
:format => 'json', :id => @student.to_param,
:account_id => Account.default.to_param }
end
context "a user with permissions" do
it "should be able to delete a user" do
json = api_call(:delete, @path, @path_options)
@student.reload.should be_deleted
json.should == {
'id' => @student.id,
'name' => 'Student',
'short_name' => 'Student',
'sortable_name' => 'Student'
}
end
it "should be able to delete a user by SIS ID" do
@student.pseudonym.update_attribute(:sis_user_id, '12345')
id_param = "sis_user_id:#{@student.sis_user_id}"
path = "/api/v1/accounts/#{Account.default.id}/users/#{id_param}"
path_options = @path_options.merge(:id => id_param)
json = api_call(:delete, path, path_options)
response.code.should eql '200'
@student.reload.should be_deleted
end
it 'should be able to delete itself' do
path = "/api/v1/accounts/#{Account.default.to_param}/users/#{@user.id}"
json = api_call(:delete, path, @path_options.merge(:id => @user.to_param))
@user.reload.should be_deleted
json.should == {
'id' => @user.id,
'name' => @user.name,
'short_name' => @user.short_name,
'sortable_name' => @user.sortable_name
}
end
end
context 'an unauthorized user' do
it "should receive a 401" do
user
raw_api_call(:delete, @path, @path_options)
response.code.should eql '401'
end
end
end
context "user files" do
it_should_behave_like "file uploads api with folders"

View File

@ -106,6 +106,27 @@ describe ContextModulesController do
assigns[:tool].should == @tool2
end
it "should find the preferred tool even if the url is different, but only if the url was inserted as part of a resourse_selection directive" do
course_with_student_logged_in(:active_all => true)
@module = @course.context_modules.create!
@tool1 = @course.context_external_tools.create!(:name => "a", :url => "http://www.google.com", :consumer_key => '12345', :shared_secret => 'secret')
@tool1.settings[:resource_selection] = {:url => "http://www.google.com", :selection_width => 400, :selection_height => 400}
@tool1.save!
tag1 = @module.add_item :type => 'context_external_tool', :id => @tool1.id, :url => "http://www.yahoo.com"
tag1.content_id.should == @tool1.id
get "item_redirect", :course_id => @course.id, :id => tag1.id
response.should be_success
assigns[:tool].should == @tool1
@tool1.settings.delete :resource_selection
@tool1.save!
get "item_redirect", :course_id => @course.id, :id => tag1.id
response.should be_redirect
assigns[:tool].should == nil
end
it "should fail if there is no matching tool" do
course_with_student_logged_in(:active_all => true)

View File

@ -112,6 +112,33 @@ describe ExternalToolsController do
assigns[:tool].shared_secret.should == "secret"
end
it "should fail on basic xml with no url or domain set" do
rescue_action_in_public!
course_with_teacher_logged_in(:active_all => true)
xml = <<-XML
<?xml version="1.0" encoding="UTF-8"?>
<cartridge_basiclti_link xmlns="http://www.imsglobal.org/xsd/imslticc_v1p0"
xmlns:blti = "http://www.imsglobal.org/xsd/imsbasiclti_v1p0"
xmlns:lticm ="http://www.imsglobal.org/xsd/imslticm_v1p0"
xmlns:lticp ="http://www.imsglobal.org/xsd/imslticp_v1p0"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation = "http://www.imsglobal.org/xsd/imslticc_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticc_v1p0.xsd
http://www.imsglobal.org/xsd/imsbasiclti_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imsbasiclti_v1p0.xsd
http://www.imsglobal.org/xsd/imslticm_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticm_v1p0.xsd
http://www.imsglobal.org/xsd/imslticp_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticp_v1p0.xsd">
<blti:title>Other Name</blti:title>
<blti:description>Description</blti:description>
<blti: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>
XML
post 'create', :course_id => @course.id, :external_tool => {:name => "tool name", :consumer_key => "key", :shared_secret => "secret", :config_type => "by_xml", :config_xml => xml}
response.should_not be_success
end
it "should handle advanced xml configurations" do
course_with_teacher_logged_in(:active_all => true)
xml = <<-XML
@ -154,6 +181,48 @@ describe ExternalToolsController do
assigns[:tool].has_editor_button.should be_true
end
it "should handle advanced xml configurations with no url or domain set" do
course_with_teacher_logged_in(:active_all => true)
xml = <<-XML
<?xml version="1.0" encoding="UTF-8"?>
<cartridge_basiclti_link xmlns="http://www.imsglobal.org/xsd/imslticc_v1p0"
xmlns:blti = "http://www.imsglobal.org/xsd/imsbasiclti_v1p0"
xmlns:lticm ="http://www.imsglobal.org/xsd/imslticm_v1p0"
xmlns:lticp ="http://www.imsglobal.org/xsd/imslticp_v1p0"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation = "http://www.imsglobal.org/xsd/imslticc_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticc_v1p0.xsd
http://www.imsglobal.org/xsd/imsbasiclti_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imsbasiclti_v1p0.xsd
http://www.imsglobal.org/xsd/imslticm_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticm_v1p0.xsd
http://www.imsglobal.org/xsd/imslticp_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticp_v1p0.xsd">
<blti:title>Other Name</blti:title>
<blti:description>Description</blti:description>
<blti:extensions platform="canvas.instructure.com">
<lticm:property name="privacy_level">public</lticm:property>
<lticm:options name="editor_button">
<lticm:property name="url">http://example.com/editor</lticm:property>
<lticm:property name="icon_url">http://example.com/icon.png</lticm:property>
<lticm:property name="text">Editor Button</lticm:property>
<lticm:property name="selection_width">500</lticm:property>
<lticm:property name="selection_height">300</lticm:property>
</lticm:options>
</blti:extensions>
<cartridge_bundle identifierref="BLTI001_Bundle"/>
<cartridge_icon identifierref="BLTI001_Icon"/>
</cartridge_basiclti_link>
XML
post 'create', :course_id => @course.id, :external_tool => {:name => "tool name", :consumer_key => "key", :shared_secret => "secret", :config_type => "by_xml", :config_xml => xml}
response.should be_success
assigns[:tool].should_not be_nil
# User-entered name overrides name provided in xml
assigns[:tool].name.should == "tool name"
assigns[:tool].description.should == "Description"
assigns[:tool].url.should be_nil
assigns[:tool].domain.should be_nil
assigns[:tool].consumer_key.should == "key"
assigns[:tool].shared_secret.should == "secret"
assigns[:tool].has_editor_button.should be_true
end
it "should fail gracefully on invalid xml configurations" do
course_with_teacher_logged_in(:active_all => true)
xml = "bob"

View File

@ -78,12 +78,7 @@ describe PageViewsController do
page_view(@user, '/somewhere/in/app/2', :created_at => '2012-05-01 20:48:04') # 3rd day
get 'index', :user_id => @user.id, :format => 'csv'
response.should be_success
response.body.should == <<-EOS
url,context_type,asset_type,controller,action,contributed,interaction_seconds,created_at,updated_at,user_request,render_time,user_agent,participated,summarized
/somewhere/in/app/2,,,,,,5,2012-05-01 20:48:04 UTC,2012-05-01 20:48:04 UTC,,,Firefox/12.0,,
/somewhere/in/app,,,,,,5,2012-04-30 20:48:04 UTC,2012-04-30 20:48:04 UTC,,,Firefox/12.0,,
/somewhere/in/app/1,,,,,,5,2012-04-29 20:48:04 UTC,2012-04-29 20:48:04 UTC,,,Firefox/12.0,,
EOS
response.body.should match /2012-05-01 20:48:04 UTC.*\n.*2012-04-30 20:48:04 UTC.*\n.*2012-04-29 20:48:04 UTC/
end
end
end

View File

@ -102,6 +102,17 @@ describe PseudonymSessionsController do
response.should render_template('pseudonym_sessions/new')
end
it "should login a site admin user with other identical pseudonyms" do
account1 = Account.create!
Account.any_instance.stubs(:trusted_account_ids).returns([account1.id, Account.site_admin.id])
user_with_pseudonym(:username => 'jt@instructure.com', :active_all => 1, :password => 'qwerty', :account => account1)
user_with_pseudonym(:username => 'jt@instructure.com', :active_all => 1, :password => 'qwerty', :account => Account.site_admin)
post 'create', :pseudonym_session => { :unique_id => 'jt@instructure.com', :password => 'qwerty'}
response.should redirect_to(dashboard_url(:login_success => 1))
# it should have preferred the site admin pseudonym
assigns[:pseudonym].should == @pseudonym
end
context "sharding" do
it_should_behave_like "sharding"

32
spec/fixtures/selection_test_lti.xml vendored Normal file
View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<cartridge_basiclti_link xmlns="http://www.imsglobal.org/xsd/imslticc_v1p0"
xmlns:blti = "http://www.imsglobal.org/xsd/imsbasiclti_v1p0"
xmlns:lticm ="http://www.imsglobal.org/xsd/imslticm_v1p0"
xmlns:lticp ="http://www.imsglobal.org/xsd/imslticp_v1p0"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation = "http://www.imsglobal.org/xsd/imslticc_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticc_v1p0.xsd
http://www.imsglobal.org/xsd/imsbasiclti_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imsbasiclti_v1p0.xsd
http://www.imsglobal.org/xsd/imslticm_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticm_v1p0.xsd
http://www.imsglobal.org/xsd/imslticp_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticp_v1p0.xsd">
<blti:title>Test Selector</blti:title>
<blti:description>Testing resource selection</blti:description>
<blti:launch_url>{{ canvas-domain }}/selection_test</blti:launch_url>
<blti:extensions platform="canvas.instructure.com">
<lticm:property name="privacy_level">public</lticm:property>
<lticm:options name="resource_selection">
<lticm:property name="url">{{ canvas-domain }}/selection_test</lticm:property>
<lticm:property name="text">Resource Selection</lticm:property>
<lticm:property name="selection_width">500</lticm:property>
<lticm:property name="selection_height">300</lticm:property>
</lticm:options>
<lticm:options name="editor_button">
<lticm:property name="url">{{ canvas-domain }}/selection_test</lticm:property>
<lticm:property name="icon_url">{{ canvas-domain }}/images/add.png</lticm:property>
<lticm:property name="text">Resource Selection</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>

View File

@ -101,5 +101,6 @@ describe "External Tools" do
doc.at_css('#tool_form').should_not be_nil
doc.at_css("input[name='launch_presentation_return_url']")['value'].should match(/^http/)
end
end
end

View File

@ -146,12 +146,14 @@ describe "Canvas Cartridge importing" do
tool1.privacy_level = 'name_only'
tool1.consumer_key = 'haha'
tool1.shared_secret = "don't share me"
tool1.tool_id = "test_tool"
tool1.settings[:custom_fields] = {"key1" => "value1", "key2" => "value2"}
tool1.settings[:user_navigation] = {:url => "http://www.example.com", :text => "hello", :labels => {'en' => 'hello', 'es' => 'hola'}, :extra => 'extra'}
tool1.settings[:course_navigation] = {:url => "http://www.example.com", :text => "hello", :labels => {'en' => 'hello', 'es' => 'hola'}, :default => 'disabled', :visibility => 'members', :extra => 'extra'}
tool1.settings[:account_navigation] = {:url => "http://www.example.com", :text => "hello", :labels => {'en' => 'hello', 'es' => 'hola'}, :extra => 'extra'}
tool1.settings[:resource_selection] = {:url => "http://www.example.com", :text => "hello", :labels => {'en' => 'hello', 'es' => 'hola'}, :selection_width => 100, :selection_height => 50, :extra => 'extra'}
tool1.settings[:editor_button] = {:url => "http://www.example.com", :text => "hello", :labels => {'en' => 'hello', 'es' => 'hola'}, :selection_width => 100, :selection_height => 50, :icon_url => "http://www.example.com", :extra => 'extra'}
tool1.settings[:icon_url] = "http://www.example.com/favicon.ico"
tool1.save!
tool2 = @copy_from.context_external_tools.new
tool2.domain = 'example.com'
@ -190,6 +192,8 @@ describe "Canvas Cartridge importing" do
t1.domain.should == nil
t1.consumer_key.should == 'fake'
t1.shared_secret.should == 'fake'
t1.tool_id.should == 'test_tool'
t1.settings[:icon_url].should == 'http://www.example.com/favicon.ico'
[:user_navigation, :course_navigation, :account_navigation].each do |type|
t1.settings[type][:url].should == "http://www.example.com"
t1.settings[type][:text].should == "hello"
@ -228,6 +232,8 @@ describe "Canvas Cartridge importing" do
t2.workflow_state.should == tool2.workflow_state
t2.consumer_key.should == 'fake'
t2.shared_secret.should == 'fake'
t2.tool_id.should be_nil
t2.settings[:icon_url].should be_nil
t2.settings[:user_navigation].should be_nil
t2.settings[:course_navigation].should be_nil
t2.settings[:account_navigation].should be_nil

View File

@ -126,7 +126,7 @@ describe "Standard Common Cartridge importing" do
et.name.should == "BLTI Test"
et.url.should == 'http://www.imsglobal.org/developers/BLTI/tool.php'
et.settings[:custom_fields].should == {"key1"=>"value1", "key2"=>"value2"}
et.settings[:vendor_extensions].should == [{:platform=>"my.lms.com", :custom_fields=>{"key"=>"value"}}, {:platform=>"your.lms.com", :custom_fields=>{"key"=>"value", "key2"=>"value2"}}]
et.settings[:vendor_extensions].should == [{:platform=>"my.lms.com", :custom_fields=>{"key"=>"value"}}, {:platform=>"your.lms.com", :custom_fields=>{"key"=>"value", "key2"=>"value2"}}].map(&:with_indifferent_access)
@migration.warnings.member?("The security parameters for the external tool \"#{et.name}\" need to be set in Course Settings.").should be_true
end

View File

@ -941,6 +941,83 @@ equation: <img class="equation_image" title="Log_216" src="/equation_images/Log_
aq.question_data[:answers][1][:left_html].should == data2[:answers][1][:left_html]
end
context "copying frozen assignments" do
append_before (:each) do
@setting = PluginSetting.create!(:name => "assignment_freezer", :settings => {"no_copying" => "yes"})
@asmnt = @copy_from.assignments.create!(:title => 'lock locky')
@asmnt.copied = true
@asmnt.freeze_on_copy = true
@asmnt.save!
@quiz = @copy_from.quizzes.create(:title => "quiz", :quiz_type => "assignment")
@quiz.workflow_state = 'available'
@quiz.save!
@quiz.assignment.copied = true
@quiz.assignment.freeze_on_copy = true
@quiz.save!
@topic = @copy_from.discussion_topics.build(:title => "topic")
assignment = @copy_from.assignments.build(:submission_types => 'discussion_topic', :title => @topic.title)
assignment.infer_due_at
assignment.saved_by = :discussion_topic
assignment.copied = true
assignment.freeze_on_copy = true
@topic.assignment = assignment
@topic.save
@admin = account_admin_user(opts={})
end
it "should copy for admin" do
@cm.user = @admin
@cm.save!
run_course_copy
@copy_to.assignments.count.should == (Qti.qti_enabled? ? 3 : 2)
@copy_to.quizzes.count.should == 1 if Qti.qti_enabled?
@copy_to.discussion_topics.count.should == 1
@cm.content_export.error_messages.should == []
end
it "should copy for teacher if flag not set" do
@setting.settings = {}
@setting.save!
run_course_copy
@copy_to.assignments.count.should == (Qti.qti_enabled? ? 3 : 2)
@copy_to.quizzes.count.should == 1 if Qti.qti_enabled?
@copy_to.discussion_topics.count.should == 1
@cm.content_export.error_messages.should == []
end
it "should not copy for teacher" do
run_course_copy
@copy_to.assignments.count.should == 0
@copy_to.quizzes.count.should == 0
@copy_to.discussion_topics.count.should == 0
@cm.content_export.error_messages.should == [
["The assignment \"lock locky\" could not be copied because it is locked.", nil],
["The topic \"topic\" could not be copied because it is locked.", nil],
["The quiz \"quiz\" could not be copied because it is locked.", nil]]
end
it "should not mark assignment as copied if not set to be frozen" do
@asmnt.freeze_on_copy = false
@asmnt.copied = false
@asmnt.save!
run_course_copy
asmnt_2 = @copy_to.assignments.find_by_migration_id(mig_id(@asmnt))
asmnt_2.freeze_on_copy.should be_nil
asmnt_2.copied.should be_nil
end
end
context "notifications" do
before(:each) do
Notification.create!(:name => 'Migration Export Ready', :category => 'Migration')

View File

@ -29,6 +29,40 @@ describe ContextExternalTool do
@account.parent_account.should eql(@root_account)
@account.root_account.should eql(@root_account)
end
describe "url or domain validation" do
it "should validate with a domain setting" do
@tool = @course.context_external_tools.create(:name => "a", :domain => "google.com", :consumer_key => '12345', :shared_secret => 'secret')
@tool.should_not be_new_record
@tool.errors.should be_empty
end
it "should validate with a url setting" do
@tool = @course.context_external_tools.create(:name => "a", :url => "http://google.com", :consumer_key => '12345', :shared_secret => 'secret')
@tool.should_not be_new_record
@tool.errors.should be_empty
end
it "should validate with a canvas lti extension url setting" do
@tool = @course.context_external_tools.new(:name => "a", :consumer_key => '12345', :shared_secret => 'secret')
@tool.settings[:editor_button] = {
"icon_url"=>"http://www.example.com/favicon.ico",
"text"=>"Example",
"url"=>"http://www.example.com",
"selection_height"=>400,
"selection_width"=>600
}
@tool.save
@tool.should_not be_new_record
@tool.errors.should be_empty
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
@tool.errors['url'].should == "Either the url or domain should be set."
@tool.errors['domain'].should == "Either the url or domain should be set."
end
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')
@ -140,10 +174,26 @@ describe ContextExternalTool do
@found_tool.should eql(@tool2)
end
it "should not find the preferred tool if there is a higher priority tool configured" do
it "should find the preferred tool even if there is a higher priority tool configured, provided the preferred tool has resource_selection set" do
@tool = @course.context_external_tools.create!(:name => "a", :url => "http://www.google.com", :consumer_key => '12345', :shared_secret => 'secret')
@course.context_external_tools.create!(:name => "b", :domain => "google.com", :consumer_key => '12345', :shared_secret => 'secret')
@preferred = @account.context_external_tools.create!(:name => "c", :url => "http://www.google.com", :consumer_key => '12345', :shared_secret => 'secret')
@account.context_external_tools.create!(:name => "c", :url => "http://www.google.com", :consumer_key => '12345', :shared_secret => 'secret')
@account.context_external_tools.create!(:name => "d", :domain => "google.com", :consumer_key => '12345', :shared_secret => 'secret')
@root_account.context_external_tools.create!(:name => "e", :url => "http://www.google.com", :consumer_key => '12345', :shared_secret => 'secret')
@preferred = @root_account.context_external_tools.create!(:name => "f", :domain => "google.com", :consumer_key => '12345', :shared_secret => 'secret')
@found_tool = ContextExternalTool.find_external_tool("http://www.google.com", Course.find(@course.id), @preferred.id)
@found_tool.should eql(@tool)
@preferred.settings[:resource_selection] = {:url => "http://www.example.com", :selection_width => 400, :selection_height => 400}
@preferred.save!
@found_tool = ContextExternalTool.find_external_tool("http://www.google.com", Course.find(@course.id), @preferred.id)
@found_tool.should eql(@preferred)
end
it "should not find the preferred tool if it is deleted" do
@preferred = @course.context_external_tools.create!(:name => "a", :url => "http://www.google.com", :consumer_key => '12345', :shared_secret => 'secret')
@preferred.destroy
@course.context_external_tools.create!(:name => "b", :domain => "google.com", :consumer_key => '12345', :shared_secret => 'secret')
@tool = @account.context_external_tools.create!(:name => "c", :url => "http://www.google.com", :consumer_key => '12345', :shared_secret => 'secret')
@account.context_external_tools.create!(:name => "d", :domain => "google.com", :consumer_key => '12345', :shared_secret => 'secret')
@root_account.context_external_tools.create!(:name => "e", :url => "http://www.google.com", :consumer_key => '12345', :shared_secret => 'secret')
@root_account.context_external_tools.create!(:name => "f", :domain => "google.com", :consumer_key => '12345', :shared_secret => 'secret')
@ -285,6 +335,21 @@ describe ContextExternalTool do
tool.has_editor_button.should be_true
end
it "should allow setting tool_id and icon_url" do
tool = new_external_tool
tool.tool_id = "new_tool"
tool.settings[:icon_url] = "http://www.example.com/favicon.ico"
tool.save
tool.tool_id.should == "new_tool"
tool.settings[:icon_url].should == "http://www.example.com/favicon.ico"
end
it "should use editor button's icon_url if none is set on the tool" do
tool = new_external_tool
tool.settings = {:editor_button => {:url => "http://www.example.com", :icon_url => "http://www.example.com/favicon.ico", :selection_width => 100, :selection_height => 100}}
tool.save
tool.settings[:icon_url].should == "http://www.example.com/favicon.ico"
end
end
describe "standardize_url" do

View File

@ -40,7 +40,7 @@ describe PageView do
PageView.count.should == 0
PageView.process_cache_queue
PageView.count.should == 1
PageView.first.attributes.except('created_at', 'updated_at').should == @page_view.attributes.except('created_at', 'updated_at')
PageView.first.attributes.except('created_at', 'updated_at', 'summarized').should == @page_view.attributes.except('created_at', 'updated_at', 'summarized')
end
it "should store into redis in transactional batches" do

View File

@ -765,5 +765,42 @@ describe Quiz do
end
it "should ignore lockdown-browser setting if that plugin is not enabled" do
q = @course.quizzes.build(:title => "some quiz")
q1 = @course.quizzes.build(:title => "some quiz", :require_lockdown_browser => true, :require_lockdown_browser_for_results => false)
q2 = @course.quizzes.build(:title => "some quiz", :require_lockdown_browser => true, :require_lockdown_browser_for_results => true)
# first, disable any lockdown browsers that might be configured already
Canvas::Plugin.all_for_tag(:lockdown_browser).each { |p| p.settings[:enabled] = false }
# nothing should be restricted
Quiz.lockdown_browser_plugin_enabled?.should be_false
[q, q1, q2].product([:require_lockdown_browser, :require_lockdown_browser?, :require_lockdown_browser_for_results, :require_lockdown_browser_for_results?]).
each { |qs| qs[0].send(qs[1]).should be_false }
# register a plugin
Canvas::Plugin.register(:example_spec_lockdown_browser, :lockdown_browser, {
:settings => {:enabled => false}})
# nothing should change yet
Quiz.lockdown_browser_plugin_enabled?.should be_false
[q, q1, q2].product([:require_lockdown_browser, :require_lockdown_browser?, :require_lockdown_browser_for_results, :require_lockdown_browser_for_results?]).
each { |qs| qs[0].send(qs[1]).should be_false }
# now actually enable the plugin
setting = PluginSetting.find_or_create_by_name('example_spec_lockdown_browser')
setting.settings = {:enabled => true}
setting.save!
# now the restrictions should take effect
Quiz.lockdown_browser_plugin_enabled?.should be_true
[:require_lockdown_browser, :require_lockdown_browser?, :require_lockdown_browser_for_results, :require_lockdown_browser_for_results?].
each { |s| q.send(s).should be_false }
[:require_lockdown_browser, :require_lockdown_browser?].
each { |s| q1.send(s).should be_true }
[:require_lockdown_browser_for_results, :require_lockdown_browser_for_results?].
each { |s| q1.send(s).should be_false }
[:require_lockdown_browser, :require_lockdown_browser?, :require_lockdown_browser_for_results, :require_lockdown_browser_for_results?].
each { |s| q2.send(s).should be_true }
end
end

View File

@ -259,6 +259,13 @@ describe "assignments" do
f('#edit_assignment_form #assignment_description').should_not be_nil
end
end
it "should not allow assignment group to be deleted" do
get "/courses/#{@course.id}/assignments"
f("#group_#{@asmnt.assignment_group_id} .delete_group_link").should be_nil
f("#assignment_#{@asmnt.id} .delete_assignment_link").should be_nil
end
end
it "should show a \"more errors\" errorBox if any invalid fields are hidden" do

View File

@ -73,6 +73,31 @@ describe "editing external tools" do
@tag.url.should == "http://www.example.com"
end
it "should not list external tools that don't have a url, domain, or resource_selection configured" do
@module = @course.context_modules.create!(:name => "module")
@tool1 = @course.context_external_tools.create!(:name => "First Tool", :url => "http://www.example.com", :consumer_key => "key", :shared_secret => "secret")
@tool2 = @course.context_external_tools.new(:name => "Another Tool", :consumer_key => "key", :shared_secret => "secret")
@tool2.settings[:editor_button] = {:url => "http://www.example.com", :icon_url => "http://www.example.com", :selection_width => 100, :selection_height => 100}.with_indifferent_access
@tool2.save!
@tool3 = @course.context_external_tools.new(:name => "Third Tool", :consumer_key => "key", :shared_secret => "secret")
@tool3.settings[:resource_selection] = {:url => "http://www.example.com", :icon_url => "http://www.example.com", :selection_width => 100, :selection_height => 100}.with_indifferent_access
@tool3.save!
get "/courses/#{@course.id}/modules"
keep_trying_until { driver.execute_script("return window.modules.refreshed == true") }
driver.find_element(:css, "#context_module_#{@module.id} .add_module_item_link").click
driver.find_element(:css, "#add_module_item_select option[value='context_external_tool']").click
keep_trying_until { driver.find_elements(:css, "#context_external_tools_select .tool .name").length > 0 }
names = driver.find_elements(:css, "#context_external_tools_select .tool .name").map(&:text)
names.should be_include(@tool1.name)
names.should_not be_include(@tool2.name)
names.should be_include(@tool3.name)
end
it "should allow adding an existing external tool to a course module, and should pick the correct tool" do
@module = @course.context_modules.create!(:name => "module")
@tool1 = @course.context_external_tools.create!(:name => "a", :url => "http://www.google.com", :consumer_key => '12345', :shared_secret => 'secret')

View File

@ -15,7 +15,7 @@ describe "grades" do
@group = @course.assignment_groups.create!(:name => 'first assignment group')
@first_assignment = assignment_model({
:course => @course,
:name => 'first assignment',
:title => 'first assignment',
:due_at => due_date,
:points_possible => 10,
:submission_types => 'online_text_entry',
@ -54,7 +54,7 @@ describe "grades" do
due_date = due_date + 1.days
@second_assignment = assignment_model({
:course => @course,
:name => 'second assignment',
:title => 'second assignment',
:due_at => due_date,
:points_possible => 5,
:submission_types => 'online_text_entry',
@ -68,7 +68,7 @@ describe "grades" do
#third assignment data
due_date = due_date + 1.days
@third_assignment = assignment_model({ :name => 'third assignment', :due_at => due_date, :course => @course })
@third_assignment = assignment_model({ :title => 'third assignment', :due_at => due_date, :course => @course })
end
context "as a teacher" do
@ -117,14 +117,13 @@ describe "grades" do
it "should display rubric on assignment" do
#click rubric
driver.find_element(:css, '.toggle_rubric_assessments_link').click
f("#submission_#{@first_assignment.id} .toggle_rubric_assessments_link").click
wait_for_animations
driver.find_element(:css, '#assessor .rubric_title').should include_text(@rubric.title)
driver.find_element(:css, '#assessor .rubric_total').should include_text('2')
fj('.rubric_assessments:visible .rubric_title').should include_text(@rubric.title)
fj('.rubric_assessments:visible .rubric_total').should include_text('2')
#check rubric comment
driver.find_element(:css, '.assessment-comments div').text.should == 'cool, yo'
fj('.assessment-comments:visible div').text.should == 'cool, yo'
end
it "should not display rubric on muted assignment" do
@ -138,7 +137,7 @@ describe "grades" do
it "should not display letter grade score on muted assignment" do
@another_assignment = assignment_model({
:course => @course,
:name => 'another assignment',
:title => 'another assignment',
:points_possible => 100,
:submission_types => 'online_text_entry',
:assignment_group => @group,
@ -166,6 +165,34 @@ describe "grades" do
#statistics_text.include?('High: 4').should be_true
#statistics_text.include?('Low: 3').should be_true
end
it "should show rubric even if there are no comments" do
@third_association = @rubric.associate_with(@third_assignment, @course, :purpose => 'grading')
@third_submission = @third_assignment.submissions.create!(:user => @student_1) # unsubmitted submission :/
@third_association.assess({
:user => @student_1,
:assessor => @teacher,
:artifact => @third_submission,
:assessment => {
:assessment_type => 'grading',
:criterion_crit1 => {
:points => 2,
:comments => "not bad, not bad"
}
}
})
get "/courses/#{@course.id}/grades"
#click rubric
f("#submission_#{@third_assignment.id} .toggle_rubric_assessments_link").click
fj('.rubric_assessments:visible .rubric_title').should include_text(@rubric.title)
fj('.rubric_assessments:visible .rubric_total').should include_text('2')
#check rubric comment
fj('.assessment-comments:visible div').text.should == 'not bad, not bad'
end
end
context "as an observer" do

View File

@ -184,8 +184,8 @@ describe "jobs ui" do
true
end
wait_for_ajax_requests
f("#jobs-grid .odd").should be_nil
f("#jobs-grid .even").should be_nil
fj("#jobs-grid .odd").should be_nil # using fj to bypass selenium cache
fj("#jobs-grid .even").should be_nil #using fj to bypass selenium cache
Delayed::Job.count.should eql 0
end
end

View File

@ -416,6 +416,14 @@ describe "speedgrader" do
driver.find_element(:css, '#settings_form .submit_button').click
}
keep_trying_until { driver.find_element(:css, '#combo_box_container .ui-selectmenu .ui-selectmenu-item-header').text == "Student 1" }
# unselect the hide option
driver.find_element(:css, "#settings_link").click
driver.find_element(:css, '#hide_student_names').click
expect_new_page_load {
driver.find_element(:css, '#settings_form .submit_button').click
}
keep_trying_until { driver.find_element(:css, '#combo_box_container .ui-selectmenu .ui-selectmenu-item-header').text.should == "student@example.com" }
end
it "should leave the full rubric open when switching submissions" do