canvas-lms/app/models/learning_outcome_group.rb

261 lines
9.6 KiB
Ruby

#
# Copyright (C) 2011 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
class LearningOutcomeGroup < ActiveRecord::Base
include Workflow
attr_accessible :context, :title, :description, :learning_outcome_group
belongs_to :learning_outcome_group
belongs_to :root_learning_outcome_group, :class_name => "LearningOutcomeGroup"
has_many :learning_outcome_groups
has_many :content_tags, :as => :associated_asset, :order => :position
belongs_to :context, :polymorphic => true
before_save :infer_defaults
validates_length_of :description, :maximum => maximum_text_length, :allow_nil => true, :allow_blank => true
sanitize_field :description, Instructure::SanitizeField::SANITIZE
attr_accessor :building_default
def infer_defaults
self.context ||= self.learning_outcome_group && self.learning_outcome_group.context
if self.context && !self.context.learning_outcome_groups.empty? && !building_default
default = LearningOutcomeGroup.default_for(self.context)
self.learning_outcome_group ||= default unless self == default
self.root_learning_outcome_group ||= default
end
true
end
workflow do
state :active
state :deleted
end
def sorted_content(outcome_ids=[])
tags = self.content_tags.active
positions = {}
tags.each{|t| positions[t.content_asset_string] = t.position }
ids_to_find = tags.select{|t| t.content_type == 'LearningOutcome'}.map(&:content_id)
ids_to_find = (ids_to_find & outcome_ids) unless outcome_ids.empty?
group_ids_to_find = tags.select { |t| t.content_type == 'LearningOutcomeGroup' && !is_ancestor?(t.content_id) }.map(&:content_id)
objects = []
objects += LearningOutcome.active.find_all_by_id(ids_to_find).compact unless ids_to_find.empty?
objects += LearningOutcomeGroup.active.find_all_by_id(group_ids_to_find).compact unless group_ids_to_find.empty?
if self.learning_outcome_group_id == nil
all_tags = all_tags_for_context
codes = all_tags.map(&:content_asset_string).uniq
all_objects = LearningOutcome.active.find_all_by_id_and_context_id_and_context_type(outcome_ids, self.context_id, self.context_type).select{|o| !codes.include?(o.asset_string) } unless outcome_ids.empty?
all_objects ||= LearningOutcome.active.find_all_by_context_id_and_context_type(self.context_id, self.context_type).select{|o| !codes.include?(o.asset_string) }
objects += all_objects
end
sorted_objects = objects.uniq.sort_by{|o| positions[o.asset_string] || 999 }
end
def sorted_all_outcomes(ids=[])
res = []
self.sorted_content(ids).each do |obj|
if obj.is_a?(LearningOutcome)
res << obj
else
res += obj.sorted_all_outcomes(ids)
end
end
res.uniq.compact
end
def reorder_content(orders)
orders ||= {}
all_tags = all_tags_for_context
orders = orders.sort_by{|asset_string, position| position.to_i }.map{|asset_string, position| asset_string}
orders += self.content_tags.active.map(&:content_asset_string)
ordered = []
updates = []
orders.compact.uniq.each_with_index do |asset_string, idx|
if asset_string =~ /learning_outcome_group_(\d*)/
next if self.is_ancestor?($1.to_i)
end
tag = all_tags.detect{|t| t.content_asset_string == asset_string }
if !tag
tag ||= ContentTag.new(:content_asset_string => asset_string)
tag.context = self.context
tag.associated_asset = self
tag.tag_type = 'learning_outcome_association'
tag.save!
end
tag.position = idx + 1
updates << "WHEN id=#{tag.id} THEN #{tag.position || 999}"
ordered << tag
end
sql = "UPDATE content_tags SET associated_asset_id=#{self.id}, position=CASE #{updates.join(" ")} ELSE position END WHERE id IN (#{ordered.map(&:id).join(",")})"
ContentTag.connection.execute(sql) unless updates.empty?
ordered
end
def all_tags_for_context
self.context.learning_outcome_tags.active
end
def parent_ids
self.all_tags_for_context.find_all_by_content_type_and_content_id("LearningOutcomeGroup", self.id).map(&:associated_asset_id).uniq
end
# this finds all the ids of the ancestors avoiding relation loops
# because of old broken behavior a group can have multiple parents, including itself
def ancestor_ids
if !@ancestor_ids
@ancestor_ids = [self.id]
ids_to_check = parent_ids - @ancestor_ids
until ids_to_check.empty?
@ancestor_ids += ids_to_check
new_ids = []
ids_to_check.each do |id|
group = self.context.learning_outcome_groups.active.find_by_id(id)
new_ids += group.parent_ids if group
end
ids_to_check = new_ids.uniq - @ancestor_ids
end
end
@ancestor_ids
end
def is_ancestor?(id)
ancestor_ids.member?(id)
end
# use_outcome should be a lambda that takes an outcome and returns a boolean
def clone_for(context, parent, use_outcome=nil)
# don't create a group if none of the items in it are being copied
return nil if use_outcome && !self.sorted_content.any? {|lo| use_outcome[lo]}
new_group = context.learning_outcome_groups.new
new_group.context = context
new_group.title = self.title
new_group.description = self.description
new_group.save!
parent.add_item(new_group)
self.sorted_content.each do |lo|
next if use_outcome && !use_outcome[lo]
lo.clone_for(context, new_group)
end
new_group
end
def add_item(item, opts={})
return if item == self
if item.is_a?(LearningOutcome)
all_tags = all_tags_for_context
tag = all_tags.detect{|t| t.content_asset_string == item.asset_string }
tag ||= ContentTag.new(:content_asset_string => item.asset_string)
tag.context = self.context
tag.position ||= (self.content_tags.map(&:position).compact.max || 0) + 1
tag.tag_type = 'learning_outcome_association'
tag.associated_asset = self
tag.save!
tag
elsif item.is_a?(LearningOutcomeGroup)
return if is_ancestor?(item.id)
all_tags = all_tags_for_context
tag = all_tags.detect{|t| t.content_asset_string == item.asset_string }
if !tag
group = item
if item.context != self.context
group = self.learning_outcome_groups.build
group.title = item.title
group.learning_outcome_group_id = self.id
group.description = item.description
group.context = self.context
else
group.root_learning_outcome_group_id = self.root_learning_outcome_group_id
group.learning_outcome_group_id = self.id
end
group.save!
tag = ContentTag.new(:content_asset_string => group.asset_string)
end
tag.context = self.context
tag.position ||= (self.content_tags.map(&:position).compact.max || 0) + 1
tag.tag_type = 'learning_outcome_association'
tag.associated_asset = self
tag.save!
group = tag.content
outcome_ids = item.content_tags.select{|t| t.content_type == 'LearningOutcome'}.map(&:content_id)
unless outcome_ids.empty?
LearningOutcome.find_all_by_id(outcome_ids).each{|o| group.add_item(o) if !opts[:only] || opts[:only][o.id] == "1" }
end
tag
end
end
alias_method :destroy!, :destroy
def destroy
self.workflow_state = 'deleted'
ContentTag.delete_for(self)
# also delete any tags for held outcomes
# if we really do multi-nesting, you'll need it for sub-groups as well
LearningOutcome.delete_if_unused(self.content_tags.select{|t| t.content_type == 'LearningOutcome'}.map(&:content_id))
save!
end
def self.default_for(context)
outcome = LearningOutcomeGroup.find_by_context_type_and_context_id_and_learning_outcome_group_id(context.class.to_s, context.id, nil)
return outcome if outcome
outcome = LearningOutcomeGroup.new(:context => context)
outcome.building_default = true
outcome.save!
outcome.root_learning_outcome_group_id = outcome.id
outcome.save!
outcome
end
def self.import_from_migration(hash, context, item=nil)
hash = hash.with_indifferent_access
item ||= find_by_context_id_and_context_type_and_migration_id(context.id, context.class.to_s, hash[:migration_id]) if hash[:migration_id]
item ||= context.learning_outcome_groups.new
item.context = context
item.migration_id = hash[:migration_id]
item.title = hash[:title]
item.description = hash[:description]
# make sure the root group is created before saving the new group
log = LearningOutcomeGroup.default_for(context)
item.save!
context.imported_migration_items << item if context.imported_migration_items && item.new_record?
if hash[:outcomes]
hash[:outcomes].each do |outcome|
outcome[:learning_outcome_group] = item
LearningOutcome.import_from_migration(outcome, context)
end
end
log.add_item(item)
item
end
named_scope :active, lambda{
{:conditions => ['learning_outcome_groups.workflow_state != ?', 'deleted'] }
}
end