2011-02-01 09:57:29 +08:00
|
|
|
#
|
|
|
|
# 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 AssignmentGroup < ActiveRecord::Base
|
2013-05-25 06:28:18 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
include Workflow
|
2013-05-25 06:28:18 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
attr_accessible :name, :rules, :assignment_weighting_scheme, :group_weight, :position, :default_assignment_name
|
|
|
|
attr_readonly :context_id, :context_type
|
2013-11-26 06:33:50 +08:00
|
|
|
belongs_to :context, :polymorphic => true
|
|
|
|
acts_as_list scope: { context: self, workflow_state: 'available' }
|
2011-02-01 09:57:29 +08:00
|
|
|
has_a_broadcast_policy
|
2013-05-25 06:28:18 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
has_many :assignments, :order => 'position, due_at, title', :dependent => :destroy
|
|
|
|
has_many :active_assignments, :class_name => 'Assignment', :conditions => ['assignments.workflow_state != ?', 'deleted'], :order => 'assignments.position, assignments.due_at, assignments.title'
|
2013-12-13 02:11:38 +08:00
|
|
|
has_many :published_assignments, :class_name => 'Assignment', :conditions => "assignments.workflow_state = 'published'", :order => 'assignments.position, assignments.due_at, assignments.title'
|
2011-02-01 09:57:29 +08:00
|
|
|
|
2013-08-08 06:19:48 +08:00
|
|
|
validates_presence_of :context_id, :context_type, :workflow_state
|
2011-02-01 09:57:29 +08:00
|
|
|
validates_length_of :rules, :maximum => maximum_text_length, :allow_nil => true, :allow_blank => true
|
2011-04-02 00:59:20 +08:00
|
|
|
validates_length_of :default_assignment_name, :maximum => maximum_string_length, :allow_nil => true
|
|
|
|
validates_length_of :name, :maximum => maximum_string_length, :allow_nil => true
|
2013-05-25 06:28:18 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
before_save :set_context_code
|
|
|
|
before_save :generate_default_values
|
|
|
|
before_save :group_weight_changed
|
|
|
|
after_save :course_grading_change
|
|
|
|
after_save :touch_context
|
|
|
|
after_save :update_student_grades
|
2013-05-25 06:28:18 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
def generate_default_values
|
2011-06-15 00:56:55 +08:00
|
|
|
self.name ||= t 'default_title', "Assignments"
|
2011-02-01 09:57:29 +08:00
|
|
|
if !self.group_weight
|
|
|
|
self.group_weight = 0
|
|
|
|
end
|
|
|
|
@grades_changed = self.rules_changed? || self.group_weight_changed?
|
2011-06-15 00:56:55 +08:00
|
|
|
self.default_assignment_name = self.name
|
|
|
|
self.default_assignment_name = self.default_assignment_name.singularize if I18n.locale == :en
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
|
|
|
protected :generate_default_values
|
2013-05-25 06:28:18 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
def update_student_grades
|
|
|
|
if @grades_changed
|
2012-06-30 00:44:42 +08:00
|
|
|
connection.after_transaction_commit { self.context.recompute_student_scores }
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
|
|
|
end
|
2013-05-25 06:28:18 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
def set_context_code
|
|
|
|
self.context_code = "#{self.context_type.underscore}_#{self.context_id}"
|
|
|
|
end
|
|
|
|
|
|
|
|
set_policy do
|
2013-12-10 07:09:43 +08:00
|
|
|
given { |user, session| self.context.grants_rights?(user, session, :read, :view_all_grades, :manage_grades).any?(&:last) }
|
2011-07-14 00:24:17 +08:00
|
|
|
can :read
|
2012-07-28 00:34:43 +08:00
|
|
|
|
2013-12-10 07:09:43 +08:00
|
|
|
given { |user, session| self.context.grants_rights?(user, session, :manage_assignments).any?(&:last) }
|
2013-07-24 04:39:59 +08:00
|
|
|
can :read and can :create and can :update and can :delete
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
2013-05-25 06:28:18 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
workflow do
|
|
|
|
state :available
|
|
|
|
state :deleted
|
|
|
|
end
|
2013-05-25 06:28:18 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
alias_method :destroy!, :destroy
|
|
|
|
def destroy
|
|
|
|
self.workflow_state = 'deleted'
|
|
|
|
self.assignments.active.include_quiz_and_topic.each{|a| a.destroy }
|
|
|
|
self.save
|
|
|
|
end
|
|
|
|
|
|
|
|
def restore(try_to_selectively_undelete_assignments = true)
|
|
|
|
to_restore = self.assignments.include_quiz_and_topic
|
|
|
|
if try_to_selectively_undelete_assignments
|
|
|
|
# It's a pretty good guess that if an assignment was modified at the same
|
|
|
|
# time that this group was last modified, that assignment was deleted
|
|
|
|
# along with this group. This might help avoid undeleting assignments that
|
|
|
|
# were deleted earlier.
|
2013-03-19 03:07:47 +08:00
|
|
|
to_restore = to_restore.where('updated_at >= ?', self.updated_at.utc)
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
|
|
|
self.workflow_state = 'available'
|
|
|
|
self.save
|
|
|
|
to_restore.each { |assignment| assignment.restore(:assignment_group) }
|
|
|
|
end
|
2013-05-25 06:28:18 +08:00
|
|
|
|
2013-10-22 23:33:57 +08:00
|
|
|
def rules_hash (options={})
|
2011-02-01 09:57:29 +08:00
|
|
|
return @rules_hash if @rules_hash
|
|
|
|
@rules_hash = {}.with_indifferent_access
|
2013-05-25 06:28:18 +08:00
|
|
|
(rules || "").split("\n").each do |rule|
|
2011-02-01 09:57:29 +08:00
|
|
|
split = rule.split(":", 2)
|
|
|
|
if split.length > 1
|
|
|
|
if split[0] == 'never_drop'
|
|
|
|
@rules_hash[split[0]] ||= []
|
2013-10-22 23:33:57 +08:00
|
|
|
@rules_hash[split[0]] << (options[:stringify_json_ids] ? split[1].to_s : split[1].to_i)
|
2011-02-01 09:57:29 +08:00
|
|
|
else
|
|
|
|
@rules_hash[split[0]] = split[1].to_i
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
@rules_hash
|
|
|
|
end
|
2013-05-25 06:28:18 +08:00
|
|
|
|
|
|
|
# Converts a hash representation of rules to the string representation of rules in the database
|
|
|
|
# {
|
|
|
|
# "drop_lowest" => '1',
|
|
|
|
# "drop_highest" => '1',
|
|
|
|
# "never_drop" => ['33','17','24']
|
|
|
|
# }
|
|
|
|
#
|
|
|
|
# drop_lowest:2\ndrop_highest:1\nnever_drop:12\nnever_drop:14\n
|
|
|
|
def rules_hash=(incoming_hash)
|
|
|
|
rule_string = ""
|
|
|
|
rule_string += "drop_lowest:#{incoming_hash['drop_lowest']}\n" if incoming_hash['drop_lowest']
|
|
|
|
rule_string += "drop_highest:#{incoming_hash['drop_highest']}\n" if incoming_hash['drop_highest']
|
|
|
|
if incoming_hash['never_drop']
|
|
|
|
incoming_hash['never_drop'].each do |r|
|
|
|
|
rule_string += "never_drop:#{r}\n"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
self.rules = rule_string
|
|
|
|
end
|
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
def points_possible
|
|
|
|
self.assignments.map{|a| a.points_possible || 0}.sum
|
|
|
|
end
|
2013-05-25 06:28:18 +08:00
|
|
|
|
2013-03-21 03:38:19 +08:00
|
|
|
scope :include_active_assignments, includes(:active_assignments)
|
|
|
|
scope :active, where("assignment_groups.workflow_state<>'deleted'")
|
|
|
|
scope :before, lambda { |date| where("assignment_groups.created_at<?", date) }
|
|
|
|
scope :for_context_codes, lambda { |codes| active.where(:context_code => codes).order(:position) }
|
2013-04-02 02:55:32 +08:00
|
|
|
scope :for_course, lambda { |course| where(:context_id => course, :context_type => 'Course') }
|
2013-03-21 03:38:19 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
def group_weight_changed
|
|
|
|
@group_weight_changed = self.group_weight_changed?
|
|
|
|
true
|
|
|
|
end
|
2013-05-25 06:28:18 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
def course_grading_change
|
|
|
|
self.context.grade_weight_changed! if @group_weight_changed && self.context && self.context.group_weighting_scheme == 'percent'
|
|
|
|
true
|
|
|
|
end
|
|
|
|
|
|
|
|
set_broadcast_policy do |p|
|
|
|
|
p.dispatch :grade_weight_changed
|
|
|
|
p.to { context.participating_students }
|
2013-05-25 06:28:18 +08:00
|
|
|
p.whenever { |record|
|
|
|
|
false &&
|
2011-02-01 09:57:29 +08:00
|
|
|
record.changed_in_state(:available, :fields => :group_weight)
|
|
|
|
}
|
|
|
|
end
|
2013-05-25 06:28:18 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
def students
|
|
|
|
assignments.map(&:students).flatten
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.process_migration(data, migration)
|
2013-07-27 01:34:29 +08:00
|
|
|
add_groups_for_imported_assignments(data, migration)
|
2011-02-01 09:57:29 +08:00
|
|
|
groups = data['assignment_groups'] ? data['assignment_groups']: []
|
|
|
|
groups.each do |group|
|
2012-04-03 06:38:05 +08:00
|
|
|
if migration.import_object?("assignment_groups", group['migration_id'])
|
2011-06-18 00:58:18 +08:00
|
|
|
begin
|
|
|
|
import_from_migration(group, migration.context)
|
|
|
|
rescue
|
2013-04-24 01:12:19 +08:00
|
|
|
migration.add_import_warning(t('#migration.assignment_group_type', "Assignment Group"), group[:title], $!)
|
2011-06-18 00:58:18 +08:00
|
|
|
end
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
|
|
|
end
|
fix up module positions after import
test plan:
CNVS-4067
- create a course (A) with three modules
- use the modules API to retrieve positions, and verify
they're unique (1, 2, 3)
- export course A
- create a second course (B) with three modules having
different names from course A
- import course A into course B
- use the modules API to retrieve positions in course B,
and verify that the imported modules' positions from A
do not overlap course B's module positions
(e.g., they should be 4, 5, 6)
- also test positions of imported assignment groups
CNVS-8256
- import the course attached to the ticket
- use the modules API to list the modules in the course
- the positions should be unique: (1, 2, 3), not (1, 1, 1)
fixes CNVS-4067
fixes CNVS-8256
Change-Id: Ib81a238e073e46eb01d72565a569ab24762d37bf
Reviewed-on: https://gerrit.instructure.com/24525
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Bracken Mosbacker <bracken@instructure.com>
Product-Review: Bracken Mosbacker <bracken@instructure.com>
QA-Review: Matt Fairbourn <mfairbourn@instructure.com>
2013-09-20 00:09:07 +08:00
|
|
|
migration.context.assignment_groups.first.try(:fix_position_conflicts)
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
2013-05-25 06:28:18 +08:00
|
|
|
|
2013-07-27 01:34:29 +08:00
|
|
|
def self.add_groups_for_imported_assignments(data, migration)
|
|
|
|
return unless data['assignments'] && migration.migration_settings[:migration_ids_to_import] &&
|
|
|
|
migration.migration_settings[:migration_ids_to_import][:copy] &&
|
|
|
|
migration.migration_settings[:migration_ids_to_import][:copy].length > 0
|
|
|
|
|
|
|
|
migration.migration_settings[:migration_ids_to_import][:copy]['assignment_groups'] ||= {}
|
|
|
|
data['assignments'].each do |assignment_hash|
|
|
|
|
a_hash = assignment_hash.with_indifferent_access
|
|
|
|
if migration.import_object?("assignments", a_hash['migration_id']) &&
|
|
|
|
group_mig_id = a_hash['assignment_group_migration_id']
|
|
|
|
migration.migration_settings[:migration_ids_to_import][:copy]['assignment_groups'][group_mig_id] = true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
def self.import_from_migration(hash, context, item=nil)
|
|
|
|
hash = hash.with_indifferent_access
|
|
|
|
return nil if hash[:migration_id] && hash[:assignment_groups_to_import] && !hash[:assignment_groups_to_import][hash[:migration_id]]
|
|
|
|
item ||= find_by_context_id_and_context_type_and_id(context.id, context.class.to_s, hash[:id])
|
|
|
|
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.assignment_groups.new
|
|
|
|
context.imported_migration_items << item if context.imported_migration_items && item.new_record?
|
|
|
|
item.migration_id = hash[:migration_id]
|
2012-05-03 02:46:11 +08:00
|
|
|
item.workflow_state = 'available' if item.deleted?
|
2011-02-01 09:57:29 +08:00
|
|
|
item.name = hash[:title]
|
2011-04-12 21:41:47 +08:00
|
|
|
item.position = hash[:position].to_i if hash[:position] && hash[:position].to_i > 0
|
|
|
|
item.group_weight = hash[:group_weight] if hash[:group_weight]
|
2013-05-25 06:28:18 +08:00
|
|
|
|
2011-04-12 21:41:47 +08:00
|
|
|
if hash[:rules] && hash[:rules].length > 0
|
|
|
|
rules = ""
|
|
|
|
hash[:rules].each do |rule|
|
|
|
|
if rule[:drop_type] == "drop_lowest" || rule[:drop_type] == "drop_highest"
|
|
|
|
rules += "#{rule[:drop_type]}:#{rule[:drop_count]}\n"
|
|
|
|
elsif rule[:drop_type] == "never_drop"
|
|
|
|
if context.respond_to?(:assignment_group_no_drop_assignments)
|
|
|
|
context.assignment_group_no_drop_assignments[rule[:assignment_migration_id]] = item
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
item.rules = rules unless rules == ''
|
|
|
|
end
|
2013-05-25 06:28:18 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
item.save!
|
|
|
|
item
|
|
|
|
end
|
2013-05-25 06:28:18 +08:00
|
|
|
|
2011-04-12 21:41:47 +08:00
|
|
|
def self.add_never_drop_assignment(group, assignment)
|
|
|
|
rule = "never_drop:#{assignment.id}\n"
|
|
|
|
if group.rules
|
|
|
|
group.rules += rule
|
|
|
|
else
|
|
|
|
group.rules = rule
|
|
|
|
end
|
|
|
|
group.save
|
|
|
|
end
|
2011-02-01 09:57:29 +08:00
|
|
|
|
2012-05-24 05:45:22 +08:00
|
|
|
def has_frozen_assignments?(user)
|
|
|
|
return false unless PluginSetting.settings_for_plugin(:assignment_freezer)
|
|
|
|
return false unless self.active_assignments.length > 0
|
|
|
|
|
|
|
|
self.active_assignments.each do |asmnt|
|
|
|
|
return true if asmnt.frozen_for_user?(user)
|
|
|
|
end
|
|
|
|
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
2013-01-01 06:25:44 +08:00
|
|
|
def has_frozen_assignment_group_id_assignment?(user)
|
|
|
|
return false unless PluginSetting.settings_for_plugin(:assignment_freezer)
|
|
|
|
return false unless self.active_assignments.length > 0
|
|
|
|
|
|
|
|
self.active_assignments.each do |asmnt|
|
|
|
|
return true if asmnt.att_frozen?(:assignment_group_id,user)
|
|
|
|
end
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
2013-05-25 06:28:18 +08:00
|
|
|
def move_assignments_to(move_to_id)
|
|
|
|
new_group = context.assignment_groups.active.find(move_to_id)
|
|
|
|
order = new_group.assignments.active.pluck(:id)
|
|
|
|
ids_to_change = self.assignments.active.pluck(:id)
|
|
|
|
order += ids_to_change
|
|
|
|
Assignment.where(:id => ids_to_change).update_all(:assignment_group_id => new_group, :updated_at => Time.now.utc) unless ids_to_change.empty?
|
|
|
|
Assignment.find_by_id(order).update_order(order) unless order.empty?
|
|
|
|
new_group.touch
|
|
|
|
self.reload
|
|
|
|
end
|
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|