canvas-lms/app/models/rubric_association.rb

315 lines
13 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/>.
#
# Assocates a rubric with an "association", or idea. An assignment, for example.
# RubricAssessments, then, are concrete assessments of the artifacts associated
# with this idea, such as assignment submissions.
# The other purpose of this class is just to make rubrics reusable.
class RubricAssociation < ActiveRecord::Base
attr_accessible :rubric, :association, :context, :use_for_grading, :title, :description, :summary_data, :purpose, :url, :hide_score_total, :bookmarked
belongs_to :rubric
belongs_to :association, :polymorphic => true
belongs_to :context, :polymorphic => true
has_many :rubric_assessments, :dependent => :destroy
has_many :assessment_requests, :dependent => :destroy
adheres_to_policy
has_a_broadcast_policy
validates_presence_of :purpose
validates_length_of :description, :maximum => maximum_text_length, :allow_nil => true, :allow_blank => true
before_save :update_assignment_points
before_save :update_values
after_create :update_rubric
before_save :update_old_rubric
after_destroy :update_rubric
after_save :assert_uniqueness
after_save :update_outcome_relations
serialize :summary_data
ValidAssociationModels = {
'Course' => ::Course,
'Assignment' => ::Assignment,
'Account' => ::Account,
}
# takes params[:association_type] and params[:association_id] and finds the
# valid association object, if possible. Valid types are listed in
# ValidAssociationModels. This doesn't verify the user has access to the
# object.
def self.get_association_object(params)
return nil unless params
a_type = params.delete(:association_type)
a_id = params.delete(:association_id)
return @context if a_type == @context.class.to_s && a_id == @context.id
klass = ValidAssociationModels[a_type]
return nil unless klass
klass.find_by_id(a_id) if a_id.present? # authorization is checked in the calling method
end
set_broadcast_policy do |p|
p.dispatch :rubric_association_created
p.to { self.context.students rescue [] }
p.whenever {|record|
record.just_created && !record.context.is_a?(Course)
}
end
named_scope :bookmarked, lambda {
{:conditions => {:bookmarked => true} }
}
named_scope :for_purpose, lambda {|purpose|
{:conditions => {:purpose => purpose} }
}
named_scope :for_grading, lambda {
{:conditions => {:purpose => 'grading'}}
}
named_scope :for_context_codes, lambda{|codes|
{:conditions => {:context_code => codes} }
}
named_scope :include_rubric, lambda{
{:include => :rubric}
}
named_scope :before, lambda{|date|
{:conditions => ['rubric_associations.created_at < ?', date]}
}
def assert_uniqueness
if purpose == 'grading'
RubricAssociation.find_all_by_association_id_and_association_type_and_purpose(association_id, association_type, 'grading').each do |ra|
ra.destroy unless ra == self
end
end
end
def update_outcome_relations
if self.association && self.association.is_a?(Assignment)
assignment = self.association
tags = assignment.learning_outcome_tags
rubric_tags = self.rubric.learning_outcome_tags rescue []
rubric_outcome_ids = rubric_tags.map(&:learning_outcome_id).compact.uniq
existing_outcome_ids = tags.map(&:learning_outcome_id)
tags_to_delete = tags.select{|t| t.rubric_association_id && !rubric_outcome_ids.include?(t.learning_outcome_id) }
ids_to_add = rubric_outcome_ids.select{|id| !existing_outcome_ids.include?(id) }
tags_to_delete.each{|t| t.destroy }
tags_to_update = tags.select{|t| rubric_outcome_ids.include?(t.learning_outcome_id) }
ContentTag.update_all({:rubric_association_id => self.id}, {:id => tags_to_update.map(&:id)})
ids_to_add.each do |id|
assignment.learning_outcome_tags.create(:context => assignment.context, :learning_outcome_id => id, :rubric_association_id => self.id, :tag_type => 'learning_outcome')
end
end
end
def update_old_rubric
if self.rubric_id_changed? && self.rubric_id_was && self.rubric_id_was != self.rubric_id
rubric = Rubric.find(self.rubric_id_was)
rubric.destroy if rubric.rubric_associations.count == 0 && rubric.rubric_assessments.count == 0
end
end
def context_name
@cached_context_name ||= Rails.cache.fetch(['short_name_lookup', self.context_code].cache_key) do
self.context.short_name rescue ""
end
end
def update_values
self.bookmarked = true if self.purpose == 'bookmark' || self.bookmarked.nil?
self.context_code ||= "#{self.context_type.underscore}_#{self.context_id}" rescue nil
self.title ||= (self.association.title rescue self.association.name) rescue nil
end
protected :update_values
attr_accessor :assessing_user_id
set_policy do
given {|user, session| self.cached_context_grants_right?(user, session, :manage) }
set { can :update and can :delete and can :manage and can :assess }
given {|user, session| user && @assessing_user_id && self.assessment_requests.for_assessee(@assessing_user_id).map{|r| r.assessor_id}.include?(user.id) }
set { can :assess }
given {|user, session| self.cached_context_grants_right?(user, session, :participate_as_student) }
set { can :submit }
end
def update_assignment_points
if self.use_for_grading && self.association && self.association.respond_to?(:points_possible=) && self.rubric && self.rubric.points_possible && self.association.points_possible != self.rubric.points_possible
self.association.update_attribute(:points_possible, self.rubric.points_possible)
end
end
protected :update_assignment_points
def invite_assessor(assessee, assessor, asset, invite=false)
# Invitations should be unique per asset, user and assessor
assessment_request = self.assessment_requests.find_or_initialize_by_user_id_and_assessor_id(assessee.id, assessor.id)
assessment_request.workflow_state = "assigned" if assessment_request.new_record?
assessment_request.asset = asset
invite ? assessment_request.send_invitation! : assessment_request.save!
assessment_request
end
def remind_user(assessee)
assessment_request = self.assessment_requests.find_or_initialize_by_user_id(assessee.id)
assessment_request.send_reminder! if assessment_request.assigned?
assessment_request
end
def invite_assessors(assessee, invitations, asset)
assessors = TmailParser.new(invitations).parse
assessors.map do |assessor|
cc = CommunicationChannel.find_or_create_by_path(assessor[:email])
user = cc.user || User.create() { |u| u.workflow_state = :creation_pending }
user.assert_name(assessor[:name] || assessor[:email])
cc.user ||= user
cc.save!
user.reload
invite_assessor(assessee, user, asset, true)
end
end
def update_rubric
cnt = self.rubric.rubric_associations.for_grading.length rescue 0
if self.rubric
self.rubric.with_versioning(false) do
self.rubric.read_only = cnt > 1
self.rubric.association_count = cnt
self.rubric.save
self.rubric.destroy if cnt == 0 && self.rubric.rubric_associations.count == 0 && !self.rubric.public
end
end
end
protected :update_rubric
def unsubmitted_users
self.context.students - self.rubric_assessments.map{|a| a.user} - self.assessment_requests.map{|a| a.user}
end
def self.generate_with_invitees(current_user, rubric, context, params, invitees=nil)
raise "context required" unless context
association_object = params.delete :association
if (association_id = params.delete(:id)) && association_id.present?
association = RubricAssociation.find_by_id(association_id)
end
association = nil unless association && association.context == context && association.association == association_object
raise "association required" unless association || association_object
# Update/create the association -- this is what ties the rubric to an entity
update_if_existing = params.delete(:update_if_existing)
association ||= rubric.associate_with(association_object, context, :use_for_grading => params[:use_for_grading] == "1", :purpose => params[:purpose], :update_if_existing => update_if_existing)
association.rubric = rubric
association.context = context
association.update_attributes(params)
association.association = association_object
# Invite any recipients from the get-go
if invitees && association
assessments = association.invite_assessors(current_user, invitees, association_object.find_asset_for_assessment(association, current_user.id)[0])
end
association
end
def assessments_unique_per_asset?(assessment_type)
self.association.is_a?(Assignment) && self.purpose == "grading" && assessment_type == "grading"
end
def assess(opts={})
# TODO: what if this is for a group assignment? Seems like it should
# give all students for the group assignment the same rubric assessment
# results.
association = self
params = opts[:assessment]
raise "User required for assessing" unless opts[:user]
raise "Assessor required for assessing" unless opts[:assessor]
raise "Artifact required for assessing" unless opts[:artifact]
raise "Assessment type required for assessing" unless params[:assessment_type]
if self.association.is_a?(Assignment) && !self.association.grade_group_students_individually
students_to_assess = self.association.group_students(opts[:artifact].user).last
artifacts_to_assess = students_to_assess.map do |student|
self.association.find_asset_for_assessment(self, student).first
end
else
artifacts_to_assess = [opts[:artifact]]
end
ratings = []
score = 0
replace_ratings = false
self.rubric.criteria_object.each do |criterion|
data = params["criterion_#{criterion.id}".to_sym]
rating = {}
if data
replace_ratings = true
rating[:points] = [criterion.points, data[:points].to_f].min || 0
rating[:criterion_id] = criterion.id
rating[:learning_outcome_id] = criterion.learning_outcome_id
score += rating[:points]
rating[:description] = data[:description]
rating[:comments_enabled] = true
rating[:comments] = data[:comments]
rating[:above_threshold] = rating[:points] > criterion.mastery_points if criterion.mastery_points && rating[:points]
cached_description = nil
criterion.ratings.each do |r|
if r.points.to_f == rating[:points].to_f
cached_description = r.description
rating[:id] = r.id
end
end
if !rating[:description] || rating[:description].empty?
rating[:description] = cached_description
end
if rating[:comments] && !rating[:comments].empty? && data[:save_comment] == '1'
self.summary_data ||= {}
self.summary_data[:saved_comments] ||= {}
self.summary_data[:saved_comments][criterion.id.to_s] ||= []
self.summary_data[:saved_comments][criterion.id.to_s] << rating[:comments]
self.summary_data[:saved_comments][criterion.id.to_s] = self.summary_data[:saved_comments][criterion.id.to_s].select{|desc| desc && !desc.empty? && desc != "No Details"}.uniq.sort
self.save
end
rating[:description] = "No details" if !rating[:description] || rating[:description].empty?
ratings << rating
end
end
assessment_to_return = nil
artifacts_to_assess.each do |artifact|
assessment = nil
if assessments_unique_per_asset?(params[:assessment_type])
# Unless it's for grading, in which case assessments are unique per artifact (the assessor can change, depending on if the teacher/TA updates it)
assessment = association.rubric_assessments.find_by_artifact_id_and_artifact_type_and_assessment_type(artifact.id, artifact.class.to_s, params[:assessment_type])
else
# Assessments are unique per artifact/assessor/assessment_type.
assessment = association.rubric_assessments.find_by_artifact_id_and_artifact_type_and_assessor_id_and_assessment_type(artifact.id, artifact.class.to_s, opts[:assessor].id, params[:assessment_type])
end
assessment ||= association.rubric_assessments.build(:assessor => opts[:assessor], :artifact => artifact, :user => artifact.user, :rubric => self.rubric, :assessment_type => params[:assessment_type])
assessment.score = score if replace_ratings
assessment.data = ratings if replace_ratings
assessment.comments = params[:comments] if params[:comments]
assessment.save
if self.association.is_a?(Assignment)
self.association.learning_outcome_tags.each do |tag|
tag.create_outcome_result(opts[:user], self.association, assessment)
end
end
assessment_to_return = assessment if assessment.artifact == opts[:artifact]
end
assessment_to_return
end
end