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:
Cody Cutrer 2021-11-05 09:06:30 -06:00
parent 718513d12f
commit bb9fe29416
26 changed files with 1297 additions and 1255 deletions

View File

@ -59,6 +59,8 @@ Lint/DuplicateMethods:
Severity: error
Lint/EmptyBlock:
Severity: error
Lint/IneffectiveAccessModifier:
Severity: error
Lint/MissingSuper:
Severity: error
Lint/NonDeterministicRequireOrder:

View File

@ -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 = {}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
#

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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