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

Conflicts:
	app/controllers/collections_controller.rb
	app/views/layouts/application.html.erb
	config/build.js
	lib/tasks/parallel_exclude.rb

Change-Id: Ic9664c29d1469c13b514343915c5929dfb15c6ad
This commit is contained in:
Ryan Shaw 2012-06-26 10:14:46 -06:00
commit 77138c4d4a
135 changed files with 2135 additions and 920 deletions

2
.gitignore vendored
View File

@ -15,6 +15,7 @@ db/*.sqlite
db/demo
config/GEM_HOME
config/*.yml
config/build.js
config/environments/*-local.rb
models.dot
db/*sql
@ -63,6 +64,7 @@ public/optimized
!node_modules
chromedriver.log
public/plugins/
public/javascripts/plugins/
app/coffeescripts/plugins/
app/views/jst/plugins/
app/stylesheets/plugins/

View File

@ -48,7 +48,7 @@ gem 'require_relative', '1.0.1'
gem 'ritex', '1.0.1'
gem 'rscribd', '1.2.0'
gem 'ruby-net-ldap', '0.0.4', :require => 'net/ldap'
gem 'ruby-saml-mod', '0.1.14'
gem 'ruby-saml-mod', '0.1.15'
gem 'rubycas-client', '2.2.1'
gem 'rubyzip', '0.9.4', :require => 'zip/zip'
gem 'sanitize', '2.0.3'

View File

@ -93,10 +93,6 @@ define [
item.render($contextsList)
@contextSelectorItems[item.context.asset_string] = item
for contextCode in @apptGroup.context_codes when @contextSelectorItems[contextCode]
@contextSelectorItems[contextCode].setState('on')
@contextSelectorItems[contextCode].lock()
if @apptGroup.sub_context_codes.length > 0
if @apptGroup.sub_context_codes[0].match /^group_category_/
for c, item of @contextSelectorItems
@ -113,6 +109,10 @@ define [
item = @contextSelectorItems[context]
item.sectionChange()
item.lock()
else
for contextCode in @apptGroup.context_codes when @contextSelectorItems[contextCode]
@contextSelectorItems[contextCode].setState('on')
@contextSelectorItems[contextCode].lock()
$('.ag_contexts_done').click preventDefault closeCB

View File

@ -6,9 +6,12 @@ define [
class TimeBlockRow
constructor: (@TimeBlockList, data={}) ->
@locked = data.locked
timeoutId = null
@$row = $(timeBlockRowTemplate(data)).bind
focusin: @focus
focusout: => @$row.removeClass('focused')
focusin: =>
clearTimeout timeoutId
@focus()
focusout: => timeoutId = setTimeout((=> @$row.removeClass('focused')), 50)
@inputs = {}
unless @locked
@$row.find('.date_field').date_field()

View File

@ -11,6 +11,7 @@ define [
# we don't currently save the model directly, rather we do inbox actions
inboxAction: (options) ->
defaults =
url: @url()
method: 'POST'
success: (data) => @list.updateItem(data)
options = $.extend(true, {}, defaults, options)

View File

@ -62,8 +62,13 @@ define [
updated: (conversation, $node) ->
@emptyCheck()
if @isActive(conversation) and conversation.messages?[0]
@app.addMessages(conversation.messages, 'prepend', 'slide')
if @isActive(conversation.id) and conversation.get('workflow_state') is 'unread'
@markAsUnread = setTimeout =>
conversation.inboxAction
method: 'PUT'
data: {conversation: {workflow_state: 'read'}}
success: (data) -> data.defer_visibility_check = true
, 2000
removed: (data, $node) ->
@emptyCheck()
@ -99,11 +104,11 @@ define [
@active and @active.id is id
deactivate: ->
return unless @active and @item(@active.id)
@$item(@active.id)?.removeClass('selected')
if @scope is 'unread' # TODO: do an ajax request to set unread state, then remove when we deselect, depending on visible-ness
@removeItem(@active)
return unless @active and item = @item(@active.id)
delete @active
@$item(item.id)?.removeClass('selected')
@removeItem(item) unless item.get('visible')
clearTimeout @markAsUnread
ensureSelected: (id, activate=true) ->
if activate # deselect any existing selection(s) ... soon we will have bulk conversation actions, so this will make more sense

View File

@ -32,7 +32,8 @@ define [
data[pre + "[grade]"] = curves[idx]
cnt++
if cnt == 0
@$dialog.errorBox I18n.t("errors.none_to_update", "None to Update")
errorBox = @$dialog.errorBox I18n.t("errors.none_to_update", "None to Update")
setTimeout((-> errorBox.fadeOut(-> errorBox.remove())), 3500)
return false
data
success: (data) =>

View File

@ -17,6 +17,8 @@ define [
'jquery.disableWhileLoading'
], (I18n, helpDialogTemplate, $, _, INST, htmlEscape, preventDefault) ->
showEmail = not ENV.current_user_id
helpDialog =
defaultLinks: [
{
@ -60,7 +62,7 @@ define [
role is 'user' or
(ENV.current_user_roles and role in ENV.current_user_roles)
locals =
showEmail: not ENV.current_user_id
showEmail: showEmail
helpLinks: links
showBadBrowserMessage: INST.browser.ie
browserVersion: INST.browser.version
@ -75,7 +77,10 @@ define [
initTicketForm: ->
$form = @$dialog.find('#create_ticket').formSubmit
disableWhileLoading: true
required: ['error[subject]', 'error[comments]', 'error[user_perceived_severity]']
required: ->
requiredFields = ['error[subject]', 'error[comments]', 'error[user_perceived_severity]']
requiredFields.push 'error[email]' if showEmail
requiredFields
success: =>
@$dialog.dialog('close')
$form.find(':input').val('')

View File

@ -53,7 +53,7 @@ define [
items = @modelize(items)
doTransitions = (items.length <= 1)
for item in items
if not item.get('visible')
if not item.get('visible') and not item.get('defer_visibility_check')
@_removeItem(item, doTransitions)
else if @itemMap[item.id]?
@_updateItem(item, doTransitions)

View File

@ -287,50 +287,10 @@ class AccountsController < ApplicationController
@terms = @account.enrollment_terms.active
respond_to do |format|
format.html
format.json { render :json => @current_batch.to_json(:include => :sis_batch_log_entries) }
format.json { render :json => @current_batch.try(:api_json) }
end
end
end
def sis_import_submit
raise "SIS imports can only be executed on root accounts" unless @account.root_account?
raise "SIS imports can only be executed on enabled accounts" unless @account.allow_sis_import
if authorized_action(@account, @current_user, :manage_sis)
SisBatch.transaction do
if !@account.current_sis_batch || !@account.current_sis_batch.importing?
batch = SisBatch.create_with_attachment(@account, params[:import_type], params[:attachment])
if params[:batch_mode].to_i > 0
batch.batch_mode = true
if params[:batch_mode_term_id].present?
batch.batch_mode_term = @account.enrollment_terms.active.find(params[:batch_mode_term_id])
end
end
batch.options ||= {}
if params[:override_sis_stickiness].to_i > 0
batch.options[:override_sis_stickiness] = true
[:add_sis_stickiness, :clear_sis_stickiness].each do |option|
batch.options[option] = true if params[option].to_i > 0
end
end
batch.save!
@account.current_sis_batch_id = batch.id
@account.save
batch.process
render :json => batch.to_json(:include => :sis_batch_log_entries),
:as_text => true
else
render :json => {:error=>true, :error_message=> t(:sis_import_in_process_notice, "An SIS import is already in process."), :batch_in_progress=>true}.to_json,
:as_text => true
end
end
end
end
def courses_redirect
redirect_to course_url(params[:id])

View File

@ -24,7 +24,7 @@ class AnnouncementsController < ApplicationController
add_crumb(t(:announcements_crumb, "Announcements"))
if authorized_action(@context, @current_user, :read)
return if @context.class.const_defined?('TAB_ANNOUNCEMENTS') && !tab_enabled?(@context.class::TAB_ANNOUNCEMENTS)
@announcements = @context.active_announcements.paginate(:page => params[:page]).reject{|a| a.locked_for?(@current_user, :check_policies => true) }
@announcements = @context.active_announcements.paginate(:page => params[:page], :order => 'posted_at DESC').reject{|a| a.locked_for?(@current_user, :check_policies => true) }
log_asset_access("announcements:#{@context.asset_string}", "announcements", "other")
respond_to do |format|
format.html { render }

View File

@ -922,16 +922,17 @@ class ApplicationController < ActionController::Base
end
def content_tag_redirect(context, tag, error_redirect_symbol)
url_params = { :module_item_id => tag.id }
if tag.content_type == 'Assignment'
redirect_to named_context_url(context, :context_assignment_url, tag.content_id)
redirect_to named_context_url(context, :context_assignment_url, tag.content_id, url_params)
elsif tag.content_type == 'WikiPage'
redirect_to named_context_url(context, :context_wiki_page_url, tag.content.url)
redirect_to named_context_url(context, :context_wiki_page_url, tag.content.url, url_params)
elsif tag.content_type == 'Attachment'
redirect_to named_context_url(context, :context_file_url, tag.content_id)
redirect_to named_context_url(context, :context_file_url, tag.content_id, url_params)
elsif tag.content_type == 'Quiz'
redirect_to named_context_url(context, :context_quiz_url, tag.content_id)
redirect_to named_context_url(context, :context_quiz_url, tag.content_id, url_params)
elsif tag.content_type == 'DiscussionTopic'
redirect_to named_context_url(context, :context_discussion_topic_url, tag.content_id)
redirect_to named_context_url(context, :context_discussion_topic_url, tag.content_id, url_params)
elsif tag.content_type == 'ExternalUrl'
@tag = tag
@module = tag.context_module

View File

@ -67,7 +67,7 @@ class AssignmentsController < ApplicationController
end
@locked = @assignment.locked_for?(@current_user, :check_policies => true, :deep_check_if_needed => true)
@unlocked = !@locked || @assignment.grants_rights?(@current_user, session, :update)[:update]
@assignment_module = @assignment.context_module_tag
@assignment_module = ContextModuleItem.find_tag_with_preferred([@assignment], params[:module_item_id])
@assignment.context_module_action(@current_user, :read) if @unlocked && !@assignment.new_record?
if @assignment.grants_right?(@current_user, session, :grade)
visible_student_ids = @context.enrollments_visible_to(@current_user).find(:all, :select => 'user_id').map(&:user_id)

View File

@ -64,11 +64,14 @@ class CollectionsController < ApplicationController
SETTABLE_ATTRIBUTES = %w(name visibility)
# @API List collections
# @API List user/group collections
#
# Returns the visible collections for the given user, returned most-recently-created first.
# If the given user is the current user, then all collections will be
# returned, otherwise only public collections will be returned.
# Returns the visible collections for the given group or user, returned
# most-recently-created first. If the given context is the current user or
# a group to which the current user belongs, then all collections will be
# returned, otherwise only public collections will be returned. In the former
# case, if no collections exist for the context, a default, private
# collection will be created and returned.
#
# @example_request
# curl -H 'Authorization: Bearer <token>' \
@ -80,10 +83,7 @@ class CollectionsController < ApplicationController
scope = @context.collections.active.newest_first
view_private = is_authorized_action?(@context.collections.new(:visibility => 'private'), @current_user, :read)
if view_private && scope.empty?
name = @context.try(:default_collection_name)
@context.collections.create(:name => name, :visibility => 'private') if name
end
ensure_default_collection_for(@context) if view_private
unless view_private
scope = scope.public
@ -93,6 +93,38 @@ class CollectionsController < ApplicationController
render :json => collections_json(@collections, @current_user, session)
end
# @API List pinnable collections
#
# Returns the list of collections to which the current user has permission to
# post. For each possible collection context (the current user and each
# community she belongs to) if no collections exist for the context,
# a default, private collection will be created and included in the returned
# list.
#
# @example_request
# curl -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/collections
#
def list
route = polymorphic_url([:api, :v1, :collections])
# make sure there is a default colleciton for the current user and all
# communities to which they belong
ensure_default_collection_for(@current_user)
current_communities = @current_user.current_groups.scoped(:joins => :group_category, :conditions => { :group_categories => { :role => 'communities' } }).all
if current_communities.present?
preload_groups_collections_counts(current_communities)
current_communities.each{ |g| ensure_default_collection_for(g) }
end
scope = Collection.active.newest_first.scoped(:conditions => [<<-SQL, @current_user.id, current_communities.map(&:id)])
(context_type='User' AND context_id=?) OR (context_type='Group' AND context_id IN (?))
SQL
@collections = Api.paginate(scope, self, route)
render :json => collections_json(@collections, @current_user, session)
end
# @API Get a single collection
#
# Returns information on an individual collection. If the collection is
@ -251,5 +283,26 @@ class CollectionsController < ApplicationController
return false
end
end
end
def ensure_default_collection_for(context)
precount = @collections_counts.try(:[], context.id) if context.is_a?(Group)
if (precount.present? && precount == 0) || (!precount.present? && context.collections.active.empty?)
name = context.try(:default_collection_name)
context.collections.create(:name => name, :visibility => 'private') if name
end
end
def preload_groups_collections_counts(groups)
counts_data = Collection.connection.execute(Collection.send(:sanitize_sql_array, [<<-SQL, groups.map(&:id)])).to_a
SELECT context_id AS group_id, COUNT(*) AS collections_count
FROM collections
WHERE context_id IN (?) AND context_type='Group' AND workflow_state='active'
GROUP BY context_id
SQL
@collections_counts = {}
counts_data.each do |cd|
@collections_counts[cd['group_id'].to_i] = cd['collections_count'].to_i
end
end
end

View File

@ -269,16 +269,25 @@ class ContextModulesController < ApplicationController
@modules = @context.context_modules.active
@tags = @context.context_module_tags.active.sort_by{|t| t.position ||= 999}
result = {}
result[:current_item] = @tags.detect{|t| t.content_type == type && t.content_id == id }
if !result[:current_item]
obj = @context.find_asset(params[:id], [:attachment, :discussion_topic, :assignment, :quiz, :wiki_page, :content_tag])
if obj.is_a?(ContentTag)
result[:current_item] = @tags.detect{|t| t.id == obj.id }
elsif obj.is_a?(DiscussionTopic) && obj.assignment_id
result[:current_item] = @tags.detect{|t| t.content_type == 'Assignment' && t.content_id == obj.assignment_id }
elsif obj.is_a?(Quiz) && obj.assignment_id
result[:current_item] = @tags.detect{|t| t.content_type == 'Assignment' && t.content_id == obj.assignment_id }
possible_tags = @tags.find_all {|t| t.content_type == type && t.content_id == id }
if possible_tags.size > 1
# if there's more than one tag for the item, but the caller didn't
# specify which one they want, we don't want to return any information.
# this way the module item prev/next links won't appear with misleading navigation info.
if params[:module_item_id]
result[:current_item] = possible_tags.detect { |t| t.id == params[:module_item_id].to_i }
end
else
result[:current_item] = possible_tags.first
if !result[:current_item]
obj = @context.find_asset(params[:id], [:attachment, :discussion_topic, :assignment, :quiz, :wiki_page, :content_tag])
if obj.is_a?(ContentTag)
result[:current_item] = @tags.detect{|t| t.id == obj.id }
elsif obj.is_a?(DiscussionTopic) && obj.assignment_id
result[:current_item] = @tags.detect{|t| t.content_type == 'Assignment' && t.content_id == obj.assignment_id }
elsif obj.is_a?(Quiz) && obj.assignment_id
result[:current_item] = @tags.detect{|t| t.content_type == 'Assignment' && t.content_id == obj.assignment_id }
end
end
end
result[:current_item].evaluate_for(@current_user) rescue nil

View File

@ -222,6 +222,10 @@ class ConversationsController < ApplicationController
# @argument filter [optional, course_id|group_id|user_id]
# Used when generating "visible" in the API response. See the explanation
# under the index API action
# @argument auto_mark_as_read Boolean, default true. If true, unread
# conversations will be automatically marked as read. This will default
# to false in a future API release, so clients should explicitly send
# true if that is the desired behavior.
#
# @response_field participants Array of relevant users. Includes current
# user. If there are forwarded messages in this conversation, the authors
@ -306,7 +310,7 @@ class ConversationsController < ApplicationController
return redirect_to conversations_path(:scope => scope, :id => @conversation.conversation_id, :message => params[:message])
end
@conversation.update_attribute(:workflow_state, "read") if @conversation.unread?
@conversation.update_attribute(:workflow_state, "read") if @conversation.unread? && auto_mark_as_read?
messages = @conversation.messages
ConversationMessage.send(:preload_associations, messages, :asset)
submissions = messages.map(&:submission).compact
@ -974,6 +978,7 @@ class ConversationsController < ApplicationController
}
end
# TODO API v2: default to true, like we do in the UI
def interleave_submissions
params[:interleave_submissions] || !api_request?
end
@ -986,4 +991,10 @@ class ConversationsController < ApplicationController
def blank_fallback
params[:blank_avatar_fallback] || @blank_fallback
end
# TODO API v2: default to false, like we do in the UI
def auto_mark_as_read?
params[:auto_mark_as_read] ||= api_request?
Canvas::Plugin.value_to_boolean(params[:auto_mark_as_read])
end
end

View File

@ -191,6 +191,8 @@ class DiscussionTopicsController < ApplicationController
else
format.html do
@context_module_tag = ContextModuleItem.find_tag_with_preferred([@topic, @topic.root_topic, @topic.assignment], params[:module_item_id])
@sequence_asset = @context_module_tag.try(:content)
env_hash = {
:TOPIC => {
:ID => @topic.id,

View File

@ -54,6 +54,8 @@ class EnrollmentsApiController < ApplicationController
# @response_field user_id The unique id of the user.
# @response_field html_url The URL to the Canvas web UI page for this course enrollment.
# @response_field grades[html_url] The URL to the Canvas web UI page for the user's grades, if this is a student enrollment.
# @response_field grades[current_grade] The user's current grade in the class. Only included if user has permissions to view this grade.
# @response_field grades[final_grade] The user's final grade for the class. Only included if user has permissions to view this grade.
# @response_field user[id] The unique id of the user.
# @response_field user[login_id] The unique login of the user.
# @response_field user[name] The name of the user.
@ -239,9 +241,11 @@ class EnrollmentsApiController < ApplicationController
# Returns an ActiveRecord scope of enrollments on success, false on failure.
def course_index_enrollments(scope_arguments)
if authorized_action(@context, @current_user, :read_roster)
scope_arguments[:conditions].include?(:workflow_state) ?
@context.enrollments.scoped(scope_arguments) :
@context.current_enrollments.scoped(scope_arguments)
scope = @context.enrollments_visible_to(@current_user, :type => :all, :include_priors => true).scoped(scope_arguments)
unless scope_arguments[:conditions].include?(:workflow_state)
scope = scope.scoped(:conditions => ['enrollments.workflow_state NOT IN (?)', ['rejected', 'completed', 'deleted', 'inactive']])
end
scope
else
false
end

View File

@ -124,8 +124,6 @@ class FilesController < ApplicationController
if authorized_action(@attachment,@current_user,:read)
if @attachment.grants_right?(@current_user, nil, :download)
@headers = false
@tag = @attachment.context_module_tag
@module = @attachment.context_module_tag.context_module rescue nil
render
else
show

View File

@ -175,7 +175,7 @@ class GradebooksController < ApplicationController
cancel_cache_buster
Enrollment.recompute_final_score_if_stale @context
send_data(
@context.gradebook_to_csv(:include_sis_id => @context.grants_rights?(@current_user, session, :read_sis, :manage_sis).values.any?),
@context.gradebook_to_csv(:include_sis_id => @context.grants_rights?(@current_user, session, :read_sis, :manage_sis).values.any?, :user => @current_user),
:type => "text/csv",
:filename => t('grades_filename', "Grades").gsub(/ /, "_") + "-" + @context.name.to_s.gsub(/ /, "_") + ".csv",
:disposition => "attachment"

View File

@ -20,8 +20,7 @@
#
# Group memberships are the objects that tie users and groups together.
#
# A Group Membership object looks like:
# !!!javascript
# @object Group Membership
# {
# // The id of the membership object
# id: 92
@ -62,6 +61,8 @@ class GroupMembershipsController < ApplicationController
# curl https://<canvas>/api/v1/groups/<group_id>/memberships \
# -F 'filter_states[]=invited&filter_states[]=requested' \
# -H 'Authorization: Bearer <token>'
#
# @returns [Group Membership]
def index
if authorized_action(@group, @current_user, :read_roster)
memberships_route = polymorphic_url([:api_v1, @group, :memberships])
@ -91,14 +92,7 @@ class GroupMembershipsController < ApplicationController
# -F 'user_id=self'
# -H 'Authorization: Bearer <token>'
#
# @example_response
# {
# id: 102,
# group_id: 6,
# user_id: 3,
# workflow_state: "requested",
# moderator: false
# }
# @returns Group Membership
def create
@user = api_find(User, params[:user_id])
if authorized_action(GroupMembership.new(:group => @group, :user => @user), @current_user, :create)
@ -123,14 +117,7 @@ class GroupMembershipsController < ApplicationController
# -F 'moderator=true'
# -H 'Authorization: Bearer <token>'
#
# @example_response
# {
# id: 102,
# group_id: 6,
# user_id: 3,
# workflow_state: "accepted",
# moderator: true
# }
# @returns Group Membership
def update
find_membership
if authorized_action(@membership, @current_user, :update)

View File

@ -30,9 +30,7 @@
# context for many other types of functionality and interaction, such as
# collections, discussions, wikis, and shared files.
#
# A Group object looks like:
#
# !!!javascript
# @object Group
# {
# // The ID of the group.
# id: 17,
@ -170,17 +168,7 @@ class GroupsController < ApplicationController
# curl https://<canvas>/api/v1/groups/<group_id> \
# -H 'Authorization: Bearer <token>'
#
# @example_response
# {
# id: 13,
# name: "Mary's Group",
# description: "A group for my friends",
# is_public: false,
# join_level: "parent_context_request",
# members_count: 3,
# avatar_url: "https://<canvas>/files/avatar_image.png",
# group_category_id: 2,
# }
# @returns Group
def show
find_group
@ -262,17 +250,7 @@ class GroupsController < ApplicationController
# -F 'join_level=parent_context_auto_join' \
# -H 'Authorization: Bearer <token>'
#
# @example_response
# {
# id: 25,
# name: "Math Teachers",
# description: "A place to gather resources for our classes.",
# is_public: true,
# join_level: "parent_context_auto_join",
# members_count: 13,
# avatar_url: "https://<canvas>/files/avatar_image.png",
# group_category_id: 7
# }
# @returns Group
def create
# only allow community groups from the api right now
if api_request?
@ -338,17 +316,7 @@ class GroupsController < ApplicationController
# -F 'join_level=parent_context_request' \
# -H 'Authorization: Bearer <token>'
#
# @example_response
# {
# id: 25,
# name: "Algebra Teachers",
# description: "A place to gather resources for our classes.",
# is_public: true,
# join_level: "parent_context_request",
# members_count: 13,
# avatar_url: "https://<canvas>/files/avatar_image.png",
# group_category_id: 7
# }
# @returns Group
def update
find_group
if !api_request? && params[:group] && params[:group][:group_category_id]
@ -386,17 +354,7 @@ class GroupsController < ApplicationController
# -X DELETE \
# -H 'Authorization: Bearer <token>'
#
# @example_response
# {
# id: 144,
# name: "My Group",
# description: null,
# is_public: false,
# join_level: "invitation_only",
# members_count: 0,
# avatar_url: "https://<canvas>/files/avatar_image.png",
# group_category_id: 9
# }
# @returns Group
def destroy
find_group
if authorized_action(@group, @current_user, :delete)

View File

@ -41,13 +41,7 @@ class PseudonymSessionsController < ApplicationController
@is_cas = @domain_root_account.cas_authentication? && @is_delegated
@is_saml = @domain_root_account.saml_authentication? && @is_delegated
if @is_cas && !params[:no_auto]
if session[:exit_frame]
session.delete(:exit_frame)
render :template => 'shared/exit_frame', :layout => false, :locals => {
:url => login_url(params)
}
return
elsif params[:ticket]
if params[:ticket]
# handle the callback from CAS
logger.info "Attempting CAS login with ticket #{params[:ticket]} in account #{@domain_root_account.id}"
st = CASClient::ServiceTicket.new(params[:ticket], login_url)

View File

@ -134,6 +134,8 @@ class QuizzesController < ApplicationController
@locked_reason = @quiz.locked_for?(@current_user, :check_policies => true, :deep_check_if_needed => true)
@locked = @locked_reason && !@quiz.grants_right?(@current_user, session, :update)
@context_module_tag = ContextModuleItem.find_tag_with_preferred([@quiz, @quiz.assignment], params[:module_item_id])
@sequence_asset = @context_module_tag.try(:content)
@quiz.context_module_action(@current_user, :read) if !@locked
@assignment = @quiz.assignment

View File

@ -91,9 +91,15 @@ class SisImportsApiController < ApplicationController
#
# @argument clear_sis_stickiness ["1"] This option, if present, will clear "stickiness" from all fields touched by this import. Requires that 'override_sis_stickiness' is also provided. If 'add_sis_stickiness' is also provided, 'clear_sis_stickiness' will overrule the behavior of 'add_sis_stickiness'
def create
if authorized_action(@account, @current_user, :manage)
if authorized_action(@account, @current_user, :manage_sis)
params[:import_type] ||= 'instructure_csv'
raise "invalid import type parameter" unless SisBatch.valid_import_types.has_key?(params[:import_type])
if !api_request? && @account.current_sis_batch.try(:importing?)
return render :json => {:error=>true, :error_message=> t(:sis_import_in_process_notice, "An SIS import is already in process."), :batch_in_progress=>true}.to_json,
:as_text => true
end
file_obj = nil
if params.has_key?(:attachment)
file_obj = params[:attachment]
@ -156,6 +162,12 @@ class SisImportsApiController < ApplicationController
unless Setting.get('skip_sis_jobs_account_ids', '').split(',').include?(@account.global_id.to_s)
batch.process
end
unless api_request?
@account.current_sis_batch_id = batch.id
@account.save
end
render :json => batch.api_json
end
end
@ -164,7 +176,7 @@ class SisImportsApiController < ApplicationController
#
# Get the status of an already created SIS import.
def show
if authorized_action(@account, @current_user, :manage)
if authorized_action(@account, @current_user, :manage_sis)
@batch = SisBatch.find(params[:id])
raise "Sis Import not found" unless @batch
raise "Batch does not match account" unless @batch.account.id == @account.id

View File

@ -310,7 +310,7 @@ class SubmissionsApiController < ApplicationController
def visible_user_ids
scope = if @section
@context.enrollments_visible_to(@current_user, false, false, [@section.id])
@context.enrollments_visible_to(@current_user, :section_ids => [@section.id])
else
@context.enrollments_visible_to(@current_user)
end

View File

@ -17,6 +17,74 @@
#
# @API Submissions
#
# @object Submission
# {
# // The submissions's assignment id
# assignment_id: 23,
#
# // The submission's assignment (see the assignments API) (optional)
# assignment: Assignment
#
# // The submission's course (see the course API) (optional)
# course: Course
#
# // If multiple submissions have been made, this is the attempt number.
# attempt: 1,
#
# // The content of the submission, if it was submitted directly in a
# // text field.
# body: "There are three factors too...",
#
# // The grade for the submission, translated into the assignment grading
# // scheme (so a letter grade, for example).
# grade: "A-",
#
# // A boolean flag which is false if the student has re-submitted since
# // the submission was last graded.
# grade_matches_current_submission: true,
#
# // URL to the submission. This will require the user to log in.
# html_url: "http://example.com/courses/255/assignments/543/submissions/134",
#
# // URL to the submission preview. This will require the user to log in.
# preview_url: "http://example.com/courses/255/assignments/543/submissions/134?preview=1",
#
# // The raw score
# score: 13.5
#
# // Associated comments for a submission (optional)
# submission_comments: [
# {
# author_id: 134
# author_name: "Toph Beifong",
# comment: "Well here's the thing...",
# created_at: "2012-01-01T01:00:00Z",
# media_comment: {
# content-type: "audio/mp4",
# display_name: "something",
# media_id: "3232",
# media_type: "audio",
# url: "http://example.com/media_url"
# }
# }
# ],
#
# // The types of submission
# // ex: ("online_text_entry"|"online_url"|"online_upload"|"media_recording")
# submission_type: "online_text_entry",
#
# // The timestamp when the assignment was submitted, if an actual
# // submission has been made.
# submitted_at: "2012-01-01T01:00:00Z",
#
# // The URL of the submission if the submission is a "online_url" submission.
# url: null,
#
# // The id of the user who created the submission
# user_id: 134
# }
#
class SubmissionsController < ApplicationController
include GoogleDocs
before_filter :get_course_from_section, :only => :create

View File

@ -325,17 +325,7 @@ class UsersController < ApplicationController
#
# Submission:
#
# !!!javascript
# {
# 'type': 'Submission',
# 'grade': '12',
# 'score': 12,
# 'assignment': {
# 'title': 'Assignment 3',
# 'id': 5678,
# 'points_possible': 15
# }
# }
# Returns an API {api:Submissions:Submission Submission} with its Course and Assignment data.
#
# Conference:
#
@ -1114,7 +1104,7 @@ class UsersController < ApplicationController
enrollments = student.student_enrollments.active.all(:include => :course)
enrollments.each do |enrollment|
should_include = enrollment.course.user_has_been_teacher?(@teacher) &&
enrollment.course.enrollments_visible_to(@teacher, true).find_by_id(enrollment.id) &&
enrollment.course.enrollments_visible_to(@teacher, :include_priors => true).find_by_id(enrollment.id) &&
enrollment.course.grants_right?(@current_user, :read_reports)
if should_include
Enrollment.recompute_final_score_if_stale(enrollment.course, student) { enrollment.reload }
@ -1134,7 +1124,7 @@ class UsersController < ApplicationController
redirect_to_referrer_or_default(root_url)
elsif authorized_action(course, @current_user, :read_reports)
Enrollment.recompute_final_score_if_stale(course)
@courses[course] = teacher_activity_report(@teacher, course, course.enrollments_visible_to(@teacher, true))
@courses[course] = teacher_activity_report(@teacher, course, course.enrollments_visible_to(@teacher, :include_priors => true))
end
end

View File

@ -336,10 +336,11 @@ module ApplicationHelper
# Returns a <script> tag for each registered js_bundle
def include_js_bundles
paths = js_bundles.map do |(bundle,plugin)|
paths = js_bundles.inject([]) do |ary, (bundle, plugin)|
base_url = js_base_url
base_url = "/plugins/#{plugin}#{base_url}" if plugin
"#{base_url}/compiled/bundles/#{bundle}.js"
base_url += "/plugins/#{plugin}" if plugin
ary.concat(Canvas::RequireJs.extensions_for(bundle, 'plugins/')) unless use_optimized_js?
ary << "#{base_url}/compiled/bundles/#{bundle}.js"
end
javascript_include_tag *paths
end

View File

@ -24,6 +24,7 @@ class Assignment < ActiveRecord::Base
include HasContentTags
include CopyAuthorizedLinks
include Mutable
include ContextModuleItem
attr_accessible :title, :name, :description, :due_at, :points_possible,
:min_score, :max_score, :mastery_score, :grading_type, :submission_types,
@ -40,7 +41,6 @@ class Assignment < ActiveRecord::Base
has_one :quiz
belongs_to :assignment_group
has_one :discussion_topic, :conditions => ['discussion_topics.root_topic_id IS NULL'], :order => 'created_at'
has_one :context_module_tag, :as => :content, :class_name => 'ContentTag', :conditions => ['content_tags.tag_type = ? AND workflow_state != ?', 'context_module', 'deleted'], :include => {:context_module => [:context_module_progressions, :content_tags]}
has_many :learning_outcome_tags, :as => :content, :class_name => 'ContentTag', :conditions => ['content_tags.tag_type = ? AND content_tags.workflow_state != ?', 'learning_outcome', 'deleted'], :include => :learning_outcome
has_one :rubric_association, :as => :association, :conditions => ['rubric_associations.purpose = ?', "grading"], :order => :created_at, :include => :rubric
has_one :rubric, :through => :rubric_association
@ -115,7 +115,8 @@ class Assignment < ActiveRecord::Base
:process_if_quiz,
:default_values,
:update_submissions_if_details_changed,
:maintain_group_category_attribute
:maintain_group_category_attribute,
:process_if_topic
after_save :update_grades_if_details_changed,
:generate_reminders_if_changed,
@ -335,12 +336,13 @@ class Assignment < ActiveRecord::Base
end
def context_module_action(user, action, points=nil)
self.context_module_tag.context_module_action(user, action, points) if self.context_module_tag
if self.submission_types == 'discussion_topic' && self.discussion_topic && self.discussion_topic.context_module_tag
self.discussion_topic.context_module_tag.context_module_action(user, action, points)
elsif self.submission_types == 'online_quiz' && self.quiz && self.quiz.context_module_tag
self.quiz.context_module_tag.context_module_action(user, action, points)
tags_to_update = self.context_module_tags.to_a
if self.submission_types == 'discussion_topic' && self.discussion_topic
tags_to_update += self.discussion_topic.context_module_tags
elsif self.submission_types == 'online_quiz' && self.quiz
tags_to_update += self.quiz.context_module_tags
end
tags_to_update.each { |tag| tag.context_module_action(user, action, points) }
end
set_broadcast_policy do |p|
@ -494,6 +496,15 @@ class Assignment < ActiveRecord::Base
end
protected :process_if_quiz
def process_if_topic
if self.submission_types == "discussion_topic"
#8569: discussion topics don't have lock-after date, so clear this on conversion
self.lock_at = nil
end
self
end
protected :process_if_topic
def grading_scheme
if self.grading_standard
self.grading_standard.grading_scheme
@ -680,17 +691,16 @@ class Assignment < ActiveRecord::Base
end
def locked_for?(user=nil, opts={})
@locks ||= {}
locked = false
return false if opts[:check_policies] && self.grants_right?(user, nil, :update)
@locks[user ? user.id : 0] ||= Rails.cache.fetch(locked_cache_key(user), :expires_in => 1.minute) do
Rails.cache.fetch(locked_cache_key(user), :expires_in => 1.minute) do
locked = false
if (self.unlock_at && self.unlock_at > Time.now)
locked = {:asset_string => self.asset_string, :unlock_at => self.unlock_at}
elsif (self.lock_at && self.lock_at <= Time.now)
locked = {:asset_string => self.asset_string, :lock_at => self.lock_at}
elsif (self.could_be_locked && self.context_module_tag && self.context_module_tag.locked_for?(user, opts[:deep_check_if_needed]))
locked = {:asset_string => self.asset_string, :context_module => self.context_module_tag.context_module.attributes}
elsif self.could_be_locked && item = locked_by_module_item?(user, opts[:deep_check_if_needed])
locked = {:asset_string => self.asset_string, :context_module => item.context_module.attributes}
end
locked
end
@ -1237,10 +1247,6 @@ class Assignment < ActiveRecord::Base
named_scope :no_graded_quizzes_or_topics, :conditions=>"submission_types NOT IN ('online_quiz', 'discussion_topic')"
named_scope :with_context_module_tags, lambda {
{:include => :context_module_tag }
}
named_scope :with_submissions, lambda {
{:include => :submissions }
}

View File

@ -20,7 +20,8 @@
class Attachment < ActiveRecord::Base
attr_accessible :context, :folder, :filename, :display_name, :user, :locked, :position, :lock_at, :unlock_at, :uploaded_data
include HasContentTags
include ContextModuleItem
belongs_to :context, :polymorphic => true
belongs_to :cloned_item
belongs_to :folder
@ -29,7 +30,6 @@ class Attachment < ActiveRecord::Base
has_one :media_object
has_many :submissions
has_many :attachment_associations
has_one :context_module_tag, :as => :content, :class_name => 'ContentTag', :conditions => ['content_tags.tag_type = ? AND workflow_state != ?', 'context_module', 'deleted'], :include => {:context_module => :context_module_progressions}
belongs_to :root_attachment, :class_name => 'Attachment'
belongs_to :scribd_mime_type
belongs_to :scribd_account
@ -112,7 +112,8 @@ class Attachment < ActiveRecord::Base
send_later_enqueue_args(:submit_to_scribd!, { :n_strand => 'scribd', :max_attempts => 1 })
end
send_later(:infer_encoding) if self.encoding.nil? && self.content_type =~ /text/
# try an infer encoding if it would be useful to do so
send_later(:infer_encoding) if self.encoding.nil? && self.content_type =~ /text/ && self.context_type != 'SisBatch'
if respond_to?(:process_attachment_with_processing) && thumbnailable? && !attachment_options[:thumbnails].blank? && parent_id.nil?
temp_file = temp_path || create_temp_file
self.class.attachment_options[:thumbnails].each { |suffix, size| send_later_if_production(:create_thumbnail_size, suffix) }
@ -990,17 +991,16 @@ class Attachment < ActiveRecord::Base
end
def locked_for?(user, opts={})
@locks ||= {}
return false if opts[:check_policies] && self.grants_right?(user, nil, :update)
return {:manually_locked => true} if self.locked || (self.folder && self.folder.locked?)
@locks[user ? user.id : 0] ||= Rails.cache.fetch(locked_cache_key(user), :expires_in => 1.minute) do
Rails.cache.fetch(locked_cache_key(user), :expires_in => 1.minute) do
locked = false
if (self.unlock_at && Time.now < self.unlock_at)
locked = {:asset_string => self.asset_string, :unlock_at => self.unlock_at}
elsif (self.lock_at && Time.now > self.lock_at)
locked = {:asset_string => self.asset_string, :lock_at => self.lock_at}
elsif (self.could_be_locked && self.context_module_tag && !self.context_module_tag.available_for?(user, opts[:deep_check_if_needed]))
locked = {:asset_string => self.asset_string, :context_module => self.context_module_tag.context_module.attributes}
elsif self.could_be_locked && item = locked_by_module_item?(user, opts[:deep_check_if_needed])
locked = {:asset_string => self.asset_string, :context_module => item.context_module.attributes}
end
locked
end
@ -1033,7 +1033,7 @@ class Attachment < ActiveRecord::Base
end
def context_module_action(user, action)
self.context_module_tag.context_module_action(user, action) if self.context_module_tag
self.context_module_tags.each { |tag| tag.context_module_action(user, action) }
end
include Workflow

View File

@ -33,7 +33,7 @@ class ContentTag < ActiveRecord::Base
validates_presence_of :context, :unless => proc { |tag| tag.context_id && tag.context_type }
validates_length_of :comments, :maximum => maximum_text_length, :allow_nil => true, :allow_blank => true
before_save :default_values
after_save :enforce_unique_in_modules
after_save :update_could_be_locked
after_save :touch_context_module
after_save :touch_context_if_learning_outcome
include CustomValidations
@ -86,23 +86,17 @@ class ContentTag < ActiveRecord::Base
def context_name
self.context.name rescue ""
end
def enforce_unique_in_modules
if self.workflow_state != 'deleted' && self.content_id && self.content_id > 0 && self.tag_type == 'context_module' && self.content_type != 'ContextExternalTool'
tags = ContentTag.find_all_by_content_id_and_content_type_and_tag_type_and_context_id_and_context_type(self.content_id, self.content_type, 'context_module', self.context_id, self.context_type)
tags.select{|t| t != self }.each do |tag|
tag.destroy
end
end
def update_could_be_locked
if self.content_id && self.content_type
klass = self.content_type.constantize
if klass.new.respond_to?(:could_be_locked=)
self.content_type.constantize.update_all({:could_be_locked => true}, {:id => self.content_id}) rescue nil
klass.update_all({:could_be_locked => true}, {:id => self.content_id})
end
end
true
end
def confirm_valid_module_requirements
self.context_module && self.context_module.confirm_valid_requirements
end

View File

@ -303,7 +303,6 @@ class ContextModule < ActiveRecord::Base
added_item
else
return nil unless item
added_item ||= ContentTag.find_by_content_id_and_content_type_and_context_id_and_context_type_and_tag_type(item.id, item.class.to_s, self.context_id, self.context_type, 'context_module')
title = params[:title] || (item.title rescue item.name)
added_item ||= self.content_tags.build(:context => context)
added_item.attributes = {

View File

@ -0,0 +1,61 @@
#
# Copyright (C) 2011 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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/>.
#
# This isn't a record on its own, but a module included in other records such
# as Attachment and Assignment.
#
# ContextModules contain items indirectly, through ContentTags that contain the
# information on position in the module, progression requirements, etc.
module ContextModuleItem
# set up the association for the AR class that included this module
def self.included(klass)
klass.has_many :context_module_tags, :as => :content, :class_name => 'ContentTag', :conditions => ['content_tags.tag_type = ? AND content_tags.workflow_state != ?', 'context_module', 'deleted'], :include => {:context_module => [:context_module_progressions, :content_tags]}
end
# Check if this item is locked for the given user.
# If we are locked, this will return the module item (ContentTag) that is
# locking the item for the given user
def locked_by_module_item?(user, deep_check)
if self.context_module_tags.present? && self.context_module_tags.all? { |tag| tag.locked_for?(user, deep_check) }
item = self.context_module_tags.first
end
item || false
end
# searches the ContextModuleItems in objs_to_search, in order, for the first
# context module tag -- returns the tag with id preferred_id if given and it
# exists
#
# If no preferred is found, but more than one tag exists for the same obj, we
# return nothing, since we can't know which tag is appropriate to return.
def self.find_tag_with_preferred(objs_to_search, preferred_id)
objs_to_search.each do |obj|
next unless obj.present?
tag = obj.context_module_tags.find_by_id(preferred_id)
return tag if tag
end
objs_to_search.each do |obj|
next unless obj.present?
tags = obj.context_module_tags.to_a
return nil if tags.size > 1
tag = tags.first
return tag if tag
end
return nil
end
end

View File

@ -71,13 +71,13 @@ class Course < ActiveRecord::Base
has_many :course_sections
has_many :active_course_sections, :class_name => 'CourseSection', :conditions => {:workflow_state => 'active'}
has_many :enrollments, :include => [:user, :course], :conditions => ['enrollments.workflow_state != ?', 'deleted'], :dependent => :destroy
has_many :current_enrollments, :class_name => 'Enrollment', :conditions => ['enrollments.workflow_state != ? AND enrollments.workflow_state != ? AND enrollments.workflow_state != ? AND enrollments.workflow_state != ?', 'rejected', 'completed', 'deleted', 'inactive'], :include => :user
has_many :current_enrollments, :class_name => 'Enrollment', :conditions => "enrollments.workflow_state NOT IN ('rejected', 'completed', 'deleted', 'inactive')", :include => :user
has_many :prior_enrollments, :class_name => 'Enrollment', :include => [:user, :course], :conditions => "enrollments.workflow_state = 'completed'"
has_many :students, :through => :student_enrollments, :source => :user
has_many :all_students, :through => :all_student_enrollments, :source => :user
has_many :participating_students, :through => :enrollments, :source => :user, :conditions => "enrollments.type IN ('StudentEnrollment', 'StudentViewEnrollment') and enrollments.workflow_state = 'active'"
has_many :student_enrollments, :class_name => 'Enrollment', :conditions => ["enrollments.workflow_state != ? AND enrollments.workflow_state != ? AND enrollments.workflow_state != ? AND enrollments.workflow_state != ? AND enrollments.type IN ('StudentEnrollment', 'StudentViewEnrollment')", 'deleted', 'completed', 'rejected', 'inactive'], :include => :user
has_many :all_student_enrollments, :class_name => 'Enrollment', :conditions => ["enrollments.workflow_state != ? AND enrollments.type IN ('StudentEnrollment', 'StudentViewEnrollment')", 'deleted'], :include => :user
has_many :student_enrollments, :class_name => 'Enrollment', :conditions => "enrollments.workflow_state NOT IN ('rejected', 'completed', 'deleted', 'inactive') AND enrollments.type IN ('StudentEnrollment', 'StudentViewEnrollment')", :include => :user
has_many :all_student_enrollments, :class_name => 'Enrollment', :conditions => "enrollments.workflow_state != 'deleted' AND enrollments.type IN ('StudentEnrollment', 'StudentViewEnrollment')", :include => :user
has_many :all_real_students, :through => :all_real_student_enrollments, :source => :user
has_many :all_real_student_enrollments, :class_name => 'StudentEnrollment', :conditions => ["enrollments.workflow_state != ?", 'deleted'], :include => :user
has_many :detailed_enrollments, :class_name => 'Enrollment', :conditions => ['enrollments.workflow_state != ?', 'deleted'], :include => {:user => {:pseudonym => :communication_channel}}
@ -120,7 +120,7 @@ class Course < ActiveRecord::Base
has_many :all_discussion_topics, :as => :context, :class_name => "DiscussionTopic", :include => :user, :dependent => :destroy
has_many :discussion_entries, :through => :discussion_topics, :include => [:discussion_topic, :user], :dependent => :destroy
has_many :announcements, :as => :context, :class_name => 'Announcement', :dependent => :destroy
has_many :active_announcements, :as => :context, :class_name => 'Announcement', :conditions => ['discussion_topics.workflow_state != ?', 'deleted'], :order => 'created_at DESC'
has_many :active_announcements, :as => :context, :class_name => 'Announcement', :conditions => ['discussion_topics.workflow_state != ?', 'deleted']
has_many :attachments, :as => :context, :dependent => :destroy, :extend => Attachment::FindInContextAssociation
has_many :active_images, :as => :context, :class_name => 'Attachment', :conditions => ["attachments.file_state != ? AND attachments.content_type LIKE 'image%'", 'deleted'], :order => 'attachments.display_name', :include => :thumbnail
has_many :active_assignments, :as => :context, :class_name => 'Assignment', :conditions => ['assignments.workflow_state != ?', 'deleted'], :order => 'assignments.title, assignments.position'
@ -1259,7 +1259,8 @@ class Course < ActiveRecord::Base
single = assignments.length == 1
includes = [:user, :course_section]
includes = {:user => :pseudonyms, :course_section => []} if options[:include_sis_id]
student_enrollments = self.student_enrollments.scoped(:include => includes).find(:all, :order => User.sortable_name_order_by_clause('users'))
scope = options[:user] ? self.enrollments_visible_to(options[:user]) : self.student_enrollments
student_enrollments = scope.scoped(:include => includes).find(:all, :order => User.sortable_name_order_by_clause('users'))
# remove duplicate enrollments for students enrolled in multiple sections
seen_users = []
student_enrollments.reject! { |e| seen_users.include?(e.user_id) ? true : (seen_users << e.user_id; false) }
@ -2349,17 +2350,32 @@ class Course < ActiveRecord::Base
# returns a scope, not an array of users/enrollments
def students_visible_to(user, include_priors=false)
enrollments_visible_to(user, include_priors, true)
enrollments_visible_to(user, :include_priors => include_priors, :return_users => true)
end
def enrollments_visible_to(user, include_priors=false, return_users=false, limit_to_section_ids=nil)
def enrollments_visible_to(user, opts = {})
visibilities = section_visibilities_for(user)
if return_users
scope = include_priors ? self.all_students : self.students
relation = []
relation << 'all' if opts[:include_priors]
if opts[:type] == :all
relation << 'user' if opts[:return_users]
else
scope = include_priors ? self.all_student_enrollments : self.student_enrollments
relation << (opts[:type].try(:to_s) || 'student')
end
if limit_to_section_ids
scope = scope.scoped(:conditions => { 'enrollments.course_section_id' => limit_to_section_ids.to_a })
if opts[:return_users]
relation.last << 's'
else
relation << 'enrollments'
end
relation = relation.join('_')
# our relations don't all follow the same pattern
relation = case relation
when 'all_enrollments'; 'enrollments'
when 'enrollments'; 'current_enrollments'
else; relation
end
scope = self.send(relation.to_sym)
if opts[:section_ids]
scope = scope.scoped(:conditions => { 'enrollments.course_section_id' => opts[:section_ids].to_a })
end
unless visibilities.any?{|v|v[:admin]}
scope = scope.scoped(:conditions => "enrollments.type != 'StudentViewEnrollment'")
@ -2367,7 +2383,7 @@ class Course < ActiveRecord::Base
# See also Users#messageable_users (same logic used to get users across multiple courses)
case enrollment_visibility_level_for(user, visibilities)
when :full then scope
when :sections then scope.scoped({:conditions => "enrollments.course_section_id IN (#{visibilities.map{|s| s[:course_section_id]}.join(",")})"})
when :sections then scope.scoped(:conditions => ["enrollments.course_section_id IN (?) OR (enrollments.limit_privileges_to_course_section=? AND enrollments.type IN ('TeacherEnrollment', 'TaEnrollment', 'DesignerEnrollment'))", visibilities.map{|s| s[:course_section_id]}, false])
when :restricted then scope.scoped({:conditions => "enrollments.user_id IN (#{(visibilities.map{|s| s[:associated_user_id]}.compact + [user.id]).join(",")})"})
else scope.scoped({:conditions => "FALSE"})
end

View File

@ -23,6 +23,7 @@ class DiscussionTopic < ActiveRecord::Base
include HasContentTags
include CopyAuthorizedLinks
include TextHelper
include ContextModuleItem
attr_accessible :title, :message, :user, :delayed_post_at, :assignment,
:plaintext_message, :podcast_enabled, :podcast_has_student_posts,
@ -39,7 +40,6 @@ class DiscussionTopic < ActiveRecord::Base
has_many :discussion_entries, :order => :created_at, :dependent => :destroy
has_many :root_discussion_entries, :class_name => 'DiscussionEntry', :include => [:user], :conditions => ['discussion_entries.parent_id IS NULL AND discussion_entries.workflow_state != ?', 'deleted']
has_one :context_module_tag, :as => :content, :class_name => 'ContentTag', :conditions => ['content_tags.tag_type = ? AND workflow_state != ?', 'context_module', 'deleted'], :include => {:context_module => [:content_tags, :context_module_progressions]}
has_one :external_feed_entry, :as => :asset
belongs_to :external_feed
belongs_to :context, :polymorphic => true
@ -135,8 +135,8 @@ class DiscussionTopic < ActiveRecord::Base
attr_accessor :saved_by
def update_assignment
if !self.assignment_id && @old_assignment_id && self.context_module_tag
self.context_module_tag.confirm_valid_module_requirements
if !self.assignment_id && @old_assignment_id
self.context_module_tags.each { |tag| tag.confirm_valid_module_requirements }
end
if @old_assignment_id
Assignment.update_all({:workflow_state => 'deleted', :updated_at => Time.now.utc}, {:id => @old_assignment_id, :context_id => self.context_id, :context_type => self.context_type, :submission_types => 'discussion_topic'})
@ -542,11 +542,12 @@ class DiscussionTopic < ActiveRecord::Base
end
def context_module_action(user, action, points=nil)
self.context_module_tag.context_module_action(user, action, points) if self.context_module_tag
tags_to_update = self.context_module_tags.to_a
if self.for_assignment?
self.assignment.context_module_tag.context_module_action(user, action, points) if self.assignment.context_module_tag
tags_to_update += self.assignment.context_module_tags
self.ensure_submission(user) if self.assignment.context.students.include?(user) && action == :contributed
end
tags_to_update.each { |tag| tag.context_module_action(user, action, points) }
end
def ensure_submission(user)
@ -597,16 +598,15 @@ class DiscussionTopic < ActiveRecord::Base
end
def locked_for?(user=nil, opts={})
@locks ||= {}
return false if opts[:check_policies] && self.grants_right?(user, nil, :update)
@locks[user ? user.id : 0] ||= Rails.cache.fetch(locked_cache_key(user), :expires_in => 1.minute) do
Rails.cache.fetch(locked_cache_key(user), :expires_in => 1.minute) do
locked = false
if (self.delayed_post_at && self.delayed_post_at > Time.now)
locked = {:asset_string => self.asset_string, :unlock_at => self.delayed_post_at}
elsif (self.assignment && l = self.assignment.locked_for?(user, opts))
locked = l
elsif (self.could_be_locked && self.context_module_tag && !self.context_module_tag.available_for?(user, opts[:deep_check_if_needed]))
locked = {:asset_string => self.asset_string, :context_module => self.context_module_tag.context_module.attributes}
elsif self.could_be_locked && item = locked_by_module_item?(user, opts[:deep_check_if_needed])
locked = {:asset_string => self.asset_string, :context_module => item.context_module.attributes}
elsif (self.root_topic && l = self.root_topic.locked_for?(user, opts))
locked = l
end

View File

@ -29,19 +29,20 @@ class Enrollment < ActiveRecord::Base
has_many :pseudonyms, :primary_key => :user_id, :foreign_key => :user_id
has_many :course_account_associations, :foreign_key => 'course_id', :primary_key => 'course_id'
validates_presence_of :user_id
validates_presence_of :course_id
validates_presence_of :user_id, :course_id
validates_inclusion_of :limit_privileges_to_course_section, :in => [true, false]
before_save :assign_uuid
before_save :assert_section
before_save :update_user_account_associations_if_necessary
before_save :audit_groups_for_deleted_enrollments
before_validation :infer_privileges
after_create :create_linked_enrollments
after_save :clear_email_caches
after_save :cancel_future_appointments
after_save :update_linked_enrollments
attr_accessible :user, :course, :workflow_state, :course_section, :limit_priveleges_to_course_section, :limit_privileges_to_course_section
attr_accessible :user, :course, :workflow_state, :course_section, :limit_privileges_to_course_section
def self.active_student_conditions(prefix = 'enrollments')
"(#{prefix}.type IN ('StudentEnrollment', 'StudentViewEnrollment') AND #{prefix}.workflow_state = 'active')"
@ -355,6 +356,24 @@ class Enrollment < ActiveRecord::Base
self.root_account_id = self.course_section.root_account_id rescue nil
end
def infer_privileges
# limit_privileges_to_course_section affects whether this user can see
# users from other sections (for any purpose - messaging, roster, grading)
# admins (teacher, ta, designer) that have this flag are also visible TO
# users from any section (but not students/observers).
# currently, this flag is actually only configurable for teachers and
# TAs; designers are always course-wide, and so are students.
# In the future, we should probably allow configuring it for students,
# possibly section-wide (i.e. "Students in this section can see students
# from all other sections")
if self.is_a?(TeacherEnrollment) || self.is_a?(TaEnrollment)
self.limit_privileges_to_course_section = false if self.limit_privileges_to_course_section.nil?
else
self.limit_privileges_to_course_section = false
end
true
end
def course_name
self.course.name || t('#enrollment.default_course_name', "Course")
end
@ -811,20 +830,6 @@ class Enrollment < ActiveRecord::Base
read_attribute(:uuid)
end
# overwrite the accessors to limit_priveleges and limit_privileges to return the value wherever
# it exists.
[:limit_privileges_to_course_section, :limit_priveleges_to_course_section].each do |method_name|
define_method(method_name) do
read_attribute(:limit_privileges_to_course_section).nil? ?
read_attribute(:limit_priveleges_to_course_section) :
read_attribute(:limit_privileges_to_course_section)
end
end
def limit_priveleges_to_course_section=(value)
self.limit_privileges_to_course_section = value
end
def self.limit_privileges_to_course_section!(course, user, limit)
Enrollment.update_all({:limit_privileges_to_course_section => !!limit}, {:course_id => course.id, :user_id => user.id})
user.touch

View File

@ -27,7 +27,10 @@ class ErrorReport < ActiveRecord::Base
before_save :guess_email
# Define a custom callback for external notification of an error report.
define_callbacks :on_send_to_external
# Setup callback to default behavior.
on_send_to_external :send_via_email_or_post
attr_accessible
@ -141,12 +144,14 @@ class ErrorReport < ActiveRecord::Base
distinct('category')
end
on_send_to_external do |error_report|
# Send the error report based on configuration either via a POST or email to an external location.
def send_via_email_or_post
error_report = self
config = Canvas::Plugin.find('error_reporting').try(:settings) || {}
message_type = (error_report.backtrace || "").split("\n").first.match(/\APosted as[^_]*_([A-Z]*)_/)[1] rescue nil
message_type ||= "ERROR"
body = %{From #{error_report.email}, #{(error_report.user.name rescue "")}
#{message_type} #{error_report.comments + "\n" if error_report.comments}
#{"url: " + error_report.url + "\n" if error_report.url }
@ -173,4 +178,6 @@ error_id: #{error_report.id}
)
end
end
private :send_via_email_or_post
end

View File

@ -62,7 +62,9 @@ class GradingStandard < ActiveRecord::Base
# e.g. convert 89.7 to B+
def self.score_to_grade(scheme, score)
score = 0 if score < 0
scheme.max_by {|s| score >= s[1] * 100 ? s[1] : -1 }[0]
# assign the highest grade whose min cutoff is less than the score
# if score is less than all scheme cutoffs, assign the lowest grade
scheme.max_by {|s| score >= s[1] * 100 ? s[1] : -s[1] }[0]
end
# e.g. convert B to 86

View File

@ -134,15 +134,18 @@ class Group < ActiveRecord::Base
end
def membership_for_user(user)
self.group_memberships.find_by_user_id(user && user.id)
return nil unless user.present?
self.shard.activate { self.group_memberships.find_by_user_id(user.id) }
end
def has_member?(user)
self.participating_group_memberships.find_by_user_id(user && user.id)
return nil unless user.present?
self.shard.activate { self.participating_group_memberships.find_by_user_id(user.id) }
end
def has_moderator?(user)
self.participating_group_memberships.moderators.find_by_user_id(user && user.id)
return nil unless user.present?
self.shard.activate { self.participating_group_memberships.moderators.find_by_user_id(user.id) }
end
def should_add_creator?

View File

@ -112,7 +112,7 @@ class GroupMembership < ActiveRecord::Base
if (self.id_changed? || self.workflow_state_changed?) && self.active?
UserFollow.create_follow(self.user, self.group)
elsif self.destroyed? || (self.workflow_state_changed? && self.deleted?)
user_follow = self.user.user_follows.find(:first, :conditions => { :followed_item_id => self.group_id, :followed_item_type => 'Group' })
user_follow = self.user.shard.activate { self.user.user_follows.find(:first, :conditions => { :followed_item_id => self.group_id, :followed_item_type => 'Group' }) }
user_follow.try(:destroy)
end
end

View File

@ -24,6 +24,8 @@ class Quiz < ActiveRecord::Base
include CopyAuthorizedLinks
include ActionView::Helpers::SanitizeHelper
extend ActionView::Helpers::SanitizeHelper::ClassMethods
include ContextModuleItem
attr_accessible :title, :description, :points_possible, :assignment_id, :shuffle_answers,
:show_correct_answers, :time_limit, :allowed_attempts, :scoring_policy, :quiz_type,
:lock_at, :unlock_at, :due_at, :access_code, :anonymous_submissions, :assignment_group_id,
@ -36,7 +38,6 @@ class Quiz < ActiveRecord::Base
has_many :quiz_questions, :dependent => :destroy, :order => 'position'
has_many :quiz_submissions, :dependent => :destroy
has_many :quiz_groups, :dependent => :destroy, :order => 'position'
has_one :context_module_tag, :as => :content, :class_name => 'ContentTag', :conditions => ['content_tags.tag_type = ? AND workflow_state != ?', 'context_module', 'deleted'], :include => {:context_module => [:context_module_progressions, :content_tags]}
belongs_to :context, :polymorphic => true
belongs_to :assignment
belongs_to :cloned_item
@ -218,8 +219,8 @@ class Quiz < ActiveRecord::Base
attr_accessor :saved_by
def update_assignment
send_later_if_production(:set_unpublished_question_count) if self.id
if !self.assignment_id && @old_assignment_id && self.context_module_tag
self.context_module_tag.confirm_valid_module_requirements
if !self.assignment_id && @old_assignment_id
self.context_module_tags.each { |tag| tag.confirm_valid_module_requirements }
end
if !self.graded? && (@old_assignment_id || self.last_assignment_id)
Assignment.update_all({:workflow_state => 'deleted', :updated_at => Time.now.utc}, {:id => [@old_assignment_id, self.last_assignment_id].compact, :submission_types => 'online_quiz'})
@ -588,9 +589,8 @@ class Quiz < ActiveRecord::Base
alias_method :to_s, :quiz_title
def locked_for?(user=nil, opts={})
@locks ||= {}
return false if opts[:check_policies] && self.grants_right?(user, nil, :update)
@locks[user ? user.id : 0] ||= Rails.cache.fetch(locked_cache_key(user), :expires_in => 1.minute) do
Rails.cache.fetch(locked_cache_key(user), :expires_in => 1.minute) do
locked = false
if (self.unlock_at && self.unlock_at > Time.now)
sub = user && quiz_submissions.find_by_user_id(user.id)
@ -607,10 +607,10 @@ class Quiz < ActiveRecord::Base
if !sub || !sub.manually_unlocked
locked = l
end
elsif (self.context_module_tag && !self.context_module_tag.available_for?(user, opts[:deep_check_if_needed]))
elsif item = locked_by_module_item?(user, opts[:deep_check_if_needed])
sub = user && quiz_submissions.find_by_user_id(user.id)
if !sub || !sub.manually_unlocked
locked = {:asset_string => self.asset_string, :context_module => self.context_module_tag.context_module.attributes}
locked = {:asset_string => self.asset_string, :context_module => item.context_module.attributes}
end
end
locked
@ -618,8 +618,11 @@ class Quiz < ActiveRecord::Base
end
def context_module_action(user, action, points=nil)
self.context_module_tag.context_module_action(user, action, points) if self.context_module_tag
self.assignment.context_module_tag.context_module_action(user, action, points) if self.assignment && self.assignment.context_module_tag
tags_to_update = self.context_module_tags.to_a
if self.assignment
tags_to_update += self.assignment.context_module_tags
end
tags_to_update.each { |tag| tag.context_module_action(user, action, points) }
end
# virtual attribute

View File

@ -29,16 +29,18 @@ class ReportSnapshot < ActiveRecord::Base
def self.report_value_over_time(report, key)
items = []
now = Time.now.utc.to_i
report['monthly'].each do |month|
if month[key]
date = Date
stamp = ((Time.utc(month['year'], month['month'], 1).to_date >> 1) - 1.day).to_time.to_i
items << [stamp*1000, month[key]]
next if stamp > now
items << [stamp.to_i*1000, month[key]]
end
end
report['weekly'].each do |week|
if week[key]
stamp = (week['week'] * 604800) + ((week['year'] - 1970) * 31556926)
next if stamp > now
items << [stamp*1000, week[key]]
end
end

View File

@ -19,7 +19,6 @@
class SisBatch < ActiveRecord::Base
include Workflow
belongs_to :account
has_many :sis_batch_log_entries, :order => :created_at
serialize :data
serialize :options
serialize :processing_errors, Array
@ -205,7 +204,6 @@ class SisBatch < ActiveRecord::Base
}
data["processing_errors"] = self.processing_errors if self.processing_errors.present?
data["processing_warnings"] = self.processing_warnings if self.processing_warnings.present?
data["sis_batch_log_entries"] = self.sis_batch_log_entries if self.sis_batch_log_entries.present?
return data.to_json
end

View File

@ -1,33 +0,0 @@
#
# Copyright (C) 2011 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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/>.
#
class SisBatchLogEntry < ActiveRecord::Base
validates_length_of :text, :maximum => maximum_text_length, :allow_nil => true, :allow_blank => true
belongs_to :sis_batch
attr_accessible :text, :sis_batch
def text=(val)
if !val || val.length < self.class.maximum_text_length
write_attribute(:text, val)
else
write_attribute(:text, val[0,self.class.maximum_text_length])
end
end
end

View File

@ -162,6 +162,7 @@ class StreamItem < ActiveRecord::Base
hash['user_short_name'] = comment.author.short_name if comment.author
hash
end
res[:course_id] = object.context.id
when Collaboration
res = object.attributes
res['users'] = object.users.map{|u| prepare_user(u)}

View File

@ -205,44 +205,53 @@ class Submission < ActiveRecord::Base
strip_tags((self.body || "").gsub(/\<\s*br\s*\/\>/, "\n<br/>").gsub(/\<\/p\>/, "</p>\n"))
end
def check_turnitin_status(asset_string, attempt=1)
def check_turnitin_status(attempt=1)
self.turnitin_data ||= {}
data = self.turnitin_data[asset_string]
return unless data && data[:object_id]
if data[:similarity_score].blank?
if attempt < TURNITIN_RETRY
turnitin = Turnitin::Client.new(*self.context.turnitin_settings)
res = turnitin.generateReport(self, asset_string)
if res[:similarity_score]
data[:similarity_score] = res[:similarity_score].to_f
data[:web_overlap] = res[:web_overlap].to_f
data[:publication_overlap] = res[:publication_overlap].to_f
data[:student_overlap] = res[:student_overlap].to_f
data[:state] = 'failure'
data[:state] = 'problem' if data[:similarity_score] < 75
data[:state] = 'warning' if data[:similarity_score] < 50
data[:state] = 'acceptable' if data[:similarity_score] < 25
data[:state] = 'none' if data[:similarity_score] == 0
data[:status] = 'scored'
turnitin = nil
needs_retry = false
# check all assets in the turnitin_data (self.turnitin_assets is only the
# current assets) so that we get the status for assets of previous versions
# of the submission as well
self.turnitin_data.keys.each do |asset_string|
data = self.turnitin_data[asset_string]
next unless data && data.is_a?(Hash) && data[:object_id]
if data[:similarity_score].blank?
if attempt < TURNITIN_RETRY
turnitin ||= Turnitin::Client.new(*self.context.turnitin_settings)
res = turnitin.generateReport(self, asset_string)
if res[:similarity_score]
data[:similarity_score] = res[:similarity_score].to_f
data[:web_overlap] = res[:web_overlap].to_f
data[:publication_overlap] = res[:publication_overlap].to_f
data[:student_overlap] = res[:student_overlap].to_f
data[:state] = 'failure'
data[:state] = 'problem' if data[:similarity_score] < 75
data[:state] = 'warning' if data[:similarity_score] < 50
data[:state] = 'acceptable' if data[:similarity_score] < 25
data[:state] = 'none' if data[:similarity_score] == 0
data[:status] = 'scored'
else
needs_retry ||= true
end
else
send_at((5 * attempt).minutes.from_now, :check_turnitin_status, asset_string, attempt + 1)
data[:status] = 'error'
end
else
data[:status] = 'error'
data[:status] = 'scored'
end
else
data[:status] = 'scored'
self.turnitin_data[asset_string] = data
end
self.turnitin_data[asset_string] = data
send_at((5 * attempt).minutes.from_now, :check_turnitin_status, attempt + 1) if needs_retry
self.turnitin_data_changed!
self.save
data
end
def turnitin_report_url(asset_string, user)
if self.turnitin_data && self.turnitin_data[asset_string] && self.turnitin_data[asset_string][:similarity_score]
turnitin = Turnitin::Client.new(*self.context.turnitin_settings)
self.send_later(:check_turnitin_status, asset_string)
self.send_later(:check_turnitin_status)
if self.grants_right?(user, nil, :grade)
turnitin.submissionReportUrl(self, asset_string)
elsif self.grants_right?(user, nil, :view_turnitin_report)
@ -278,7 +287,7 @@ class Submission < ActiveRecord::Base
turnitin = Turnitin::Client.new(*self.context.turnitin_settings)
reset_turnitin_assets
# 1. Make sure the assignment exists and user is enrolled
# Make sure the assignment exists and user is enrolled
assign_status = self.assignment.create_in_turnitin
enroll_status = turnitin.enrollStudent(self.context, self.user)
unless assign_status && enroll_status
@ -296,18 +305,20 @@ class Submission < ActiveRecord::Base
return false
end
# 2. Submit the file(s)
# Submit the file(s)
submission_response = turnitin.submitPaper(self)
submission_response.each do |res_asset_string, response|
self.turnitin_data[res_asset_string].merge!(response)
self.turnitin_data_changed!
if response[:object_id]
self.send_at(5.minutes.from_now, :check_turnitin_status, res_asset_string)
elsif !(attempt < TURNITIN_RETRY)
if !response[:object_id] && !(attempt < TURNITIN_RETRY)
self.turnitin_data[res_asset_string][:status] = 'error'
end
end
self.send_at(5.minutes.from_now, :check_turnitin_status)
self.save
# Schedule retry if there were failures
submit_status = submission_response.present? && submission_response.values.all?{ |v| v[:object_id] }
unless submit_status
send_at(5.minutes.from_now, :submit_to_turnitin, attempt + 1) if attempt < TURNITIN_RETRY

View File

@ -1730,7 +1730,7 @@ class User < ActiveRecord::Base
end
def recent_stream_items(opts={})
visible_stream_item_instances(opts).scoped(:include => :stream_item, :limit => 21).map(&:stream_item)
visible_stream_item_instances(opts).scoped(:include => :stream_item, :limit => 21).map(&:stream_item).compact
end
memoize :recent_stream_items

View File

@ -68,14 +68,25 @@ class UserFollow < ActiveRecord::Base
#
# this way both associations work as expected
set_shard_override do |record|
record.following_user.shard unless record.complementary_record
record.following_user.shard unless record.complementary_record?
end
after_create :create_complementary_record
attr_accessor :complementary_record
attr_writer :complementary_record
# returns true if the following user isn't on the same shard as the followed
# item, and this UserFollow is the secondary copy that's on the followed
# item's shard
def complementary_record?
if new_record?
@complementary_record
else
self.shard != following_user.shard
end
end
def create_complementary_record
if !complementary_record && followed_item.shard != following_user.shard
if !complementary_record? && followed_item.shard != following_user.shard
followed_item.shard.activate do
UserFollow.create_follow(following_user, followed_item, true)
end
@ -85,10 +96,10 @@ class UserFollow < ActiveRecord::Base
after_destroy :destroy_complementary_record
def destroy_complementary_record
complementary_record.try(:destroy)
find_complementary_record.try(:destroy)
end
def complementary_record
def find_complementary_record
return nil if followed_item.shard == following_user.shard
if self.shard == followed_item.shard
finding_shard = following_user.shard
@ -110,7 +121,7 @@ class UserFollow < ActiveRecord::Base
# when a user follows a group or other user, they auto-follow all existing
# collections in that context as well
def check_auto_follow_collections
return true if complementary_record
return true if self.complementary_record?
case followed_item
when User, Group
if !followed_item.collections.empty?
@ -128,6 +139,40 @@ class UserFollow < ActiveRecord::Base
end
end
after_destroy :check_auto_unfollow_collections
# when a user leaves a group, they auto-unfollow all private collections in
# that group
def check_auto_unfollow_collections
return true if self.complementary_record?
case followed_item
when Group
if !followed_item.collections.empty?
UserFollow.send_later_enqueue_args(:auto_unfollow_collections_for,
{ :priority => Delayed::LOW_PRIORITY },
self.following_user_id,
self.followed_item_type,
self.followed_item_id)
end
end
true
end
# this is called after the UserFollow object is destroyed, so it needs to
# re-lookup the user and context
def self.auto_unfollow_collections_for(following_user_id, followed_item_type, followed_item_id)
if context = Object.const_get(followed_item_type).find_by_id(followed_item_id)
following_user = User.find(following_user_id)
context.collections.active.each do |coll|
if !coll.grants_right?(following_user, :follow)
user_follow = following_user.user_follows.scoped(:conditions => { :followed_item_type => 'Collection',
:followed_item_id => coll.id }).first
user_follow.try(:destroy)
end
end
end
end
trigger.after(:insert) do |t|
t.where("NEW.followed_item_type = 'Collection'") do
<<-SQL

View File

@ -24,12 +24,12 @@ class WikiPage < ActiveRecord::Base
include Workflow
include HasContentTags
include CopyAuthorizedLinks
include ContextModuleItem
belongs_to :wiki, :touch => true
belongs_to :wiki_with_participants, :class_name => 'Wiki', :foreign_key => 'wiki_id', :include => {:wiki_namespaces => :context }
belongs_to :cloned_item
belongs_to :user
has_many :context_module_tags, :as => :content, :class_name => 'ContentTag', :conditions => ['content_tags.tag_type = ? AND workflow_state != ?', 'context_module', 'deleted'], :include => {:context_module => [:content_tags, :context_module_progressions]}
has_many :wiki_page_comments, :order => "created_at DESC"
acts_as_url :title, :scope => [:wiki_id, :not_deleted], :sync_url => true
@ -195,8 +195,7 @@ class WikiPage < ActiveRecord::Base
def locked_for?(context, user, opts={})
return false unless self.could_be_locked
@locks ||= {}
@locks[user ? user.id : 0] ||= Rails.cache.fetch(locked_cache_key(user), :expires_in => 1.minute) do
Rails.cache.fetch(locked_cache_key(user), :expires_in => 1.minute) do
m = context_module_tag_for(context, user).context_module rescue nil
locked = false
if (m && !m.available_for?(user))

View File

@ -10,6 +10,8 @@ body
min-height: 425px
a
color: #2571bd
.spinner
width: 50px
#actions
min-height: 42px
background: #dddde1 url(/images/messages/actions-bg.png) 0 0 repeat-x
@ -409,8 +411,6 @@ ul.messages, ul.messages.private, ul.messages.private li:hover
padding: 0 0 0 1px
overflow: hidden
position: relative
.spinner
width: 50px
#message_actions
display: none
position: absolute

View File

@ -23,16 +23,6 @@
<span class="auth_info auth_base"><%= @account_config.auth_base %></span>
</td>
</tr>
<tr>
<td><%= f.blabel :log_in_url, :en => "Alternate Login URL" %></td>
<td class="nobr">
<%= f.text_field :log_in_url, :class => "auth_form", :style => "width: 450px;" %>
<span class="auth_info log_in_url"><%= @account_config.log_in_url %></span>
<span class="auth_form" style="font-size: smaller;">
<br><%= t(:alternate_login_url_description, "An alternate URL for logging into CAS. You probably should not set this.") %>
</span>
</td>
</tr>
<tr>
<td style="vertical-align: top; width: 200px;"><%= f.blabel :login_handle_name, :en => "Login Label" %></td>
<td style="vertical-align: top;" class="nobr">

View File

@ -15,7 +15,7 @@
</div>
<div class="form" style="<%= hidden if @current_batch && @current_batch.importing? %>">
<% form_tag account_sis_import_submit_path(@account.id), :multipart => true, :id => "sis_importer" do %>
<% form_tag account_sis_imports_path(@account.id), :multipart => true, :id => "sis_importer" do %>
<p class="instruction"><%= mt(:select_file_instructions,
"Select the zip file that you want imported. \n" +
"For a description of how to generate these zip files, [please see this documentation](%{uri}).",

View File

@ -19,7 +19,7 @@
<% elsif e.is_a?(AppointmentGroup) %>
<a href="<%= appointment_group_url(e.id) %>"><%= e.title %></a>
<% else %>
<a href="<%= context_url(@context, :context_calendar_event_url, e.parent_calendar_event_id || e.id) %>"><%= e.title %></a>
<a href="<%= calendar_url_for(e.effective_context, :event => e) %>"><%= e.title %></a>
<% end %>
</td>
<td class="dates <%= "not_last" unless idx == events.length - 1 %>">

View File

@ -104,5 +104,5 @@ $(document).ready(function() {
<%= render :partial => "shared/rubric_forms" %>
<% end %>
<%= render :partial => "shared/aligned_outcomes", :locals => {:asset => @assignment} %>
<%= render :partial => "shared/sequence_footer", :locals => {:asset => @assignment} if @assignment.context_module_tag %>
<%= render :partial => "shared/sequence_footer", :locals => {:asset => @assignment} if !@assignment.context_module_tags.empty? %>
<% end %>

View File

@ -12,8 +12,10 @@
'other' => image_tag("blank.png", :class => "image", :alt => '')
}
%>
<% criterion = completion_criteria && completion_criteria.find{|c| c[:id] == tag.id} %>
<table id="context_module_item_<%= tag ? tag.id : "blank" %>" class="context_module_item <%= module_item.content_type_class if module_item %> <%= 'also_assignment' if module_item && module_item.graded? %> indent_<%= tag.try_rescue(:indent) || '0' %> <%= 'progression_requirement' if criterion %> <%= criterion[:type] if criterion %>_requirement" style="<%= hidden unless module_item %>">
<% criterion = completion_criteria && completion_criteria.find{|c| c[:id] == tag.id}
item_class = "#{module_item.content_type}_#{module_item.content_id}" if module_item
%>
<table id="context_module_item_<%= tag ? tag.id : "blank" %>" class="context_module_item <%= module_item.content_type_class if module_item %> <%= 'also_assignment' if module_item && module_item.graded? %> indent_<%= tag.try_rescue(:indent) || '0' %> <%= 'progression_requirement' if criterion %> <%= criterion[:type] if criterion %>_requirement <%= item_class %>" style="<%= hidden unless module_item %>">
<tr>
<td class="module_item_icons">
<div class="nobr">

View File

@ -34,7 +34,7 @@ $(document).ready(function() {
<h3><%= t('headings.next_steps', %{Next Steps}) %></h3>
<ul class="wizard_options_list">
<% if can_do @context, @current_user, :manage_content %>
<li class="option download_step <%= 'completed' unless @context.attachments.active.empty? %>">
<li class="option download_step <%= 'completed' unless @context.attachments.active.first.nil? %>">
<a href="<%= context_url(@context, :context_imports_url) %>" class="header"><%= t('links.import', %{Import Content}) %></a>
<div class="details" style="display: none;">
<%= t 'details.import', %{If you've been using another course management system, you probably have stuff in there that you're going to want moved over to Canvas. We can walk you through the process of easily migrating your content into Canvas.} %>
@ -42,7 +42,7 @@ $(document).ready(function() {
</li>
<% end %>
<% if can_do @context, @current_user, :manage_content, :manage_assignments %>
<li class="option assignments_step <%= 'completed' unless @context.assignments.active.empty? %>">
<li class="option assignments_step <%= 'completed' unless @context.assignments.active.first.nil? %>">
<a href="<%= context_url(@context, :context_assignments_url, :wizard => 1) %>" class="header"><%= t('links.assignments', %{Add Course Assignments}) %></a>
<div class="details" style="display: none;">
<%= t 'details.assignments', %{Add your assignments. You can just make a long list, or break them up into groups -- and even specify weights for each assignment group.} %>
@ -50,7 +50,7 @@ $(document).ready(function() {
</li>
<% end %>
<% if can_do @context, @current_user, :manage_students %>
<li class="option <%= 'completed' unless @context.students.empty? %>">
<li class="option <%= 'completed' unless @context.students.first.nil? %>">
<a href="<%= context_url(@context, :context_details_url, :wizard => 1) %>#add_students" class="header"><%= t('links.students', %{Add Students to the Course}) %></a>
<div class="details" style="display: none;">
<%= t 'details.students', %{You'll definitely want some of these. What's the fun of teaching a course if nobody's even listening?} %>
@ -58,7 +58,7 @@ $(document).ready(function() {
</li>
<% end %>
<% if can_do @context, @current_user, :manage_content, :manage_files %>
<li class="option download_step <%= 'completed' unless @context.attachments.active.empty? %>" style="display: none;">
<li class="option download_step <%= 'completed' unless @context.attachments.active.first.nil? %>" style="display: none;">
<a href="<%= context_url(@context, :context_files_url, :wizard => 1) %>" class="header"><%= t('links.files', %{Add Course Files}) %></a>
<div class="details" style="display: none;">
<%= t 'details.files', %{The Files tab is the place to share lecture slides, example documents, study helps -- anything your students will want to download. Uploading and organizing your files is easy with Canvas. We'll show you how.} %>
@ -82,7 +82,7 @@ $(document).ready(function() {
</li>
<% end %>
<% if can_do @context, @current_user, :manage_calendar %>
<li class="option calendar_step <%= 'completed' unless @context.calendar_events.active.empty? %>">
<li class="option calendar_step <%= 'completed' unless @context.calendar_events.active.first.nil? %>">
<a href="<%= calendar_path(:wizard => 1) %>" class="header"><%= t('links.calendar', %{Add Course Calendar Events}) %></a>
<div class="details" style="display: none;">
<%= t 'details.calendar', %{Here's a great chance to get to know the calendar -- and add any non-assignment events you might have to the course. Don't worry, we'll help you through it.} %>

View File

@ -225,10 +225,7 @@
</div>
<%=
sequence_asset = @topic
sequence_asset = @topic.root_topic if @topic.root_topic && !@topic.context_module_tag && @topic.root_topic.context_module_tag
sequence_asset = @topic.assignment if @topic.assignment && !@topic.context_module_tag && @topic.assignment.context_module_tag
render :partial => "shared/sequence_footer", :locals => {:asset => sequence_asset, :context => sequence_asset.context} if sequence_asset.context_module_tag
render :partial => "shared/sequence_footer", :locals => {:asset => @sequence_asset, :context => @sequence_asset.context} if @sequence_asset
%>
<% end %>
<% if @headers == false || @locked %>

View File

@ -26,7 +26,7 @@
{{#if showBadBrowserMessage}}
<div class="ui-state-error">
<span class="ui-icon ui-icon-alert"></span>
<strong>{{#t "wait_a_better_browser_may_help_solve_the_problem"}}Wait! A <a target="_blank" href="http://guides.instructure.com/s/2204/m/4214/l/41056-Which-browsers-does-Canvas-support-">better browser</a> may help solve the problem.{{/t}}</strong>
<strong>{{#t "wait_a_different_browser_may_help_solve_the_problem"}}Wait! A <a target="_blank" href="http://guides.instructure.com/s/2204/m/4214/l/41056-Which-browsers-does-Canvas-support-">different browser</a> may help solve the problem.{{/t}}</strong>
<div>{{#t "you_are_using_browser_version_version_try_again_using_the_latest_version_of_chrome_or_firefox"}}You are using Internet Explorer version {{browserVersion}}, Try again using the latest version of <a href="http://google.com/chrome">Chrome</a> or <a href="http://getfirefox.com">Firefox</a>{{/t}}</div>
</div>
{{else}}

View File

@ -8,42 +8,8 @@
require = {
translate: <%= use_optimized_js? %>,
baseUrl: '<%= js_base_url %>',
paths: {
common: 'compiled/bundles/common',
jqueryui: 'vendor/jqueryui',
uploadify: '../flash/uploadify/jquery.uploadify.v2.1.4',
use: 'vendor/use'
},
use: {
'vendor/backbone': {
deps: ['underscore', 'jquery'],
attach: function(_, $){
return Backbone;
}
},
// slick grid shim
'vendor/slickgrid/lib/jquery.event.drag-2.0.min': {
deps: ['jquery'],
attach: '$'
},
'vendor/slickgrid/slick.core': {
deps: ['jquery', 'use!vendor/slickgrid/lib/jquery.event.drag-2.0.min'],
attach: 'Slick'
},
'vendor/slickgrid/slick.grid': {
deps: ['use!vendor/slickgrid/slick.core'],
attach: 'Slick'
},
'vendor/slickgrid/slick.editors': {
deps: ['use!vendor/slickgrid/slick.core'],
attach: 'Slick'
},
'vendor/slickgrid/plugins/slick.rowselectionmodel': {
deps: ['use!vendor/slickgrid/slick.core'],
attach: 'Slick'
}
}
paths: <%= raw Canvas::RequireJs.paths %>,
use: <%= raw Canvas::RequireJs.shims %>
};
</script>

View File

@ -220,8 +220,4 @@
<%= render :partial => "shared/message_students" %>
<% end %>
<% end %>
<%
sequence_asset = @quiz
sequence_asset = @quiz.assignment if @quiz.assignment && !@quiz.context_module_tag && @quiz.assignment.context_module_tag
%>
<%= render :partial => "shared/sequence_footer", :locals => {:asset => sequence_asset} if sequence_asset.context_module_tag %>
<%= render :partial => "shared/sequence_footer", :locals => {:asset => @sequence_asset} if @sequence_asset %>

View File

@ -9,7 +9,7 @@
<%= image_tag "forward.png" %> <span class="text"><%= t(:next, "Next") %></span>
<span class="title ellipsis"></span>
</a>
<a href="<%= context_url(context, :context_context_modules_item_details_url, asset.asset_string) %>" style="display: none;" class="sequence_details_url">&nbsp;</a>
<a href="<%= context_url(context, :context_context_modules_item_details_url, asset.asset_string, :module_item_id => params[:module_item_id]) %>" style="display: none;" class="sequence_details_url">&nbsp;</a>
<a href="<%= context_url(context, :context_context_modules_item_redirect_url, "{{ id }}") %>" class="module_item_url" style="display: none;">&nbsp;</a>
<a href="<%= context_url(context, :context_context_module_url, "{{ id }}") %>" class="module_url" style="display: none;">&nbsp;</a>
<div class="all">

View File

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html class="not-ie" lang="en">
<head>
<meta charset="utf-8">
<script type="text/javascript">
top.location = '<%= url %>'
</script>
</head>
<body>
<%= t('redirecting', %{Redirecting...}) %>
</body>
</html>

96
config/build.js.erb Normal file
View File

@ -0,0 +1,96 @@
({
// file optimizations
optimize: "uglify",
// continue to let Jammit do its thing
optimizeCss: "none",
// where to place optimized javascript, relative to this file
dir: "../public/optimized",
// where the "app" is, relative to this file
appDir: "../public/javascripts",
// base path for modules, relative to appDir
baseUrl: "./",
translate: true,
paths: <%= paths %>,
// non-amd shims
use: <%= shims %>,
// which modules should have their dependencies concatenated into them
modules: [
// non "app" bundles, should be careful not to try to have too many of these
{
name: "compiled/tinymce",
// this stuff is already in common, should be able to make this a smaller
// list since some things depend on others in the list, yes, its a bit crazy
// this is the intersection of common and tinymce, we need to script this
// config file...
exclude: [
'order',
'i18n',
'str/escapeRegex',
'vendor/date',
'jquery',
'str/pluralize',
'INST',
'str/htmlEscape',
'i18nObj',
'vendor/jquery.scrollTo',
'vendor/jqueryui/core',
'vendor/jqueryui/widget',
'vendor/jqueryui/mouse',
'vendor/jqueryui/position',
'translations/instructure',
'i18n!instructure',
'compiled/util/objectCollection',
'vendor/spin',
'vendor/jquery.spin',
'jquery.google-analytics',
'vendor/jquery.ba-hashchange',
'vendor/jqueryui/effects/core',
'vendor/jqueryui/effects/drop',
'jquery.rails_flash_notifications',
'translations/scribd',
'i18n!scribd',
'vendor/scribd.view',
'jquery.dropdownList',
'vendor/jqueryui/progressbar',
'translations/media_comments',
'i18n!media_comments',
'vendor/jqueryui/button',
'vendor/jqueryui/draggable',
'jqueryui/draggable',
'vendor/jqueryui/resizable',
'vendor/jqueryui/dialog',
'jquery.instructure_jquery_patches',
'vendor/jqueryui/datepicker',
'vendor/jqueryui/sortable',
'jquery.scrollToVisible',
'vendor/jqueryui/tabs',
'jquery.disableWhileLoading',
'jquery.keycodes',
'jquery.instructure_date_and_time',
'jquery.instructure_misc_plugins',
'tinymce.editor_box',
'jquery.instructure_forms',
'jquery.ajaxJSON',
'jquery.instructure_misc_helpers',
'media_comments'
]
},
{ name: "common" },
// "apps"
<%= app_bundles %>
]
})

View File

@ -975,4 +975,26 @@ ActiveRecord::ConnectionAdapters::SchemaStatements.class_eval do
add_index_without_length_raise(table_name, column_name, options)
end
alias_method_chain :add_index, :length_raise
# 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)
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")
end
return unless foreign_keys(table).find { |k| k.options[:name] == foreign_key_name }
remove_foreign_key(table, options)
end
end

View File

@ -30,7 +30,7 @@ class ActiveRecord::Base
'courses' => %w(section hidden_tabs sis_name sis_course_code),
'discussion_topics' => %w(authorization_list_id),
'enrollment_terms' => %w(sis_data sis_name),
'enrollments' => %w(invitation_email can_participate_before_start_at),
'enrollments' => %w(invitation_email can_participate_before_start_at limit_priveleges_to_course_sections),
'groups' => %w(sis_name type groupable_id groupable_type),
'notification_policies' => %w(user_id),
'pseudonyms' => %w(sis_update_data deleted_unique_id sis_source_id crypted_webdav_access_code),

View File

@ -100,11 +100,19 @@ I18n.class_eval do
end
alias_method_chain :localize, :whitespace_removal
def translate_with_default_and_count_magic(key, *args)
# Public: If a localizer has been set, use it to set the locale and then
# delete it.
#
# Returns nothing.
def set_locale_with_localizer
if @localizer
self.locale = @localizer.call
@localizer = nil
end
end
def translate_with_default_and_count_magic(key, *args)
set_locale_with_localizer
default = args.shift if args.first.is_a?(String) || args.size > 1
options = args.shift || {}

View File

@ -13,6 +13,9 @@ def maintain_plugin_symlinks(local_path, plugin_path=nil)
end
maintain_plugin_symlinks('public')
# our new unified build.js and friends require these two symlinks
maintain_plugin_symlinks('public/javascripts')
maintain_plugin_symlinks('public/optimized')
maintain_plugin_symlinks('app/coffeescripts')
maintain_plugin_symlinks('app/views/jst')
maintain_plugin_symlinks('app/stylesheets')

View File

@ -423,8 +423,8 @@ ActionController::Routing::Routes.draw do |map|
account.resources :terms
account.resources :sub_accounts
account.avatars 'avatars', :controller => 'accounts', :action => 'avatars'
account.sis_import 'sis_import', :controller => 'accounts', :action => 'sis_import'
account.sis_import_submit 'sis_import_submit', :controller => 'accounts', :action => 'sis_import_submit'
account.sis_import 'sis_import', :controller => 'accounts', :action => 'sis_import', :conditions => { :method => :get }
account.resources :sis_imports, :controller => 'sis_imports_api', :only => [:create, :show]
account.add_user 'users', :controller => 'users', :action => 'create', :conditions => {:method => :post}
account.confirm_delete_user 'users/:user_id/delete', :controller => 'accounts', :action => 'confirm_delete_user'
account.delete_user 'users/:user_id', :controller => 'accounts', :action => 'remove_user', :conditions => {:method => :delete}
@ -861,6 +861,7 @@ ActionController::Routing::Routes.draw do |map|
end
api.with_options(:controller => :collections) do |collections|
collections.get "collections", :action => :list, :path_name => 'collections'
collections.resources :collections, :path_prefix => "users/:user_id", :name_prefix => "user_", :only => [:index, :create]
collections.resources :collections, :path_prefix => "groups/:group_id", :name_prefix => "group_", :only => [:index, :create]
collections.resources :collections, :except => [:index, :create]

View File

@ -1,13 +1,28 @@
development:
username: put_your_zendesk_username_here
password: put_your_zendesk_password_here
# URL used in redirects, links, etc.
site: http://support.put_your_zendesk_support_site_here.com
auth_token: put_auth_token_from_zendes_website_here
# URL to Zendesk API, not user facing. (HTTPS required)
api_url: https://support.put_your_zendesk_support_site_here.com
auth_token: put_auth_token_from_zendesk_website_here
enabled: false
production:
username: put_your_zendesk_username_here
password: put_your_zendesk_password_here
# URL used in redirects, links, etc.
site: http://support.put_your_zendesk_support_site_here.com
auth_token: put_auth_token_from_zendes_website_here
# URL to Zendesk API, not user facing. (HTTPS required)
api_url: https://support.put_your_zendesk_support_site_here.com
auth_token: put_auth_token_from_zendesk_website_here
enabled: true
test:
username: test.username
password: test.password
# Must be https URL unless it is localhost or 127.0.0.1
site: http://127.0.0.1
api_url: http://127.0.0.1
enabled: false
auth_token:

View File

@ -0,0 +1,44 @@
class AddForeignKeys1 < ActiveRecord::Migration
self.transactional = false
tag :postdeploy
def self.up
if Shard.current.default?
add_foreign_key_if_not_exists :attachments, :scribd_mime_types, :delay_validation => true
add_foreign_key_if_not_exists :notification_policies, :notifications, :delay_validation => true
end
add_foreign_key_if_not_exists :abstract_courses, :accounts, :delay_validation => true
add_foreign_key_if_not_exists :abstract_courses, :enrollment_terms, :delay_validation => true
add_foreign_key_if_not_exists :abstract_courses, :accounts, :column => :root_account_id, :delay_validation => true
add_foreign_key_if_not_exists :access_tokens, :users, :delay_validation => true
add_foreign_key_if_not_exists :account_authorization_configs, :accounts, :delay_validation => true
add_foreign_key_if_not_exists :account_notifications, :accounts, :delay_validation => true
add_foreign_key_if_not_exists :account_reports, :accounts, :delay_validation => true
add_foreign_key_if_not_exists :account_reports, :attachments, :delay_validation => true
add_foreign_key_if_not_exists :account_users, :accounts, :delay_validation => true
add_foreign_key_if_not_exists :accounts, :accounts, :column => :parent_account_id, :delay_validation => true
add_foreign_key_if_not_exists :accounts, :accounts, :column => :root_account_id, :delay_validation => true
add_foreign_key_if_not_exists :alert_criteria, :alerts, :delay_validation => true
end
def self.down
remove_foreign_key_if_exists :alert_criteria, :alerts
remove_foreign_key_if_exists :accounts, :column => :root_account_id
remove_foreign_key_if_exists :accounts, :column => :parent_account_id
remove_foreign_key_if_exists :account_users, :accounts
remove_foreign_key_if_exists :account_reports, :attachments
remove_foreign_key_if_exists :account_reports, :accounts
remove_foreign_key_if_exists :account_notifications, :accounts
remove_foreign_key_if_exists :account_authorization_configs, :accounts
remove_foreign_key_if_exists :access_tokens, :users
remove_foreign_key_if_exists :abstract_courses, :column => :root_account_id
remove_foreign_key_if_exists :abstract_courses, :enrollment_terms
remove_foreign_key_if_exists :abstract_courses, :accounts
if Shard.current.default?
remove_foreign_key_if_exists :notification_policies, :notifications
remove_foreign_key_if_exists :attachments, :scribd_mime_types
end
end
end

View File

@ -1,11 +0,0 @@
class AddSisBatchesIndex < ActiveRecord::Migration
tag :predeploy
def self.up
add_index :sis_batches, [:account_id, :workflow_state, :created_at], :name => "index_sis_batches_for_accounts"
end
def self.down
remove_index :sis_batches, :name => "index_sis_batches_for_accounts"
end
end

View File

@ -0,0 +1,17 @@
class DropSisBatchLogEntries < ActiveRecord::Migration
tag :postdeploy
def self.up
drop_table :sis_batch_log_entries
end
def self.down
create_table "sis_batch_log_entries", :force => true do |t|
t.integer "sis_batch_id", :limit => 8
t.string "log_type"
t.text "text"
t.datetime "created_at"
t.datetime "updated_at"
end
end
end

View File

@ -0,0 +1,28 @@
class AddSisBatchesIndex < ActiveRecord::Migration
tag :predeploy
self.transactional = false
def self.up
# this index may or may not have been created on dev boxes
remove_index :sis_batches, :name => "index_sis_batches_for_accounts" rescue nil
case connection.adapter_name
when 'PostgreSQL'
# select * from sis_batches where account_id = ? and workflow_state = 'created' order by created_at
# select count(*) from sis_batches where account_id = ? and workflow_state = 'created'
# this index is highly optimized for the sis batch job processor workflow
connection.execute "CREATE INDEX CONCURRENTLY index_sis_batches_pending_for_accounts ON sis_batches (account_id, created_at) WHERE workflow_state = 'created'"
# select * from sis_batches where account_id = ? order by created_at desc limit 1
connection.execute "CREATE INDEX CONCURRENTLY index_sis_batches_account_id_created_at ON sis_batches (account_id, created_at)"
else
add_index :sis_batches, [:workflow_state, :account_id, :created_at], :name => "index_sis_batches_pending_for_accounts"
add_index :sis_batches, [:account_id, :created_at], :name => "index_sis_batches_account_id_created_at"
end
end
def self.down
remove_index :sis_batches, :name => "index_sis_batches_pending_for_accounts"
remove_index :sis_batches, :name => "index_sis_batches_account_id_created_at"
end
end

View File

@ -0,0 +1,13 @@
class MigrateToLimitPrivilegesToCourseSection < ActiveRecord::Migration
tag :predeploy, :postdeploy
self.transactional = false
def self.up
Enrollment.find_ids_in_ranges do |(start_id, end_id)|
Enrollment.update_all 'limit_privileges_to_course_section=limit_priveleges_to_course_section', ["limit_privileges_to_course_section IS NULL AND id>=? AND id<=?", start_id, end_id]
end
end
def self.down
end
end

View File

@ -0,0 +1,11 @@
class DropLimitPrivelegesToCourseSectionFromEnrollments < ActiveRecord::Migration
tag :postdeploy
def self.up
remove_column :enrollments, :limit_priveleges_to_course_section
end
def self.down
add_column :enrollments, :limit_priveleges_to_course_section, :boolean
end
end

View File

@ -0,0 +1,14 @@
class FixDefaultLimitPrivilegesToCourseSection < ActiveRecord::Migration
tag :predeploy, :postdeploy
self.transactional = false
def self.up
Enrollment.find_ids_in_ranges do |(start_id, end_id)|
Enrollment.update_all({ :limit_privileges_to_course_section => false }, ["type IN ('StudentEnrollment', 'ObserverEnrollment', 'StudentViewEnrollment', 'DesignerEnrollment') AND id>=? AND id <=?", start_id, end_id])
Enrollment.update_all({ :limit_privileges_to_course_section => false }, ["type IN ('TeacherEnrollment', 'TaEnrollment') AND limit_privileges_to_course_section IS NULL AND id>=? AND id <=?", start_id, end_id])
end
end
def self.down
end
end

View File

@ -0,0 +1,13 @@
class AddBasicIndicesToGroupCategories < ActiveRecord::Migration
tag :postdeploy
def self.up
add_index :group_categories, [:context_id, :context_type], :name => "index_group_categories_on_context"
add_index :group_categories, :role, :name => "index_group_categories_on_role"
end
def self.down
remove_index :group_categories, :name => "index_group_categories_on_context"
remove_index :group_categories, :name => "index_group_categories_on_role"
end
end

View File

@ -37,6 +37,8 @@ module YARD::Templates::Helpers::BaseHelper
else
raise "couldn't find API link for #{args.first}"
end
elsif args.first.is_a?(String) && args.first =~ %r{^api:([^:]+):(.*)}
link_url("#{$1.downcase}.html##{$2.gsub('+', ' ')}", args[1])
else
linkify_without_api(*args)
end

View File

@ -19,6 +19,7 @@
module Api::V1::StreamItem
include Api::V1::Context
include Api::V1::Collection
include Api::V1::Submission
def stream_item_json(stream_item, current_user, session)
data = stream_item.stream_data(current_user.id)
@ -72,22 +73,12 @@ module Api::V1::StreamItem
hash['notification_category'] = data.notification_category
hash['html_url'] = hash['url'] = data.url
when 'Submission'
hash['title'] = data.assignment.try(:title)
hash['grade'] = data.grade
hash['score'] = data.score
hash['html_url'] = course_assignment_submission_url(context_id, data.assignment.id, data.user_id)
hash['submission_comments'] = data.submission_comments.map do |comment|
{
'body' => comment.formatted_body,
'user_name' => comment.user_short_name,
'user_id' => comment.author_id,
}
end unless data.submission_comments.blank?
hash['assignment'] = {
'title' => hash['title'],
'id' => data.assignment.try(:id),
'points_possible' => data.assignment.try(:points_possible),
}
hash.merge! submission_json(Submission.find(data.id), Assignment.find(data.assignment.id), current_user, session, nil, ['submission_comments', 'assignment', 'course', 'html_url'])
# backwards compat from before using submission_json
hash['assignment']['title'] = hash['assignment']['name']
hash['title'] = hash['assignment']['name']
hash['submission_comments'].each {|c| c['body'] = c['comment']}
when /Conference/
hash['web_conference_id'] = data.id
hash['type'] = 'WebConference'
@ -116,9 +107,9 @@ module Api::V1::StreamItem
opts[:contexts] = contexts if contexts.present?
items = @current_user.shard.activate do
scope = @current_user.visible_stream_item_instances(opts)
scope = @current_user.visible_stream_item_instances(opts).scoped(:include => :stream_item)
Api.paginate(scope, self, self.send(paginate_url, @context)).to_a
end
render :json => items.map { |i| stream_item_json(i.stream_item, @current_user, session) }
render :json => items.map(&:stream_item).compact.map { |i| stream_item_json(i, @current_user, session) }
end
end

View File

@ -21,6 +21,7 @@ module Api::V1::Submission
include Api::V1::Assignment
include Api::V1::Attachment
include Api::V1::DiscussionTopics
include Api::V1::Course
def submission_json(submission, assignment, user, session, context = nil, includes = [])
context ||= assignment.context
@ -58,10 +59,18 @@ module Api::V1::Submission
hash['assignment'] = assignment_json(assignment, user, session)
end
if includes.include?("course")
hash['course'] = course_json(submission.context, user, session, ['html_url'], nil)
end
if includes.include?("html_url")
hash['html_url'] = course_assignment_submission_url(submission.context.id, assignment.id, user.id)
end
hash
end
SUBMISSION_JSON_FIELDS = %w(user_id url score grade attempt submission_type submitted_at body assignment_id grade_matches_current_submission).freeze
SUBMISSION_JSON_FIELDS = %w(user_id url score grade attempt submission_type submitted_at body assignment_id grade_matches_current_submission workflow_state).freeze
SUBMISSION_OTHER_FIELDS = %w(attachments discussion_entries)
def submission_attempt_json(attempt, assignment, user, session, version_idx = nil, context = nil)
@ -86,7 +95,7 @@ module Api::V1::Submission
hash = api_json(attempt, user, session, :only => json_fields)
hash['preview_url'] = course_assignment_submission_url(
@context, assignment, attempt[:user_id], 'preview' => '1',
context, assignment, attempt[:user_id], 'preview' => '1',
'version' => version_idx)
unless attempt.media_comment_id.blank?

View File

@ -247,14 +247,9 @@ module AuthenticationMethods
def initiate_cas_login(cas_client = nil)
reset_session_for_login
if @domain_root_account.account_authorization_config.log_in_url.present? && !in_oauth_flow?
session[:exit_frame] = true
delegated_auth_redirect(@domain_root_account.account_authorization_config.log_in_url)
else
config = { :cas_base_url => @domain_root_account.account_authorization_config.auth_base }
cas_client ||= CASClient::Client.new(config)
delegated_auth_redirect(cas_client.add_service_to_login_url(login_url))
end
config = { :cas_base_url => @domain_root_account.account_authorization_config.auth_base }
cas_client ||= CASClient::Client.new(config)
delegated_auth_redirect(cas_client.add_service_to_login_url(login_url))
end
def initiate_saml_login(current_host=nil)

View File

@ -94,6 +94,11 @@ module BasicLTI
hash['custom_canvas_course_id'] = context.id
end
# need to set the locale here (instead of waiting for the first call to
# I18n.t like we usually do), because otherwise we'll have the wrong code
# for the launch_presentation_locale.
I18n.set_locale_with_localizer
hash['context_id'] = context.opaque_identifier(:asset_string)
hash['context_title'] = context.name
hash['context_label'] = context.course_code rescue nil

104
lib/canvas/require_js.rb Normal file
View File

@ -0,0 +1,104 @@
module Canvas
module RequireJs
class << self
def get_binding
binding
end
PATH_REGEX = %r{.*?/javascripts/(plugins/)?(.*)\.js\z}
JS_ROOT = "#{Rails.root}/public/javascripts"
# get all regular canvas (and plugin) bundles
def app_bundles
app_bundles = (
Dir["#{JS_ROOT}/compiled/bundles/*.js"] +
Dir["#{JS_ROOT}/plugins/*/compiled/bundles/*.js"]
).inject({}) { |hash, file|
# plugins have their name prepended, since that's we do the paths
name = file.sub(PATH_REGEX, '\2')
unless name == 'compiled/bundles/common'
hash[name] = { :name => name, :exclude => ['common', 'compiled/tinymce'] }
end
hash
}
# inject any bundle extensions defined in plugins
extensions_for("*").each do |bundle, extensions|
if app_bundles["compiled/bundles/#{bundle}"]
app_bundles["compiled/bundles/#{bundle}"][:include] = extensions
else
$stderr.puts "WARNING: can't extend #{bundle}, it doesn't exist"
end
end
app_bundles.values.sort_by{ |b| b[:name] }.to_json[1...-1].gsub(/,\{/, ",\n {")
end
# get extensions for a particular bundle (or all, if "*")
def extensions_for(bundle, plugin_path = '')
result = {}
Dir["#{JS_ROOT}/plugins/*/compiled/bundles/extensions/#{bundle}.js"].each do |file|
name = file.sub(PATH_REGEX, '\2')
b = name.sub(%r{.*/}, '')
result[b] ||= []
result[b] << plugin_path + name
end
bundle == '*' ? result : (result[bundle.to_s] || [])
end
def paths
@paths ||= {
:common => 'compiled/bundles/common',
:jqueryui => 'vendor/jqueryui',
:uploadify => '../flash/uploadify/jquery.uploadify.v2.1.4',
:use => 'vendor/use',
}.update(plugin_paths).to_json.gsub(/([,{])/, "\\1\n ")
end
def plugin_paths
@plugin_paths ||= begin
Dir['public/javascripts/plugins/*'].inject({}) { |hash, plugin|
plugin = plugin.sub(%r{public/javascripts/plugins/}, '')
hash[plugin] = "plugins/#{plugin}"
hash
}
end
end
def shims
<<-JS.gsub(%r{\A +|^ {8}}, '')
{
'vendor/backbone': {
deps: ['underscore', 'jquery'],
attach: function(_, $){
return Backbone;
}
},
// slick grid shim
'vendor/slickgrid/lib/jquery.event.drag-2.0.min': {
deps: ['jquery'],
attach: '$'
},
'vendor/slickgrid/slick.core': {
deps: ['jquery', 'use!vendor/slickgrid/lib/jquery.event.drag-2.0.min'],
attach: 'Slick'
},
'vendor/slickgrid/slick.grid': {
deps: ['use!vendor/slickgrid/slick.core'],
attach: 'Slick'
},
'vendor/slickgrid/slick.editors': {
deps: ['use!vendor/slickgrid/slick.core'],
attach: 'Slick'
},
'vendor/slickgrid/plugins/slick.rowselectionmodel': {
deps: ['use!vendor/slickgrid/slick.core'],
attach: 'Slick'
}
}
JS
end
end
end
end

View File

@ -29,6 +29,7 @@ module HasContentTags
def check_if_associated_content_tags_need_updating
@associated_content_tags_need_updating = false
return if self.new_record?
return if self.respond_to?(:context_type) && self.context_type == 'SisBatch'
@associated_content_tags_need_updating = true if self.respond_to?(:title_changed?) && self.title_changed?
@associated_content_tags_need_updating = true if self.respond_to?(:name_changed?) && self.name_changed?
@associated_content_tags_need_updating = true if self.respond_to?(:display_name_changed?) && self.display_name_changed?

View File

@ -8,7 +8,13 @@ namespace :js do
Rake::Task['js:generate'].invoke
end
puts "--> executing phantomjs tests"
`erb spec/javascripts/runner.html.erb > spec/javascripts/runner.html`
require 'canvas/require_js'
require 'erubis'
output = Erubis::Eruby.new(File.read("#{Rails.root}/spec/javascripts/runner.html.erb")).
result(Canvas::RequireJs.get_binding)
File.open("#{Rails.root}/spec/javascripts/runner.html", 'w') { |f| f.write(output) }
phantomjs_output = `phantomjs spec/javascripts/support/qunit/test.js file:///#{Dir.pwd}/spec/javascripts/runner.html`
exit_status = $?.exitstatus
puts phantomjs_output
@ -83,25 +89,19 @@ namespace :js do
desc "optimize and build js for production"
task :build do
require 'config/initializers/plugin_symlinks'
require 'parallel'
require 'canvas/require_js'
require 'erubis'
commands = []
commands << ['canvas-lms', "node #{Rails.root}/node_modules/requirejs/bin/r.js -o #{Rails.root}/config/build.js 2>&1"]
output = Erubis::Eruby.new(File.read("#{Rails.root}/config/build.js.erb")).
result(Canvas::RequireJs.get_binding)
File.open("#{Rails.root}/config/build.js", 'w') { |f| f.write(output) }
files = Dir[Rails.root+'vendor/plugins/*/config/build.js']
files.each do |buildfile|
plugin = buildfile.gsub(%r{.*/vendor/plugins/(.*)/config/build\.js}, '\\1')
commands << ["#{plugin} plugin", "node #{Rails.root}/node_modules/requirejs/bin/r.js -o #{buildfile} 2>&1"]
end
Parallel.each(commands, :in_threads => Parallel.processor_count) do |(plugin, command)|
puts "--> Optimizing #{plugin}"
optimize_time = Benchmark.realtime do
output = `#{command}`
raise "Error running js:build: \n#{output}\nABORTING" if $?.exitstatus != 0
end
puts "--> Optimized #{plugin} in #{optimize_time}"
puts "--> Optimizing canvas-lms"
optimize_time = Benchmark.realtime do
output = `node #{Rails.root}/node_modules/requirejs/bin/r.js -o #{Rails.root}/config/build.js 2>&1`
raise "Error running js:build: \n#{output}\nABORTING" if $?.exitstatus != 0
end
puts "--> Optimized canvas-lms in #{optimize_time}"
end
end

View File

@ -13,6 +13,7 @@ module ParallelExclude
'spec/apis/general_api_spec.rb',
'spec/apis/user_content_spec.rb',
'spec/apis/v1/groups_api_spec.rb',
'spec/apis/v1/courses_api_spec.rb',
'spec/apis/auth_spec.rb',
'spec/integration/files_spec.rb',
'spec/lib/acts_as_list.rb',
@ -25,6 +26,8 @@ module ParallelExclude
'spec/models/zip_file_import_spec.rb',
'spec/models/content_migration_spec.rb',
'spec/models/collections_spec.rb',
'spec/lib/canvas/http_spec.rb',
'spec/migrations/count_existing_collection_items_and_followers_spec.rb'
]
test_files = FileList['vendor/plugins/*/spec_canvas/**/*_spec.rb'].exclude('vendor/plugins/*/spec_canvas/selenium/*_spec.rb') + FileList['spec/**/*_spec.rb'].exclude('spec/selenium/**/*_spec.rb')

View File

@ -87,7 +87,7 @@ unless ARGV.any? { |a| a =~ /\Agems/ }
desc "Run all specs in spec directory with RCov (excluding plugin specs)"
Spec::Rake::SpecTask.new(:rcov) do |t|
t.spec_opts = ['--options', "\"#{RAILS_ROOT}/spec/spec.opts\""]
t.spec_files = FileList['spec/**/*/*_spec.rb'].exclude('spec/selenium/*_spec.rb')
t.spec_files = FileList['vendor/plugins/*/spec_canvas/**/*_spec.rb'].exclude('vendor/plugins/*/spec_canvas/selenium/*_spec.rb') + FileList['spec/**/*_spec.rb'].exclude('spec/selenium/**/*_spec.rb')
t.rcov = true
t.rcov_opts = lambda do
IO.readlines("#{RAILS_ROOT}/spec/rcov.opts").map { |l| l.chomp.split " " }.flatten

View File

@ -195,7 +195,7 @@ module Turnitin
responses[asset_string] = object_id ?
{ :object_id => object_id } :
{ :error_code => rcode, :error_message => rmessage, :public_error_message => public_error_message(:rcode) }
{ :error_code => rcode, :error_message => rmessage, :public_error_message => public_error_message(rcode) }
end
responses

View File

@ -120,7 +120,7 @@ define([
}
$form.fillFormData(data, { object_name: "assignment" });
$form.find(":text:first").focus().select();
$("html,body").scrollToVisible($assignment);
//$("html,body").scrollToVisible($assignment);
}
function hideGroupForm() {
var $form = $("#add_group_form");
@ -265,7 +265,7 @@ define([
$assignment.find(".links,.move").css('display', '');
$assignment.toggleClass('group_assignment_editable', assignment.permissions && assignment.permissions.update);
addAssignmentToGroup($("#group_" + assignment.assignment_group_id), $assignment);
$("html,body").scrollToVisible($assignment);
//$("html,body").scrollToVisible($assignment);
}
function addAssignmentToGroup($group, $assignment) {
var data = $assignment.getTemplateData({textValues: ['timestamp', 'title', 'position']}),
@ -843,7 +843,7 @@ define([
addAssignmentToGroup($assignment.parents(".assignment_group"), $assignment);
$assignment.find(".links").hide();
$assignment.loadingImage({image_size: 'small', paddingTop: 5});
$("html,body").scrollToVisible($assignment);
//$("html,body").scrollToVisible($assignment);
var isNew = false;
if($assignment.attr('id') == "assignment_new") {

View File

@ -182,6 +182,14 @@ define([
}, function() {
});
},
itemClass: function(content_tag) {
return content_tag.content_type + "_" + content_tag.content_id;
},
updateAllItemInstances: function(content_tag) {
$(".context_module_item."+modules.itemClass(content_tag)+" .title").each(function() {
$(this).text(content_tag.title);
});
},
editModule: function($module) {
var $form = $("#add_context_module_form");
$form.data('current_module', $module);
@ -279,6 +287,7 @@ define([
$item.removeClass('indent_' + idx);
}
$item.addClass('indent_' + (data.indent || 0));
$item.addClass(modules.itemClass(data));
// don't just tack onto the bottom, put it in its correct position
var $before = null;
$module.find(".context_module_items").children().each(function() {
@ -663,6 +672,7 @@ define([
var $module = $("#context_module_" + data.content_tag.context_module_id);
var $item = modules.addItemToModule($module, data.content_tag);
$module.find(".context_module_items").sortable('refresh');
modules.updateAllItemInstances(data.content_tag);
modules.updateAssignmentData();
$(this).dialog('close');
},

View File

@ -1134,7 +1134,15 @@ define([
cnt++;
}
if(cnt === 0) {
$curve_grade_dialog.errorBox(I18n.t('errors.none_to_update', 'None to Update'));
var errorBox = $curve_grade_dialog.errorBox(
I18n.t('errors.none_to_update', 'None to Update'));
setTimeout(function() {
errorBox.fadeOut(function() {
errorBox.remove();
});
}, 3500);
return false;
}
return data;

View File

@ -805,7 +805,8 @@ define([
options.property_validations = $._addObjectName(options.property_validations, options.object_name);
}
if (options.required) {
$.each(options.required, function(i, name) {
var required = _.result(options, 'required')
$.each(required, function(i, name) {
if (!data[name]) {
if (!errors[name]) {
errors[name] = [];

View File

@ -118,27 +118,12 @@ $(document).ready(function(event) {
var waitTime = 1500;
$.ajaxJSON(location.href, 'GET', {}, function(data) {
state = "updating";
var sis_batch = data.sis_batch;
var sis_batch = data;
var progress = 0;
if(sis_batch) {
progress = Math.max($(".copy_progress").progressbar('option', 'value') || 0, sis_batch.progress);
$(".copy_progress").progressbar('option', 'value', progress);
$("#import_log").empty();
if(sis_batch.sis_batch_log_entries) {
for(var idx in sis_batch.sis_batch_log_entries) {
var entry = sis_batch.sis_batch_log_entries[idx].sis_batch_log_entry;
var lines = entry.text.split("\\n");
if($("#import_log #log_" + entry.id).length == 0) {
var $holder = $("<div id='log_" + entry.id + "'/>");
for(var jdx in lines) {
var $div = $("<div/>");
$div.text(lines[jdx]);
$holder.append($div);
}
$("#import_log").append($holder);
}
}
}
}
if(!sis_batch || sis_batch.workflow_state == 'imported') {
$("#sis_importer").hide();
@ -185,7 +170,7 @@ $(document).ready(function(event) {
$("#sis_importer").formSubmit({
fileUpload: true,
success: function(data) {
if(data && data.sis_batch) {
if(data && data.id) {
startPoll();
} else {
//show error message
@ -206,7 +191,7 @@ $(document).ready(function(event) {
state = "checking";
$.ajaxJSON(location.href, 'GET', {}, function(data) {
state = "nothing";
var sis_batch = data.sis_batch;
var sis_batch = data;
var progress = 0;
if(sis_batch && (sis_batch.workflow_state == "importing" || sis_batch.workflow_state == "created")) {
state = "nothing";

View File

@ -136,6 +136,7 @@ describe "Collections API", :type => :integration do
def create_collections(context)
@c1 = context.collections.create!(:name => 'test1', :visibility => 'private')
@c2 = context.collections.create!(:name => 'test2', :visibility => 'public')
[@c1, @c2]
end
def create_collection_items(user)
@ -642,4 +643,45 @@ describe "Collections API", :type => :integration do
end
end
end
context "unscoped collections" do
before do
user_with_pseudonym
group_model({:group_category => GroupCategory.communities_for(Account.default), :is_public => true})
@group_membership = @group.add_user(@user, 'accepted', true)
end
it "should list all pinnable collections" do
@gc1, @gc2 = create_collections(@group)
@uc1, @uc2 = create_collections(@user)
json = api_call(:get, "/api/v1/collections", { :controller => "collections", :action => "list", :format => "json" })
json.should == [@gc1, @gc2, @uc1, @uc2].sort_by(&:id).reverse.map{ |c| collection_json(c) }
end
it "should create a default collection for each pinnable context" do
json = api_call(:get, "/api/v1/collections", { :controller => "collections", :action => "list", :format => "json" })
json.count.should == 2
json.map{ |j| j['name']}.sort.should == [@user.default_collection_name, @group.default_collection_name].sort
end
it "should not create a default collection for non-community groups" do
@community = @group
@group = group_model
@group.add_user(@user, 'accepted', true)
json = api_call(:get, "/api/v1/collections", { :controller => "collections", :action => "list", :format => "json" })
json.count.should == 2
json.map{ |j| j['name']}.sort.should == [@user.default_collection_name, @community.default_collection_name].sort
end
it "should not return collections the user does not have permission to pin to" do
@community = @group
@community2 = group_model({:group_category => GroupCategory.communities_for(Account.default), :is_public => true})
@gc1, @gc2 = create_collections(@community)
@uc1, @uc2 = create_collections(@user)
@no1, @no2 = create_collections(@community2)
json = api_call(:get, "/api/v1/collections", { :controller => "collections", :action => "list", :format => "json" })
json.should == [@gc1, @gc2, @uc1, @uc2].sort_by(&:id).reverse.map{ |c| collection_json(c) }
end
end
end

View File

@ -743,6 +743,24 @@ describe ConversationsController, :type => :integration do
})
end
it "should auto-mark-as-read if unread" do
conversation = conversation(@bob, :workflow_state => 'unread')
json = api_call(:get, "/api/v1/conversations/#{conversation.conversation_id}?scope=unread",
{ :controller => 'conversations', :action => 'show', :id => conversation.conversation_id.to_s, :scope => 'unread', :format => 'json' })
json["visible"].should be_false
conversation.reload.should be_read
end
it "should not auto-mark-as-read if auto_mark_as_read = false" do
conversation = conversation(@bob, :workflow_state => 'unread')
json = api_call(:get, "/api/v1/conversations/#{conversation.conversation_id}?scope=unread&auto_mark_as_read=0",
{ :controller => 'conversations', :action => 'show', :id => conversation.conversation_id.to_s, :scope => 'unread', :auto_mark_as_read => "0", :format => 'json' })
json["visible"].should be_true
conversation.reload.should be_unread
end
it "should properly flag if starred in the response" do
conversation1 = conversation(@bob)
conversation2 = conversation(@billy, :starred => true)

View File

@ -49,7 +49,7 @@ describe EnrollmentsApiController, :type => :integration do
'id' => new_enrollment.id,
'user_id' => @unenrolled_user.id,
'course_section_id' => @section.id,
'limit_privileges_to_course_section' => true,
'limit_privileges_to_course_section' => false,
'enrollment_state' => 'active',
'course_id' => @course.id,
'type' => 'StudentEnrollment',
@ -65,7 +65,6 @@ describe EnrollmentsApiController, :type => :integration do
new_enrollment.root_account_id.should eql @course.account.id
new_enrollment.user_id.should eql @unenrolled_user.id
new_enrollment.course_section_id.should eql @section.id
new_enrollment.limit_privileges_to_course_section.should eql true
new_enrollment.workflow_state.should eql 'active'
new_enrollment.course_id.should eql @course.id
new_enrollment.should be_an_instance_of StudentEnrollment
@ -207,7 +206,7 @@ describe EnrollmentsApiController, :type => :integration do
'id' => new_enrollment.id,
'user_id' => @unenrolled_user.id,
'course_section_id' => @section.id,
'limit_privileges_to_course_section' => true,
'limit_privileges_to_course_section' => false,
'enrollment_state' => 'active',
'course_id' => @course.id,
'type' => 'StudentEnrollment',
@ -223,7 +222,6 @@ describe EnrollmentsApiController, :type => :integration do
new_enrollment.root_account_id.should eql @course.account.id
new_enrollment.user_id.should eql @unenrolled_user.id
new_enrollment.course_section_id.should eql @section.id
new_enrollment.limit_privileges_to_course_section.should eql true
new_enrollment.workflow_state.should eql 'active'
new_enrollment.course_id.should eql @course.id
new_enrollment.should be_an_instance_of StudentEnrollment

View File

@ -19,6 +19,8 @@
require File.expand_path(File.dirname(__FILE__) + '/../api_spec_helper')
describe UsersController, :type => :integration do
before do
course_with_student(:active_all => true)
end
@ -40,6 +42,10 @@ describe UsersController, :type => :integration do
{ :controller => "users", :action => "activity_stream", :format => 'json' })
json.size.should == 0
google_docs_collaboration_model(:user_id => @user.id)
@context = @course
@topic1 = discussion_topic_model
# introduce a dangling StreamItemInstance
StreamItem.delete_all(:id => @user.visible_stream_item_instances.last.stream_item_id)
json = api_call(:get, "/api/v1/users/activity_stream.json",
{ :controller => "users", :action => "activity_stream", :format => 'json' })
json.size.should == 1
@ -194,6 +200,7 @@ describe UsersController, :type => :integration do
@sub.save!
json = api_call(:get, "/api/v1/users/activity_stream.json",
{ :controller => "users", :action => "activity_stream", :format => 'json' })
json.should == [{
'id' => StreamItem.last.id,
'title' => "assignment 1",
@ -204,24 +211,61 @@ describe UsersController, :type => :integration do
'grade' => '12',
'score' => 12,
'html_url' => "http://www.example.com/courses/#{@course.id}/assignments/#{@assignment.id}/submissions/#{@user.id}",
'workflow_state' => 'graded',
'assignment' => {
'title' => 'assignment 1',
'id' => @assignment.id,
'points_possible' => 14.2,
'assignment_group_id' => @assignment.assignment_group.id,
'course_id' => @course.id,
'description' => @assignment.description,
'due_at' => @assignment.due_at,
'grading_type' => @assignment.grading_type,
'group_category_id' => nil,
'html_url' => "http://www.example.com/courses/#{@course.id}/assignments/#{@assignment.id}",
'muted' => @assignment.muted,
'name' => @assignment.title,
'position' => @assignment.position,
'submission_types' => ['online_text_entry']
},
'assignment_id' => @assignment.id,
'attempt' => nil,
'body' => nil,
'grade_matches_current_submission' => true,
'preview_url' => "http://www.example.com/courses/#{@course.id}/assignments/#{@assignment.id}/submissions/#{@user.id}?preview=1",
'submission_type' => nil,
'submitted_at' => nil,
'url' => nil,
'user_id' => @sub.user_id,
'submission_comments' => [{
'body' => '<p>c1</p>',
'user_name' => 'teacher',
'user_id' => @teacher.id,
'body' => 'c1',
'comment' => 'c1',
'author_name' => 'teacher',
'author_id' => @teacher.id,
'created_at' => @sub.submission_comments[0].created_at.as_json,
},
{
'body' => '<p>c2</p>',
'user_name' => 'User',
'user_id' => @user.id,
'body' => 'c2',
'comment' => 'c2',
'author_name' => 'User',
'author_id' => @user.id,
'created_at' => @sub.submission_comments[1].created_at.as_json,
},],
'course' => {
'name' => @course.name,
'end_at' => @course.end_at,
'account_id' => @course.account_id,
'start_at' => @course.start_at.as_json,
'id' => @course.id,
'course_code' => @course.course_code,
'calendar' => { 'ics' => "http://www.example.com/feeds/calendars/course_#{@course.uuid}.ics" },
'html_url' => course_url(@course, :host => HostUrl.context_host(@course)),
},
'context_type' => 'Course',
'course_id' => @course.id,
}]
@ -248,29 +292,66 @@ describe UsersController, :type => :integration do
'grade' => nil,
'score' => nil,
'html_url' => "http://www.example.com/courses/#{@course.id}/assignments/#{@assignment.id}/submissions/#{@user.id}",
'workflow_state' => 'unsubmitted',
'assignment' => {
'title' => 'assignment 1',
'id' => @assignment.id,
'points_possible' => 14.2,
'assignment_group_id' => @assignment.assignment_group.id,
'course_id' => @course.id,
'description' => @assignment.description,
'due_at' => @assignment.due_at,
'grading_type' => @assignment.grading_type,
'group_category_id' => nil,
'html_url' => "http://www.example.com/courses/#{@course.id}/assignments/#{@assignment.id}",
'muted' => @assignment.muted,
'name' => @assignment.title,
'position' => @assignment.position,
'submission_types' => ['online_text_entry']
},
'assignment_id' => @assignment.id,
'attempt' => nil,
'body' => nil,
'grade_matches_current_submission' => nil,
'preview_url' => "http://www.example.com/courses/#{@course.id}/assignments/#{@assignment.id}/submissions/#{@user.id}?preview=1",
'submission_type' => nil,
'submitted_at' => nil,
'url' => nil,
'user_id' => @sub.user_id,
'submission_comments' => [{
'body' => '<p>c1</p>',
'user_name' => 'teacher',
'user_id' => @teacher.id,
'body' => 'c1',
'comment' => 'c1',
'author_name' => 'teacher',
'author_id' => @teacher.id,
'created_at' => @sub.submission_comments[0].created_at.as_json,
},
{
'body' => '<p>c2</p>',
'user_name' => 'User',
'user_id' => @user.id,
'body' => 'c2',
'comment' => 'c2',
'author_name' => 'User',
'author_id' => @user.id,
'created_at' => @sub.submission_comments[1].created_at.as_json,
},],
'course' => {
'name' => @course.name,
'end_at' => @course.end_at,
'account_id' => @course.account_id,
'start_at' => @course.start_at.as_json,
'id' => @course.id,
'course_code' => @course.course_code,
'calendar' => { 'ics' => "http://www.example.com/feeds/calendars/course_#{@course.uuid}.ics" },
'html_url' => course_url(@course, :host => HostUrl.context_host(@course)),
},
'context_type' => 'Course',
'course_id' => @course.id,
}]
end
it "should format graded Submission without comments" do
@assignment = @course.assignments.create!(:title => 'assignment 1', :description => 'hai', :points_possible => '14.2', :submission_types => 'online_text_entry')
@teacher = User.create!(:name => 'teacher')
@ -280,28 +361,13 @@ describe UsersController, :type => :integration do
@sub.save!
json = api_call(:get, "/api/v1/users/activity_stream.json",
{ :controller => "users", :action => "activity_stream", :format => 'json' })
json.should == [{
'id' => StreamItem.last.id,
'title' => "assignment 1",
'message' => nil,
'type' => 'Submission',
'created_at' => StreamItem.last.created_at.as_json,
'updated_at' => StreamItem.last.updated_at.as_json,
'grade' => '12',
'score' => 12,
'html_url' => "http://www.example.com/courses/#{@course.id}/assignments/#{@assignment.id}/submissions/#{@user.id}",
'assignment' => {
'title' => 'assignment 1',
'id' => @assignment.id,
'points_possible' => 14.2,
},
'context_type' => 'Course',
'course_id' => @course.id,
}]
json[0]['grade'].should == '12'
json[0]['score'].should == 12
json[0]['workflow_state'].should == 'graded'
json[0]['submission_comments'].should == []
end
it "should not format ungraded Submission without comments" do
@assignment = @course.assignments.create!(:title => 'assignment 1', :description => 'hai', :points_possible => '14.2', :submission_types => 'online_text_entry')
@teacher = User.create!(:name => 'teacher')

View File

@ -61,7 +61,8 @@ describe 'Submissions API', :type => :integration do
"submission_type"=>nil,
"submission_comments"=>[],
"grade_matches_current_submission"=>nil,
"score"=>nil
"score"=>nil,
"workflow_state"=>nil
}
end
@ -395,7 +396,8 @@ describe 'Submissions API', :type => :integration do
"created_at"=>comment.created_at.as_json,
"author_name"=>"User",
"author_id"=>student1.id}],
"score"=>13.5}
"score"=>13.5,
"workflow_state"=>"graded"}
# can't access other students' submissions
@user = student2
@ -516,7 +518,8 @@ describe 'Submissions API', :type => :integration do
"user_id"=>student1.id,
"preview_url" => "http://www.example.com/courses/#{@course.id}/assignments/#{a1.id}/submissions/#{student1.id}?preview=1&version=0",
"grade_matches_current_submission"=>nil,
"score"=>nil},
"score"=>nil,
"workflow_state" => "submitted"},
{"grade"=>nil,
"assignment_id" => a1.id,
"media_comment" =>
@ -533,7 +536,8 @@ describe 'Submissions API', :type => :integration do
"user_id"=>student1.id,
"preview_url" => "http://www.example.com/courses/#{@course.id}/assignments/#{a1.id}/submissions/#{student1.id}?preview=1&version=1",
"grade_matches_current_submission"=>nil,
"score"=>nil},
"score"=>nil,
"workflow_state" => "submitted"},
{"grade"=>"A-",
"assignment_id" => a1.id,
"media_comment" =>
@ -558,7 +562,8 @@ describe 'Submissions API', :type => :integration do
"user_id"=>student1.id,
"preview_url" => "http://www.example.com/courses/#{@course.id}/assignments/#{a1.id}/submissions/#{student1.id}?preview=1&version=2",
"grade_matches_current_submission"=>true,
"score"=>13.5}],
"score"=>13.5,
"workflow_state" => "graded"}],
"attempt"=>3,
"url"=>nil,
"submission_type"=>"online_text_entry",
@ -581,7 +586,8 @@ describe 'Submissions API', :type => :integration do
"content-type" => "video/mp4",
"url" => "http://www.example.com/users/#{@user.id}/media_download?entryId=54321&redirect=1&type=mp4",
"display_name" => nil },
"score"=>13.5},
"score"=>13.5,
"workflow_state"=>"graded"},
{"grade"=>"F",
"assignment_id" => a1.id,
"body"=>nil,
@ -598,7 +604,7 @@ describe 'Submissions API', :type => :integration do
"submission_type"=>"online_url",
"user_id"=>student2.id,
"preview_url" => "http://www.example.com/courses/#{@course.id}/assignments/#{a1.id}/submissions/#{student2.id}?preview=1&version=0",
"grade_matches_current_submission"=>true,
"grade_matches_current_submission"=>true,
"attachments" =>
[
{"content-type" => "image/png",
@ -616,7 +622,8 @@ describe 'Submissions API', :type => :integration do
"size" => sub2.attachment.size,
},
],
"score"=>9}],
"score"=>9,
"workflow_state" => "graded"}],
"attempt"=>1,
"url"=>"http://www.instructure.com",
"submission_type"=>"online_url",
@ -642,7 +649,8 @@ describe 'Submissions API', :type => :integration do
"score"=>9,
"rubric_assessment"=>
{"crit2"=>{"comments"=>"Hmm", "points"=>2},
"crit1"=>{"comments"=>nil, "points"=>7}}}]
"crit1"=>{"comments"=>nil, "points"=>7}},
"workflow_state"=>"graded"}]
json.sort_by { |h| h['user_id'] }.should == res.sort_by { |h| h['user_id'] }
end

View File

@ -128,74 +128,6 @@ describe AccountsController do
end
end
describe "SIS imports" do
it "should set batch mode and term if given" do
account_with_admin_logged_in
@account.update_attribute(:allow_sis_import, true)
post 'sis_import_submit', :account_id => @account.id, :import_type => 'instructure_csv', :batch_mode => '1'
batch = SisBatch.last
batch.should_not be_nil
batch.batch_mode.should be_true
batch.batch_mode_term.should be_nil
batch.destroy
post 'sis_import_submit', :account_id => @account.id, :import_type => 'instructure_csv', :batch_mode => '1', :batch_mode_term_id => @account.enrollment_terms.first.id
batch = SisBatch.last
batch.should_not be_nil
batch.batch_mode.should be_true
batch.batch_mode_term.should == @account.enrollment_terms.first
end
it "should set sis stickiness options if given" do
account_with_admin_logged_in
@account.update_attribute(:allow_sis_import, true)
post 'sis_import_submit', :account_id => @account.id,
:import_type => 'instructure_csv'
batch = SisBatch.last
batch.should_not be_nil
batch.options.should == {}
batch.destroy
post 'sis_import_submit', :account_id => @account.id,
:import_type => 'instructure_csv', :override_sis_stickiness => '1'
batch = SisBatch.last
batch.should_not be_nil
batch.options.should == { :override_sis_stickiness => true }
batch.destroy
post 'sis_import_submit', :account_id => @account.id,
:import_type => 'instructure_csv', :override_sis_stickiness => '1',
:add_sis_stickiness => '1'
batch = SisBatch.last
batch.should_not be_nil
batch.options.should == { :override_sis_stickiness => true, :add_sis_stickiness => true }
batch.destroy
post 'sis_import_submit', :account_id => @account.id,
:import_type => 'instructure_csv', :override_sis_stickiness => '1',
:clear_sis_stickiness => '1'
batch = SisBatch.last
batch.should_not be_nil
batch.options.should == { :override_sis_stickiness => true, :clear_sis_stickiness => true }
batch.destroy
post 'sis_import_submit', :account_id => @account.id,
:import_type => 'instructure_csv', :clear_sis_stickiness => '1'
batch = SisBatch.last
batch.should_not be_nil
batch.options.should == {}
batch.destroy
post 'sis_import_submit', :account_id => @account.id,
:import_type => 'instructure_csv', :add_sis_stickiness => '1'
batch = SisBatch.last
batch.should_not be_nil
batch.options.should == {}
batch.destroy
end
end
describe "add_account_user" do
it "should allow adding a new account admin" do
account_with_admin_logged_in

Some files were not shown because too many files have changed in this diff Show More