237 lines
8.3 KiB
Ruby
237 lines
8.3 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 LearningOutcome < ActiveRecord::Base
|
|
include Workflow
|
|
attr_accessible :context, :description, :short_description, :rubric_criterion
|
|
belongs_to :context, :polymorphic => true
|
|
has_many :learning_outcome_results
|
|
has_many :content_tags, :order => :position
|
|
has_many :learning_outcome_group_associations, :as => :content, :class_name => 'ContentTag'
|
|
serialize :data
|
|
before_save :infer_defaults
|
|
validates_length_of :description, :maximum => maximum_text_length, :allow_nil => true, :allow_blank => true
|
|
sanitize_field :description, Instructure::SanitizeField::SANITIZE
|
|
|
|
set_policy do
|
|
given {|user, session| self.cached_context_grants_right?(user, session, :manage_outcomes) }
|
|
can :create and can :read and can :update and can :delete
|
|
end
|
|
|
|
def infer_defaults
|
|
if self.data && self.data[:rubric_criterion]
|
|
self.data[:rubric_criterion][:description] = self.short_description
|
|
end
|
|
self.context_code = "#{self.context_type.underscore}_#{self.context_id}" rescue nil
|
|
end
|
|
|
|
def align(asset, context, opts={})
|
|
tag = self.content_tags.find_by_content_id_and_content_type_and_tag_type_and_context_id_and_context_type(asset.id, asset.class.to_s, 'learning_outcome', context.id, context.class.to_s)
|
|
tag ||= self.content_tags.create(:content => asset, :tag_type => 'learning_outcome', :context => context)
|
|
mastery_type = opts[:mastery_type]
|
|
if mastery_type == 'points'
|
|
mastery_type = 'points_mastery'
|
|
else
|
|
mastery_type = 'explicit_mastery'
|
|
end
|
|
tag.tag = mastery_type
|
|
tag.position = (self.content_tags.map(&:position).compact.max || 1) + 1
|
|
tag.save
|
|
tag
|
|
end
|
|
|
|
def reorder_alignments(context, order)
|
|
order_hash = {}
|
|
order.each_with_index{|o, i| order_hash[o.to_i] = i; order_hash[o] = i }
|
|
tags = self.content_tags.find_all_by_context_id_and_context_type_and_tag_type(context.id, context.class.to_s, 'learning_outcome')
|
|
tags = tags.sort_by{|t| order_hash[t.id] || order_hash[t.content_asset_string] || 999 }
|
|
updates = []
|
|
tags.each_with_index do |tag, idx|
|
|
tag.position = idx + 1
|
|
updates << "WHEN id=#{tag.id} THEN #{idx + 1}"
|
|
end
|
|
ContentTag.connection.execute("UPDATE content_tags SET position=CASE #{updates.join(" ")} ELSE position END WHERE id IN (#{tags.map(&:id).join(",")})")
|
|
self.touch
|
|
tags
|
|
end
|
|
|
|
def remove_alignment(asset, context, opts={})
|
|
tag = self.content_tags.find_by_content_id_and_content_type_and_tag_type_and_context_id_and_context_type(asset.id, asset.class.to_s, 'learning_outcome', context.id, context.class.to_s)
|
|
tag.destroy if tag
|
|
tag
|
|
end
|
|
|
|
workflow do
|
|
state :active
|
|
state :retired
|
|
state :deleted
|
|
end
|
|
|
|
named_scope :active, lambda{
|
|
{:conditions => ['workflow_state != ?', 'deleted'] }
|
|
}
|
|
|
|
def cached_context_short_name
|
|
@cached_context_name ||= Rails.cache.fetch(['short_name_lookup', self.context_code].cache_key) do
|
|
self.context.short_name rescue ""
|
|
end
|
|
end
|
|
|
|
def rubric_criterion=(hash)
|
|
criterion = {}
|
|
if hash[:enable] != '1'
|
|
self.data ||= {}
|
|
self.data[:rubric_criterion] = nil
|
|
return
|
|
end
|
|
criterion[:description] = hash[:description] || t(:no_description, "No Description")
|
|
criterion[:ratings] = []
|
|
(hash[:ratings] || []).each do |key, rating|
|
|
criterion[:ratings] << {
|
|
:description => rating[:description] || t(:no_comment, "No Comment"),
|
|
:points => rating[:points].to_f || 0
|
|
}
|
|
end
|
|
criterion[:ratings] = criterion[:ratings].sort_by{|r| r[:points] }.reverse
|
|
criterion[:mastery_points] = (hash[:mastery_points] || criterion[:ratings][0][:points]).to_f
|
|
criterion[:points_possible] = criterion[:ratings][0][:points] rescue 0
|
|
self.data ||= {}
|
|
self.data[:rubric_criterion] = criterion
|
|
end
|
|
|
|
alias_method :destroy!, :destroy
|
|
def destroy
|
|
self.workflow_state = 'deleted'
|
|
ContentTag.delete_for(self)
|
|
ContentTag.find_all_by_learning_outcome_id(self.id).each{|t| t.destroy }
|
|
save!
|
|
end
|
|
|
|
def tie_to(context)
|
|
@tied_context = context
|
|
end
|
|
|
|
def artifacts_count_for_tied_context
|
|
codes = [@tied_context.asset_string]
|
|
if @tied_context.is_a?(Account)
|
|
if @tied_context == context
|
|
codes = "all"
|
|
else
|
|
codes = @tied_context.all_courses.scoped({:select => 'id'}).map(&:asset_string)
|
|
end
|
|
end
|
|
self.learning_outcome_results.for_context_codes(codes).count
|
|
end
|
|
|
|
def clone_for(context, parent)
|
|
lo = context.learning_outcomes.new
|
|
lo.context = context
|
|
lo.short_description = self.short_description
|
|
lo.description = self.description
|
|
lo.data = self.data
|
|
lo.save
|
|
parent.add_item(lo)
|
|
|
|
lo
|
|
end
|
|
|
|
def self.available_in_context(context, ids=[])
|
|
account_contexts = context.associated_accounts rescue []
|
|
codes = account_contexts.map(&:asset_string)
|
|
order = {}
|
|
codes.each_with_index{|c, idx| order[c] = idx }
|
|
outcomes = []
|
|
([context] + account_contexts).uniq.each do |context|
|
|
outcomes += LearningOutcomeGroup.default_for(context).try(:sorted_all_outcomes, ids) || []
|
|
end
|
|
outcomes.uniq
|
|
end
|
|
|
|
def self.non_rubric_outcomes?
|
|
false
|
|
end
|
|
|
|
def self.delete_if_unused(ids)
|
|
tags = ContentTag.active.find_all_by_content_id_and_content_type(ids, 'LearningOutcome')
|
|
to_delete = []
|
|
ids.each do |id|
|
|
to_delete << id unless tags.any?{|t| t.content_id == id }
|
|
end
|
|
LearningOutcome.update_all({:workflow_state => 'deleted', :updated_at => Time.now.utc}, {:id => to_delete})
|
|
end
|
|
|
|
def self.enabled?
|
|
true
|
|
end
|
|
|
|
def self.process_migration(data, migration)
|
|
outcomes = data['learning_outcomes'] ? data['learning_outcomes'] : []
|
|
outcomes.each do |outcome|
|
|
begin
|
|
if outcome[:type] == 'learning_outcome_group'
|
|
LearningOutcomeGroup.import_from_migration(outcome, migration.context)
|
|
else
|
|
import_from_migration(outcome, migration.context)
|
|
end
|
|
rescue
|
|
migration.add_warning("Couldn't import learning outcome \"#{outcome[:title]}\"", $!)
|
|
end
|
|
end
|
|
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_outcomes.new
|
|
item.context = context
|
|
item.migration_id = hash[:migration_id]
|
|
item.workflow_state = 'active' if item.deleted?
|
|
item.short_description = hash[:title]
|
|
item.description = hash[:description]
|
|
|
|
if hash[:ratings]
|
|
item.data = {:rubric_criterion=>{}}
|
|
item.data[:rubric_criterion][:ratings] = hash[:ratings] ? hash[:ratings].map(&:symbolize_keys) : []
|
|
item.data[:rubric_criterion][:mastery_points] = hash[:mastery_points]
|
|
item.data[:rubric_criterion][:points_possible] = hash[:points_possible]
|
|
item.data[:rubric_criterion][:description] = item.short_description || item.description
|
|
end
|
|
|
|
item.save!
|
|
context.imported_migration_items << item if context.imported_migration_items && item.new_record?
|
|
|
|
log = hash[:learning_outcome_group] || LearningOutcomeGroup.default_for(context)
|
|
log.add_item(item)
|
|
|
|
item
|
|
end
|
|
|
|
named_scope :for_context_codes, lambda{|codes|
|
|
{:conditions => {:context_code => Array(codes)} }
|
|
}
|
|
named_scope :active, lambda{
|
|
{:conditions => ['learning_outcomes.workflow_state != ?', 'deleted'] }
|
|
}
|
|
named_scope :has_result_for, lambda{|user|
|
|
{:joins => [:learning_outcome_results],
|
|
:conditions => ['learning_outcomes.id = learning_outcome_results.learning_outcome_id AND learning_outcome_results.user_id = ?', user.id],
|
|
:order => 'short_description'
|
|
}
|
|
}
|
|
end
|