689 lines
22 KiB
Ruby
689 lines
22 KiB
Ruby
class ContextExternalTool < ActiveRecord::Base
|
|
include Workflow
|
|
include SearchTermHelper
|
|
|
|
has_many :content_tags, :as => :content
|
|
has_many :context_external_tool_placements, :autosave => true
|
|
|
|
belongs_to :context, :polymorphic => true
|
|
validates_inclusion_of :context_type, :allow_nil => true, :in => ['Course', 'Account']
|
|
attr_accessible :privacy_level, :domain, :url, :shared_secret, :consumer_key,
|
|
:name, :description, :custom_fields, :custom_fields_string,
|
|
:course_navigation, :account_navigation, :user_navigation,
|
|
:resource_selection, :editor_button, :homework_submission,
|
|
:course_home_sub_navigation, :course_settings_sub_navigation,
|
|
:config_type, :config_url, :config_xml, :tool_id,
|
|
:integration_type, :not_selectable
|
|
|
|
validates_presence_of :context_id, :context_type, :workflow_state
|
|
validates_presence_of :name, :consumer_key, :shared_secret
|
|
validates_length_of :name, :maximum => maximum_string_length
|
|
validates_presence_of :config_url, :if => lambda { |t| t.config_type == "by_url" }
|
|
validates_presence_of :config_xml, :if => lambda { |t| t.config_type == "by_xml" }
|
|
validates_length_of :domain, :maximum => 253, :allow_blank => true
|
|
validate :url_or_domain_is_set
|
|
serialize :settings
|
|
attr_accessor :config_type, :config_url, :config_xml
|
|
|
|
before_save :infer_defaults, :validate_vendor_help_link
|
|
after_save :touch_context, :check_global_navigation_cache
|
|
validate :check_for_xml_error
|
|
|
|
workflow do
|
|
state :anonymous
|
|
state :name_only
|
|
state :email_only
|
|
state :public
|
|
state :deleted
|
|
end
|
|
|
|
set_policy do
|
|
given { |user, session| self.context.grants_right?(user, session, :update) }
|
|
can :read and can :update and can :delete
|
|
end
|
|
|
|
EXTENSION_TYPES = [
|
|
:user_navigation, :course_navigation, :account_navigation, :resource_selection,
|
|
:editor_button, :homework_submission, :migration_selection, :course_home_sub_navigation,
|
|
:course_settings_sub_navigation, :global_navigation,
|
|
:assignment_menu, :file_menu, :discussion_topic_menu, :module_menu, :quiz_menu, :wiki_page_menu,
|
|
:tool_configuration, :link_selection, :assignment_selection, :post_grades
|
|
].freeze
|
|
|
|
CUSTOM_EXTENSION_KEYS = {:file_menu => [:accept_media_types].freeze}.freeze
|
|
|
|
EXTENSION_TYPES.each do |type|
|
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
def #{type}(setting=nil)
|
|
extension_setting(:#{type}, setting)
|
|
end
|
|
|
|
def #{type}=(hash)
|
|
set_extension_setting(:#{type}, hash)
|
|
end
|
|
RUBY
|
|
end
|
|
|
|
def extension_setting(type, property = nil)
|
|
type = type.to_sym
|
|
return settings[type] unless property && settings[type]
|
|
settings[type][property] || settings[property] || extension_default_value(type, property)
|
|
end
|
|
|
|
def set_extension_setting(type, hash)
|
|
if !hash || !hash.is_a?(Hash)
|
|
settings.delete type
|
|
return
|
|
end
|
|
|
|
hash = hash.with_indifferent_access
|
|
hash[:enabled] = Canvas::Plugin.value_to_boolean(hash[:enabled]) if hash[:enabled]
|
|
settings[type] = {}.with_indifferent_access
|
|
|
|
extension_keys = [:custom_fields, :default, :display_type, :enabled, :icon_url,
|
|
:selection_height, :selection_width, :text, :url, :message_type]
|
|
if custom_keys = CUSTOM_EXTENSION_KEYS[type]
|
|
extension_keys += custom_keys
|
|
end
|
|
extension_keys += {
|
|
:visibility => lambda{|v| %w{members admins}.include?(v)}
|
|
}.to_a
|
|
|
|
extension_keys.each do |key, validator|
|
|
if hash.has_key?(key) && (!validator || validator.call(hash[key]))
|
|
settings[type][key] = hash[key]
|
|
end
|
|
end
|
|
|
|
settings[type]
|
|
end
|
|
|
|
def has_placement?(type)
|
|
if Lti::ResourcePlacement::DEFAULT_PLACEMENTS.include? type.to_s
|
|
!!(self.selectable && (self.domain || self.url))
|
|
else
|
|
self.context_external_tool_placements.to_a.any?{|p| p.placement_type == type.to_s}
|
|
end
|
|
end
|
|
|
|
def sync_placements!(placements)
|
|
old_placements = self.context_external_tool_placements.pluck(:placement_type)
|
|
(placements - old_placements).each do |new_placement|
|
|
self.context_external_tool_placements.new(:placement_type => new_placement)
|
|
end
|
|
placements_to_delete = EXTENSION_TYPES.map(&:to_s) - placements
|
|
if placements_to_delete.any?
|
|
self.context_external_tool_placements.where(placement_type: placements_to_delete).delete_all if self.persisted?
|
|
self.context_external_tool_placements.delete_if{|p| placements_to_delete.include?(p.placement_type)}
|
|
end
|
|
end
|
|
private :sync_placements!
|
|
|
|
def url_or_domain_is_set
|
|
setting_types = EXTENSION_TYPES
|
|
# url or domain (or url on canvas lti extension) is required
|
|
if url.blank? && domain.blank? && setting_types.all?{|k| !settings[k] || settings[k]['url'].blank? }
|
|
errors.add(:url, t('url_or_domain_required', "Either the url or domain should be set."))
|
|
errors.add(:domain, t('url_or_domain_required', "Either the url or domain should be set."))
|
|
end
|
|
end
|
|
|
|
def settings
|
|
read_attribute(:settings) || write_attribute(:settings, {})
|
|
end
|
|
|
|
def label_for(key, lang=nil)
|
|
lang = lang.to_s if lang
|
|
labels = settings[key] && settings[key][:labels]
|
|
labels2 = settings[:labels]
|
|
(labels && labels[lang]) ||
|
|
(labels && lang && labels[lang.split('-').first]) ||
|
|
(settings[key] && settings[key][:text]) ||
|
|
(labels2 && labels2[lang]) ||
|
|
(labels2 && lang && labels2[lang.split('-').first]) ||
|
|
settings[:text] || name || "External Tool"
|
|
end
|
|
|
|
def check_for_xml_error
|
|
(@config_errors || []).each { |attr,msg|
|
|
errors.add attr, msg
|
|
}
|
|
end
|
|
protected :check_for_xml_error
|
|
|
|
def readable_state
|
|
workflow_state.titleize
|
|
end
|
|
|
|
def privacy_level=(val)
|
|
if ['anonymous', 'name_only', 'email_only', 'public'].include?(val)
|
|
self.workflow_state = val
|
|
end
|
|
end
|
|
|
|
def privacy_level
|
|
self.workflow_state
|
|
end
|
|
|
|
def custom_fields_string
|
|
(settings[:custom_fields] || {}).map{|key, val|
|
|
"#{key}=#{val}"
|
|
}.sort.join("\n")
|
|
end
|
|
|
|
def vendor_help_link
|
|
settings[:vendor_help_link]
|
|
end
|
|
|
|
def vendor_help_link=(val)
|
|
settings[:vendor_help_link] = val
|
|
end
|
|
|
|
def validate_vendor_help_link
|
|
return if self.vendor_help_link.blank?
|
|
begin
|
|
value, uri = CanvasHttp.validate_url(self.vendor_help_link)
|
|
self.vendor_help_link = uri.to_s
|
|
rescue URI::InvalidURIError, ArgumentError
|
|
self.vendor_help_link = nil
|
|
end
|
|
end
|
|
|
|
def config_type=(val)
|
|
@config_type = val
|
|
process_extended_configuration
|
|
end
|
|
|
|
def config_xml=(val)
|
|
@config_xml = val
|
|
process_extended_configuration
|
|
end
|
|
|
|
def config_url=(val)
|
|
@config_url = val
|
|
process_extended_configuration
|
|
end
|
|
|
|
def process_extended_configuration
|
|
return unless (config_type == 'by_url' && config_url) || (config_type == 'by_xml' && config_xml)
|
|
tool_hash = nil
|
|
begin
|
|
converter = CC::Importer::BLTIConverter.new
|
|
if config_type == 'by_url'
|
|
tool_hash = converter.retrieve_and_convert_blti_url(config_url)
|
|
else
|
|
tool_hash = converter.convert_blti_xml(config_xml)
|
|
end
|
|
rescue CC::Importer::BLTIConverter::CCImportError => e
|
|
tool_hash = {:error => e.message}
|
|
end
|
|
|
|
|
|
@config_errors = []
|
|
error_field = config_type == 'by_xml' ? 'config_xml' : 'config_url'
|
|
|
|
converter = CC::Importer::BLTIConverter.new
|
|
tool_hash = if config_type == 'by_url'
|
|
uri = URI.parse(config_url)
|
|
raise URI::InvalidURIError unless uri.host && uri.port
|
|
converter.retrieve_and_convert_blti_url(config_url)
|
|
else
|
|
converter.convert_blti_xml(config_xml)
|
|
end
|
|
|
|
real_name = self.name
|
|
if tool_hash[:error]
|
|
@config_errors << [error_field, tool_hash[:error]]
|
|
else
|
|
Importers::ContextExternalToolImporter.import_from_migration(tool_hash, context, nil, self)
|
|
end
|
|
self.name = real_name unless real_name.blank?
|
|
rescue CC::Importer::BLTIConverter::CCImportError => e
|
|
@config_errors << [error_field, e.message]
|
|
rescue URI::InvalidURIError
|
|
@config_errors << [:config_url, "Invalid URL"]
|
|
rescue ActiveRecord::RecordInvalid => e
|
|
@config_errors += Array(e.record.errors)
|
|
end
|
|
|
|
def custom_fields_string=(str)
|
|
hash = {}
|
|
str.split(/[\r\n]+/).each do |line|
|
|
key, val = line.split(/=/)
|
|
hash[key] = val if key.present? && val.present?
|
|
end
|
|
settings[:custom_fields] = hash
|
|
end
|
|
|
|
def custom_fields=(hash)
|
|
settings[:custom_fields] = hash if hash.is_a?(Hash)
|
|
end
|
|
|
|
def custom_fields
|
|
settings[:custom_fields]
|
|
end
|
|
|
|
def icon_url=(i_url)
|
|
settings[:icon_url] = i_url
|
|
end
|
|
|
|
def icon_url
|
|
settings[:icon_url]
|
|
end
|
|
|
|
def text=(val)
|
|
settings[:text] = val
|
|
end
|
|
|
|
def text
|
|
settings[:text]
|
|
end
|
|
|
|
def not_selectable
|
|
!!read_attribute(:not_selectable)
|
|
end
|
|
|
|
def not_selectable=(bool)
|
|
write_attribute(:not_selectable, Canvas::Plugin.value_to_boolean(bool))
|
|
end
|
|
|
|
def selectable
|
|
!not_selectable
|
|
end
|
|
|
|
def shared_secret=(val)
|
|
write_attribute(:shared_secret, val) unless val.blank?
|
|
end
|
|
|
|
def display_type(extension_type)
|
|
extension_setting(extension_type, :display_type) || 'in_context'
|
|
end
|
|
|
|
def extension_default_value(type, property)
|
|
case property
|
|
when :url
|
|
url
|
|
when :selection_width
|
|
800
|
|
when :selection_height
|
|
400
|
|
when :message_type
|
|
if type == :resource_selection
|
|
'resource_selection'
|
|
else
|
|
'basic-lti-launch-request'
|
|
end
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
def self.normalize_sizes!(settings)
|
|
settings[:selection_width] = settings[:selection_width].to_i if settings[:selection_width]
|
|
settings[:selection_height] = settings[:selection_height].to_i if settings[:selection_height]
|
|
|
|
EXTENSION_TYPES.each do |type|
|
|
if settings[type]
|
|
settings[type][:selection_width] = settings[type][:selection_width].to_i if settings[type][:selection_width]
|
|
settings[type][:selection_height] = settings[type][:selection_height].to_i if settings[type][:selection_height]
|
|
end
|
|
end
|
|
end
|
|
|
|
def infer_defaults
|
|
self.url = nil if url.blank?
|
|
self.domain = nil if domain.blank?
|
|
|
|
ContextExternalTool.normalize_sizes!(self.settings)
|
|
|
|
EXTENSION_TYPES.each do |type|
|
|
if settings[type]
|
|
if !(extension_setting(type, :url)) || (settings[type].has_key?(:enabled) && !settings[type][:enabled])
|
|
settings.delete(type)
|
|
end
|
|
end
|
|
end
|
|
|
|
settings.delete(:editor_button) if !editor_button(:icon_url)
|
|
|
|
sync_placements!(EXTENSION_TYPES.select{|type| !!settings[type]}.map(&:to_s))
|
|
true
|
|
end
|
|
|
|
#This aggressively updates the domain on all URLs in this tool
|
|
def change_domain!(new_domain)
|
|
replace_host = lambda do |url, host|
|
|
uri = URI.parse(url)
|
|
uri.host = host
|
|
uri.to_s
|
|
end
|
|
|
|
self.domain = new_domain if self.domain
|
|
|
|
self.url = replace_host.call(self.url, new_domain) if self.url
|
|
|
|
settings.keys.each do |setting|
|
|
next if [:custom_fields, :environments].include? setting.to_sym
|
|
if settings[setting].is_a?(Hash)
|
|
settings[setting].keys.each do |property|
|
|
if settings[setting][property] =~ URI::regexp
|
|
settings[setting][property] = replace_host.call(settings[setting][property], new_domain)
|
|
end
|
|
end
|
|
elsif settings[setting] =~ URI::regexp
|
|
settings[setting] = replace_host.call(settings[setting], new_domain)
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.standardize_url(url)
|
|
return "" if url.empty?
|
|
url = "http://" + url unless url.match(/:\/\//)
|
|
res = URI.parse(url).normalize
|
|
res.query = res.query.split(/&/).sort.join('&') if !res.query.blank?
|
|
res.to_s
|
|
end
|
|
|
|
alias_method :destroy!, :destroy
|
|
def destroy
|
|
self.workflow_state = 'deleted'
|
|
save!
|
|
end
|
|
|
|
def include_email?
|
|
email_only? || public?
|
|
end
|
|
|
|
def include_name?
|
|
name_only? || public?
|
|
end
|
|
|
|
def precedence
|
|
if domain
|
|
# Somebody tell me if we should be expecting more than
|
|
# 25 dots in a url host...
|
|
25 - domain.split(/\./).length
|
|
elsif url
|
|
25
|
|
else
|
|
26
|
|
end
|
|
end
|
|
|
|
def standard_url
|
|
if !defined?(@standard_url)
|
|
@standard_url = !self.url.blank? && ContextExternalTool.standardize_url(self.url)
|
|
end
|
|
@standard_url
|
|
end
|
|
|
|
def matches_url?(url, match_queries_exactly=true)
|
|
if match_queries_exactly
|
|
url = ContextExternalTool.standardize_url(url)
|
|
return true if url == standard_url
|
|
elsif standard_url.present?
|
|
if !defined?(@url_params)
|
|
res = URI.parse(standard_url)
|
|
@url_params = res.query.present? ? res.query.split(/&/) : []
|
|
end
|
|
res = URI.parse(url).normalize
|
|
res.query = res.query.split(/&/).select{|p| @url_params.include?(p)}.sort.join('&') if res.query.present?
|
|
res.query = nil if res.query.blank?
|
|
res.normalize!
|
|
return true if res.to_s == standard_url
|
|
end
|
|
host = URI.parse(url).host rescue nil
|
|
!!(host && ('.' + host).match(/\.#{domain}\z/))
|
|
end
|
|
|
|
def matches_domain?(url)
|
|
url = ContextExternalTool.standardize_url(url)
|
|
host = URI.parse(url).host
|
|
if domain
|
|
domain == host
|
|
elsif standard_url
|
|
URI.parse(standard_url).host == host
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
def self.contexts_to_search(context)
|
|
case context
|
|
when Course
|
|
[context] + context.account_chain
|
|
when Group
|
|
[context] + (context.context ? contexts_to_search(context.context) : context.account_chain)
|
|
when Account
|
|
context.account_chain
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
LOR_TYPES = [:course_home_sub_navigation, :course_settings_sub_navigation, :global_navigation,
|
|
:assignment_menu, :file_menu, :discussion_topic_menu, :module_menu, :quiz_menu,
|
|
:wiki_page_menu]
|
|
def self.all_tools_for(context, options={})
|
|
#options[:type] is deprecated, use options[:placements] instead
|
|
placements =* options[:placements] || options[:type]
|
|
|
|
#special LOR feature flag
|
|
unless (options[:root_account] && options[:root_account].feature_enabled?(:lor_for_account)) ||
|
|
(options[:current_user] && options[:current_user].feature_enabled?(:lor_for_user))
|
|
valid_placements = placements.select{|placement| !LOR_TYPES.include?(placement.to_sym)}
|
|
return [] if valid_placements.size == 0 && placements.size > 0
|
|
placements = valid_placements
|
|
end
|
|
|
|
contexts = []
|
|
if options[:user]
|
|
contexts << options[:user]
|
|
end
|
|
contexts.concat contexts_to_search(context)
|
|
return nil if contexts.empty?
|
|
|
|
scope = ContextExternalTool.shard(context.shard).polymorphic_where(context: contexts).active
|
|
scope = scope.placements(*placements)
|
|
scope = scope.selectable if Canvas::Plugin.value_to_boolean(options[:selectable])
|
|
scope.order("#{ContextExternalTool.best_unicode_collation_key('context_external_tools.name')}, context_external_tools.id")
|
|
end
|
|
|
|
def self.find_external_tool_by_id(id, context)
|
|
self.where(:id => id).polymorphic_where(:context => contexts_to_search(context)).first
|
|
end
|
|
|
|
# Order of precedence: Basic LTI defines precedence as first
|
|
# checking for a match on domain. Subdomains count as a match
|
|
# on less-specific domains, but the most-specific domain will
|
|
# match first. So awesome.bob.example.com matches an
|
|
# external_tool with example.com as the domain, but only if
|
|
# there isn't another external_tool where awesome.bob.example.com
|
|
# or bob.example.com is set as the domain.
|
|
#
|
|
# If there is no domain match then check for an exact url match
|
|
# as configured by an admin. If there is still no match
|
|
# then check for a match on the current context (configured by
|
|
# the teacher).
|
|
def self.find_external_tool(url, context, preferred_tool_id=nil)
|
|
contexts = contexts_to_search(context)
|
|
preferred_tool = ContextExternalTool.active.where(id: preferred_tool_id).first if preferred_tool_id
|
|
if preferred_tool && contexts.member?(preferred_tool.context) && preferred_tool.matches_domain?(url)
|
|
return preferred_tool
|
|
end
|
|
|
|
all_external_tools = ContextExternalTool.shard(context.shard).polymorphic_where(context: contexts).active.to_a
|
|
sorted_external_tools = all_external_tools.sort_by { |t| [contexts.index { |c| c.id == t.context_id && c.class.name == t.context_type }, t.precedence, t.id == preferred_tool_id ? CanvasSort::First : CanvasSort::Last] }
|
|
|
|
res = sorted_external_tools.detect{|tool| tool.url && tool.matches_url?(url) }
|
|
return res if res
|
|
|
|
# If exactly match doesn't work, try to match by ignoring extra query parameters
|
|
res = sorted_external_tools.detect{|tool| tool.url && tool.matches_url?(url, false) }
|
|
return res if res
|
|
|
|
res = sorted_external_tools.detect{|tool| tool.domain && tool.matches_url?(url) }
|
|
return res if res
|
|
|
|
nil
|
|
end
|
|
|
|
def self.find_integration_for(context, type)
|
|
contexts_to_search(context).each do |context|
|
|
tools = context.context_external_tools.active.where(integration_type: type)
|
|
return tools.first unless tools.empty?
|
|
end
|
|
|
|
nil
|
|
end
|
|
|
|
scope :having_setting, lambda { |setting| setting ? joins(:context_external_tool_placements).
|
|
where("context_external_tool_placements.placement_type = ?", setting) : scoped }
|
|
|
|
scope :placements, lambda { |*placements|
|
|
if placements.present?
|
|
default_placement_sql = if (placements.map(&:to_s) & Lti::ResourcePlacement::DEFAULT_PLACEMENTS).present?
|
|
"(context_external_tools.not_selectable IS NOT TRUE AND
|
|
((COALESCE(context_external_tools.url, '') <> '' ) OR
|
|
(COALESCE(context_external_tools.domain, '') <> ''))) OR "
|
|
else
|
|
''
|
|
end
|
|
where(default_placement_sql + 'EXISTS (
|
|
SELECT * FROM context_external_tool_placements
|
|
WHERE context_external_tools.id = context_external_tool_placements.context_external_tool_id
|
|
AND context_external_tool_placements.placement_type IN (?) )', placements || [])
|
|
else
|
|
scoped
|
|
end
|
|
}
|
|
|
|
scope :selectable, lambda { where("context_external_tools.not_selectable IS NOT TRUE") }
|
|
|
|
def self.find_for(id, context, type, raise_error=true)
|
|
id = id[Api::ID_REGEX] if id.is_a?(String)
|
|
unless id.present?
|
|
if raise_error
|
|
raise ActiveRecord::RecordNotFound
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
context = context.context if context.is_a?(Group)
|
|
|
|
tool = context.context_external_tools.having_setting(type).where(id: id).first
|
|
tool ||= ContextExternalTool.having_setting(type).where(context_type: 'Account', context_id: context.account_chain, id: id).first
|
|
raise ActiveRecord::RecordNotFound if !tool && raise_error
|
|
|
|
tool
|
|
end
|
|
scope :active, -> { where("context_external_tools.workflow_state<>'deleted'") }
|
|
|
|
def self.find_all_for(context, type)
|
|
tools = []
|
|
if !context.is_a?(Account) && context.respond_to?(:context_external_tools)
|
|
tools += context.context_external_tools.having_setting(type.to_s)
|
|
end
|
|
tools += ContextExternalTool.having_setting(type.to_s).where(context_type: 'Account', context_id: context.account_chain)
|
|
end
|
|
|
|
def self.serialization_excludes; [:shared_secret,:settings]; end
|
|
|
|
# sets the custom fields from the main tool settings, and any on individual resource type settings
|
|
def set_custom_fields(resource_type)
|
|
hash = {}
|
|
fields = [settings[:custom_fields] || {}]
|
|
fields << (settings[resource_type.to_sym][:custom_fields] || {}) if resource_type && settings[resource_type.to_sym]
|
|
fields.each do |field_set|
|
|
field_set.each do |key, val|
|
|
key = key.to_s.gsub(/[^\w]/, '_').downcase
|
|
if key.match(/^custom_/)
|
|
hash[key] = val
|
|
else
|
|
hash["custom_#{key}"] = val
|
|
end
|
|
end
|
|
end
|
|
hash
|
|
end
|
|
|
|
def resource_selection_settings
|
|
settings[:resource_selection]
|
|
end
|
|
|
|
def opaque_identifier_for(asset)
|
|
ContextExternalTool.opaque_identifier_for(asset, self.shard)
|
|
end
|
|
|
|
def self.opaque_identifier_for(asset, shard)
|
|
shard.activate do
|
|
lti_context_id = context_id_for(asset, shard)
|
|
set_asset_context_id(asset, lti_context_id)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def self.set_asset_context_id(asset, context_id)
|
|
lti_context_id = context_id
|
|
if asset.respond_to?('lti_context_id')
|
|
if asset.new_record?
|
|
asset.lti_context_id = context_id
|
|
else
|
|
asset.reload unless asset.lti_context_id?
|
|
unless asset.lti_context_id
|
|
asset.lti_context_id = context_id
|
|
asset.save!
|
|
end
|
|
lti_context_id = asset.lti_context_id
|
|
end
|
|
end
|
|
lti_context_id
|
|
end
|
|
|
|
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?
|
|
%w{members admins}.each do |visibility|
|
|
Rails.cache.delete("external_tools/global_navigation/#{self.context.asset_string}/#{visibility}")
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.global_navigation_visibility_for_user(root_account, user)
|
|
Rails.cache.fetch(['external_tools/global_navigation/visibility', root_account.asset_string, user].cache_key) do
|
|
# let them see admin level tools if there are any courses they can manage
|
|
if root_account.grants_right?(user, :manage_content) ||
|
|
Course.manageable_by_user(user.id, true).not_deleted.where(:root_account_id => root_account).exists?
|
|
'admins'
|
|
else
|
|
'members'
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.global_navigation_tools(root_account, visibility)
|
|
tools = root_account.context_external_tools.active.having_setting(:global_navigation)
|
|
if visibility == 'members'
|
|
# reject the admin only tools
|
|
tools.reject!{|tool| tool.global_navigation[:visibility] == 'admins'}
|
|
end
|
|
tools
|
|
end
|
|
|
|
def self.global_navigation_menu_cache_key(root_account, visibility)
|
|
# only reload the menu if one of the global nav tools has changed
|
|
key = "external_tools/global_navigation/#{root_account.asset_string}/#{visibility}"
|
|
Rails.cache.fetch(key) do
|
|
tools = global_navigation_tools(root_account, visibility)
|
|
Digest::MD5.hexdigest(tools.map(&:cache_key).join('/'))
|
|
end
|
|
end
|
|
end
|