RuboCop: Lint/IneffectiveAccessModifier
all manual note that for any methods moved/re-indented, I made no other changes (except possibly a remove of a `self.` on a method call to call another now-private method) for ease of review Change-Id: I0cca6644264a0b46a45a1a5c99021c9deb64fca0 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/277532 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Simon Williams <simon@instructure.com> QA-Review: Cody Cutrer <cody@instructure.com> Product-Review: Cody Cutrer <cody@instructure.com>
This commit is contained in:
parent
718513d12f
commit
bb9fe29416
|
@ -59,6 +59,8 @@ Lint/DuplicateMethods:
|
|||
Severity: error
|
||||
Lint/EmptyBlock:
|
||||
Severity: error
|
||||
Lint/IneffectiveAccessModifier:
|
||||
Severity: error
|
||||
Lint/MissingSuper:
|
||||
Severity: error
|
||||
Lint/NonDeterministicRequireOrder:
|
||||
|
|
|
@ -80,6 +80,37 @@ class ApplicationController < ActionController::Base
|
|||
after_action :update_enrollment_last_activity_at
|
||||
set_callback :html_render, :after, :add_csp_for_root
|
||||
|
||||
class << self
|
||||
def instance_id
|
||||
nil
|
||||
end
|
||||
|
||||
def region
|
||||
nil
|
||||
end
|
||||
|
||||
def test_cluster_name
|
||||
nil
|
||||
end
|
||||
|
||||
def test_cluster?
|
||||
false
|
||||
end
|
||||
|
||||
def google_drive_timeout
|
||||
Setting.get('google_drive_timeout', 30).to_i
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def batch_jobs_in_actions(opts = {})
|
||||
batch_opts = opts.delete(:batch)
|
||||
around_action(opts) do |_controller, action|
|
||||
Delayed::Batch.serial_batch(batch_opts || {}, &action)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
add_crumb(proc {
|
||||
title = I18n.t('links.dashboard', 'My Dashboard')
|
||||
crumb = <<-END
|
||||
|
@ -2550,13 +2581,6 @@ class ApplicationController < ActionController::Base
|
|||
common_courses + common_groups
|
||||
end
|
||||
|
||||
def self.batch_jobs_in_actions(opts = {})
|
||||
batch_opts = opts.delete(:batch)
|
||||
around_action(opts) do |_controller, action|
|
||||
Delayed::Batch.serial_batch(batch_opts || {}, &action)
|
||||
end
|
||||
end
|
||||
|
||||
def not_found
|
||||
raise ActionController::RoutingError.new('Not Found')
|
||||
end
|
||||
|
@ -2677,10 +2701,6 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
end
|
||||
|
||||
def self.google_drive_timeout
|
||||
Setting.get('google_drive_timeout', 30).to_i
|
||||
end
|
||||
|
||||
def google_drive_connection
|
||||
return @google_drive_connection if @google_drive_connection
|
||||
|
||||
|
@ -2722,22 +2742,6 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
end
|
||||
|
||||
def self.instance_id
|
||||
nil
|
||||
end
|
||||
|
||||
def self.region
|
||||
nil
|
||||
end
|
||||
|
||||
def self.test_cluster_name
|
||||
nil
|
||||
end
|
||||
|
||||
def self.test_cluster?
|
||||
false
|
||||
end
|
||||
|
||||
def setup_live_events_context
|
||||
proc = -> do
|
||||
ctx = {}
|
||||
|
|
|
@ -34,6 +34,39 @@
|
|||
class Mutations::BaseMutation < GraphQL::Schema::Mutation
|
||||
field :errors, [Types::ValidationErrorType], null: true
|
||||
|
||||
class << self
|
||||
# this is copied from GraphQL::Schema::RelayClassicMutation - it moves all
|
||||
# the arguments defined with the ruby-graphql DSL into an auto-generated
|
||||
# input type
|
||||
#
|
||||
# this is a bit more convenient that defining the types by hand
|
||||
#
|
||||
# we could base this class on RelayClassicMutation but then we get the weird
|
||||
# {client_mutation_id} fields that we don't care about
|
||||
def field_options
|
||||
super.tap do |res|
|
||||
res[:arguments].clear
|
||||
res[:arguments][:input] = { type: input_type, required: true }
|
||||
end
|
||||
end
|
||||
|
||||
def input_type
|
||||
@input_type ||= begin
|
||||
mutation_args = arguments
|
||||
mutation_name = graphql_name
|
||||
mutation_class = self
|
||||
Class.new(Types::BaseInputObject) do
|
||||
graphql_name("#{mutation_name}Input")
|
||||
description("Autogenerated input type of #{mutation_name}")
|
||||
mutation(mutation_class)
|
||||
mutation_args.each do |_name, arg|
|
||||
add_argument(arg)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def current_user
|
||||
context[:current_user]
|
||||
end
|
||||
|
@ -75,35 +108,4 @@ class Mutations::BaseMutation < GraphQL::Schema::Mutation
|
|||
}
|
||||
}
|
||||
end
|
||||
|
||||
# this is copied from GraphQL::Schema::RelayClassicMutation - it moves all
|
||||
# the arguments defined with the ruby-graphql DSL into an auto-generated
|
||||
# input type
|
||||
#
|
||||
# this is a bit more convenient that defining the types by hand
|
||||
#
|
||||
# we could base this class on RelayClassicMutation but then we get the weird
|
||||
# {client_mutation_id} fields that we don't care about
|
||||
def self.field_options
|
||||
super.tap do |res|
|
||||
res[:arguments].clear
|
||||
res[:arguments][:input] = { type: input_type, required: true }
|
||||
end
|
||||
end
|
||||
|
||||
def self.input_type
|
||||
@input_type ||= begin
|
||||
mutation_args = arguments
|
||||
mutation_name = graphql_name
|
||||
mutation_class = self
|
||||
Class.new(Types::BaseInputObject) do
|
||||
graphql_name("#{mutation_name}Input")
|
||||
description("Autogenerated input type of #{mutation_name}")
|
||||
mutation(mutation_class)
|
||||
mutation_args.each do |_name, arg|
|
||||
add_argument(arg)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -50,6 +50,74 @@ class BigBlueButtonConference < WebConference
|
|||
visible: false
|
||||
}
|
||||
|
||||
class << self
|
||||
def send_request(action, options, use_fallback_config: false)
|
||||
url_str = generate_request(action, options, use_fallback_config: use_fallback_config)
|
||||
http_response = nil
|
||||
Canvas.timeout_protection("big_blue_button") do
|
||||
logger.debug "big blue button api call: #{url_str}"
|
||||
http_response = CanvasHttp.get(url_str, redirect_limit: 5)
|
||||
end
|
||||
case http_response
|
||||
when Net::HTTPSuccess
|
||||
response = xml_to_hash(http_response.body)
|
||||
if response[:returncode] == 'SUCCESS'
|
||||
return response
|
||||
else
|
||||
logger.error "big blue button api error #{response[:message]} (#{response[:messageKey]})"
|
||||
end
|
||||
else
|
||||
logger.error "big blue button http error #{http_response}"
|
||||
end
|
||||
nil
|
||||
rescue
|
||||
logger.error "big blue button unhandled exception #{$ERROR_INFO}"
|
||||
nil
|
||||
end
|
||||
|
||||
def generate_request(action, options, use_fallback_config: false)
|
||||
config_to_use = (use_fallback_config && fallback_config.presence) || config
|
||||
query_string = options.to_query
|
||||
query_string << ("&checksum=" + Digest::SHA1.hexdigest(action.to_s + query_string + config_to_use[:secret_dec]))
|
||||
"https://#{config_to_use[:domain]}/bigbluebutton/api/#{action}?#{query_string}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fallback_config
|
||||
Canvas::Plugin.find(:big_blue_button_fallback).settings&.with_indifferent_access
|
||||
end
|
||||
|
||||
def xml_to_hash(xml_string)
|
||||
doc = Nokogiri::XML(xml_string)
|
||||
# assumes the top level value will be a hash
|
||||
xml_to_value(doc.root)
|
||||
end
|
||||
|
||||
def xml_to_value(node)
|
||||
child_elements = node.element_children
|
||||
|
||||
# if there are no children at all, then this is an empty node
|
||||
if node.children.empty?
|
||||
nil
|
||||
# If no child_elements, this is probably a text node, so just return its content
|
||||
elsif child_elements.empty?
|
||||
node.content
|
||||
# The BBB API follows the pattern where a plural element (ie <bars>)
|
||||
# contains many singular elements (ie <bar>) and nothing else. Detect this
|
||||
# and return an array to be assigned to the plural element.
|
||||
# It excludes the playback node as all of them may be showing different content.
|
||||
elsif node.name.singularize == child_elements.first.name || node.name == "playback"
|
||||
child_elements.map { |child| xml_to_value(child) }
|
||||
# otherwise, make a hash of the child elements
|
||||
else
|
||||
child_elements.each_with_object({}) do |child, hash|
|
||||
hash[child.name.to_sym] = xml_to_value(child)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def initiate_conference
|
||||
return conference_key if conference_key && !retouch?
|
||||
|
||||
|
@ -247,75 +315,10 @@ class BigBlueButtonConference < WebConference
|
|||
self.class.generate_request(*args)
|
||||
end
|
||||
|
||||
def self.fallback_config
|
||||
Canvas::Plugin.find(:big_blue_button_fallback).settings&.with_indifferent_access
|
||||
end
|
||||
|
||||
def self.generate_request(action, options, use_fallback_config: false)
|
||||
config_to_use = (use_fallback_config && fallback_config.presence) || config
|
||||
query_string = options.to_query
|
||||
query_string << ("&checksum=" + Digest::SHA1.hexdigest(action.to_s + query_string + config_to_use[:secret_dec]))
|
||||
"https://#{config_to_use[:domain]}/bigbluebutton/api/#{action}?#{query_string}"
|
||||
end
|
||||
|
||||
def send_request(action, options)
|
||||
self.class.send_request(action, options, use_fallback_config: use_fallback_config?)
|
||||
end
|
||||
|
||||
def self.send_request(action, options, use_fallback_config: false)
|
||||
url_str = generate_request(action, options, use_fallback_config: use_fallback_config)
|
||||
http_response = nil
|
||||
Canvas.timeout_protection("big_blue_button") do
|
||||
logger.debug "big blue button api call: #{url_str}"
|
||||
http_response = CanvasHttp.get(url_str, redirect_limit: 5)
|
||||
end
|
||||
|
||||
case http_response
|
||||
when Net::HTTPSuccess
|
||||
response = xml_to_hash(http_response.body)
|
||||
if response[:returncode] == 'SUCCESS'
|
||||
return response
|
||||
else
|
||||
logger.error "big blue button api error #{response[:message]} (#{response[:messageKey]})"
|
||||
end
|
||||
else
|
||||
logger.error "big blue button http error #{http_response}"
|
||||
end
|
||||
nil
|
||||
rescue
|
||||
logger.error "big blue button unhandled exception #{$ERROR_INFO}"
|
||||
nil
|
||||
end
|
||||
|
||||
def self.xml_to_hash(xml_string)
|
||||
doc = Nokogiri::XML(xml_string)
|
||||
# assumes the top level value will be a hash
|
||||
xml_to_value(doc.root)
|
||||
end
|
||||
|
||||
def self.xml_to_value(node)
|
||||
child_elements = node.element_children
|
||||
|
||||
# if there are no children at all, then this is an empty node
|
||||
if node.children.empty?
|
||||
nil
|
||||
# If no child_elements, this is probably a text node, so just return its content
|
||||
elsif child_elements.empty?
|
||||
node.content
|
||||
# The BBB API follows the pattern where a plural element (ie <bars>)
|
||||
# contains many singular elements (ie <bar>) and nothing else. Detect this
|
||||
# and return an array to be assigned to the plural element.
|
||||
# It excludes the playback node as all of them may be showing different content.
|
||||
elsif node.name.singularize == child_elements.first.name || node.name == "playback"
|
||||
child_elements.map { |child| xml_to_value(child) }
|
||||
# otherwise, make a hash of the child elements
|
||||
else
|
||||
child_elements.each_with_object({}) do |child, hash|
|
||||
hash[child.name.to_sym] = xml_to_value(child)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def filter_duration(recording_formats)
|
||||
# This is a filter to take the duration from any of the playback formats that include a value in length.
|
||||
# As not all the formats are the actual recording, identify the first one that has :length <> nil
|
||||
|
|
|
@ -77,6 +77,135 @@ class ContextExternalTool < ActiveRecord::Base
|
|||
can :read and can :update and can :delete and can :update_manually
|
||||
end
|
||||
|
||||
class << self
|
||||
# because global navigation tool visibility can depend on a user having particular permissions now
|
||||
# this needs to expand from being a simple "admins/members" check to something more full-fledged
|
||||
# this will return a hash with the original visibility setting alone with a computed list of
|
||||
# all other permissions (as needed) granted by the current context so all users with the same
|
||||
# set of computed permissions will share the same global nav cache
|
||||
def global_navigation_granted_permissions(root_account:, user:, context:, session: nil)
|
||||
return { :original_visibility => 'members' } unless user
|
||||
|
||||
permissions_hash = {}
|
||||
# still use the original visibility setting
|
||||
permissions_hash[:original_visibility] = Rails.cache.fetch_with_batched_keys(
|
||||
['external_tools/global_navigation/visibility', root_account.asset_string].cache_key,
|
||||
batch_object: user, batched_keys: [:enrollments, :account_users]
|
||||
) do
|
||||
# let them see admin level tools if there are any courses they can manage
|
||||
if root_account.grants_right?(user, :manage_content) ||
|
||||
GuardRail.activate(:secondary) { Course.manageable_by_user(user.id, false).not_deleted.where(:root_account_id => root_account).exists? }
|
||||
'admins'
|
||||
else
|
||||
'members'
|
||||
end
|
||||
end
|
||||
required_permissions = global_navigation_permissions_to_check(root_account)
|
||||
required_permissions.each do |permission|
|
||||
# run permission checks against the context if any of the tools are configured to require them
|
||||
permissions_hash[permission] = context.grants_right?(user, session, permission)
|
||||
end
|
||||
permissions_hash
|
||||
end
|
||||
|
||||
def filtered_global_navigation_tools(root_account, granted_permissions)
|
||||
tools = all_global_navigation_tools(root_account)
|
||||
|
||||
if granted_permissions[:original_visibility] != 'admins'
|
||||
# reject the admin only tools
|
||||
tools.reject! { |tool| tool.global_navigation[:visibility] == 'admins' }
|
||||
end
|
||||
# check against permissions if needed
|
||||
tools.select! do |tool|
|
||||
required_permissions_str = tool.extension_setting(:global_navigation, 'required_permissions')
|
||||
if required_permissions_str
|
||||
required_permissions_str.split(",").map(&:to_sym).all? { |p| granted_permissions[p] }
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
tools
|
||||
end
|
||||
|
||||
# returns a key composed of the updated_at times for all the tools visible to someone with the granted_permissions
|
||||
# i.e. if it hasn't changed since the last time we rendered the erb template for the menu then we can re-use the same html
|
||||
def global_navigation_menu_render_cache_key(root_account, granted_permissions)
|
||||
# only re-render the menu if one of the global nav tools has changed
|
||||
perm_key = key_for_granted_permissions(granted_permissions)
|
||||
compiled_key = ['external_tools/global_navigation/compiled_tools_updated_at', root_account.global_asset_string, perm_key].cache_key
|
||||
|
||||
# shameless plug for the cache register system:
|
||||
# batching with the :global_navigation key means that we can easily mark every one of these for recalculation
|
||||
# in the :check_global_navigation_cache callback instead of having to explicitly delete multiple keys
|
||||
# (which was fine when we only had two visibility settings but not when an infinite combination of permissions is in play)
|
||||
Rails.cache.fetch_with_batched_keys(compiled_key, batch_object: root_account, batched_keys: :global_navigation) do
|
||||
tools = filtered_global_navigation_tools(root_account, granted_permissions)
|
||||
Digest::MD5.hexdigest(tools.sort.map(&:cache_key).join('/'))
|
||||
end
|
||||
end
|
||||
|
||||
def visible?(visibility, user, context, session = nil)
|
||||
visibility = visibility.to_s
|
||||
return true unless %w(public members admins).include?(visibility)
|
||||
return true if visibility == 'public'
|
||||
return true if visibility == 'members' &&
|
||||
context.grants_any_right?(user, session, :participate_as_student, :read_as_admin)
|
||||
return true if visibility == 'admins' && context.grants_right?(user, session, :read_as_admin)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def editor_button_json(tools, context, user, session = nil)
|
||||
tools.select! { |tool| visible?(tool.editor_button['visibility'], user, context, session) }
|
||||
markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new({ link_attributes: { target: '_blank' } }))
|
||||
tools.map do |tool|
|
||||
{
|
||||
:name => tool.label_for(:editor_button, I18n.locale),
|
||||
:id => tool.id,
|
||||
:favorite => tool.is_rce_favorite_in_context?(context),
|
||||
:url => tool.editor_button(:url),
|
||||
:icon_url => tool.editor_button(:icon_url),
|
||||
:canvas_icon_class => tool.editor_button(:canvas_icon_class),
|
||||
:width => tool.editor_button(:selection_width),
|
||||
:height => tool.editor_button(:selection_height),
|
||||
:use_tray => tool.editor_button(:use_tray) == "true",
|
||||
:description => if tool.description
|
||||
Sanitize.clean(markdown.render(tool.description), CanvasSanitize::SANITIZE)
|
||||
else
|
||||
""
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def context_id_for(asset, shard)
|
||||
str = asset.asset_string.to_s
|
||||
raise "Empty value" if str.blank?
|
||||
|
||||
Canvas::Security.hmac_sha1(str, shard.settings[:encryption_key])
|
||||
end
|
||||
|
||||
def global_navigation_permissions_to_check(root_account)
|
||||
# look at the list of tools that are configured for the account and see if any are asking for permissions checks
|
||||
Rails.cache.fetch_with_batched_keys("external_tools/global_navigation/permissions_to_check", batch_object: root_account, batched_keys: :global_navigation) do
|
||||
tools = all_global_navigation_tools(root_account)
|
||||
tools.map { |tool| tool.extension_setting(:global_navigation, 'required_permissions')&.split(",")&.map(&:to_sym) }.compact.flatten.uniq
|
||||
end
|
||||
end
|
||||
|
||||
def all_global_navigation_tools(root_account)
|
||||
RequestCache.cache('global_navigation_tools', root_account) do # prevent re-querying
|
||||
root_account.context_external_tools.active.having_setting(:global_navigation).to_a
|
||||
end
|
||||
end
|
||||
|
||||
def key_for_granted_permissions(granted_permissions)
|
||||
Digest::MD5.hexdigest(granted_permissions.sort.flatten.join(",")) # for consistency's sake
|
||||
end
|
||||
end
|
||||
|
||||
Lti::ResourcePlacement::PLACEMENTS.each do |type|
|
||||
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
||||
def #{type}(setting=nil)
|
||||
|
@ -1089,13 +1218,6 @@ class ContextExternalTool < ActiveRecord::Base
|
|||
|
||||
private
|
||||
|
||||
def self.context_id_for(asset, shard)
|
||||
str = asset.asset_string.to_s
|
||||
raise "Empty value" if str.blank?
|
||||
|
||||
Canvas::Security.hmac_sha1(str, shard.settings[:encryption_key])
|
||||
end
|
||||
|
||||
def check_global_navigation_cache
|
||||
if self.context.is_a?(Account) && self.context.root_account?
|
||||
self.context.clear_cache_key(:global_navigation) # it's hard to know exactly _what_ changed so clear all initial global nav caches at once
|
||||
|
@ -1107,122 +1229,4 @@ class ContextExternalTool < ActiveRecord::Base
|
|||
self.context.clear_tool_domain_cache
|
||||
end
|
||||
end
|
||||
|
||||
# because global navigation tool visibility can depend on a user having particular permissions now
|
||||
# this needs to expand from being a simple "admins/members" check to something more full-fledged
|
||||
# this will return a hash with the original visibility setting alone with a computed list of
|
||||
# all other permissions (as needed) granted by the current context so all users with the same
|
||||
# set of computed permissions will share the same global nav cache
|
||||
def self.global_navigation_granted_permissions(root_account:, user:, context:, session: nil)
|
||||
return { :original_visibility => 'members' } unless user
|
||||
|
||||
permissions_hash = {}
|
||||
# still use the original visibility setting
|
||||
permissions_hash[:original_visibility] = Rails.cache.fetch_with_batched_keys(
|
||||
['external_tools/global_navigation/visibility', root_account.asset_string].cache_key,
|
||||
batch_object: user, batched_keys: [:enrollments, :account_users]
|
||||
) do
|
||||
# let them see admin level tools if there are any courses they can manage
|
||||
if root_account.grants_right?(user, :manage_content) ||
|
||||
GuardRail.activate(:secondary) { Course.manageable_by_user(user.id, false).not_deleted.where(:root_account_id => root_account).exists? }
|
||||
'admins'
|
||||
else
|
||||
'members'
|
||||
end
|
||||
end
|
||||
required_permissions = self.global_navigation_permissions_to_check(root_account)
|
||||
required_permissions.each do |permission|
|
||||
# run permission checks against the context if any of the tools are configured to require them
|
||||
permissions_hash[permission] = context.grants_right?(user, session, permission)
|
||||
end
|
||||
permissions_hash
|
||||
end
|
||||
|
||||
def self.global_navigation_permissions_to_check(root_account)
|
||||
# look at the list of tools that are configured for the account and see if any are asking for permissions checks
|
||||
Rails.cache.fetch_with_batched_keys("external_tools/global_navigation/permissions_to_check", batch_object: root_account, batched_keys: :global_navigation) do
|
||||
tools = self.all_global_navigation_tools(root_account)
|
||||
tools.map { |tool| tool.extension_setting(:global_navigation, 'required_permissions')&.split(",")&.map(&:to_sym) }.compact.flatten.uniq
|
||||
end
|
||||
end
|
||||
|
||||
def self.all_global_navigation_tools(root_account)
|
||||
RequestCache.cache('global_navigation_tools', root_account) do # prevent re-querying
|
||||
root_account.context_external_tools.active.having_setting(:global_navigation).to_a
|
||||
end
|
||||
end
|
||||
|
||||
def self.filtered_global_navigation_tools(root_account, granted_permissions)
|
||||
tools = self.all_global_navigation_tools(root_account)
|
||||
|
||||
if granted_permissions[:original_visibility] != 'admins'
|
||||
# reject the admin only tools
|
||||
tools.reject! { |tool| tool.global_navigation[:visibility] == 'admins' }
|
||||
end
|
||||
# check against permissions if needed
|
||||
tools.select! do |tool|
|
||||
required_permissions_str = tool.extension_setting(:global_navigation, 'required_permissions')
|
||||
if required_permissions_str
|
||||
required_permissions_str.split(",").map(&:to_sym).all? { |p| granted_permissions[p] }
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
tools
|
||||
end
|
||||
|
||||
def self.key_for_granted_permissions(granted_permissions)
|
||||
Digest::MD5.hexdigest(granted_permissions.sort.flatten.join(",")) # for consistency's sake
|
||||
end
|
||||
|
||||
# returns a key composed of the updated_at times for all the tools visible to someone with the granted_permissions
|
||||
# i.e. if it hasn't changed since the last time we rendered the erb template for the menu then we can re-use the same html
|
||||
def self.global_navigation_menu_render_cache_key(root_account, granted_permissions)
|
||||
# only re-render the menu if one of the global nav tools has changed
|
||||
perm_key = key_for_granted_permissions(granted_permissions)
|
||||
compiled_key = ['external_tools/global_navigation/compiled_tools_updated_at', root_account.global_asset_string, perm_key].cache_key
|
||||
|
||||
# shameless plug for the cache register system:
|
||||
# batching with the :global_navigation key means that we can easily mark every one of these for recalculation
|
||||
# in the :check_global_navigation_cache callback instead of having to explicitly delete multiple keys
|
||||
# (which was fine when we only had two visibility settings but not when an infinite combination of permissions is in play)
|
||||
Rails.cache.fetch_with_batched_keys(compiled_key, batch_object: root_account, batched_keys: :global_navigation) do
|
||||
tools = self.filtered_global_navigation_tools(root_account, granted_permissions)
|
||||
Digest::MD5.hexdigest(tools.sort.map(&:cache_key).join('/'))
|
||||
end
|
||||
end
|
||||
|
||||
def self.visible?(visibility, user, context, session = nil)
|
||||
visibility = visibility.to_s
|
||||
return true unless %w(public members admins).include?(visibility)
|
||||
return true if visibility == 'public'
|
||||
return true if visibility == 'members' &&
|
||||
context.grants_any_right?(user, session, :participate_as_student, :read_as_admin)
|
||||
return true if visibility == 'admins' && context.grants_right?(user, session, :read_as_admin)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def self.editor_button_json(tools, context, user, session = nil)
|
||||
tools.select! { |tool| visible?(tool.editor_button['visibility'], user, context, session) }
|
||||
markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new({ link_attributes: { target: '_blank' } }))
|
||||
tools.map do |tool|
|
||||
{
|
||||
:name => tool.label_for(:editor_button, I18n.locale),
|
||||
:id => tool.id,
|
||||
:favorite => tool.is_rce_favorite_in_context?(context),
|
||||
:url => tool.editor_button(:url),
|
||||
:icon_url => tool.editor_button(:icon_url),
|
||||
:canvas_icon_class => tool.editor_button(:canvas_icon_class),
|
||||
:width => tool.editor_button(:selection_width),
|
||||
:height => tool.editor_button(:selection_height),
|
||||
:use_tray => tool.editor_button(:use_tray) == "true",
|
||||
:description => if tool.description
|
||||
Sanitize.clean(markdown.render(tool.description), CanvasSanitize::SANITIZE)
|
||||
else
|
||||
""
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -54,48 +54,50 @@ class ExternalIntegrationKey < ActiveRecord::Base
|
|||
can :write
|
||||
end
|
||||
|
||||
def self.indexed_keys_for(context)
|
||||
keys = context.external_integration_keys.index_by(&:key_type)
|
||||
key_types.each do |key_type|
|
||||
next if keys.key?(key_type)
|
||||
class << self
|
||||
def indexed_keys_for(context)
|
||||
keys = context.external_integration_keys.index_by(&:key_type)
|
||||
key_types.each do |key_type|
|
||||
next if keys.key?(key_type)
|
||||
|
||||
keys[key_type] = ExternalIntegrationKey.new
|
||||
keys[key_type].context = context
|
||||
keys[key_type].key_type = key_type
|
||||
keys[key_type] = ExternalIntegrationKey.new
|
||||
keys[key_type].context = context
|
||||
keys[key_type].key_type = key_type
|
||||
end
|
||||
keys
|
||||
end
|
||||
keys
|
||||
end
|
||||
|
||||
def self.key_type(name, options = {})
|
||||
key_types_known << name
|
||||
key_type_labels[name] = options[:label]
|
||||
key_type_rights[name] = options[:rights] || {}
|
||||
end
|
||||
|
||||
def self.label_for(key_type)
|
||||
key_label = key_type_labels[key_type]
|
||||
if key_label.respond_to? :call
|
||||
key_label.call
|
||||
else
|
||||
key_label
|
||||
def key_type(name, options = {})
|
||||
key_types_known << name
|
||||
key_type_labels[name] = options[:label]
|
||||
key_type_rights[name] = options[:rights] || {}
|
||||
end
|
||||
end
|
||||
|
||||
def self.key_types
|
||||
key_types_known.to_a
|
||||
end
|
||||
def label_for(key_type)
|
||||
key_label = key_type_labels[key_type]
|
||||
if key_label.respond_to? :call
|
||||
key_label.call
|
||||
else
|
||||
key_label
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def key_types
|
||||
key_types_known.to_a
|
||||
end
|
||||
|
||||
def self.key_types_known
|
||||
@key_types_known ||= Set.new
|
||||
end
|
||||
def key_type_rights
|
||||
@key_type_rights ||= {}
|
||||
end
|
||||
|
||||
def self.key_type_labels
|
||||
@key_types_labels ||= {}
|
||||
end
|
||||
private
|
||||
|
||||
def self.key_type_rights
|
||||
@key_type_rights ||= {}
|
||||
def key_types_known
|
||||
@key_types_known ||= Set.new
|
||||
end
|
||||
|
||||
def key_type_labels
|
||||
@key_type_labels ||= {}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -24,75 +24,77 @@ module Importers
|
|||
class AttachmentImporter < Importer
|
||||
self.item_class = Attachment
|
||||
|
||||
def self.process_migration(data, migration)
|
||||
created_usage_rights_map = {}
|
||||
attachments = data['file_map'] ? data['file_map'] : {}
|
||||
attachments = attachments.with_indifferent_access
|
||||
attachments.values.each do |att|
|
||||
if !att['is_folder'] && (migration.import_object?("attachments", att['migration_id']) || migration.import_object?("files", att['migration_id']))
|
||||
begin
|
||||
import_from_migration(att, migration.context, migration, nil, created_usage_rights_map)
|
||||
rescue
|
||||
migration.add_import_warning(I18n.t('#migration.file_type', "File"), (att[:display_name] || att[:path_name]), $!)
|
||||
class << self
|
||||
def process_migration(data, migration)
|
||||
created_usage_rights_map = {}
|
||||
attachments = data['file_map'] ? data['file_map'] : {}
|
||||
attachments = attachments.with_indifferent_access
|
||||
attachments.values.each do |att|
|
||||
if !att['is_folder'] && (migration.import_object?("attachments", att['migration_id']) || migration.import_object?("files", att['migration_id']))
|
||||
begin
|
||||
import_from_migration(att, migration.context, migration, nil, created_usage_rights_map)
|
||||
rescue
|
||||
migration.add_import_warning(I18n.t('#migration.file_type', "File"), (att[:display_name] || att[:path_name]), $!)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if data[:locked_folders]
|
||||
data[:locked_folders].each do |path|
|
||||
# TODO i18n
|
||||
if (f = migration.context.active_folders.where(full_name: "course files/#{path}").first)
|
||||
f.locked = true
|
||||
f.save
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if data[:hidden_folders]
|
||||
data[:hidden_folders].each do |path|
|
||||
# TODO i18n
|
||||
if (f = migration.context.active_folders.where(full_name: "course files/#{path}").first)
|
||||
f.workflow_state = 'hidden'
|
||||
f.save
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if data[:locked_folders]
|
||||
data[:locked_folders].each do |path|
|
||||
# TODO i18n
|
||||
if (f = migration.context.active_folders.where(full_name: "course files/#{path}").first)
|
||||
f.locked = true
|
||||
f.save
|
||||
end
|
||||
private
|
||||
|
||||
def import_from_migration(hash, context, migration, item = nil, created_usage_rights_map = {})
|
||||
return nil if hash[:files_to_import] && !hash[:files_to_import][hash[:migration_id]]
|
||||
|
||||
item ||= Attachment.where(context_type: context.class.to_s, context_id: context, id: hash[:id]).first
|
||||
item ||= Attachment.where(context_type: context.class.to_s, context_id: context, migration_id: hash[:migration_id]).first # if hash[:migration_id]
|
||||
item ||= Attachment.find_from_path(hash[:path_name], context)
|
||||
if item
|
||||
item.mark_as_importing!(migration)
|
||||
item.context = context
|
||||
item.migration_id = hash[:migration_id]
|
||||
item.locked = true if hash[:locked]
|
||||
item.lock_at = Canvas::Migration::MigratorHelper.get_utc_time_from_timestamp(hash[:lock_at]) if hash[:lock_at]
|
||||
item.unlock_at = Canvas::Migration::MigratorHelper.get_utc_time_from_timestamp(hash[:unlock_at]) if hash[:unlock_at]
|
||||
item.file_state = 'hidden' if hash[:hidden]
|
||||
item.display_name = hash[:display_name] if hash[:display_name]
|
||||
item.usage_rights_id = find_or_create_usage_rights(context, hash[:usage_rights], created_usage_rights_map) if hash[:usage_rights]
|
||||
item.set_publish_state_for_usage_rights unless hash[:locked]
|
||||
item.save_without_broadcasting!
|
||||
item.handle_duplicates(:rename)
|
||||
migration.add_imported_item(item)
|
||||
end
|
||||
item
|
||||
end
|
||||
|
||||
if data[:hidden_folders]
|
||||
data[:hidden_folders].each do |path|
|
||||
# TODO i18n
|
||||
if (f = migration.context.active_folders.where(full_name: "course files/#{path}").first)
|
||||
f.workflow_state = 'hidden'
|
||||
f.save
|
||||
end
|
||||
end
|
||||
def find_or_create_usage_rights(context, usage_rights_hash, created_usage_rights_map)
|
||||
attrs = usage_rights_hash.slice('use_justification', 'license', 'legal_copyright')
|
||||
key = attrs.values_at('use_justification', 'license', 'legal_copyright').join('/')
|
||||
id = created_usage_rights_map[key]
|
||||
return id if id
|
||||
|
||||
usage_rights = context.usage_rights.create!(attrs)
|
||||
created_usage_rights_map[key] = usage_rights.id
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.import_from_migration(hash, context, migration, item = nil, created_usage_rights_map = {})
|
||||
return nil if hash[:files_to_import] && !hash[:files_to_import][hash[:migration_id]]
|
||||
|
||||
item ||= Attachment.where(context_type: context.class.to_s, context_id: context, id: hash[:id]).first
|
||||
item ||= Attachment.where(context_type: context.class.to_s, context_id: context, migration_id: hash[:migration_id]).first # if hash[:migration_id]
|
||||
item ||= Attachment.find_from_path(hash[:path_name], context)
|
||||
if item
|
||||
item.mark_as_importing!(migration)
|
||||
item.context = context
|
||||
item.migration_id = hash[:migration_id]
|
||||
item.locked = true if hash[:locked]
|
||||
item.lock_at = Canvas::Migration::MigratorHelper.get_utc_time_from_timestamp(hash[:lock_at]) if hash[:lock_at]
|
||||
item.unlock_at = Canvas::Migration::MigratorHelper.get_utc_time_from_timestamp(hash[:unlock_at]) if hash[:unlock_at]
|
||||
item.file_state = 'hidden' if hash[:hidden]
|
||||
item.display_name = hash[:display_name] if hash[:display_name]
|
||||
item.usage_rights_id = find_or_create_usage_rights(context, hash[:usage_rights], created_usage_rights_map) if hash[:usage_rights]
|
||||
item.set_publish_state_for_usage_rights unless hash[:locked]
|
||||
item.save_without_broadcasting!
|
||||
item.handle_duplicates(:rename)
|
||||
migration.add_imported_item(item)
|
||||
end
|
||||
item
|
||||
end
|
||||
|
||||
def self.find_or_create_usage_rights(context, usage_rights_hash, created_usage_rights_map)
|
||||
attrs = usage_rights_hash.slice('use_justification', 'license', 'legal_copyright')
|
||||
key = attrs.values_at('use_justification', 'license', 'legal_copyright').join('/')
|
||||
id = created_usage_rights_map[key]
|
||||
return id if id
|
||||
|
||||
usage_rights = context.usage_rights.create!(attrs)
|
||||
created_usage_rights_map[key] = usage_rights.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -28,6 +28,15 @@ module Lti
|
|||
serialize :custom
|
||||
serialize :custom_parameters
|
||||
|
||||
class << self
|
||||
def custom_settings(tool_proxy_id, context, resource_link_id)
|
||||
tool_settings = ToolSetting.where('tool_proxy_id = ? and ((context_type = ? and context_id =?) OR context_id IS NULL) and (resource_link_id = ? OR resource_link_id IS NULL)',
|
||||
tool_proxy_id, context.class.to_s, context.id, resource_link_id)
|
||||
.order('context_id NULLS FIRST, resource_link_id NULLS FIRST').pluck(:custom).compact
|
||||
(tool_settings.present? && tool_settings.inject { |custom, h| custom.merge(h) }) || {}
|
||||
end
|
||||
end
|
||||
|
||||
def message_handler(mh_context)
|
||||
MessageHandler.by_resource_codes(vendor_code: vendor_code,
|
||||
product_code: product_code,
|
||||
|
@ -40,12 +49,5 @@ module Lti
|
|||
def has_resource_link_id?
|
||||
resource_link_id.present?
|
||||
end
|
||||
|
||||
def self.custom_settings(tool_proxy_id, context, resource_link_id)
|
||||
tool_settings = ToolSetting.where('tool_proxy_id = ? and ((context_type = ? and context_id =?) OR context_id IS NULL) and (resource_link_id = ? OR resource_link_id IS NULL)',
|
||||
tool_proxy_id, context.class.to_s, context.id, resource_link_id)
|
||||
.order('context_id NULLS FIRST, resource_link_id NULLS FIRST').pluck(:custom).compact
|
||||
(tool_settings.present? && tool_settings.inject { |custom, h| custom.merge(h) }) || {}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,6 +23,12 @@ class Quizzes::QuizQuestionBuilder
|
|||
shuffle_answers: false
|
||||
}
|
||||
|
||||
class << self
|
||||
def t(*args)
|
||||
::ActiveRecord::Base.t(*args)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(options = {})
|
||||
self.options = DEFAULT_OPTIONS.merge(options)
|
||||
end
|
||||
|
@ -219,10 +225,6 @@ class Quizzes::QuizQuestionBuilder
|
|||
|
||||
protected
|
||||
|
||||
def self.t(*args)
|
||||
::ActiveRecord::Base.t(*args)
|
||||
end
|
||||
|
||||
# @property [Integer] submission_question_index
|
||||
# @private
|
||||
#
|
||||
|
|
|
@ -421,12 +421,12 @@ class StreamItem < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.new_message?(object)
|
||||
object.is_a?(Message) && object.new_record?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Internal: Format the stream item's asset to avoid showing hidden data.
|
||||
#
|
||||
# res - The stream item asset.
|
||||
|
|
|
@ -21,6 +21,12 @@
|
|||
class GradeSummaryPresenter
|
||||
attr_reader :groups_assignments, :assignment_order
|
||||
|
||||
class << self
|
||||
def cache_key(context, method)
|
||||
['grade_summary_presenter', context, method].cache_key
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(context, current_user, id_param, assignment_order: :due_at)
|
||||
@context = context
|
||||
@current_user = current_user
|
||||
|
@ -354,10 +360,4 @@ class GradeSummaryPresenter
|
|||
module_position_comparison
|
||||
end
|
||||
end
|
||||
|
||||
private_class_method
|
||||
|
||||
def self.cache_key(context, method)
|
||||
['grade_summary_presenter', context, method].cache_key
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,6 +23,13 @@ require 'active_support/version'
|
|||
|
||||
module ActiveSupport::Callbacks
|
||||
module Suspension
|
||||
def self.included(base)
|
||||
# use extend to avoid this callback being called again
|
||||
base.extend(self)
|
||||
base.singleton_class.include(ClassMethods)
|
||||
base.include(InstanceMethods)
|
||||
end
|
||||
|
||||
# Ignores the specified callbacks for the duration of the block.
|
||||
#
|
||||
# suspend_callbacks{ ... }
|
||||
|
@ -186,12 +193,5 @@ module ActiveSupport::Callbacks
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(base)
|
||||
# use extend to avoid this callback being called again
|
||||
base.extend(self)
|
||||
base.singleton_class.include(ClassMethods)
|
||||
base.include(InstanceMethods)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,6 +26,16 @@ module CanvasSecurity
|
|||
MAX_CACHE_AGE = 10.days.to_i
|
||||
MIN_ROTATION_PERIOD = 1.hour
|
||||
|
||||
class << self
|
||||
def max_cache_age
|
||||
Setting.get('public_jwk_cache_age_in_seconds', MAX_CACHE_AGE)
|
||||
end
|
||||
|
||||
def new_key
|
||||
CanvasSecurity::RSAKeyPair.new.to_jwk.to_json
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(prefix)
|
||||
@prefix = prefix
|
||||
end
|
||||
|
@ -106,13 +116,5 @@ module CanvasSecurity
|
|||
def consul_proxy
|
||||
@consul_proxy ||= DynamicSettings.kv_proxy(@prefix, tree: :store)
|
||||
end
|
||||
|
||||
def self.max_cache_age
|
||||
Setting.get('public_jwk_cache_age_in_seconds', MAX_CACHE_AGE)
|
||||
end
|
||||
|
||||
def self.new_key
|
||||
CanvasSecurity::RSAKeyPair.new.to_jwk.to_json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -33,8 +33,131 @@ module IncomingMailProcessor
|
|||
|
||||
ImportantHeaders = %w(To From Subject Content-Type)
|
||||
|
||||
BULK_PRECEDENCE_VALUES = %w[bulk list junk].freeze
|
||||
private_constant :BULK_PRECEDENCE_VALUES
|
||||
|
||||
class << self
|
||||
attr_accessor :mailbox_accounts, :settings, :deprecated_settings, :logger
|
||||
|
||||
def create_mailbox(account)
|
||||
mailbox_class = get_mailbox_class(account)
|
||||
mailbox = mailbox_class.new(account.config)
|
||||
mailbox.set_timeout_method(&method(:timeout_method))
|
||||
return mailbox
|
||||
end
|
||||
|
||||
def error_report_category
|
||||
"incoming_message_processor"
|
||||
end
|
||||
|
||||
def bounce_message?(mail)
|
||||
mail.header.fields.any? do |field|
|
||||
case field.name
|
||||
when 'Auto-Submitted' # RFC-3834
|
||||
field.value != 'no'
|
||||
when 'Precedence' # old kludgey stuff uses this
|
||||
BULK_PRECEDENCE_VALUES.include?(field.value)
|
||||
when 'X-Auto-Response-Suppress', # Exchange sets this
|
||||
# some other random headers I found that are easy to check
|
||||
'X-Autoreply',
|
||||
'X-Autorespond',
|
||||
'X-Autoresponder'
|
||||
true
|
||||
else
|
||||
# not a bounce header we care about
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def utf8ify(string, encoding)
|
||||
encoding ||= 'UTF-8'
|
||||
encoding = encoding.upcase
|
||||
encoding = "UTF-8" if encoding == "UTF8"
|
||||
|
||||
# change encoding; if it throws an exception (i.e. unrecognized encoding), just strip invalid UTF-8
|
||||
new_string = string.encode("UTF-8", encoding) rescue nil
|
||||
new_string&.valid_encoding? ? new_string : Utf8Cleaner.strip_invalid_utf8(string)
|
||||
end
|
||||
|
||||
def extract_address_tag(message, account)
|
||||
addr, domain = account.address.split(/@/)
|
||||
regex = Regexp.new("#{Regexp.escape(addr)}\\+([^@]+)@#{Regexp.escape(domain)}")
|
||||
message.to&.each do |address|
|
||||
if (match = regex.match(address))
|
||||
return match[1]
|
||||
end
|
||||
end
|
||||
|
||||
# if no match is found, return false
|
||||
# so that self.process message stops processing.
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mailbox_keys
|
||||
MailboxClasses.keys.map(&:to_s)
|
||||
end
|
||||
|
||||
def get_mailbox_class(account)
|
||||
MailboxClasses.fetch(account.protocol)
|
||||
end
|
||||
|
||||
def timeout_method
|
||||
Canvas.timeout_protection("incoming_message_processor", raise_on_timeout: true) do
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def configure_settings(config)
|
||||
@settings = IncomingMailProcessor::Settings.new
|
||||
@deprecated_settings = IncomingMailProcessor::DeprecatedSettings.new
|
||||
|
||||
config.symbolize_keys.each do |key, value|
|
||||
if IncomingMailProcessor::Settings.members.map(&:to_sym).include?(key)
|
||||
self.settings.send("#{key}=", value)
|
||||
elsif IncomingMailProcessor::DeprecatedSettings.members.map(&:to_sym).include?(key)
|
||||
logger.warn("deprecated setting sent to IncomingMessageProcessor: #{key}") if logger
|
||||
self.deprecated_settings.send("#{key}=", value)
|
||||
else
|
||||
raise "unrecognized setting sent to IncomingMessageProcessor: #{key}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def configure_accounts(account_configs)
|
||||
flat_account_configs = flatten_account_configs(account_configs)
|
||||
self.mailbox_accounts = flat_account_configs.map do |mailbox_protocol, mailbox_config|
|
||||
error_folder = mailbox_config.delete(:error_folder)
|
||||
address = mailbox_config[:address] || mailbox_config[:username]
|
||||
IncomingMailProcessor::MailboxAccount.new({
|
||||
:protocol => mailbox_protocol.to_sym,
|
||||
:config => mailbox_config,
|
||||
:address => address,
|
||||
:error_folder => error_folder,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
def flatten_account_configs(account_configs)
|
||||
account_configs.reduce([]) do |flat_account_configs, (mailbox_protocol, mailbox_config)|
|
||||
flat_mailbox_configs = flatten_mailbox_overrides(mailbox_config)
|
||||
flat_mailbox_configs.each do |single_mailbox_config|
|
||||
flat_account_configs << [mailbox_protocol, single_mailbox_config]
|
||||
end
|
||||
|
||||
flat_account_configs
|
||||
end
|
||||
end
|
||||
|
||||
def flatten_mailbox_overrides(mailbox_config)
|
||||
mailbox_defaults = mailbox_config.except('accounts')
|
||||
mailbox_overrides = mailbox_config['accounts'] || [{}]
|
||||
mailbox_overrides.map do |override_config|
|
||||
mailbox_defaults.merge(override_config).symbolize_keys
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(message_handler, error_reporter)
|
||||
|
@ -171,113 +294,6 @@ module IncomingMailProcessor
|
|||
(Time.now - datetime).to_i * 1000 if datetime # age in ms, please
|
||||
end
|
||||
|
||||
def self.mailbox_keys
|
||||
MailboxClasses.keys.map(&:to_s)
|
||||
end
|
||||
|
||||
def self.get_mailbox_class(account)
|
||||
MailboxClasses.fetch(account.protocol)
|
||||
end
|
||||
|
||||
def self.create_mailbox(account)
|
||||
mailbox_class = get_mailbox_class(account)
|
||||
mailbox = mailbox_class.new(account.config)
|
||||
mailbox.set_timeout_method(&method(:timeout_method))
|
||||
return mailbox
|
||||
end
|
||||
|
||||
def self.timeout_method
|
||||
Canvas.timeout_protection("incoming_message_processor", raise_on_timeout: true) do
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def self.configure_settings(config)
|
||||
@settings = IncomingMailProcessor::Settings.new
|
||||
@deprecated_settings = IncomingMailProcessor::DeprecatedSettings.new
|
||||
|
||||
config.symbolize_keys.each do |key, value|
|
||||
if IncomingMailProcessor::Settings.members.map(&:to_sym).include?(key)
|
||||
self.settings.send("#{key}=", value)
|
||||
elsif IncomingMailProcessor::DeprecatedSettings.members.map(&:to_sym).include?(key)
|
||||
logger.warn("deprecated setting sent to IncomingMessageProcessor: #{key}") if logger
|
||||
self.deprecated_settings.send("#{key}=", value)
|
||||
else
|
||||
raise "unrecognized setting sent to IncomingMessageProcessor: #{key}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.configure_accounts(account_configs)
|
||||
flat_account_configs = flatten_account_configs(account_configs)
|
||||
self.mailbox_accounts = flat_account_configs.map do |mailbox_protocol, mailbox_config|
|
||||
error_folder = mailbox_config.delete(:error_folder)
|
||||
address = mailbox_config[:address] || mailbox_config[:username]
|
||||
IncomingMailProcessor::MailboxAccount.new({
|
||||
:protocol => mailbox_protocol.to_sym,
|
||||
:config => mailbox_config,
|
||||
:address => address,
|
||||
:error_folder => error_folder,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
def self.flatten_account_configs(account_configs)
|
||||
account_configs.reduce([]) do |flat_account_configs, (mailbox_protocol, mailbox_config)|
|
||||
flat_mailbox_configs = flatten_mailbox_overrides(mailbox_config)
|
||||
flat_mailbox_configs.each do |single_mailbox_config|
|
||||
flat_account_configs << [mailbox_protocol, single_mailbox_config]
|
||||
end
|
||||
|
||||
flat_account_configs
|
||||
end
|
||||
end
|
||||
|
||||
def self.flatten_mailbox_overrides(mailbox_config)
|
||||
mailbox_defaults = mailbox_config.except('accounts')
|
||||
mailbox_overrides = mailbox_config['accounts'] || [{}]
|
||||
mailbox_overrides.map do |override_config|
|
||||
mailbox_defaults.merge(override_config).symbolize_keys
|
||||
end
|
||||
end
|
||||
|
||||
def self.error_report_category
|
||||
"incoming_message_processor"
|
||||
end
|
||||
|
||||
BULK_PRECEDENCE_VALUES = %w[bulk list junk].freeze
|
||||
private_constant :BULK_PRECEDENCE_VALUES
|
||||
|
||||
def self.bounce_message?(mail)
|
||||
mail.header.fields.any? do |field|
|
||||
case field.name
|
||||
when 'Auto-Submitted' # RFC-3834
|
||||
field.value != 'no'
|
||||
when 'Precedence' # old kludgey stuff uses this
|
||||
BULK_PRECEDENCE_VALUES.include?(field.value)
|
||||
when 'X-Auto-Response-Suppress', # Exchange sets this
|
||||
# some other random headers I found that are easy to check
|
||||
'X-Autoreply',
|
||||
'X-Autorespond',
|
||||
'X-Autoresponder'
|
||||
true
|
||||
else
|
||||
# not a bounce header we care about
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.utf8ify(string, encoding)
|
||||
encoding ||= 'UTF-8'
|
||||
encoding = encoding.upcase
|
||||
encoding = "UTF-8" if encoding == "UTF8"
|
||||
|
||||
# change encoding; if it throws an exception (i.e. unrecognized encoding), just strip invalid UTF-8
|
||||
new_string = string.encode("UTF-8", encoding) rescue nil
|
||||
new_string&.valid_encoding? ? new_string : Utf8Cleaner.strip_invalid_utf8(string)
|
||||
end
|
||||
|
||||
def process_mailbox(mailbox, account, opts = {})
|
||||
error_folder = account.error_folder
|
||||
mailbox.connect
|
||||
|
@ -337,19 +353,5 @@ module IncomingMailProcessor
|
|||
:from => message.from.try(:first),
|
||||
:to => message.to.to_s)
|
||||
end
|
||||
|
||||
def self.extract_address_tag(message, account)
|
||||
addr, domain = account.address.split(/@/)
|
||||
regex = Regexp.new("#{Regexp.escape(addr)}\\+([^@]+)@#{Regexp.escape(domain)}")
|
||||
message.to&.each do |address|
|
||||
if (match = regex.match(address))
|
||||
return match[1]
|
||||
end
|
||||
end
|
||||
|
||||
# if no match is found, return false
|
||||
# so that self.process message stops processing.
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,24 +19,26 @@
|
|||
|
||||
module LtiOutbound
|
||||
class LTIModel
|
||||
protected
|
||||
class << self
|
||||
private
|
||||
|
||||
def self.proc_accessor(*methods)
|
||||
attr_writer(*methods)
|
||||
def proc_accessor(*methods)
|
||||
attr_writer(*methods)
|
||||
|
||||
proc_writer(*methods)
|
||||
end
|
||||
proc_writer(*methods)
|
||||
end
|
||||
|
||||
def self.proc_writer(*methods)
|
||||
methods.each do |method|
|
||||
define_method(method) do
|
||||
variable_name = "@#{method}"
|
||||
value = self.instance_variable_get(variable_name)
|
||||
if value.is_a?(Proc)
|
||||
value = value.call
|
||||
self.instance_variable_set(variable_name, value)
|
||||
def proc_writer(*methods)
|
||||
methods.each do |method|
|
||||
define_method(method) do
|
||||
variable_name = "@#{method}"
|
||||
value = self.instance_variable_get(variable_name)
|
||||
if value.is_a?(Proc)
|
||||
value = value.call
|
||||
self.instance_variable_set(variable_name, value)
|
||||
end
|
||||
return value
|
||||
end
|
||||
return value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -35,7 +35,7 @@ describe LtiOutbound::LTIModel do
|
|||
end
|
||||
|
||||
it 'handles multiple attributes at once' do
|
||||
dummy.proc_accessor(:test1, :test2)
|
||||
dummy.send(:proc_accessor, :test1, :test2)
|
||||
end
|
||||
|
||||
it 'evaluates a proc when assigned a proc' do
|
||||
|
|
|
@ -42,6 +42,21 @@ module Api
|
|||
|
||||
attr_reader :html
|
||||
|
||||
class << self
|
||||
def apply_mathml(node)
|
||||
equation = node['data-equation-content'] || node['alt']
|
||||
mathml = UserContent.latex_to_mathml(equation)
|
||||
return if mathml.blank?
|
||||
|
||||
# NOTE: we use "x-canvaslms-safe-mathml" instead of just "data-mathml"
|
||||
# because canvas_sanitize will strip it out on the way in but it won't
|
||||
# strip out data-mathml. This means we can gaurentee that there is never
|
||||
# user input in x-canvaslms-safe-mathml and we can safely pass it to
|
||||
# $el.html() in translateMathmlForScreenreaders in the js in the frontend
|
||||
node['x-canvaslms-safe-mathml'] = mathml
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(html_string, account = nil, include_mobile: false, rewrite_api_urls: true, host: nil, port: nil)
|
||||
@account = account
|
||||
@html = html_string
|
||||
|
@ -172,19 +187,6 @@ module Api
|
|||
def apply_mathml(node)
|
||||
self.class.apply_mathml(node)
|
||||
end
|
||||
|
||||
def self.apply_mathml(node)
|
||||
equation = node['data-equation-content'] || node['alt']
|
||||
mathml = UserContent.latex_to_mathml(equation)
|
||||
return if mathml.blank?
|
||||
|
||||
# NOTE: we use "x-canvaslms-safe-mathml" instead of just "data-mathml"
|
||||
# because canvas_sanitize will strip it out on the way in but it won't
|
||||
# strip out data-mathml. This means we can gaurentee that there is never
|
||||
# user input in x-canvaslms-safe-mathml and we can safely pass it to
|
||||
# $el.html() in translateMathmlForScreenreaders in the js in the frontend
|
||||
node['x-canvaslms-safe-mathml'] = mathml
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,21 +20,23 @@
|
|||
module AssetSignature
|
||||
DELIMITER = '-'
|
||||
|
||||
def self.generate(asset)
|
||||
"#{asset.id}#{DELIMITER}#{generate_hmac(asset.class, asset.id)}"
|
||||
end
|
||||
class << self
|
||||
def generate(asset)
|
||||
"#{asset.id}#{DELIMITER}#{generate_hmac(asset.class, asset.id)}"
|
||||
end
|
||||
|
||||
def self.find_by_signature(klass, signature)
|
||||
id, hmac = signature.split(DELIMITER, 2)
|
||||
return nil unless Canvas::Security.verify_hmac_sha1(hmac, "#{klass}#{id}", truncate: 8)
|
||||
def find_by_signature(klass, signature)
|
||||
id, hmac = signature.split(DELIMITER, 2)
|
||||
return nil unless Canvas::Security.verify_hmac_sha1(hmac, "#{klass}#{id}", truncate: 8)
|
||||
|
||||
klass.where(id: id.to_i).first
|
||||
end
|
||||
klass.where(id: id.to_i).first
|
||||
end
|
||||
|
||||
private
|
||||
private
|
||||
|
||||
def self.generate_hmac(klass, id)
|
||||
data = "#{klass}#{id}"
|
||||
Canvas::Security.hmac_sha1(data)[0, 8]
|
||||
def generate_hmac(klass, id)
|
||||
data = "#{klass}#{id}"
|
||||
Canvas::Security.hmac_sha1(data)[0, 8]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -72,194 +72,196 @@ module DataFixup::RebuildQuizSubmissionsFromQuizSubmissionEvents
|
|||
# -> Seq Scan on quiz_submissions (cost=0.00..384640.39 rows=2321739 width=8)
|
||||
# (22 rows)
|
||||
|
||||
def self.run(submission_id)
|
||||
submission = Submission.find(submission_id)
|
||||
class << self
|
||||
def run(submission_id)
|
||||
submission = Submission.find(submission_id)
|
||||
|
||||
# Run build script
|
||||
quiz_submission = build_new_submission_from_quiz_submission_events(submission)
|
||||
# Run build script
|
||||
quiz_submission = build_new_submission_from_quiz_submission_events(submission)
|
||||
|
||||
# save the result
|
||||
quiz_submission.save_with_versioning! if quiz_submission
|
||||
end
|
||||
|
||||
def self.find_missing_submissions_on_current_shard
|
||||
response = ActiveRecord::Base.connection.execute(SQL_SEARCH_STRING)
|
||||
response.values.flatten
|
||||
end
|
||||
|
||||
def self.find_and_run
|
||||
ids = GuardRail.activate(:secondary) do
|
||||
find_missing_submissions_on_current_shard
|
||||
# save the result
|
||||
quiz_submission.save_with_versioning! if quiz_submission
|
||||
end
|
||||
ids.map do |id|
|
||||
begin
|
||||
Rails.logger.info LOG_PREFIX + "#{id} data fix starting..."
|
||||
success = run(id)
|
||||
rescue => e
|
||||
Rails.logger.warn LOG_PREFIX + "#{id} failed with error: #{e}"
|
||||
ensure
|
||||
if success
|
||||
Rails.logger.info LOG_PREFIX + "#{id} completed successfully"
|
||||
|
||||
def find_missing_submissions_on_current_shard
|
||||
response = ActiveRecord::Base.connection.execute(SQL_SEARCH_STRING)
|
||||
response.values.flatten
|
||||
end
|
||||
|
||||
def find_and_run
|
||||
ids = GuardRail.activate(:secondary) do
|
||||
find_missing_submissions_on_current_shard
|
||||
end
|
||||
ids.map do |id|
|
||||
begin
|
||||
Rails.logger.info LOG_PREFIX + "#{id} data fix starting..."
|
||||
success = run(id)
|
||||
rescue => e
|
||||
Rails.logger.warn LOG_PREFIX + "#{id} failed with error: #{e}"
|
||||
ensure
|
||||
if success
|
||||
Rails.logger.info LOG_PREFIX + "#{id} completed successfully"
|
||||
else
|
||||
Rails.logger.warn LOG_PREFIX + "#{id} failed"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def grade_with_new_submission_data(qs, finished_at, submission_data = nil)
|
||||
qs.manually_scored = false
|
||||
tally = 0
|
||||
submission_data ||= qs.submission_data
|
||||
user_answers = []
|
||||
qs.questions.each do |q|
|
||||
user_answer = Quizzes::SubmissionGrader.score_question(q, submission_data)
|
||||
user_answers << user_answer
|
||||
tally += (user_answer[:points] || 0) if user_answer[:correct]
|
||||
end
|
||||
qs.score = tally
|
||||
qs.score = qs.quiz.points_possible if qs.quiz && qs.quiz.quiz_type == 'graded_survey'
|
||||
qs.submission_data = user_answers
|
||||
qs.workflow_state = "complete"
|
||||
user_answers.each do |answer|
|
||||
if answer[:correct] == "undefined" && !qs.quiz.survey?
|
||||
qs.workflow_state = 'pending_review'
|
||||
end
|
||||
end
|
||||
qs.score_before_regrade = nil
|
||||
qs.manually_unlocked = nil
|
||||
qs.finished_at = finished_at
|
||||
qs
|
||||
end
|
||||
|
||||
def pick_questions(qs, seen_question_ids)
|
||||
# Parsing all the deets from events can do it though
|
||||
quiz_group_picks = Hash.new { |h, k| h[k] = [] }
|
||||
quiz_group_pick_counts = {}
|
||||
picked_questions = qs.quiz.stored_questions.select do |question|
|
||||
if question["entry_type"] == "quiz_group"
|
||||
group_name = question["id"]
|
||||
quiz_group_pick_counts[group_name] = question["pick_count"]
|
||||
matching_questions = question["questions"].select { |q| seen_question_ids.include? q["id"].to_s }
|
||||
matching_questions.each { |q| q["points_possible"] = question["question_points"] }
|
||||
quiz_group_picks[group_name] += matching_questions
|
||||
false
|
||||
else
|
||||
Rails.logger.warn LOG_PREFIX + "#{id} failed"
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
quiz_group_picks.each do |k, v|
|
||||
qs.quiz.stored_questions.select { |q| q["id"] == k }.map do |question|
|
||||
question["questions"].shuffle.each do |q|
|
||||
break if v.count == quiz_group_pick_counts[k]
|
||||
|
||||
def self.grade_with_new_submission_data(qs, finished_at, submission_data = nil)
|
||||
qs.manually_scored = false
|
||||
tally = 0
|
||||
submission_data ||= qs.submission_data
|
||||
user_answers = []
|
||||
qs.questions.each do |q|
|
||||
user_answer = Quizzes::SubmissionGrader.score_question(q, submission_data)
|
||||
user_answers << user_answer
|
||||
tally += (user_answer[:points] || 0) if user_answer[:correct]
|
||||
end
|
||||
qs.score = tally
|
||||
qs.score = qs.quiz.points_possible if qs.quiz && qs.quiz.quiz_type == 'graded_survey'
|
||||
qs.submission_data = user_answers
|
||||
qs.workflow_state = "complete"
|
||||
user_answers.each do |answer|
|
||||
if answer[:correct] == "undefined" && !qs.quiz.survey?
|
||||
qs.workflow_state = 'pending_review'
|
||||
q["points_possible"] = question["question_points"]
|
||||
v << q unless v.include? q
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
qs.score_before_regrade = nil
|
||||
qs.manually_unlocked = nil
|
||||
qs.finished_at = finished_at
|
||||
qs
|
||||
end
|
||||
|
||||
def self.pick_questions(qs, seen_question_ids)
|
||||
# Parsing all the deets from events can do it though
|
||||
quiz_group_picks = Hash.new { |h, k| h[k] = [] }
|
||||
quiz_group_pick_counts = {}
|
||||
picked_questions = qs.quiz.stored_questions.select do |question|
|
||||
if question["entry_type"] == "quiz_group"
|
||||
group_name = question["id"]
|
||||
quiz_group_pick_counts[group_name] = question["pick_count"]
|
||||
matching_questions = question["questions"].select { |q| seen_question_ids.include? q["id"].to_s }
|
||||
matching_questions.each { |q| q["points_possible"] = question["question_points"] }
|
||||
quiz_group_picks[group_name] += matching_questions
|
||||
false
|
||||
picked_questions += quiz_group_picks.values.flatten
|
||||
if qs.quiz.question_count != picked_questions.size
|
||||
Rails.logger.error LOG_PREFIX + "#{qs.id} doesn't match it's question count"
|
||||
end
|
||||
picked_questions
|
||||
end
|
||||
|
||||
# Because we can't regenerate quiz data without getting a different set of questions,
|
||||
# we need to select the quiz_questions from the questions we can know the student has
|
||||
# seen.
|
||||
def aggregate_quiz_data_from_events(qs, events)
|
||||
question_events = events.select { |e| ["question_answered", "question_viewed", "question_flagged"].include?(e.event_type) }
|
||||
seen_question_ids = []
|
||||
question_events.each do |event|
|
||||
if event.event_type == "question_viewed"
|
||||
seen_question_ids << event.answers
|
||||
else
|
||||
seen_question_ids << event.answers.flatten.map { |h| h["quiz_question_id"] }
|
||||
end
|
||||
end
|
||||
seen_question_ids = seen_question_ids.flatten.uniq
|
||||
|
||||
builder = Quizzes::QuizQuestionBuilder.new({
|
||||
shuffle_answers: qs.quiz.shuffle_answers
|
||||
})
|
||||
|
||||
if seen_question_ids.count > 0
|
||||
picked_questions = pick_questions(qs, seen_question_ids)
|
||||
|
||||
raw_data = builder.shuffle_quiz_data!(picked_questions)
|
||||
raw_data.each_with_index do |question, index|
|
||||
Quizzes::QuizQuestionBuilder.decorate_question_for_submission(question, index)
|
||||
end
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
quiz_group_picks.each do |k, v|
|
||||
qs.quiz.stored_questions.select { |q| q["id"] == k }.map do |question|
|
||||
question["questions"].shuffle.each do |q|
|
||||
break if v.count == quiz_group_pick_counts[k]
|
||||
|
||||
q["points_possible"] = question["question_points"]
|
||||
v << q unless v.include? q
|
||||
submission.quiz_data = begin
|
||||
qs.quiz.stored_questions = nil
|
||||
builder.build_submission_questions(qs.quiz.id, qs.quiz.stored_questions)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
picked_questions += quiz_group_picks.values.flatten
|
||||
if qs.quiz.question_count != picked_questions.size
|
||||
Rails.logger.error LOG_PREFIX + "#{qs.id} doesn't match it's question count"
|
||||
end
|
||||
picked_questions
|
||||
end
|
||||
|
||||
# Because we can't regenerate quiz data without getting a different set of questions,
|
||||
# we need to select the quiz_questions from the questions we can know the student has
|
||||
# seen.
|
||||
def self.aggregate_quiz_data_from_events(qs, events)
|
||||
question_events = events.select { |e| ["question_answered", "question_viewed", "question_flagged"].include?(e.event_type) }
|
||||
seen_question_ids = []
|
||||
question_events.each do |event|
|
||||
if event.event_type == "question_viewed"
|
||||
seen_question_ids << event.answers
|
||||
else
|
||||
seen_question_ids << event.answers.flatten.map { |h| h["quiz_question_id"] }
|
||||
def build_new_submission_from_quiz_submission_events(submission)
|
||||
if submission.submission_type != "online_quiz"
|
||||
Rails.logger.warn LOG_PREFIX + "Skipping because this isn't a quiz!\tsubmission_id: #{submission.id}"
|
||||
return false
|
||||
end
|
||||
end
|
||||
seen_question_ids = seen_question_ids.flatten.uniq
|
||||
|
||||
builder = Quizzes::QuizQuestionBuilder.new({
|
||||
shuffle_answers: qs.quiz.shuffle_answers
|
||||
})
|
||||
qs_id = submission.quiz_submission_id
|
||||
|
||||
if seen_question_ids.count > 0
|
||||
picked_questions = pick_questions(qs, seen_question_ids)
|
||||
# Get QLA events
|
||||
events = Quizzes::QuizSubmissionEvent.where(quiz_submission_id: qs_id)
|
||||
|
||||
raw_data = builder.shuffle_quiz_data!(picked_questions)
|
||||
raw_data.each_with_index do |question, index|
|
||||
Quizzes::QuizQuestionBuilder.decorate_question_for_submission(question, index)
|
||||
# Check if there are any events in the QLA
|
||||
if events.size == 0
|
||||
Rails.logger.warn LOG_PREFIX + "Skipping because there are no QLA events\tsubmission_id: #{submission.id}"
|
||||
return false
|
||||
end
|
||||
else
|
||||
submission.quiz_data = begin
|
||||
qs.quiz.stored_questions = nil
|
||||
builder.build_submission_questions(qs.quiz.id, qs.quiz.stored_questions)
|
||||
|
||||
# Check if there are multiple attempts in the QLA
|
||||
attempts = events.map(&:attempt).uniq
|
||||
if attempts.size != 1
|
||||
Rails.logger.info LOG_PREFIX + "You have many attempts for qs: #{qs_id}\tsub: #{submission.id}\tattempts: #{attempts}"
|
||||
end
|
||||
|
||||
# Assume final attempt
|
||||
attempt = attempts.sort.last
|
||||
events.select!(&:attempt)
|
||||
|
||||
times = events.map(&:created_at).sort
|
||||
|
||||
aggregator = Quizzes::LogAuditing::EventAggregator.new(submission.assignment.quiz)
|
||||
submission_data_hash = aggregator.run(qs_id, attempt, submission.updated_at)
|
||||
|
||||
# Put it all together
|
||||
qs = Quizzes::QuizSubmission.new
|
||||
# Set the associations
|
||||
qs.submission = submission
|
||||
qs.id = qs_id
|
||||
qs.quiz = submission.assignment.quiz
|
||||
qs.user = submission.user
|
||||
|
||||
# This is sad, but I don't want to recreate all the attempts, nor do I
|
||||
# want to cause an error setting this to a non-concurrent number.
|
||||
qs.attempt = attempt
|
||||
|
||||
# This is sad because assumptions
|
||||
qs.quiz_version = qs.versions.map { |v| v.model.quiz_version }.sort.last
|
||||
qs.quiz_data = aggregate_quiz_data_from_events(qs, events)
|
||||
|
||||
# Set reasonable timestamps
|
||||
qs.created_at = submission.created_at
|
||||
qs.started_at = times.first
|
||||
qs.finished_at = submission.submitted_at
|
||||
|
||||
qs.submission_data = submission_data_hash
|
||||
qs.save!
|
||||
|
||||
# grade the submission!
|
||||
grade_with_new_submission_data(qs, qs.finished_at, submission_data_hash)
|
||||
end
|
||||
end
|
||||
|
||||
def self.build_new_submission_from_quiz_submission_events(submission)
|
||||
if submission.submission_type != "online_quiz"
|
||||
Rails.logger.warn LOG_PREFIX + "Skipping because this isn't a quiz!\tsubmission_id: #{submission.id}"
|
||||
return false
|
||||
end
|
||||
|
||||
qs_id = submission.quiz_submission_id
|
||||
|
||||
# Get QLA events
|
||||
events = Quizzes::QuizSubmissionEvent.where(quiz_submission_id: qs_id)
|
||||
|
||||
# Check if there are any events in the QLA
|
||||
if events.size == 0
|
||||
Rails.logger.warn LOG_PREFIX + "Skipping because there are no QLA events\tsubmission_id: #{submission.id}"
|
||||
return false
|
||||
end
|
||||
|
||||
# Check if there are multiple attempts in the QLA
|
||||
attempts = events.map(&:attempt).uniq
|
||||
if attempts.size != 1
|
||||
Rails.logger.info LOG_PREFIX + "You have many attempts for qs: #{qs_id}\tsub: #{submission.id}\tattempts: #{attempts}"
|
||||
end
|
||||
|
||||
# Assume final attempt
|
||||
attempt = attempts.sort.last
|
||||
events.select!(&:attempt)
|
||||
|
||||
times = events.map(&:created_at).sort
|
||||
|
||||
aggregator = Quizzes::LogAuditing::EventAggregator.new(submission.assignment.quiz)
|
||||
submission_data_hash = aggregator.run(qs_id, attempt, submission.updated_at)
|
||||
|
||||
# Put it all together
|
||||
qs = Quizzes::QuizSubmission.new
|
||||
# Set the associations
|
||||
qs.submission = submission
|
||||
qs.id = qs_id
|
||||
qs.quiz = submission.assignment.quiz
|
||||
qs.user = submission.user
|
||||
|
||||
# This is sad, but I don't want to recreate all the attempts, nor do I
|
||||
# want to cause an error setting this to a non-concurrent number.
|
||||
qs.attempt = attempt
|
||||
|
||||
# This is sad because assumptions
|
||||
qs.quiz_version = qs.versions.map { |v| v.model.quiz_version }.sort.last
|
||||
qs.quiz_data = aggregate_quiz_data_from_events(qs, events)
|
||||
|
||||
# Set reasonable timestamps
|
||||
qs.created_at = submission.created_at
|
||||
qs.started_at = times.first
|
||||
qs.finished_at = submission.submitted_at
|
||||
|
||||
qs.submission_data = submission_data_hash
|
||||
qs.save!
|
||||
|
||||
# grade the submission!
|
||||
grade_with_new_submission_data(qs, qs.finished_at, submission_data_hash)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,120 +20,122 @@
|
|||
module DataFixup::RebuildQuizSubmissionsFromQuizSubmissionVersions
|
||||
LOG_PREFIX = "RebuildingQuizSubmissions - ".freeze
|
||||
|
||||
def self.run(submission_id, timestamp = Time.zone.now)
|
||||
submission = Submission.find(submission_id)
|
||||
class << self
|
||||
def run(submission_id, timestamp = Time.zone.now)
|
||||
submission = Submission.find(submission_id)
|
||||
|
||||
# Run build script
|
||||
quiz_submission = restore_quiz_submission_from_versions_table_by_submission(submission, timestamp)
|
||||
# Run build script
|
||||
quiz_submission = restore_quiz_submission_from_versions_table_by_submission(submission, timestamp)
|
||||
|
||||
# save the result
|
||||
quiz_submission.save_with_versioning! if quiz_submission
|
||||
end
|
||||
# save the result
|
||||
quiz_submission.save_with_versioning! if quiz_submission
|
||||
end
|
||||
|
||||
# Time.zone.parse("2015-05-08")
|
||||
def self.run_on_array(submission_ids, timestamp = Time.zone.now)
|
||||
base_url = "#{Shard.current.id}/api/v1/"
|
||||
submission_ids.map do |id|
|
||||
begin
|
||||
Rails.logger.info LOG_PREFIX + "#{id} data fix starting..."
|
||||
success = run(id, timestamp)
|
||||
rescue => e
|
||||
Rails.logger.warn LOG_PREFIX + "#{id} failed with error: #{e}"
|
||||
ensure
|
||||
if success
|
||||
Rails.logger.info LOG_PREFIX + "#{id} completed successfully"
|
||||
sub = Submission.find(id)
|
||||
assignment = sub.assignment
|
||||
url = "#{base_url}courses/#{assignment.context.id}/assignments/#{assignment.id}/submissions/#{sub.user_id}"
|
||||
Rails.logger.info LOG_PREFIX + "You can investigate #{id} manually at #{url}"
|
||||
else
|
||||
Rails.logger.warn LOG_PREFIX + "#{id} failed"
|
||||
# Time.zone.parse("2015-05-08")
|
||||
def run_on_array(submission_ids, timestamp = Time.zone.now)
|
||||
base_url = "#{Shard.current.id}/api/v1/"
|
||||
submission_ids.map do |id|
|
||||
begin
|
||||
Rails.logger.info LOG_PREFIX + "#{id} data fix starting..."
|
||||
success = run(id, timestamp)
|
||||
rescue => e
|
||||
Rails.logger.warn LOG_PREFIX + "#{id} failed with error: #{e}"
|
||||
ensure
|
||||
if success
|
||||
Rails.logger.info LOG_PREFIX + "#{id} completed successfully"
|
||||
sub = Submission.find(id)
|
||||
assignment = sub.assignment
|
||||
url = "#{base_url}courses/#{assignment.context.id}/assignments/#{assignment.id}/submissions/#{sub.user_id}"
|
||||
Rails.logger.info LOG_PREFIX + "You can investigate #{id} manually at #{url}"
|
||||
else
|
||||
Rails.logger.warn LOG_PREFIX + "#{id} failed"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
private
|
||||
|
||||
def self.grade_with_new_submission_data(qs, finished_at, submission_data = nil)
|
||||
qs.manually_scored = false
|
||||
tally = 0
|
||||
submission_data ||= qs.submission_data
|
||||
user_answers = []
|
||||
qs.questions.each do |q|
|
||||
user_answer = Quizzes::SubmissionGrader.score_question(q, submission_data)
|
||||
user_answers << user_answer
|
||||
tally += (user_answer[:points] || 0) if user_answer[:correct]
|
||||
end
|
||||
qs.score = tally
|
||||
qs.score = qs.quiz.points_possible if qs.quiz && qs.quiz.quiz_type == 'graded_survey'
|
||||
qs.submission_data = user_answers
|
||||
qs.workflow_state = "complete"
|
||||
user_answers.each do |answer|
|
||||
if answer[:correct] == "undefined" && !qs.quiz.survey?
|
||||
qs.workflow_state = 'pending_review'
|
||||
def grade_with_new_submission_data(qs, finished_at, submission_data = nil)
|
||||
qs.manually_scored = false
|
||||
tally = 0
|
||||
submission_data ||= qs.submission_data
|
||||
user_answers = []
|
||||
qs.questions.each do |q|
|
||||
user_answer = Quizzes::SubmissionGrader.score_question(q, submission_data)
|
||||
user_answers << user_answer
|
||||
tally += (user_answer[:points] || 0) if user_answer[:correct]
|
||||
end
|
||||
end
|
||||
qs.score_before_regrade = nil
|
||||
qs.manually_unlocked = nil
|
||||
qs.finished_at = finished_at
|
||||
qs
|
||||
end
|
||||
|
||||
def self.restore_quiz_submission_from_versions_table_by_submission(submission, timestamp)
|
||||
if submission.submission_type != "online_quiz"
|
||||
Rails.logger.warn LOG_PREFIX + "Skipping because this isn't a quiz!\tsubmission_id: #{submission.id}"
|
||||
return false
|
||||
qs.score = tally
|
||||
qs.score = qs.quiz.points_possible if qs.quiz && qs.quiz.quiz_type == 'graded_survey'
|
||||
qs.submission_data = user_answers
|
||||
qs.workflow_state = "complete"
|
||||
user_answers.each do |answer|
|
||||
if answer[:correct] == "undefined" && !qs.quiz.survey?
|
||||
qs.workflow_state = 'pending_review'
|
||||
end
|
||||
end
|
||||
qs.score_before_regrade = nil
|
||||
qs.manually_unlocked = nil
|
||||
qs.finished_at = finished_at
|
||||
qs
|
||||
end
|
||||
|
||||
qs_id = submission.quiz_submission_id
|
||||
old_submission_grading_data = [submission.score, submission.grader_id]
|
||||
def restore_quiz_submission_from_versions_table_by_submission(submission, timestamp)
|
||||
if submission.submission_type != "online_quiz"
|
||||
Rails.logger.warn LOG_PREFIX + "Skipping because this isn't a quiz!\tsubmission_id: #{submission.id}"
|
||||
return false
|
||||
end
|
||||
|
||||
# Get versions
|
||||
models = Version.where(
|
||||
versionable_type: "Quizzes::QuizSubmission",
|
||||
versionable_id: qs_id
|
||||
).order("id ASC").map(&:model)
|
||||
qs_id = submission.quiz_submission_id
|
||||
old_submission_grading_data = [submission.score, submission.grader_id]
|
||||
|
||||
# Filter by attempt
|
||||
models.select! { |qs| qs.attempt = submission.attempt }
|
||||
# Get versions
|
||||
models = Version.where(
|
||||
versionable_type: "Quizzes::QuizSubmission",
|
||||
versionable_id: qs_id
|
||||
).order("id ASC").map(&:model)
|
||||
|
||||
# Find the latest version before the data fix ran. So, maybe 5/8/2015
|
||||
qs = models.detect { |s| s.created_at < timestamp }
|
||||
# Filter by attempt
|
||||
models.select! { |qs| qs.attempt = submission.attempt }
|
||||
|
||||
if qs
|
||||
persisted_qs = Quizzes::QuizSubmission.where(id: qs_id).first || Quizzes::QuizSubmission.new
|
||||
persisted_qs.assign_attributes(qs.attributes)
|
||||
else
|
||||
Rails.logger.error LOG_PREFIX + "No matching version \tsubmission_id: #{submission.id}"
|
||||
return false
|
||||
end
|
||||
# Find the latest version before the data fix ran. So, maybe 5/8/2015
|
||||
qs = models.detect { |s| s.created_at < timestamp }
|
||||
|
||||
begin
|
||||
persisted_qs.save!
|
||||
rescue => e
|
||||
Rails.logger.error LOG_PREFIX + "Failure to save on submission.id: #{submission.id}"
|
||||
Rails.logger.error LOG_PREFIX + "error: #{e}"
|
||||
return false
|
||||
end
|
||||
|
||||
# grade the submission!
|
||||
if persisted_qs.submission_data.is_a? Array
|
||||
persisted_qs
|
||||
else
|
||||
Rails.logger.warn LOG_PREFIX + "Versions contained ungraded data: submission_id: #{submission.id} version:#{model.version_number} qs:#{qs_id}"
|
||||
grade_with_new_submission_data(persisted_qs, persisted_qs.finished_at)
|
||||
end
|
||||
|
||||
if submission.reload.workflow_state == "pending_review"
|
||||
if old_submission_grading_data.first != submission.score
|
||||
Rails.logger.warn LOG_PREFIX + "GRADING REPORT - " +
|
||||
"score-- #{old_submission_grading_data.first}:#{submission.score} " +
|
||||
"grader_id-- #{old_submission_grading_data[1]}:#{submission.grader_id} "
|
||||
if qs
|
||||
persisted_qs = Quizzes::QuizSubmission.where(id: qs_id).first || Quizzes::QuizSubmission.new
|
||||
persisted_qs.assign_attributes(qs.attributes)
|
||||
else
|
||||
Rails.logger.warn LOG_PREFIX + "GRADING REPORT - " + "Grading required for quiz_submission: #{persisted_qs.id}"
|
||||
Rails.logger.error LOG_PREFIX + "No matching version \tsubmission_id: #{submission.id}"
|
||||
return false
|
||||
end
|
||||
|
||||
begin
|
||||
persisted_qs.save!
|
||||
rescue => e
|
||||
Rails.logger.error LOG_PREFIX + "Failure to save on submission.id: #{submission.id}"
|
||||
Rails.logger.error LOG_PREFIX + "error: #{e}"
|
||||
return false
|
||||
end
|
||||
|
||||
# grade the submission!
|
||||
if persisted_qs.submission_data.is_a? Array
|
||||
persisted_qs
|
||||
else
|
||||
Rails.logger.warn LOG_PREFIX + "Versions contained ungraded data: submission_id: #{submission.id} version:#{model.version_number} qs:#{qs_id}"
|
||||
grade_with_new_submission_data(persisted_qs, persisted_qs.finished_at)
|
||||
end
|
||||
|
||||
if submission.reload.workflow_state == "pending_review"
|
||||
if old_submission_grading_data.first != submission.score
|
||||
Rails.logger.warn LOG_PREFIX + "GRADING REPORT - " +
|
||||
"score-- #{old_submission_grading_data.first}:#{submission.score} " +
|
||||
"grader_id-- #{old_submission_grading_data[1]}:#{submission.grader_id} "
|
||||
else
|
||||
Rails.logger.warn LOG_PREFIX + "GRADING REPORT - " + "Grading required for quiz_submission: #{persisted_qs.id}"
|
||||
end
|
||||
end
|
||||
persisted_qs
|
||||
end
|
||||
persisted_qs
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,134 +25,135 @@ module Lti
|
|||
LtiDeepLinkingRequest
|
||||
).freeze
|
||||
|
||||
def self.external_tools_for(context, placements, options = {})
|
||||
tools_options = {}
|
||||
if options[:current_user]
|
||||
tools_options[:current_user] = options[:current_user]
|
||||
tools_options[:user] = options[:current_user]
|
||||
end
|
||||
if options[:only_visible]
|
||||
tools_options[:only_visible] = options[:only_visible]
|
||||
tools_options[:session] = options[:session] if options[:session]
|
||||
tools_options[:visibility_placements] = placements
|
||||
class << self
|
||||
def external_tools_for(context, placements, options = {})
|
||||
tools_options = {}
|
||||
if options[:current_user]
|
||||
tools_options[:current_user] = options[:current_user]
|
||||
tools_options[:user] = options[:current_user]
|
||||
end
|
||||
if options[:only_visible]
|
||||
tools_options[:only_visible] = options[:only_visible]
|
||||
tools_options[:session] = options[:session] if options[:session]
|
||||
tools_options[:visibility_placements] = placements
|
||||
end
|
||||
|
||||
ContextExternalTool.all_tools_for(context, tools_options).placements(*placements)
|
||||
end
|
||||
|
||||
ContextExternalTool.all_tools_for(context, tools_options).placements(*placements)
|
||||
end
|
||||
def message_handlers_for(context, placements)
|
||||
MessageHandler.for_context(context).has_placements(*placements)
|
||||
.by_message_types('basic-lti-launch-request')
|
||||
end
|
||||
|
||||
def self.message_handlers_for(context, placements)
|
||||
MessageHandler.for_context(context).has_placements(*placements)
|
||||
.by_message_types('basic-lti-launch-request')
|
||||
end
|
||||
def bookmarked_collection(context, placements, options = {})
|
||||
external_tools = external_tools_for(context, placements, options)
|
||||
external_tools = BookmarkedCollection.wrap(ExternalToolNameBookmarker, external_tools)
|
||||
|
||||
def self.bookmarked_collection(context, placements, options = {})
|
||||
external_tools = external_tools_for(context, placements, options)
|
||||
external_tools = BookmarkedCollection.wrap(ExternalToolNameBookmarker, external_tools)
|
||||
message_handlers = message_handlers_for(context, placements)
|
||||
message_handlers = BookmarkedCollection.wrap(MessageHandlerNameBookmarker, message_handlers)
|
||||
|
||||
message_handlers = message_handlers_for(context, placements)
|
||||
message_handlers = BookmarkedCollection.wrap(MessageHandlerNameBookmarker, message_handlers)
|
||||
BookmarkedCollection.merge(
|
||||
['external_tools', external_tools],
|
||||
['message_handlers', message_handlers]
|
||||
)
|
||||
end
|
||||
|
||||
BookmarkedCollection.merge(
|
||||
['external_tools', external_tools],
|
||||
['message_handlers', message_handlers]
|
||||
)
|
||||
end
|
||||
def any?(context, placements)
|
||||
external_tools = external_tools_for(context, placements)
|
||||
message_handlers = message_handlers_for(context, placements)
|
||||
external_tools.exists? || message_handlers.exists?
|
||||
end
|
||||
|
||||
def self.any?(context, placements)
|
||||
external_tools = external_tools_for(context, placements)
|
||||
message_handlers = message_handlers_for(context, placements)
|
||||
external_tools.exists? || message_handlers.exists?
|
||||
end
|
||||
|
||||
def self.launch_definitions(collection, placements)
|
||||
collection.map do |o|
|
||||
case o
|
||||
when ContextExternalTool
|
||||
lti1_launch_definition(o, placements)
|
||||
when MessageHandler
|
||||
lti2_launch_definition(o, placements)
|
||||
def launch_definitions(collection, placements)
|
||||
collection.map do |o|
|
||||
case o
|
||||
when ContextExternalTool
|
||||
lti1_launch_definition(o, placements)
|
||||
when MessageHandler
|
||||
lti2_launch_definition(o, placements)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
private
|
||||
|
||||
def self.selection_property_value(property, tool, placement, message_type)
|
||||
placement = placement.to_sym
|
||||
def selection_property_value(property, tool, placement, message_type)
|
||||
placement = placement.to_sym
|
||||
|
||||
# Only return selection property if the message type offers content selection
|
||||
return unless CONTENT_MESSAGE_TYPES.include?(message_type) || placement == :resource_selection
|
||||
# Only return selection property if the message type offers content selection
|
||||
return unless CONTENT_MESSAGE_TYPES.include?(message_type) || placement == :resource_selection
|
||||
|
||||
# For backward compatibility, check the "resource_selection" placement before the requested placement
|
||||
tool.extension_setting(:resource_selection, property) || tool.extension_setting(placement, property)
|
||||
end
|
||||
private_class_method :selection_property_value
|
||||
# For backward compatibility, check the "resource_selection" placement before the requested placement
|
||||
tool.extension_setting(:resource_selection, property) || tool.extension_setting(placement, property)
|
||||
end
|
||||
|
||||
def self.lti1_launch_definition(tool, placements)
|
||||
definition = {
|
||||
definition_type: tool.class.name,
|
||||
definition_id: tool.id,
|
||||
name: tool.label_for(placements.first, I18n.locale),
|
||||
description: tool.description,
|
||||
domain: tool.domain,
|
||||
placements: {}
|
||||
}
|
||||
placements.each do |p|
|
||||
if tool.has_placement?(p)
|
||||
definition[:placements][p.to_sym] = {
|
||||
message_type: tool.extension_setting(p, :message_type) || tool.extension_default_value(p, :message_type),
|
||||
url: tool.extension_setting(p, :url) || tool.extension_default_value(p, :url) || tool.extension_default_value(p, :target_link_uri),
|
||||
title: tool.label_for(p, I18n.locale || I18n.default_locale.to_s),
|
||||
}
|
||||
def lti1_launch_definition(tool, placements)
|
||||
definition = {
|
||||
definition_type: tool.class.name,
|
||||
definition_id: tool.id,
|
||||
name: tool.label_for(placements.first, I18n.locale),
|
||||
description: tool.description,
|
||||
domain: tool.domain,
|
||||
placements: {}
|
||||
}
|
||||
placements.each do |p|
|
||||
if tool.has_placement?(p)
|
||||
definition[:placements][p.to_sym] = {
|
||||
message_type: tool.extension_setting(p, :message_type) || tool.extension_default_value(p, :message_type),
|
||||
url: tool.extension_setting(p, :url) || tool.extension_default_value(p, :url) || tool.extension_default_value(p, :target_link_uri),
|
||||
title: tool.label_for(p, I18n.locale || I18n.default_locale.to_s),
|
||||
}
|
||||
|
||||
message_type = definition.dig(:placements, p.to_sym, :message_type)
|
||||
message_type = definition.dig(:placements, p.to_sym, :message_type)
|
||||
|
||||
if (width = selection_property_value(:selection_width, tool, p, message_type))
|
||||
definition[:placements][p.to_sym][:selection_width] = width
|
||||
end
|
||||
if (width = selection_property_value(:selection_width, tool, p, message_type))
|
||||
definition[:placements][p.to_sym][:selection_width] = width
|
||||
end
|
||||
|
||||
if (height = selection_property_value(:selection_height, tool, p, message_type))
|
||||
definition[:placements][p.to_sym][:selection_height] = height
|
||||
end
|
||||
if (height = selection_property_value(:selection_height, tool, p, message_type))
|
||||
definition[:placements][p.to_sym][:selection_height] = height
|
||||
end
|
||||
|
||||
%i[launch_width launch_height].each do |property|
|
||||
if tool.extension_setting(p, property)
|
||||
definition[:placements][p.to_sym][property] = tool.extension_setting(p, property)
|
||||
%i[launch_width launch_height].each do |property|
|
||||
if tool.extension_setting(p, property)
|
||||
definition[:placements][p.to_sym][property] = tool.extension_setting(p, property)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
definition
|
||||
end
|
||||
definition
|
||||
end
|
||||
|
||||
def self.lti2_launch_definition(message_handler, placements)
|
||||
{
|
||||
definition_type: message_handler.class.name,
|
||||
definition_id: message_handler.id,
|
||||
name: message_handler.resource_handler.name,
|
||||
description: message_handler.resource_handler.description,
|
||||
domain: URI(message_handler.launch_path).host,
|
||||
placements: self.lti2_placements(message_handler, placements)
|
||||
}
|
||||
end
|
||||
def lti2_launch_definition(message_handler, placements)
|
||||
{
|
||||
definition_type: message_handler.class.name,
|
||||
definition_id: message_handler.id,
|
||||
name: message_handler.resource_handler.name,
|
||||
description: message_handler.resource_handler.description,
|
||||
domain: URI(message_handler.launch_path).host,
|
||||
placements: self.lti2_placements(message_handler, placements)
|
||||
}
|
||||
end
|
||||
|
||||
def self.lti2_placements(message_handler, placements)
|
||||
resource_placements = message_handler.placements.pluck(:placement)
|
||||
valid_placements =
|
||||
if resource_placements.present?
|
||||
resource_placements & placements.map(&:to_s)
|
||||
else
|
||||
ResourcePlacement::LEGACY_DEFAULT_PLACEMENTS
|
||||
end
|
||||
valid_placements.each_with_object({}) { |p, hsh| hsh[p.to_sym] = lti2_placement(message_handler) }
|
||||
end
|
||||
def lti2_placements(message_handler, placements)
|
||||
resource_placements = message_handler.placements.pluck(:placement)
|
||||
valid_placements =
|
||||
if resource_placements.present?
|
||||
resource_placements & placements.map(&:to_s)
|
||||
else
|
||||
ResourcePlacement::LEGACY_DEFAULT_PLACEMENTS
|
||||
end
|
||||
valid_placements.each_with_object({}) { |p, hsh| hsh[p.to_sym] = lti2_placement(message_handler) }
|
||||
end
|
||||
|
||||
def self.lti2_placement(message_handler)
|
||||
{
|
||||
message_type: message_handler.message_type,
|
||||
url: message_handler.launch_path,
|
||||
title: message_handler.resource_handler.name
|
||||
}
|
||||
def lti2_placement(message_handler)
|
||||
{
|
||||
message_type: message_handler.message_type,
|
||||
url: message_handler.launch_path,
|
||||
title: message_handler.resource_handler.name
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -45,8 +45,6 @@ class MessageDispatcher < Delayed::PerformableMethod
|
|||
object.dispatch_at
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def self.deliver_batch(messages)
|
||||
if messages.first.is_a?(Message::Queued)
|
||||
queued = messages.sort_by(&:created_at)
|
||||
|
|
|
@ -30,74 +30,76 @@ module PlannerHelper
|
|||
'assessment_request' => 'AssessmentRequest'
|
||||
}.freeze
|
||||
|
||||
def self.planner_meta_cache_key(user)
|
||||
['planner_items_meta', user].cache_key
|
||||
end
|
||||
class << self
|
||||
def planner_meta_cache_key(user)
|
||||
['planner_items_meta', user].cache_key
|
||||
end
|
||||
|
||||
def self.get_planner_cache_id(user)
|
||||
Rails.cache.fetch(planner_meta_cache_key(user), expires_in: 1.week) do
|
||||
SecureRandom.uuid
|
||||
def get_planner_cache_id(user)
|
||||
Rails.cache.fetch(planner_meta_cache_key(user), expires_in: 1.week) do
|
||||
SecureRandom.uuid
|
||||
end
|
||||
end
|
||||
|
||||
def clear_planner_cache(user)
|
||||
Rails.cache.delete(planner_meta_cache_key(user))
|
||||
end
|
||||
|
||||
# Handles real Submissions associated with graded things
|
||||
def complete_planner_override_for_submission(submission)
|
||||
planner_override = find_planner_override_for_submission(submission)
|
||||
complete_planner_override planner_override
|
||||
end
|
||||
|
||||
# Ungraded surveys are submitted as a Quizzes::QuizSubmission that
|
||||
# had no submission attribute pointing to a real Submission
|
||||
def complete_planner_override_for_quiz_submission(quiz_submission)
|
||||
return if quiz_submission.submission # handled by Submission model
|
||||
|
||||
planner_override = PlannerOverride.find_or_create_by(
|
||||
plannable_id: quiz_submission.quiz_id,
|
||||
plannable_type: PLANNABLE_TYPES['quiz'],
|
||||
user_id: quiz_submission.user_id
|
||||
)
|
||||
complete_planner_override planner_override
|
||||
end
|
||||
|
||||
def complete_planner_override(planner_override)
|
||||
return unless planner_override.is_a? PlannerOverride
|
||||
|
||||
planner_override.update(marked_complete: true)
|
||||
clear_planner_cache(planner_override&.user)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# until the graded objects are handled more uniformly,
|
||||
# we have to look around for an associated override
|
||||
def find_planner_override_for_submission(submission)
|
||||
return unless submission.respond_to?(:submission_type) && submission.respond_to?(:assignment_id)
|
||||
|
||||
planner_override = case submission.submission_type
|
||||
when "discussion_topic"
|
||||
discussion_topic_id = DiscussionTopic.find_by(assignment_id: submission.assignment_id)&.id
|
||||
PlannerOverride.find_by(
|
||||
plannable_id: discussion_topic_id,
|
||||
plannable_type: PLANNABLE_TYPES['discussion_topic'],
|
||||
user_id: submission.user_id
|
||||
)
|
||||
when "online_quiz"
|
||||
quiz_id = Quizzes::Quiz.find_by(assignment_id: submission.assignment_id)&.id
|
||||
PlannerOverride.find_by(
|
||||
plannable_id: quiz_id,
|
||||
plannable_type: PLANNABLE_TYPES['quiz'],
|
||||
user_id: submission.user_id
|
||||
)
|
||||
end
|
||||
planner_override ||= PlannerOverride.find_by(
|
||||
plannable_id: submission.assignment_id,
|
||||
plannable_type: PLANNABLE_TYPES['assignment'],
|
||||
user_id: submission.user_id
|
||||
)
|
||||
planner_override
|
||||
end
|
||||
end
|
||||
|
||||
def self.clear_planner_cache(user)
|
||||
Rails.cache.delete(planner_meta_cache_key(user))
|
||||
end
|
||||
|
||||
# Handles real Submissions associated with graded things
|
||||
def self.complete_planner_override_for_submission(submission)
|
||||
planner_override = find_planner_override_for_submission(submission)
|
||||
complete_planner_override planner_override
|
||||
end
|
||||
|
||||
# Ungraded surveys are submitted as a Quizzes::QuizSubmission that
|
||||
# had no submission attribute pointing to a real Submission
|
||||
def self.complete_planner_override_for_quiz_submission(quiz_submission)
|
||||
return if quiz_submission.submission # handled by Submission model
|
||||
|
||||
planner_override = PlannerOverride.find_or_create_by(
|
||||
plannable_id: quiz_submission.quiz_id,
|
||||
plannable_type: PLANNABLE_TYPES['quiz'],
|
||||
user_id: quiz_submission.user_id
|
||||
)
|
||||
complete_planner_override planner_override
|
||||
end
|
||||
|
||||
def self.complete_planner_override(planner_override)
|
||||
return unless planner_override.is_a? PlannerOverride
|
||||
|
||||
planner_override.update(marked_complete: true)
|
||||
clear_planner_cache(planner_override&.user)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# until the graded objects are handled more uniformly,
|
||||
# we have to look around for an associated override
|
||||
def self.find_planner_override_for_submission(submission)
|
||||
return unless submission.respond_to?(:submission_type) && submission.respond_to?(:assignment_id)
|
||||
|
||||
planner_override = case submission.submission_type
|
||||
when "discussion_topic"
|
||||
discussion_topic_id = DiscussionTopic.find_by(assignment_id: submission.assignment_id)&.id
|
||||
PlannerOverride.find_by(
|
||||
plannable_id: discussion_topic_id,
|
||||
plannable_type: PLANNABLE_TYPES['discussion_topic'],
|
||||
user_id: submission.user_id
|
||||
)
|
||||
when "online_quiz"
|
||||
quiz_id = Quizzes::Quiz.find_by(assignment_id: submission.assignment_id)&.id
|
||||
PlannerOverride.find_by(
|
||||
plannable_id: quiz_id,
|
||||
plannable_type: PLANNABLE_TYPES['quiz'],
|
||||
user_id: submission.user_id
|
||||
)
|
||||
end
|
||||
planner_override ||= PlannerOverride.find_by(
|
||||
plannable_id: submission.assignment_id,
|
||||
plannable_type: PLANNABLE_TYPES['assignment'],
|
||||
user_id: submission.user_id
|
||||
)
|
||||
planner_override
|
||||
end
|
||||
end
|
||||
|
|
|
@ -66,6 +66,10 @@ module SimpleTags
|
|||
end
|
||||
|
||||
module WriterInstanceMethods
|
||||
def self.included(klass)
|
||||
klass.before_save :serialize_tags
|
||||
end
|
||||
|
||||
def tags=(new_tags)
|
||||
tags_will_change! unless tags == new_tags
|
||||
@tag_array = new_tags || []
|
||||
|
@ -84,10 +88,6 @@ module SimpleTags
|
|||
remove_instance_variable :@tag_array
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(klass)
|
||||
klass.before_save :serialize_tags
|
||||
end
|
||||
end
|
||||
|
||||
def self.normalize_tags(tags)
|
||||
|
|
|
@ -21,128 +21,130 @@ class SortsAssignments
|
|||
VALID_BUCKETS = [:past, :overdue, :undated, :ungraded, :unsubmitted, :upcoming, :future]
|
||||
AssignmentsSortedByDueDate = Struct.new(*VALID_BUCKETS)
|
||||
|
||||
def self.by_due_date(opts)
|
||||
assignments = opts.fetch(:assignments)
|
||||
user = opts.fetch(:user)
|
||||
current_user = opts[:current_user] || opts.fetch(:user)
|
||||
session = opts.fetch(:session)
|
||||
submissions = opts[:submissions]
|
||||
upcoming_limit = opts[:upcoming_limit] || 1.week.from_now
|
||||
course = opts[:course]
|
||||
class << self
|
||||
def by_due_date(opts)
|
||||
assignments = opts.fetch(:assignments)
|
||||
user = opts.fetch(:user)
|
||||
current_user = opts[:current_user] || opts.fetch(:user)
|
||||
session = opts.fetch(:session)
|
||||
submissions = opts[:submissions]
|
||||
upcoming_limit = opts[:upcoming_limit] || 1.week.from_now
|
||||
course = opts[:course]
|
||||
|
||||
AssignmentsSortedByDueDate.new(
|
||||
-> { past(assignments) },
|
||||
-> { overdue(assignments, user, session, submissions) },
|
||||
-> { undated(assignments) },
|
||||
-> { ungraded_for_user_and_session(assignments, user, current_user, session) },
|
||||
-> { unsubmitted_for_user_and_session(course, assignments, user, current_user, session) },
|
||||
-> { upcoming(assignments, upcoming_limit) },
|
||||
-> { future(assignments) }
|
||||
)
|
||||
end
|
||||
|
||||
def self.past(assignments)
|
||||
assignments ||= []
|
||||
dated(assignments).select { |assignment| assignment.due_at < Time.now }
|
||||
end
|
||||
|
||||
def self.dated(assignments)
|
||||
assignments ||= []
|
||||
assignments.reject { |assignment| assignment.due_at == nil }
|
||||
end
|
||||
|
||||
def self.undated(assignments)
|
||||
assignments ||= []
|
||||
assignments.select { |assignment| assignment.due_at == nil }
|
||||
end
|
||||
|
||||
def self.unsubmitted_for_user_and_session(course, assignments, user, current_user, session)
|
||||
return [] unless course.grants_right?(current_user, session, :manage_grades)
|
||||
|
||||
assignments ||= []
|
||||
assignments.select do |assignment|
|
||||
assignment.expects_submission? &&
|
||||
assignment.submission_for_student(user)[:id].blank?
|
||||
AssignmentsSortedByDueDate.new(
|
||||
-> { past(assignments) },
|
||||
-> { overdue(assignments, user, session, submissions) },
|
||||
-> { undated(assignments) },
|
||||
-> { ungraded_for_user_and_session(assignments, user, current_user, session) },
|
||||
-> { unsubmitted_for_user_and_session(course, assignments, user, current_user, session) },
|
||||
-> { upcoming(assignments, upcoming_limit) },
|
||||
-> { future(assignments) }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def self.upcoming(assignments, limit = 1.week.from_now)
|
||||
assignments ||= []
|
||||
dated(assignments).select { |a| due_between?(a, Time.now, limit) }
|
||||
end
|
||||
def past(assignments)
|
||||
assignments ||= []
|
||||
dated(assignments).select { |assignment| assignment.due_at < Time.now }
|
||||
end
|
||||
|
||||
def self.future(assignments)
|
||||
assignments - past(assignments)
|
||||
end
|
||||
def dated(assignments)
|
||||
assignments ||= []
|
||||
assignments.reject { |assignment| assignment.due_at == nil }
|
||||
end
|
||||
|
||||
def self.up_to(assignments, time)
|
||||
dated(assignments).select { |assignment| assignment.due_at < time }
|
||||
end
|
||||
def undated(assignments)
|
||||
assignments ||= []
|
||||
assignments.select { |assignment| assignment.due_at == nil }
|
||||
end
|
||||
|
||||
def self.down_to(assignments, time)
|
||||
dated(assignments).select { |assignment| assignment.due_at > time }
|
||||
end
|
||||
def unsubmitted_for_user_and_session(course, assignments, user, current_user, session)
|
||||
return [] unless course.grants_right?(current_user, session, :manage_grades)
|
||||
|
||||
def self.ungraded_for_user_and_session(assignments, user, current_user, session)
|
||||
assignments ||= []
|
||||
assignments.select do |assignment|
|
||||
assignment.grants_right?(current_user, session, :grade) &&
|
||||
assignments ||= []
|
||||
assignments.select do |assignment|
|
||||
assignment.expects_submission? &&
|
||||
Assignments::NeedsGradingCountQuery.new(assignment, user).count > 0
|
||||
assignment.submission_for_student(user)[:id].blank?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.without_graded_submission(assignments, submissions)
|
||||
assignments ||= []; submissions ||= [];
|
||||
submissions_by_assignment = submissions.inject({}) do |memo, sub|
|
||||
memo[sub.assignment_id] = sub
|
||||
memo
|
||||
def upcoming(assignments, limit = 1.week.from_now)
|
||||
assignments ||= []
|
||||
dated(assignments).select { |a| due_between?(a, Time.now, limit) }
|
||||
end
|
||||
assignments.select do |assignment|
|
||||
match = submissions_by_assignment[assignment.id]
|
||||
!match || match.without_graded_submission?
|
||||
|
||||
def future(assignments)
|
||||
assignments - past(assignments)
|
||||
end
|
||||
end
|
||||
|
||||
def self.user_allowed_to_submit(assignments, user, session)
|
||||
assignments ||= []
|
||||
assignments.select do |assignment|
|
||||
assignment.expects_submission? && assignment.grants_right?(user, session, :submit)
|
||||
def up_to(assignments, time)
|
||||
dated(assignments).select { |assignment| assignment.due_at < time }
|
||||
end
|
||||
end
|
||||
|
||||
def self.overdue(assignments, user, session, submissions)
|
||||
submissions ||= []
|
||||
assignments = past(assignments)
|
||||
user_allowed_to_submit(assignments, user, session) &
|
||||
without_graded_submission(assignments, submissions)
|
||||
end
|
||||
def down_to(assignments, time)
|
||||
dated(assignments).select { |assignment| assignment.due_at > time }
|
||||
end
|
||||
|
||||
def self.bucket_filter(given_scope, bucket, session, user, current_user, context, submissions_for_user)
|
||||
overridden_assignments = given_scope.map { |a| a.overridden_for(user) }
|
||||
def ungraded_for_user_and_session(assignments, user, current_user, session)
|
||||
assignments ||= []
|
||||
assignments.select do |assignment|
|
||||
assignment.grants_right?(current_user, session, :grade) &&
|
||||
assignment.expects_submission? &&
|
||||
Assignments::NeedsGradingCountQuery.new(assignment, user).count > 0
|
||||
end
|
||||
end
|
||||
|
||||
observed_students = ObserverEnrollment.observed_students(context, user)
|
||||
user_for_sorting = if observed_students.count == 1
|
||||
observed_students.keys.first
|
||||
else
|
||||
user
|
||||
end
|
||||
def without_graded_submission(assignments, submissions)
|
||||
assignments ||= []; submissions ||= [];
|
||||
submissions_by_assignment = submissions.inject({}) do |memo, sub|
|
||||
memo[sub.assignment_id] = sub
|
||||
memo
|
||||
end
|
||||
assignments.select do |assignment|
|
||||
match = submissions_by_assignment[assignment.id]
|
||||
!match || match.without_graded_submission?
|
||||
end
|
||||
end
|
||||
|
||||
sorted_assignments = self.by_due_date(
|
||||
:course => context,
|
||||
:assignments => overridden_assignments,
|
||||
:user => user_for_sorting,
|
||||
:current_user => current_user,
|
||||
:session => session,
|
||||
:submissions => submissions_for_user
|
||||
)
|
||||
filtered_assignment_ids = sorted_assignments.send(bucket).call.map(&:id)
|
||||
given_scope.where(id: filtered_assignment_ids)
|
||||
end
|
||||
def user_allowed_to_submit(assignments, user, session)
|
||||
assignments ||= []
|
||||
assignments.select do |assignment|
|
||||
assignment.expects_submission? && assignment.grants_right?(user, session, :submit)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def overdue(assignments, user, session, submissions)
|
||||
submissions ||= []
|
||||
assignments = past(assignments)
|
||||
user_allowed_to_submit(assignments, user, session) &
|
||||
without_graded_submission(assignments, submissions)
|
||||
end
|
||||
|
||||
def self.due_between?(assignment, start_time, end_time)
|
||||
assignment.due_at >= start_time && assignment.due_at <= end_time
|
||||
def bucket_filter(given_scope, bucket, session, user, current_user, context, submissions_for_user)
|
||||
overridden_assignments = given_scope.map { |a| a.overridden_for(user) }
|
||||
|
||||
observed_students = ObserverEnrollment.observed_students(context, user)
|
||||
user_for_sorting = if observed_students.count == 1
|
||||
observed_students.keys.first
|
||||
else
|
||||
user
|
||||
end
|
||||
|
||||
sorted_assignments = self.by_due_date(
|
||||
:course => context,
|
||||
:assignments => overridden_assignments,
|
||||
:user => user_for_sorting,
|
||||
:current_user => current_user,
|
||||
:session => session,
|
||||
:submissions => submissions_for_user
|
||||
)
|
||||
filtered_assignment_ids = sorted_assignments.send(bucket).call.map(&:id)
|
||||
given_scope.where(id: filtered_assignment_ids)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def due_between?(assignment, start_time, end_time)
|
||||
assignment.due_at >= start_time && assignment.due_at <= end_time
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,206 +18,208 @@
|
|||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
module UserSearch
|
||||
def self.for_user_in_context(search_term, context, searcher, session = nil, options = {})
|
||||
search_term = search_term.to_s
|
||||
return User.none if search_term.strip.empty?
|
||||
class << self
|
||||
def for_user_in_context(search_term, context, searcher, session = nil, options = {})
|
||||
search_term = search_term.to_s
|
||||
return User.none if search_term.strip.empty?
|
||||
|
||||
SearchTermHelper.validate_search_term(search_term)
|
||||
SearchTermHelper.validate_search_term(search_term)
|
||||
|
||||
@is_id = search_term =~ Api::ID_REGEX && Api::MAX_ID_RANGE.cover?(search_term.to_i)
|
||||
@include_login = context.grants_right?(searcher, session, :view_user_logins)
|
||||
@include_email = context.grants_right?(searcher, session, :read_email_addresses)
|
||||
@include_sis = context.grants_any_right?(searcher, session, :read_sis, :manage_sis)
|
||||
@is_id = search_term =~ Api::ID_REGEX && Api::MAX_ID_RANGE.cover?(search_term.to_i)
|
||||
@include_login = context.grants_right?(searcher, session, :view_user_logins)
|
||||
@include_email = context.grants_right?(searcher, session, :read_email_addresses)
|
||||
@include_sis = context.grants_any_right?(searcher, session, :read_sis, :manage_sis)
|
||||
|
||||
context.shard.activate do
|
||||
context.shard.activate do
|
||||
users_scope = context_scope(context, searcher, options.slice(:enrollment_state, :include_inactive_enrollments))
|
||||
users_scope = users_scope.from("(#{conditions_statement(search_term, context.root_account, users_scope)}) AS users")
|
||||
users_scope = order_scope(users_scope, context, options.slice(:order, :sort))
|
||||
roles_scope(users_scope, context, options.slice(:enrollment_type, :enrollment_role,
|
||||
:enrollment_role_id, :exclude_groups))
|
||||
end
|
||||
end
|
||||
|
||||
def conditions_statement(search_term, root_account, users_scope)
|
||||
pattern = like_string_for(search_term)
|
||||
params = { pattern: pattern, account: root_account, path_type: CommunicationChannel::TYPE_EMAIL, db_id: search_term }
|
||||
complex_sql(users_scope, params)
|
||||
end
|
||||
|
||||
def like_string_for(search_term)
|
||||
pattern_type = (gist_search_enabled? ? :full : :right)
|
||||
wildcard_pattern(search_term, :type => pattern_type, :case_sensitive => false)
|
||||
end
|
||||
|
||||
def like_condition(value)
|
||||
ActiveRecord::Base.like_condition(value, 'lower(:pattern)')
|
||||
end
|
||||
|
||||
def scope_for(context, searcher, options = {})
|
||||
users_scope = context_scope(context, searcher, options.slice(:enrollment_state, :include_inactive_enrollments))
|
||||
users_scope = users_scope.from("(#{conditions_statement(search_term, context.root_account, users_scope)}) AS users")
|
||||
users_scope = order_scope(users_scope, context, options.slice(:order, :sort))
|
||||
roles_scope(users_scope, context, options.slice(:enrollment_type, :enrollment_role,
|
||||
:enrollment_role_id, :exclude_groups))
|
||||
roles_scope(users_scope, context, options.slice(:enrollment_role, :enrollment_role_id, :enrollment_type, :exclude_groups))
|
||||
end
|
||||
end
|
||||
|
||||
def self.conditions_statement(search_term, root_account, users_scope)
|
||||
pattern = like_string_for(search_term)
|
||||
params = { pattern: pattern, account: root_account, path_type: CommunicationChannel::TYPE_EMAIL, db_id: search_term }
|
||||
complex_sql(users_scope, params)
|
||||
end
|
||||
|
||||
def self.like_string_for(search_term)
|
||||
pattern_type = (gist_search_enabled? ? :full : :right)
|
||||
wildcard_pattern(search_term, :type => pattern_type, :case_sensitive => false)
|
||||
end
|
||||
|
||||
def self.scope_for(context, searcher, options = {})
|
||||
users_scope = context_scope(context, searcher, options.slice(:enrollment_state, :include_inactive_enrollments))
|
||||
users_scope = order_scope(users_scope, context, options.slice(:order, :sort))
|
||||
roles_scope(users_scope, context, options.slice(:enrollment_role, :enrollment_role_id, :enrollment_type, :exclude_groups))
|
||||
end
|
||||
|
||||
def self.context_scope(context, searcher, options = {})
|
||||
enrollment_states = Array(options[:enrollment_state]) if options[:enrollment_state]
|
||||
include_prior_enrollments = !options[:enrollment_state].nil?
|
||||
include_inactive_enrollments = !!options[:include_inactive_enrollments]
|
||||
if context.is_a?(Account)
|
||||
User.of_account(context).active
|
||||
elsif context.is_a?(Course)
|
||||
context.users_visible_to(searcher, include_prior_enrollments,
|
||||
enrollment_state: enrollment_states, include_inactive: include_inactive_enrollments).distinct
|
||||
else
|
||||
context.users_visible_to(searcher, include_inactive: include_inactive_enrollments).distinct
|
||||
end
|
||||
end
|
||||
|
||||
def self.order_scope(users_scope, context, options = {})
|
||||
order = ' DESC NULLS LAST, id DESC' if options[:order] == 'desc'
|
||||
if options[:sort] == "last_login"
|
||||
users_scope.select("users.*").order(Arel.sql("last_login#{order}"))
|
||||
elsif options[:sort] == "username"
|
||||
users_scope.select("users.*").order_by_sortable_name(direction: options[:order] == 'desc' ? :descending : :ascending)
|
||||
elsif options[:sort] == "email"
|
||||
users_scope = users_scope.select("users.*, (SELECT path FROM #{CommunicationChannel.quoted_table_name}
|
||||
WHERE communication_channels.user_id = users.id AND
|
||||
communication_channels.path_type = 'email' AND
|
||||
communication_channels.workflow_state <> 'retired'
|
||||
ORDER BY communication_channels.position ASC
|
||||
LIMIT 1)
|
||||
AS email")
|
||||
users_scope.order(Arel.sql("email#{order}"))
|
||||
elsif options[:sort] == "sis_id"
|
||||
users_scope = users_scope.select(User.send(:sanitize_sql, [
|
||||
"users.*, (SELECT sis_user_id FROM #{Pseudonym.quoted_table_name}
|
||||
WHERE pseudonyms.user_id = users.id AND
|
||||
pseudonyms.workflow_state <> 'deleted' AND
|
||||
pseudonyms.account_id = ?
|
||||
LIMIT 1) AS sis_user_id",
|
||||
context.try(:resolved_root_account_id) || context.root_account_id
|
||||
]))
|
||||
users_scope.order(Arel.sql("sis_user_id#{order}"))
|
||||
else
|
||||
users_scope.select("users.*").order_by_sortable_name
|
||||
end
|
||||
end
|
||||
|
||||
def self.roles_scope(users_scope, context, options = {})
|
||||
enrollment_roles = Array(options[:enrollment_role]) if options[:enrollment_role]
|
||||
enrollment_role_ids = Array(options[:enrollment_role_id]) if options[:enrollment_role_id]
|
||||
enrollment_types = Array(options[:enrollment_type]) if options[:enrollment_type]
|
||||
exclude_groups = Array(options[:exclude_groups]) if options[:exclude_groups]
|
||||
|
||||
if enrollment_role_ids || enrollment_roles
|
||||
users_scope = users_scope.joins(:not_removed_enrollments).distinct if context.is_a?(Account)
|
||||
roles = if enrollment_role_ids
|
||||
enrollment_role_ids.map { |id| Role.get_role_by_id(id) }.compact
|
||||
else
|
||||
enrollment_roles.map do |name|
|
||||
if context.is_a?(Account)
|
||||
context.get_course_role_by_name(name)
|
||||
else
|
||||
context.account.get_course_role_by_name(name)
|
||||
end
|
||||
end.compact
|
||||
end
|
||||
users_scope = users_scope.where("role_id IN (?)", roles.map(&:id))
|
||||
elsif enrollment_types
|
||||
enrollment_types = enrollment_types.map { |e| "#{e.camelize}Enrollment" }
|
||||
if enrollment_types.any? { |et| !Enrollment.readable_types.keys.include?(et) }
|
||||
raise ArgumentError, 'Invalid Enrollment Type'
|
||||
end
|
||||
|
||||
def context_scope(context, searcher, options = {})
|
||||
enrollment_states = Array(options[:enrollment_state]) if options[:enrollment_state]
|
||||
include_prior_enrollments = !options[:enrollment_state].nil?
|
||||
include_inactive_enrollments = !!options[:include_inactive_enrollments]
|
||||
if context.is_a?(Account)
|
||||
# for example, one user can have multiple teacher enrollments, but
|
||||
# we only want one such a user record in results
|
||||
users_scope = users_scope.where("EXISTS (?)", Enrollment.where("enrollments.user_id=users.id").active.where(type: enrollment_types)).distinct
|
||||
User.of_account(context).active
|
||||
elsif context.is_a?(Course)
|
||||
context.users_visible_to(searcher, include_prior_enrollments,
|
||||
enrollment_state: enrollment_states, include_inactive: include_inactive_enrollments).distinct
|
||||
else
|
||||
if context.is_a?(Group) && context.context_type == "Course"
|
||||
users_scope = users_scope.joins(:enrollments).where(:enrollments => { :course_id => context.context_id })
|
||||
end
|
||||
users_scope = users_scope.where(:enrollments => { :type => enrollment_types })
|
||||
context.users_visible_to(searcher, include_inactive: include_inactive_enrollments).distinct
|
||||
end
|
||||
end
|
||||
|
||||
if exclude_groups
|
||||
users_scope = users_scope.where(Group.not_in_group_sql_fragment(exclude_groups))
|
||||
def order_scope(users_scope, context, options = {})
|
||||
order = ' DESC NULLS LAST, id DESC' if options[:order] == 'desc'
|
||||
if options[:sort] == "last_login"
|
||||
users_scope.select("users.*").order(Arel.sql("last_login#{order}"))
|
||||
elsif options[:sort] == "username"
|
||||
users_scope.select("users.*").order_by_sortable_name(direction: options[:order] == 'desc' ? :descending : :ascending)
|
||||
elsif options[:sort] == "email"
|
||||
users_scope = users_scope.select("users.*, (SELECT path FROM #{CommunicationChannel.quoted_table_name}
|
||||
WHERE communication_channels.user_id = users.id AND
|
||||
communication_channels.path_type = 'email' AND
|
||||
communication_channels.workflow_state <> 'retired'
|
||||
ORDER BY communication_channels.position ASC
|
||||
LIMIT 1)
|
||||
AS email")
|
||||
users_scope.order(Arel.sql("email#{order}"))
|
||||
elsif options[:sort] == "sis_id"
|
||||
users_scope = users_scope.select(User.send(:sanitize_sql, [
|
||||
"users.*, (SELECT sis_user_id FROM #{Pseudonym.quoted_table_name}
|
||||
WHERE pseudonyms.user_id = users.id AND
|
||||
pseudonyms.workflow_state <> 'deleted' AND
|
||||
pseudonyms.account_id = ?
|
||||
LIMIT 1) AS sis_user_id",
|
||||
context.try(:resolved_root_account_id) || context.root_account_id
|
||||
]))
|
||||
users_scope.order(Arel.sql("sis_user_id#{order}"))
|
||||
else
|
||||
users_scope.select("users.*").order_by_sortable_name
|
||||
end
|
||||
end
|
||||
|
||||
users_scope
|
||||
end
|
||||
def roles_scope(users_scope, context, options = {})
|
||||
enrollment_roles = Array(options[:enrollment_role]) if options[:enrollment_role]
|
||||
enrollment_role_ids = Array(options[:enrollment_role_id]) if options[:enrollment_role_id]
|
||||
enrollment_types = Array(options[:enrollment_type]) if options[:enrollment_type]
|
||||
exclude_groups = Array(options[:exclude_groups]) if options[:exclude_groups]
|
||||
|
||||
private
|
||||
if enrollment_role_ids || enrollment_roles
|
||||
users_scope = users_scope.joins(:not_removed_enrollments).distinct if context.is_a?(Account)
|
||||
roles = if enrollment_role_ids
|
||||
enrollment_role_ids.map { |id| Role.get_role_by_id(id) }.compact
|
||||
else
|
||||
enrollment_roles.map do |name|
|
||||
if context.is_a?(Account)
|
||||
context.get_course_role_by_name(name)
|
||||
else
|
||||
context.account.get_course_role_by_name(name)
|
||||
end
|
||||
end.compact
|
||||
end
|
||||
users_scope = users_scope.where("role_id IN (?)", roles.map(&:id))
|
||||
elsif enrollment_types
|
||||
enrollment_types = enrollment_types.map { |e| "#{e.camelize}Enrollment" }
|
||||
if enrollment_types.any? { |et| !Enrollment.readable_types.keys.include?(et) }
|
||||
raise ArgumentError, 'Invalid Enrollment Type'
|
||||
end
|
||||
|
||||
def self.complex_sql(users_scope, params)
|
||||
users_scope = users_scope.group(:id)
|
||||
queries = [name_sql(users_scope, params)]
|
||||
if complex_search_enabled?
|
||||
queries << id_sql(users_scope, params) if @is_id
|
||||
queries << login_sql(users_scope, params) if @include_login
|
||||
queries << sis_sql(users_scope, params) if @include_sis
|
||||
queries << email_sql(users_scope, params) if @include_email
|
||||
if context.is_a?(Account)
|
||||
# for example, one user can have multiple teacher enrollments, but
|
||||
# we only want one such a user record in results
|
||||
users_scope = users_scope.where("EXISTS (?)", Enrollment.where("enrollments.user_id=users.id").active.where(type: enrollment_types)).distinct
|
||||
else
|
||||
if context.is_a?(Group) && context.context_type == "Course"
|
||||
users_scope = users_scope.joins(:enrollments).where(:enrollments => { :course_id => context.context_id })
|
||||
end
|
||||
users_scope = users_scope.where(:enrollments => { :type => enrollment_types })
|
||||
end
|
||||
end
|
||||
|
||||
if exclude_groups
|
||||
users_scope = users_scope.where(Group.not_in_group_sql_fragment(exclude_groups))
|
||||
end
|
||||
|
||||
users_scope
|
||||
end
|
||||
queries.map(&:to_sql).join("\nUNION\n")
|
||||
end
|
||||
|
||||
def self.id_sql(users_scope, params)
|
||||
users_scope.select("users.*, MAX(current_login_at) as last_login")
|
||||
.joins("LEFT JOIN #{Pseudonym.quoted_table_name} ON pseudonyms.user_id = users.id
|
||||
AND pseudonyms.account_id = #{User.connection.quote(params[:account])}
|
||||
AND pseudonyms.workflow_state = 'active'")
|
||||
.where(id: params[:db_id])
|
||||
.group(:id)
|
||||
end
|
||||
private
|
||||
|
||||
def self.name_sql(users_scope, params)
|
||||
users_scope.select("users.*, MAX(current_login_at) as last_login")
|
||||
.joins("LEFT JOIN #{Pseudonym.quoted_table_name} ON pseudonyms.user_id = users.id
|
||||
AND pseudonyms.account_id = #{User.connection.quote(params[:account])}
|
||||
AND pseudonyms.workflow_state = 'active'")
|
||||
.where(like_condition('users.name'), pattern: params[:pattern])
|
||||
end
|
||||
def complex_sql(users_scope, params)
|
||||
users_scope = users_scope.group(:id)
|
||||
queries = [name_sql(users_scope, params)]
|
||||
if complex_search_enabled?
|
||||
queries << id_sql(users_scope, params) if @is_id
|
||||
queries << login_sql(users_scope, params) if @include_login
|
||||
queries << sis_sql(users_scope, params) if @include_sis
|
||||
queries << email_sql(users_scope, params) if @include_email
|
||||
end
|
||||
queries.map(&:to_sql).join("\nUNION\n")
|
||||
end
|
||||
|
||||
def self.login_sql(users_scope, params)
|
||||
users_scope.select("users.*, MAX(logins.current_login_at) as last_login")
|
||||
.joins(:pseudonyms)
|
||||
.joins("LEFT JOIN #{Pseudonym.quoted_table_name} AS logins ON logins.user_id = users.id
|
||||
AND logins.account_id = #{User.connection.quote(params[:account])}
|
||||
AND logins.workflow_state = 'active'")
|
||||
.where(pseudonyms: { account_id: params[:account], workflow_state: 'active' })
|
||||
.where(like_condition('pseudonyms.unique_id'), pattern: params[:pattern])
|
||||
end
|
||||
def id_sql(users_scope, params)
|
||||
users_scope.select("users.*, MAX(current_login_at) as last_login")
|
||||
.joins("LEFT JOIN #{Pseudonym.quoted_table_name} ON pseudonyms.user_id = users.id
|
||||
AND pseudonyms.account_id = #{User.connection.quote(params[:account])}
|
||||
AND pseudonyms.workflow_state = 'active'")
|
||||
.where(id: params[:db_id])
|
||||
.group(:id)
|
||||
end
|
||||
|
||||
def self.sis_sql(users_scope, params)
|
||||
users_scope.select("users.*, MAX(logins.current_login_at) as last_login")
|
||||
.joins(:pseudonyms)
|
||||
.joins("LEFT JOIN #{Pseudonym.quoted_table_name} AS logins ON logins.user_id = users.id
|
||||
AND logins.account_id = #{User.connection.quote(params[:account])}
|
||||
AND logins.workflow_state = 'active'")
|
||||
.where(pseudonyms: { account_id: params[:account], workflow_state: 'active' })
|
||||
.where(like_condition('pseudonyms.sis_user_id'), pattern: params[:pattern])
|
||||
end
|
||||
def name_sql(users_scope, params)
|
||||
users_scope.select("users.*, MAX(current_login_at) as last_login")
|
||||
.joins("LEFT JOIN #{Pseudonym.quoted_table_name} ON pseudonyms.user_id = users.id
|
||||
AND pseudonyms.account_id = #{User.connection.quote(params[:account])}
|
||||
AND pseudonyms.workflow_state = 'active'")
|
||||
.where(like_condition('users.name'), pattern: params[:pattern])
|
||||
end
|
||||
|
||||
def self.email_sql(users_scope, params)
|
||||
users_scope.select("users.*, MAX(current_login_at) as last_login")
|
||||
.joins(:communication_channels)
|
||||
.joins("LEFT JOIN #{Pseudonym.quoted_table_name} ON pseudonyms.user_id = users.id
|
||||
AND pseudonyms.account_id = #{User.connection.quote(params[:account])}
|
||||
AND pseudonyms.workflow_state = 'active'")
|
||||
.where(communication_channels: { workflow_state: ['active', 'unconfirmed'], path_type: params[:path_type] })
|
||||
.where(like_condition('communication_channels.path'), pattern: params[:pattern])
|
||||
end
|
||||
def login_sql(users_scope, params)
|
||||
users_scope.select("users.*, MAX(logins.current_login_at) as last_login")
|
||||
.joins(:pseudonyms)
|
||||
.joins("LEFT JOIN #{Pseudonym.quoted_table_name} AS logins ON logins.user_id = users.id
|
||||
AND logins.account_id = #{User.connection.quote(params[:account])}
|
||||
AND logins.workflow_state = 'active'")
|
||||
.where(pseudonyms: { account_id: params[:account], workflow_state: 'active' })
|
||||
.where(like_condition('pseudonyms.unique_id'), pattern: params[:pattern])
|
||||
end
|
||||
|
||||
def self.gist_search_enabled?
|
||||
Setting.get('user_search_with_gist', 'true') == 'true'
|
||||
end
|
||||
def sis_sql(users_scope, params)
|
||||
users_scope.select("users.*, MAX(logins.current_login_at) as last_login")
|
||||
.joins(:pseudonyms)
|
||||
.joins("LEFT JOIN #{Pseudonym.quoted_table_name} AS logins ON logins.user_id = users.id
|
||||
AND logins.account_id = #{User.connection.quote(params[:account])}
|
||||
AND logins.workflow_state = 'active'")
|
||||
.where(pseudonyms: { account_id: params[:account], workflow_state: 'active' })
|
||||
.where(like_condition('pseudonyms.sis_user_id'), pattern: params[:pattern])
|
||||
end
|
||||
|
||||
def self.complex_search_enabled?
|
||||
Setting.get('user_search_with_full_complexity', 'true') == 'true'
|
||||
end
|
||||
def email_sql(users_scope, params)
|
||||
users_scope.select("users.*, MAX(current_login_at) as last_login")
|
||||
.joins(:communication_channels)
|
||||
.joins("LEFT JOIN #{Pseudonym.quoted_table_name} ON pseudonyms.user_id = users.id
|
||||
AND pseudonyms.account_id = #{User.connection.quote(params[:account])}
|
||||
AND pseudonyms.workflow_state = 'active'")
|
||||
.where(communication_channels: { workflow_state: ['active', 'unconfirmed'], path_type: params[:path_type] })
|
||||
.where(like_condition('communication_channels.path'), pattern: params[:pattern])
|
||||
end
|
||||
|
||||
def self.like_condition(value)
|
||||
ActiveRecord::Base.like_condition(value, 'lower(:pattern)')
|
||||
end
|
||||
def gist_search_enabled?
|
||||
Setting.get('user_search_with_gist', 'true') == 'true'
|
||||
end
|
||||
|
||||
def self.wildcard_pattern(value, options)
|
||||
ActiveRecord::Base.wildcard_pattern(value, options)
|
||||
def complex_search_enabled?
|
||||
Setting.get('user_search_with_full_complexity', 'true') == 'true'
|
||||
end
|
||||
|
||||
def wildcard_pattern(value, options)
|
||||
ActiveRecord::Base.wildcard_pattern(value, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue