diff --git a/app/models/assignment.rb b/app/models/assignment.rb index 5fa7b6e9907..974486054d8 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -25,6 +25,7 @@ class Assignment < ActiveRecord::Base include CopyAuthorizedLinks include Mutable include ContextModuleItem + include DatesOverridable attr_accessible :title, :name, :description, :due_at, :points_possible, :min_score, :max_score, :mastery_score, :grading_type, :submission_types, @@ -34,7 +35,7 @@ class Assignment < ActiveRecord::Base :notify_of_update, :time_zone_edited, :turnitin_enabled, :turnitin_settings, :set_custom_field_values, :context, :position, :allowed_extensions, :external_tool_tag_attributes, :freeze_on_copy - attr_accessor :original_id, :updating_user, :copying, :applied_overrides + attr_accessor :original_id, :updating_user, :copying has_many :submissions, :class_name => 'Submission', :dependent => :destroy has_many :attachments, :as => :context, :dependent => :destroy @@ -49,12 +50,10 @@ class Assignment < ActiveRecord::Base belongs_to :cloned_item belongs_to :grading_standard belongs_to :group_category - has_many :assignment_overrides, :dependent => :destroy - has_many :active_assignment_overrides, :class_name => 'AssignmentOverride', :conditions => {:workflow_state => 'active'} has_one :external_tool_tag, :class_name => 'ContentTag', :as => :context, :dependent => :destroy validates_associated :external_tool_tag, :if => :external_tool? - validates_associated :assignment_overrides + accepts_nested_attributes_for :external_tool_tag, :reject_if => proc { |attrs| # only accept the url and new_tab params, the other accessible # params don't apply to an content tag being used as an external_tool_tag @@ -143,99 +142,10 @@ class Assignment < ActiveRecord::Base if group_category_id_changed? # needs to be .each(&:destroy) instead of .update_all(:workflow_state => # 'deleted') so that the override gets versioned properly - assignment_overrides.active.scoped(:conditions => {:set_type => 'Group'}).each(&:destroy) + active_assignment_overrides.scoped(:conditions => {:set_type => 'Group'}).each(&:destroy) end end - def overridden_for(user) - AssignmentOverrideApplicator.assignment_overridden_for(self, user) - end - - def overrides_visible_to(user, overrides=active_assignment_overrides) - # the visible_to scope is potentially expensive. skip its conditions if the - # initial scope is already empty - if overrides.first.present? - overrides.visible_to(user, context) - else - overrides - end - end - - def has_overrides? - assignment_overrides.count > 0 - end - - # returns two values indicating which due dates for this assignment apply - # and/or are visible to the user. - # - # the first is the due date as it applies to the user as a student, if any - # (nil if the user has no student enrollment(s) in the assignment's course) - # - # the second is a list of due dates a they apply to users, sections, or - # groups visible to the user as an admin (nil if the user has no - # admin/observer enrollment(s) in the assignment's course) - # - # in both cases, "due dates" is a hash with due_at (full timestamp), all_day - # flag, and all_day_date. for the "as an admin" list, each due date from - # an override will also have a 'title' key to identify which subset of the - # course is affected by that due date, and an 'override' key referencing the - # override itself. for the original due date, it will instead have a 'base' - # flag (value true). - def due_dates_for(user) - as_student, as_admin = nil, nil - return nil, nil if context.nil? - - if context.user_has_been_student?(user) - as_student = self.overridden_for(user).due_date_hash - end - - if context.user_has_been_admin?(user) - as_admin = due_dates_visible_to(user) - - elsif context.user_has_been_observer?(user) - as_admin = observed_student_due_dates(user).uniq - - if as_admin.empty? - as_admin = [self.overridden_for(user).due_date_hash] - end - - elsif context.user_has_no_enrollments?(user) - as_admin = all_due_dates - end - - return as_student, as_admin - end - - def all_due_dates - all_dates = assignment_overrides.overriding_due_at.map(&:as_hash) - all_dates << due_date_hash.merge(:base => true) - end - - def due_dates_visible_to(user) - # Overrides - overrides = overrides_visible_to(user).overriding_due_at - list = overrides.map(&:as_hash) - - # Base - list << self.due_date_hash.merge(:base => true) - end - - def observed_student_due_dates(user) - ObserverEnrollment.observed_students(context, user).map do |student, enrollments| - self.overridden_for(student).due_date_hash - end - end - - def due_date_hash - hash = { :due_at => due_at, :all_day => all_day, :all_day_date => all_day_date } - if @applied_overrides && override = @applied_overrides.find { |o| o.due_at == due_at } - hash[:override] = override - hash[:title] = override.title - end - - hash - end - def self.due_date_compare_value(date) # due dates are considered equal if they're the same up to the minute date.to_i / 60 @@ -245,72 +155,6 @@ class Assignment < ActiveRecord::Base due_date_compare_value(date1) == due_date_compare_value(date2) end - def multiple_due_dates_apply_to(user) - as_instructor = self.due_dates_for(user).second - as_instructor && as_instructor.map{ |hash| - Assignment.due_date_compare_value(hash[:due_at]) }.uniq.size > 1 - end - - # like due_dates_for, but for unlock_at values instead. for consistency, each - # unlock_at is still represented by a hash, even though the "as a student" - # value will only have one key. - def unlock_ats_for(user) - as_student, as_instructor = nil, nil - - if context.user_has_been_student?(user) - overridden = self.overridden_for(user) - as_student = { :unlock_at => overridden.unlock_at } - end - - if context.user_has_been_instructor?(user) - overrides = self.overrides_visible_to(user).overriding_unlock_at - - as_instructor = overrides.map do |override| - { - :title => override.title, - :unlock_at => override.unlock_at, - :override => override - } - end - - as_instructor << { - :base => true, - :unlock_at => self.unlock_at - } - end - - return as_student, as_instructor - end - - # like unlock_ats_for, but for lock_at values instead. - def lock_ats_for(user) - as_student, as_instructor = nil, nil - - if context.user_has_been_student?(user) - overridden = self.overridden_for(user) - as_student = { :lock_at => overridden.lock_at } - end - - if context.user_has_been_instructor?(user) - overrides = self.overrides_visible_to(user).overriding_lock_at - - as_instructor = overrides.map do |override| - { - :title => override.title, - :lock_at => override.lock_at, - :override => override - } - end - - as_instructor << { - :base => true, - :lock_at => self.lock_at - } - end - - return as_student, as_instructor - end - def schedule_do_auto_peer_review_job_if_automatic_peer_review if peer_reviews && automatic_peer_reviews && !peer_reviews_assigned # handle if it has already come due, but has not yet been auto_peer_reviewed diff --git a/app/models/assignment_override.rb b/app/models/assignment_override.rb index 8c9f364bf90..36bb1cd000b 100644 --- a/app/models/assignment_override.rb +++ b/app/models/assignment_override.rb @@ -25,10 +25,12 @@ class AssignmentOverride < ActiveRecord::Base attr_accessible belongs_to :assignment + belongs_to :quiz belongs_to :set, :polymorphic => true has_many :assignment_override_students, :dependent => :destroy - validates_presence_of :assignment, :assignment_version, :title + validates_presence_of :assignment_version, :if => :assignment + validates_presence_of :title validates_inclusion_of :set_type, :in => %w(CourseSection Group ADHOC) validates_length_of :title, :maximum => maximum_string_length, :allow_nil => true @@ -53,6 +55,12 @@ class AssignmentOverride < ActiveRecord::Base end end + validate do |record| + if [record.assignment, record.quiz].all?(&:nil?) + record.errors.add :base, "assignment or quiz required" + end + end + workflow do state :active state :deleted @@ -72,7 +80,17 @@ class AssignmentOverride < ActiveRecord::Base before_validation :default_values def default_values self.set_type ||= 'ADHOC' - self.assignment_version = assignment.version_number if assignment + + if assignment + self.assignment_version = assignment.version_number + self.quiz = assignment.quiz + self.quiz_version = quiz.version_number if quiz + elsif quiz + self.quiz_version = quiz.version_number + self.assignment = quiz.assignment + self.assignment_version = assignment.version_number if assignment + end + self.title = set.name if set_type != 'ADHOC' && set end protected :default_values diff --git a/app/models/assignment_override_student.rb b/app/models/assignment_override_student.rb index ce4d752320b..b3c904a464d 100644 --- a/app/models/assignment_override_student.rb +++ b/app/models/assignment_override_student.rb @@ -20,11 +20,12 @@ class AssignmentOverrideStudent < ActiveRecord::Base belongs_to :assignment belongs_to :assignment_override belongs_to :user + belongs_to :quiz attr_accessible :user - validates_presence_of :assignment, :assignment_override, :user - validates_uniqueness_of :user_id, :scope => :assignment_id + validates_presence_of :assignment_override, :user + validates_uniqueness_of :user_id, :scope => [:assignment_id, :quiz_id] validate :assignment_override do |record| if record.assignment_override && record.assignment_override.set_type != 'ADHOC' @@ -39,14 +40,31 @@ class AssignmentOverrideStudent < ActiveRecord::Base end validate :user do |record| - if record.user && record.assignment && record.user.student_enrollments.scoped(:conditions => {:course_id => record.assignment.context_id}).first.nil? + if record.user && record.context_id && record.user.student_enrollments.scoped(:conditions => {:course_id => record.context_id}).first.nil? record.errors.add :user, "is not in the assignment's course" end end + validate do |record| + if [record.assignment, record.quiz].all?(&:nil?) + record.errors.add :base, "requires assignment or quiz" + end + end + + def context_id + if quiz + quiz.context_id + elsif assignment + assignment.context_id + end + end + before_validation :default_values def default_values - self.assignment_id = self.assignment_override.assignment_id if self.assignment_override + if assignment_override + self.assignment_id = assignment_override.assignment_id + self.quiz_id = assignment_override.quiz_id + end end protected :default_values end diff --git a/app/models/quiz.rb b/app/models/quiz.rb index 6a5d2bd08cc..6f2d07e60bc 100644 --- a/app/models/quiz.rb +++ b/app/models/quiz.rb @@ -25,6 +25,7 @@ class Quiz < ActiveRecord::Base include ActionView::Helpers::SanitizeHelper extend ActionView::Helpers::SanitizeHelper::ClassMethods include ContextModuleItem + include DatesOverridable attr_accessible :title, :description, :points_possible, :assignment_id, :shuffle_answers, :show_correct_answers, :time_limit, :allowed_attempts, :scoring_policy, :quiz_type, @@ -53,6 +54,7 @@ class Quiz < ActiveRecord::Base before_save :set_defaults after_save :update_assignment after_save :touch_context + after_save :link_assignment_overrides, :if => :new_assignment_id? serialize :quiz_data @@ -89,6 +91,19 @@ class Quiz < ActiveRecord::Base @stored_questions = nil end protected :set_defaults + + def new_assignment_id? + last_assignment_id != assignment_id + end + + def link_assignment_overrides + collections = [assignment_overrides, assignment_override_students] + collections += [assignment.assignment_overrides, assignment.assignment_override_students] if assignment + + collections.each do |collection| + collection.update_all({ :assignment_id => assignment_id, :quiz_id => id }) + end + end def build_assignment if self.available? && !self.assignment_id && self.graded? && @saved_by != :assignment && @saved_by != :clone diff --git a/db/migrate/20121207193355_add_quiz_id_to_assignment_overrides.rb b/db/migrate/20121207193355_add_quiz_id_to_assignment_overrides.rb new file mode 100644 index 00000000000..080456b553d --- /dev/null +++ b/db/migrate/20121207193355_add_quiz_id_to_assignment_overrides.rb @@ -0,0 +1,20 @@ +class AddQuizIdToAssignmentOverrides < ActiveRecord::Migration + tag :predeploy + + def self.up + add_column :assignment_overrides, :quiz_id, :integer, :limit => 8 + add_column :assignment_overrides, :quiz_version, :integer + add_index :assignment_overrides, :quiz_id + + change_column :assignment_overrides, :assignment_id, :integer, :limit => 8, :null => true + change_column :assignment_overrides, :assignment_version, :integer, :null => true + end + + def self.down + remove_index :assignment_overrides, :quiz_id + remove_column :assignment_overrides, :quiz_id, :quiz_version + + change_column :assignment_overrides, :assignment_id, :integer, :limit => 8, :null => false + change_column :assignment_overrides, :assignment_version, :integer, :null => false + end +end diff --git a/db/migrate/20121210154140_add_quiz_id_to_assignment_override_students.rb b/db/migrate/20121210154140_add_quiz_id_to_assignment_override_students.rb new file mode 100644 index 00000000000..698aa39870f --- /dev/null +++ b/db/migrate/20121210154140_add_quiz_id_to_assignment_override_students.rb @@ -0,0 +1,15 @@ +class AddQuizIdToAssignmentOverrideStudents < ActiveRecord::Migration + tag :predeploy + + def self.up + add_column :assignment_override_students, :quiz_id, :integer, :limit => 8 + add_index :assignment_override_students, :quiz_id + change_column :assignment_override_students, :assignment_id, :integer, :limit => 8, :null => true + end + + def self.down + remove_column :assignment_override_students, :quiz_id + remove_index :assignment_override_students, :quiz_id + change_column :assignment_override_students, :assignment_id, :integer, :limit => 8, :null => false + end +end diff --git a/lib/assignment_override_applicator.rb b/lib/assignment_override_applicator.rb index 1a051389991..94794d3452a 100644 --- a/lib/assignment_override_applicator.rb +++ b/lib/assignment_override_applicator.rb @@ -17,39 +17,45 @@ # module AssignmentOverrideApplicator - # top-level method intended for consumption. given an assignment (of specific + # top-level method intended for consumption. given an assignment or quiz(of specific # version) and user, determine the list of overrides, apply them to the - # assignment, and return the overridden stand-in. - def self.assignment_overridden_for(assignment, user) - overrides = self.overrides_for_assignment_and_user(assignment, user) + # assignment or quiz, and return the overridden stand-in. + def self.assignment_overridden_for(assignment_or_quiz, user) + overrides = self.overrides_for_assignment_and_user(assignment_or_quiz, user) if overrides.empty? - assignment + assignment_or_quiz else - self.assignment_with_overrides(assignment, overrides) + self.assignment_with_overrides(assignment_or_quiz, overrides) end end + def self.quiz_overridden_for(quiz, user) + assignment_overridden_for(quiz, user) + end + # determine list of overrides (of appropriate version) that apply to the - # assignment (of specific version) for a particular user. the overrides are + # assignment or quiz(of specific version) for a particular user. the overrides are # returned in priority order; the first override to contain an overridden # value for a particular field is used for that field - def self.overrides_for_assignment_and_user(assignment, user) - Rails.cache.fetch([user, assignment, assignment.version_number, 'overrides'].cache_key) do + def self.overrides_for_assignment_and_user(assignment_or_quiz, user) + Rails.cache.fetch([user, assignment_or_quiz, assignment_or_quiz.version_number, 'overrides'].cache_key) do # return an empty array to the block if there is nothing to do here - next [] unless assignment.has_overrides? + next [] unless assignment_or_quiz.has_overrides? overrides = [] # get list of overrides that might apply. adhoc override is highest # priority, then group override, then section overrides by position. DO # NOT exclude deleted overrides, yet - adhoc_membership = AssignmentOverrideStudent.scoped(:conditions => {:assignment_id => assignment.id, :user_id => user.id}).first + key = assignment_or_quiz.is_a?(Quiz) ? :quiz_id : :assignment_id + adhoc_membership = AssignmentOverrideStudent.scoped(:conditions => {key => assignment_or_quiz.id, :user_id => user.id}).first + overrides << adhoc_membership.assignment_override if adhoc_membership - if assignment.group_category && group = user.current_groups.scoped(:conditions => {:group_category_id => assignment.group_category_id}).first - group_override = assignment.assignment_overrides. + if assignment_or_quiz.is_a?(Assignment) && assignment_or_quiz.group_category && group = user.current_groups.scoped(:conditions => {:group_category_id => assignment_or_quiz.group_category_id}).first + group_override = assignment_or_quiz.assignment_overrides. scoped(:conditions => {:set_type => 'Group', :set_id => group.id}). first overrides << group_override if group_override @@ -57,20 +63,21 @@ module AssignmentOverrideApplicator section_ids = user.enrollments.active.scoped(:conditions => { :type => ['StudentEnrollment', 'ObserverEnrollment'], - :course_id => assignment.context_id}).map(&:course_section_id) + :course_id => assignment_or_quiz.context_id}).map(&:course_section_id) - section_overrides = assignment.assignment_overrides. + section_overrides = assignment_or_quiz.assignment_overrides. scoped(:conditions => {:set_type => 'CourseSection', :set_id => section_ids}) # TODO add position column to assignment_override, nil for non-section - # overrides, (assignment, position) unique for section overrides + # overrides, (assignment_or_quiz, position) unique for section overrides overrides += section_overrides#.scoped(:order => :position) # for each potential override discovered, make sure we look at the # appropriate version overrides = overrides.map do |override| override_version = override.versions.detect do |version| - version.model.assignment_version <= assignment.version_number + model_version = assignment_or_quiz.is_a?(Quiz) ? version.model.quiz_version : version.model.assignment_version + model_version <= assignment_or_quiz.version_number end override_version ? override_version.model : nil end @@ -82,47 +89,53 @@ module AssignmentOverrideApplicator end # apply the overrides calculated by collapsed_overrides to a clone of the - # assignment which can then be used in place of the assignment. the clone is - # marked readonly to prevent saving - def self.assignment_with_overrides(assignment, overrides) + # assignment or quiz which can then be used in place of the original object. + # the clone is marked readonly to prevent saving + def self.assignment_with_overrides(assignment_or_quiz, overrides) # ActiveRecord::Base#clone nils out the primary key; put it back - cloned_assignment = assignment.clone - cloned_assignment.id = assignment.id + cloned_assignment_or_quiz = assignment_or_quiz.clone + cloned_assignment_or_quiz.id = assignment_or_quiz.id # update attributes with overrides - self.collapsed_overrides(assignment, overrides).each do |field,value| + self.collapsed_overrides(assignment_or_quiz, overrides).each do |field,value| # for any times in the value set, bring them back from raw UTC into the # current Time.zone before placing them in the assignment value = value.in_time_zone if value && value.respond_to?(:in_time_zone) && !value.is_a?(Date) - cloned_assignment.write_attribute(field, value) + cloned_assignment_or_quiz.write_attribute(field, value) end - cloned_assignment.applied_overrides = overrides - cloned_assignment.readonly! + cloned_assignment_or_quiz.applied_overrides = overrides + cloned_assignment_or_quiz.readonly! # make new_record? match the original (typically always true on AR clones, # at least until saved, which we don't want to do) - klass = class << cloned_assignment; self; end - klass.send(:define_method, :new_record?) { assignment.new_record? } + klass = class << cloned_assignment_or_quiz; self; end + klass.send(:define_method, :new_record?) { assignment_or_quiz.new_record? } - cloned_assignment + cloned_assignment_or_quiz end - # given an assignment (of specific version) and an ordered list of overrides + def self.quiz_with_overrides(quiz, overrides) + assignment_with_overrides(quiz, overrides) + end + + # given an assignment or quiz (of specific version) and an ordered list of overrides # (see overrides_for_assignment_and_user), return a hash of values for each # overrideable field. for caching, the same set of overrides should produce - # the same collapsed assignment, regardless of the user that ended up at that + # the same collapsed assignment or quiz, regardless of the user that ended up at that # set of overrides. - def self.collapsed_overrides(assignment, overrides) - Rails.cache.fetch([assignment, assignment.version_number, self.overrides_hash(overrides)].cache_key) do + def self.collapsed_overrides(assignment_or_quiz, overrides) + Rails.cache.fetch([assignment_or_quiz, assignment_or_quiz.version_number, self.overrides_hash(overrides)].cache_key) do overridden_data = {} - # clone the assignment, apply overrides, and freeze + # clone the assignment_or_quiz, apply overrides, and freeze [:due_at, :all_day, :all_day_date, :unlock_at, :lock_at].each do |field| - value = self.send("overridden_#{field}", assignment, overrides) - # force times to un-zoned UTC -- this will be a cached value and should - # not care about the TZ of the user that cached it. the user's TZ will - # be applied before it's returned. - value = value.utc if value && value.respond_to?(:utc) && !value.is_a?(Date) - overridden_data[field] = value + if assignment_or_quiz.respond_to?(field) + value = self.send("overridden_#{field}", assignment_or_quiz, overrides) + # force times to un-zoned UTC -- this will be a cached value and should + # not care about the TZ of the user that cached it. the user's TZ will + # be applied before it's returned. + value = value.utc if value && value.respond_to?(:utc) && !value.is_a?(Date) + overridden_data[field] = value + end end overridden_data end @@ -135,24 +148,24 @@ module AssignmentOverrideApplicator end # perform overrides of specific fields - def self.override_for_due_at(assignment, overrides) + def self.override_for_due_at(assignment_or_quiz, overrides) applicable_overrides = overrides.select(&:due_at_overridden) if applicable_overrides.empty? - assignment + assignment_or_quiz elsif override = applicable_overrides.detect{ |o| o.due_at.nil? } override else override = applicable_overrides.sort_by(&:due_at).last - if assignment.due_at && assignment.due_at > override.due_at - assignment + if assignment_or_quiz.due_at && assignment_or_quiz.due_at > override.due_at + assignment_or_quiz else override end end end - def self.overridden_due_at(assignment, overrides) - override_for_due_at(assignment, overrides).due_at + def self.overridden_due_at(assignment_or_quiz, overrides) + override_for_due_at(assignment_or_quiz, overrides).due_at end def self.overridden_all_day(assignment, overrides) @@ -163,13 +176,13 @@ module AssignmentOverrideApplicator override_for_due_at(assignment, overrides).all_day_date end - def self.overridden_unlock_at(assignment, overrides) + def self.overridden_unlock_at(assignment_or_quiz, overrides) unlock_ats = overrides.select(&:unlock_at_overridden).map(&:unlock_at) - unlock_ats.any?(&:nil?) ? nil : [assignment.unlock_at, *unlock_ats].compact.min + unlock_ats.any?(&:nil?) ? nil : [assignment_or_quiz.unlock_at, *unlock_ats].compact.min end - def self.overridden_lock_at(assignment, overrides) + def self.overridden_lock_at(assignment_or_quiz, overrides) lock_ats = overrides.select(&:lock_at_overridden).map(&:lock_at) - lock_ats.any?(&:nil?) ? nil : [assignment.lock_at, *lock_ats].compact.max + lock_ats.any?(&:nil?) ? nil : [assignment_or_quiz.lock_at, *lock_ats].compact.max end end diff --git a/lib/dates_overridable.rb b/lib/dates_overridable.rb new file mode 100644 index 00000000000..fed16b8d2ab --- /dev/null +++ b/lib/dates_overridable.rb @@ -0,0 +1,171 @@ +module DatesOverridable + attr_accessor :applied_overrides + + def self.included(base) + base.has_many :assignment_overrides, :dependent => :destroy + base.has_many :active_assignment_overrides, :class_name => 'AssignmentOverride', :conditions => {:workflow_state => 'active'} + base.has_many :assignment_override_students, :dependent => :destroy + + base.validates_associated :assignment_overrides + end + + def overridden_for(user) + AssignmentOverrideApplicator.assignment_overridden_for(self, user) + end + + def overrides_visible_to(user, overrides=active_assignment_overrides) + # the visible_to scope is potentially expensive. skip its conditions if the + # initial scope is already empty + if overrides.first.present? + overrides.visible_to(user, context) + else + overrides + end + end + + def has_overrides? + assignment_overrides.count > 0 + end + + # returns two values indicating which due dates for this assignment apply + # and/or are visible to the user. + # + # the first is the due date as it applies to the user as a student, if any + # (nil if the user has no student enrollment(s) in the assignment's course) + # + # the second is a list of due dates a they apply to users, sections, or + # groups visible to the user as an admin (nil if the user has no + # admin/observer enrollment(s) in the assignment's course) + # + # in both cases, "due dates" is a hash with due_at (full timestamp), all_day + # flag, and all_day_date. for the "as an admin" list, each due date from + # an override will also have a 'title' key to identify which subset of the + # course is affected by that due date, and an 'override' key referencing the + # override itself. for the original due date, it will instead have a 'base' + # flag (value true). + def due_dates_for(user) + as_student, as_admin = nil, nil + return nil, nil if context.nil? + + if context.user_has_been_student?(user) + as_student = self.overridden_for(user).due_date_hash + end + + if context.user_has_been_admin?(user) + as_admin = due_dates_visible_to(user) + + elsif context.user_has_been_observer?(user) + as_admin = observed_student_due_dates(user).uniq + + if as_admin.empty? + as_admin = [self.overridden_for(user).due_date_hash] + end + + elsif context.user_has_no_enrollments?(user) + as_admin = all_due_dates + end + + return as_student, as_admin + end + + def all_due_dates + all_dates = assignment_overrides.overriding_due_at.map(&:as_hash) + all_dates << due_date_hash.merge(:base => true) + end + + def due_dates_visible_to(user) + # Overrides + overrides = overrides_visible_to(user).overriding_due_at + list = overrides.map(&:as_hash) + + # Base + list << self.due_date_hash.merge(:base => true) + end + + def observed_student_due_dates(user) + ObserverEnrollment.observed_students(context, user).map do |student, enrollments| + self.overridden_for(student).due_date_hash + end + end + + def due_date_hash + hash = { :due_at => due_at } + + if self.is_a?(Assignment) + hash.merge!({ :all_day => all_day, :all_day_date => all_day_date }) + end + + if @applied_overrides && override = @applied_overrides.find { |o| o.due_at == due_at } + hash[:override] = override + hash[:title] = override.title + end + + hash + end + + def multiple_due_dates_apply_to(user) + as_instructor = self.due_dates_for(user).second + as_instructor && as_instructor.map{ |hash| + self.class.due_date_compare_value(hash[:due_at]) }.uniq.size > 1 + end + + # like due_dates_for, but for unlock_at values instead. for consistency, each + # unlock_at is still represented by a hash, even though the "as a student" + # value will only have one key. + def unlock_ats_for(user) + as_student, as_instructor = nil, nil + + if context.user_has_been_student?(user) + overridden = self.overridden_for(user) + as_student = { :unlock_at => overridden.unlock_at } + end + + if context.user_has_been_instructor?(user) + overrides = self.overrides_visible_to(user).overriding_unlock_at + + as_instructor = overrides.map do |override| + { + :title => override.title, + :unlock_at => override.unlock_at, + :override => override + } + end + + as_instructor << { + :base => true, + :unlock_at => self.unlock_at + } + end + + return as_student, as_instructor + end + + # like unlock_ats_for, but for lock_at values instead. + def lock_ats_for(user) + as_student, as_instructor = nil, nil + + if context.user_has_been_student?(user) + overridden = self.overridden_for(user) + as_student = { :lock_at => overridden.lock_at } + end + + if context.user_has_been_instructor?(user) + overrides = self.overrides_visible_to(user).overriding_lock_at + + as_instructor = overrides.map do |override| + { + :title => override.title, + :lock_at => override.lock_at, + :override => override + } + end + + as_instructor << { + :base => true, + :lock_at => self.lock_at + } + end + + return as_student, as_instructor + end +end \ No newline at end of file diff --git a/spec/factories/assignment_override_factory.rb b/spec/factories/assignment_override_factory.rb index 51cccea1f4e..b99a00e266d 100644 --- a/spec/factories/assignment_override_factory.rb +++ b/spec/factories/assignment_override_factory.rb @@ -17,7 +17,7 @@ # def assignment_override_model(opts={}) - assignment = opts.delete(:assignment) || assignment_model(opts) + assignment = opts.delete(:assignment) || opts.delete(:quiz) || assignment_model(opts) override_for = opts.delete(:set) @override = factory_with_protected_attributes(assignment.assignment_overrides, assignment_override_valid_attributes.merge(opts)) @override.due_at_overridden = true if opts[:due_at] diff --git a/spec/lib/dates_overridable_spec.rb b/spec/lib/dates_overridable_spec.rb new file mode 100644 index 00000000000..2b6c84596ea --- /dev/null +++ b/spec/lib/dates_overridable_spec.rb @@ -0,0 +1,422 @@ + +# +# Copyright (C) 2012 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 . +# + +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb') + +shared_examples_for "an object whose dates are overridable" do + # let(:overridable) - an Assignment or Quiz + # let(:overridable_type) - :assignment or :quiz + + let(:course) { overridable.context } + let(:override) { assignment_override_model(overridable_type => overridable) } + + describe "overridden_for" do + before do + student_in_course(:course => course) + end + + context "when there are overrides" do + before do + override.override_due_at(7.days.from_now) + override.save! + + override_student = override.assignment_override_students.build + override_student.user = @student + override_student.save! + end + + it "returns a clone of the object with the relevant override(s) applied" do + overridden = overridable.overridden_for(@student) + overridden.due_at.should == override.due_at + end + end + + context "with no overrides" do + it "returns the original object" do + @overridden = overridable.overridden_for(@student) + @overridden.due_at.should == overridable.due_at + end + end + end + + describe "has_overrides?" do + subject { overridable.has_overrides? } + + context "when it does" do + before { override } + it { should be_true } + end + + context "when it doesn't" do + it { should be_false } + end + end + + describe "#overrides_visible_to(user)" do + before :each do + override.set = course.default_section + override.save! + end + + it "delegates to visible_to on the active overrides by default" do + @expected_value = stub("expected value") + overridable.active_assignment_overrides.expects(:visible_to).with(@teacher, course).returns(@expected_value) + overridable.overrides_visible_to(@teacher).should == @expected_value + end + + it "allows overriding the scope" do + override.destroy + overridable.overrides_visible_to(@teacher).should be_empty + overridable.overrides_visible_to(@teacher, overridable.assignment_overrides(true)).should == [override] + end + + it "skips the visible_to application if the scope is already empty" do + override.destroy + overridable.active_assignment_overrides.expects(:visible_to).times(0) + overridable.overrides_visible_to(@teacher) + end + + it "returns a scope" do + # can't use "should respond_to", because that delegates to the instantiated Array + lambda{ overridable.overrides_visible_to(@teacher).scoped({}) }.should_not raise_exception + end + end + + describe "#due_dates_for(user)" do + before :each do + course_with_student(:course => course) + + override.set = course.default_section + override.override_due_at(2.days.ago) + override.save! + end + + context "for a student" do + before do + @as_student, @as_instructor = overridable.due_dates_for(@student) + end + + it "does not return instructor dates" do + @as_instructor.should be_nil + end + + it "returns a relevant student date" do + @as_student.should_not be_nil + end + end + + context "for a teacher" do + before do + @as_student, @as_instructor = overridable.due_dates_for(@teacher) + end + + it "does not return a student date" do + @as_student.should be_nil + end + + it "returns a list of instructor dates" do + @as_instructor.should_not be_nil + end + end + + it "returns both for a user that's both a student and a teacher" do + course_with_ta(:course => course, :user => @student, :active_all => true) + as_student, as_instructor = overridable.due_dates_for(@student) + as_student.should_not be_nil + as_instructor.should_not be_nil + end + + it "uses the overridden due date as the applicable due date" do + as_student, _ = overridable.due_dates_for(@student) + as_student[:due_at].should == override.due_at + + if overridable.is_a?(Assignment) + as_student[:all_day].should == override.all_day + as_student[:all_day_date].should == override.all_day_date + end + end + + it "includes the base due date in the list of due dates" do + _, as_instructor = overridable.due_dates_for(@teacher) + + expected_params = { :base => true, :due_at => overridable.due_at } + + if overridable.is_a?(Assignment) + expected_params.merge!({ + :all_day => overridable.all_day, + :all_day_date => overridable.all_day_date + }) + end + + as_instructor.should include expected_params + end + + it "includes visible due date overrides in the list of due dates" do + _, as_instructor = overridable.due_dates_for(@teacher) + as_instructor.should include({ + :title => @course.default_section.name, + :due_at => override.due_at, + :all_day => override.all_day, + :all_day_date => override.all_day_date, + :override => override + }) + end + + it "excludes visible overrides that don't override due_at from the list of due dates" do + override.clear_due_at_override + override.save! + + _, as_instructor = overridable.due_dates_for(@teacher) + as_instructor.size.should == 1 + as_instructor.first[:base].should be_true + end + + it "excludes overrides that aren't visible from the list of due dates" do + @enrollment = @teacher.enrollments.first + @enrollment.limit_privileges_to_course_section = true + @enrollment.save! + + @section2 = course.course_sections.create! + override.set = @section2 + override.save! + + _, as_instructor = overridable.due_dates_for(@teacher) + as_instructor.size.should == 1 + as_instructor.first[:base].should be_true + end + end + + describe "due_date_hash" do + it "returns the due at, all day, and all day date params" do + due = 5.days.from_now + a = Assignment.new(:due_at => due) + a.due_date_hash.should == { :due_at => due, :all_day => false, :all_day_date => nil } + end + end + + describe "observed_student_due_dates" do + it "returns a list of overridden due date hashes" do + a = Assignment.new + u = User.new + student1, student2 = [mock, mock] + + { student1 => '1', student2 => '2' }.each do |student, value| + a.expects(:overridden_for).with(student).returns \ + mock(:due_date_hash => { :student => value }) + end + + ObserverEnrollment.expects(:observed_students).returns({student1 => [], student2 => []}) + + override_hashes = a.observed_student_due_dates(u) + override_hashes.should =~ [ { :student => '1' }, { :student => '2' } ] + end + end + + describe "#unlock_ats_for(user)" do + before :each do + course_with_student(:course => course, :active_all => true) + + overridable.update_attributes(:unlock_at => 2.days.ago) + + override.set = course.default_section + override.override_unlock_at(5.days.ago) + override.save! + end + + context "for a student" do + before do + @as_student, @as_instructor = overridable.unlock_ats_for(@student) + end + + it "does not return instructor dates" do + @as_instructor.should be_nil + end + + it "returns a relevant student date" do + @as_student.should_not be_nil + end + end + + context "for a teacher" do + before do + @as_student, @as_instructor = overridable.unlock_ats_for(@teacher) + end + + it "does not return a student date" do + @as_student.should be_nil + end + + it "returns a list of instructor dates" do + @as_instructor.should_not be_nil + end + end + + it "returns both for a user that's both a student and a teacher" do + course_with_ta(:course => course, :user => @student, :active_all => true) + as_student, as_instructor = overridable.unlock_ats_for(@student) + as_student.should_not be_nil + as_instructor.should_not be_nil + end + + it "uses the overridden unlock date as the applicable unlock date" do + as_student, _ = overridable.unlock_ats_for(@student) + as_student.should == { :unlock_at => override.unlock_at } + end + + it "includes the base unlock date in the list of unlock dates" do + _, as_instructor = overridable.unlock_ats_for(@teacher) + as_instructor.should include({ :base => true, :unlock_at => overridable.unlock_at }) + end + + it "includes visible unlock date overrides in the list of unlock dates" do + _, as_instructor = overridable.unlock_ats_for(@teacher) + as_instructor.should include({ + :title => @course.default_section.name, + :unlock_at => override.unlock_at, + :override => override + }) + end + + it "excludes visible overrides that don't override unlock_at from the list of unlock dates" do + override.clear_unlock_at_override + override.save! + + _, as_instructor = overridable.unlock_ats_for(@teacher) + as_instructor.size.should == 1 + as_instructor.first[:base].should be_true + end + + it "excludes overrides that aren't visible from the list of unlock dates" do + @enrollment = @teacher.enrollments.first + @enrollment.limit_privileges_to_course_section = true + @enrollment.save! + + @section2 = @course.course_sections.create! + override.set = @section2 + override.save! + + _, as_instructor = overridable.unlock_ats_for(@teacher) + as_instructor.size.should == 1 + as_instructor.first[:base].should be_true + end + end + + describe "#lock_ats_for(user)" do + before :each do + course_with_student(:course => course, :active_all => true) + + overridable.update_attributes(:unlock_at => 5.days.ago) + + override.set = course.default_section + override.override_lock_at(2.days.ago) + override.save! + end + + context "for a student" do + before do + @as_student, @as_instructor = overridable.lock_ats_for(@student) + end + + it "does not return instructor dates" do + @as_instructor.should be_nil + end + + it "returns a relevant student date" do + @as_student.should_not be_nil + end + end + + context "for a teacher" do + before do + @as_student, @as_instructor = overridable.lock_ats_for(@teacher) + end + + it "does not return a student date" do + @as_student.should be_nil + end + + it "returns a list of instructor dates" do + @as_instructor.should_not be_nil + end + end + + it "returns both for a user that's both a student and a teacher" do + course_with_ta(:course => course, :user => @student, :active_all => true) + as_student, as_instructor = overridable.lock_ats_for(@student) + as_student.should_not be_nil + as_instructor.should_not be_nil + end + + it "uses the overridden lock date as the applicable lock date" do + as_student, _ = overridable.lock_ats_for(@student) + as_student.should == { :lock_at => override.lock_at } + end + + it "includes the base lock date in the list of lock dates" do + _, as_instructor = overridable.lock_ats_for(@teacher) + as_instructor.should include({ :base => true, :lock_at => overridable.lock_at }) + end + + it "includes visible lock date overrides in the list of lock dates" do + _, as_instructor = overridable.lock_ats_for(@teacher) + as_instructor.detect { |a| a[:override].present? }.should == { + :title => course.default_section.name, + :lock_at => override.lock_at, + :override => override + } + end + + it "excludes visible overrides that don't override lock_at from the list of lock dates" do + override.clear_lock_at_override + override.save! + + _, as_instructor = overridable.lock_ats_for(@teacher) + as_instructor.size.should == 1 + as_instructor.first[:base].should be_true + end + + it "excludes overrides that aren't visible from the list of lock dates" do + @enrollment = @teacher.enrollments.first + @enrollment.limit_privileges_to_course_section = true + @enrollment.save! + + @section2 = course.course_sections.create! + override.set = @section2 + override.save! + + _, as_instructor = overridable.lock_ats_for(@teacher) + as_instructor.size.should == 1 + as_instructor.first[:base].should be_true + end + end +end + +describe Assignment do + it_should_behave_like "an object whose dates are overridable" + + let(:overridable) { assignment_model(:due_at => 5.days.ago) } + let(:overridable_type) { :assignment } +end + +describe Quiz do + it_should_behave_like "an object whose dates are overridable" + + let(:overridable) { quiz_model(:due_at => 5.days.ago) } + let(:overridable_type) { :quiz } +end \ No newline at end of file diff --git a/spec/models/assignment_override_spec.rb b/spec/models/assignment_override_spec.rb index aba728cb4a4..f8971ef1831 100644 --- a/spec/models/assignment_override_spec.rb +++ b/spec/models/assignment_override_spec.rb @@ -233,6 +233,12 @@ describe AssignmentOverride do @override.set = @course.default_section @override.should be_valid end + + it "is valid when the assignment is nil if it has a quiz" do + @override.assignment = nil + @override.quiz = quiz_model + @override.should be_valid + end end describe "title" do @@ -482,4 +488,54 @@ describe AssignmentOverride do visible_overrides.should include @override2 end end + + describe "default_values" do + subject { override } + + let(:override) { AssignmentOverride.new } + let(:quiz) { Quiz.new } + let(:assignment) { Assignment.new } + + context "when the override belongs to a quiz" do + before do + override.quiz = quiz + end + + context "that has an assignment" do + it "uses the quiz's assignment" do + override.quiz.assignment = assignment + override.send(:default_values) + override.assignment.should == assignment + end + end + + context "that has no assignment" do + it "has a nil assignment" do + override.send(:default_values) + override.assignment.should be_nil + end + end + end + + context "when the override belongs to an assignment" do + before do + override.assignment = assignment + end + + context "that has a quiz" do + it "uses the assignment's quiz" do + override.assignment.quiz = quiz + override.send(:default_values) + override.quiz.should == quiz + end + end + + context "that has no quiz" do + it "has a nil quiz" do + override.send(:default_values) + override.quiz.should be_nil + end + end + end + end end diff --git a/spec/models/assignment_override_student_spec.rb b/spec/models/assignment_override_student_spec.rb index 7e542f4f8ae..4f08b93f713 100644 --- a/spec/models/assignment_override_student_spec.rb +++ b/spec/models/assignment_override_student_spec.rb @@ -78,4 +78,46 @@ describe AssignmentOverrideStudent do @override_student.valid? # trigger maintenance @override_student.assignment_id.should == @override2.assignment_id end + + describe "default_values" do + let(:override_student) { AssignmentOverrideStudent.new } + let(:override) { AssignmentOverride.new } + let(:quiz_id) { 1 } + let(:assignment_id) { 2 } + + before do + override_student.assignment_override = override + end + + context "when the override has an assignment" do + before do + override.assignment_id = assignment_id + override_student.send(:default_values) + end + + it "has the assignment's ID" do + override_student.assignment_id.should == assignment_id + end + + it "has a nil quiz ID" do + override_student.quiz_id.should be_nil + end + end + + context "when the override has a quiz and assignment" do + before do + override.assignment_id = assignment_id + override.quiz_id = quiz_id + override_student.send(:default_values) + end + + it "has the assignment's ID" do + override_student.assignment_id.should == assignment_id + end + + it "has the quiz's ID" do + override_student.quiz_id.should == quiz_id + end + end + end end diff --git a/spec/models/assignment_spec.rb b/spec/models/assignment_spec.rb index 081c6695874..bdd97eddfa4 100644 --- a/spec/models/assignment_spec.rb +++ b/spec/models/assignment_spec.rb @@ -598,357 +598,6 @@ describe Assignment do end end - it "should respond to #overridden_for(user)" do - student_in_course - - @assignment = assignment_model(:course => @course, :due_at => 5.days.from_now) - @assignment.reload - - @override = assignment_override_model(:assignment => @assignment) - @override.override_due_at(7.days.from_now) - @override.save! - @override.reload - - @override_student = @override.assignment_override_students.build - @override_student.user = @student - @override_student.save! - - @overridden = @assignment.overridden_for(@student) - @overridden.due_at.should == @override.due_at - end - - describe "has_overrides?" do - let(:assignment) { assignment_model(:course => @course, :due_at => 5.days.from_now) } - - it "returns true when it does" do - assignment_override_model(:assignment => assignment) - assignment.has_overrides?.should be_true - end - - it "returns false when it doesn't" do - assignment.has_overrides?.should be_false - end - end - - describe "#overrides_visible_to(user)" do - before :each do - @assignment = assignment_model - @override = assignment_override_model(:assignment => @assignment) - @override.set = @course.default_section - @override.save! - end - - it "should delegate to visible_to on the active overrides by default" do - @expected_value = stub("expected value") - @assignment.active_assignment_overrides.expects(:visible_to).with(@teacher, @course).returns(@expected_value) - @assignment.overrides_visible_to(@teacher).should == @expected_value - end - - it "should allow overriding the scope" do - @override.destroy - @assignment.overrides_visible_to(@teacher).should be_empty - @assignment.overrides_visible_to(@teacher, @assignment.assignment_overrides(true)).should == [@override] - end - - it "should skip the visible_to application if the scope is already empty" do - @override.destroy - @assignment.active_assignment_overrides.expects(:visible_to).times(0) - @assignment.overrides_visible_to(@teacher) - end - - it "should return a scope" do - # can't use "should respond_to", because that delegates to the instantiated Array - lambda{ @assignment.overrides_visible_to(@teacher).scoped({}) }.should_not raise_exception - end - end - - describe "#due_dates_for(user)" do - before :each do - course_with_student(:active_all => true) - - @assignment = assignment_model(:course => @course, :due_at => 5.days.ago) - @assignment.reload - - @override = assignment_override_model(:assignment => @assignment) - @override.set = @course.default_section - @override.override_due_at(2.days.ago) - @override.save! - @override.reload - end - - it "should not return the list of due dates for a student" do - _, as_instructor = @assignment.due_dates_for(@student) - as_instructor.should be_nil - end - - it "should not return an applicable due date for a teacher" do - as_student, _ = @assignment.due_dates_for(@teacher) - as_student.should be_nil - end - - it "should return the applicable due date for a student" do - as_student, _ = @assignment.due_dates_for(@student) - as_student.should_not be_nil - end - - it "should return the list of due dates for a teacher" do - _, as_instructor = @assignment.due_dates_for(@teacher) - as_instructor.should_not be_nil - end - - it "should return both for a user that's both a student and a teacher" do - course_with_ta(:course => @course, :user => @student, :active_all => true) - as_student, as_instructor = @assignment.due_dates_for(@student) - as_student.should_not be_nil - as_instructor.should_not be_nil - end - - it "should use the overridden due date as the applicable due date" do - as_student, _ = @assignment.due_dates_for(@student) - as_student[:due_at].should == @override.due_at - as_student[:all_day].should == @override.all_day - as_student[:all_day_date].should == @override.all_day_date - end - - it "should include the base due date in the list of due dates" do - _, as_instructor = @assignment.due_dates_for(@teacher) - as_instructor.should include({ - :base => true, - :due_at => @assignment.due_at, - :all_day => @assignment.all_day, - :all_day_date => @assignment.all_day_date - }) - end - - it "should include visible due date overrides in the list of due dates" do - _, as_instructor = @assignment.due_dates_for(@teacher) - as_instructor.should include({ - :title => @course.default_section.name, - :due_at => @override.due_at, - :all_day => @override.all_day, - :all_day_date => @override.all_day_date, - :override => @override - }) - end - - it "should exclude visible overrides that don't override due_at from the list of due dates" do - @override.clear_due_at_override - @override.save! - - _, as_instructor = @assignment.due_dates_for(@teacher) - as_instructor.size.should == 1 - as_instructor.first[:base].should be_true - end - - it "should exclude overrides that aren't visible from the list of due dates" do - @enrollment = @teacher.enrollments.first - @enrollment.limit_privileges_to_course_section = true - @enrollment.save! - - @section2 = @course.course_sections.create! - @override.set = @section2 - @override.save! - - _, as_instructor = @assignment.due_dates_for(@teacher) - as_instructor.size.should == 1 - as_instructor.first[:base].should be_true - end - end - - describe "due_date_hash" do - it "returns the due at, all day, and all day date params" do - due = 5.days.from_now - a = Assignment.new(:due_at => due) - a.due_date_hash.should == { :due_at => due, :all_day => false, :all_day_date => nil } - end - end - - describe "observed_student_due_dates" do - it "returns a list of overridden due date hashes" do - a = Assignment.new - u = User.new - student1, student2 = [mock, mock] - - { student1 => '1', student2 => '2' }.each do |student, value| - a.expects(:overridden_for).with(student).returns \ - mock(:due_date_hash => { :student => value }) - end - - ObserverEnrollment.expects(:observed_students).returns({student1 => [], student2 => []}) - - override_hashes = a.observed_student_due_dates(u).sort_by { |h| h[:student] } - override_hashes.should == [ { :student => '1' }, { :student => '2' } ] - end - end - - describe "#unlock_ats_for(user)" do - before :each do - course_with_student(:active_all => true) - - @assignment = assignment_model(:course => @course, :unlock_at => 2.days.ago) - @assignment.reload - - @override = assignment_override_model(:assignment => @assignment) - @override.set = @course.default_section - @override.override_unlock_at(5.days.ago) - @override.save! - @override.reload - end - - it "should not return the list of unlock dates for a student" do - _, as_instructor = @assignment.unlock_ats_for(@student) - as_instructor.should be_nil - end - - it "should not return an applicable unlock date for a teacher" do - as_student, _ = @assignment.unlock_ats_for(@teacher) - as_student.should be_nil - end - - it "should return the applicable unlock date for a student" do - as_student, _ = @assignment.unlock_ats_for(@student) - as_student.should_not be_nil - end - - it "should return the list of unlock dates for a teacher" do - _, as_instructor = @assignment.unlock_ats_for(@teacher) - as_instructor.should_not be_nil - end - - it "should return both for a user that's both a student and a teacher" do - course_with_ta(:course => @course, :user => @student, :active_all => true) - as_student, as_instructor = @assignment.unlock_ats_for(@student) - as_student.should_not be_nil - as_instructor.should_not be_nil - end - - it "should use the overridden unlock date as the applicable unlock date" do - as_student, _ = @assignment.unlock_ats_for(@student) - as_student.should == { :unlock_at => @override.unlock_at } - end - - it "should include the base unlock date in the list of unlock dates" do - _, as_instructor = @assignment.unlock_ats_for(@teacher) - as_instructor.should include({ :base => true, :unlock_at => @assignment.unlock_at }) - end - - it "should include visible unlock date overrides in the list of unlock dates" do - _, as_instructor = @assignment.unlock_ats_for(@teacher) - as_instructor.should include({ - :title => @course.default_section.name, - :unlock_at => @override.unlock_at, - :override => @override - }) - end - - it "should exclude visible overrides that don't override unlock_at from the list of unlock dates" do - @override.clear_unlock_at_override - @override.save! - - _, as_instructor = @assignment.unlock_ats_for(@teacher) - as_instructor.size.should == 1 - as_instructor.first[:base].should be_true - end - - it "should exclude overrides that aren't visible from the list of unlock dates" do - @enrollment = @teacher.enrollments.first - @enrollment.limit_privileges_to_course_section = true - @enrollment.save! - - @section2 = @course.course_sections.create! - @override.set = @section2 - @override.save! - - _, as_instructor = @assignment.unlock_ats_for(@teacher) - as_instructor.size.should == 1 - as_instructor.first[:base].should be_true - end - end - - describe "#lock_ats_for(user)" do - before :each do - course_with_student(:active_all => true) - - @assignment = assignment_model(:course => @course, :lock_at => 5.days.ago) - @assignment.reload - - @override = assignment_override_model(:assignment => @assignment) - @override.set = @course.default_section - @override.override_lock_at(2.days.ago) - @override.save! - @override.reload - end - - it "should not return the list of lock dates for a student" do - _, as_instructor = @assignment.lock_ats_for(@student) - as_instructor.should be_nil - end - - it "should not return an applicable lock date for a teacher" do - as_student, _ = @assignment.lock_ats_for(@teacher) - as_student.should be_nil - end - - it "should return the applicable lock date for a student" do - as_student, _ = @assignment.lock_ats_for(@student) - as_student.should_not be_nil - end - - it "should return the list of lock dates for a teacher" do - _, as_instructor = @assignment.lock_ats_for(@teacher) - as_instructor.should_not be_nil - end - - it "should return both for a user that's both a student and a teacher" do - course_with_ta(:course => @course, :user => @student, :active_all => true) - as_student, as_instructor = @assignment.lock_ats_for(@student) - as_student.should_not be_nil - as_instructor.should_not be_nil - end - - it "should use the overridden lock date as the applicable lock date" do - as_student, _ = @assignment.lock_ats_for(@student) - as_student.should == { :lock_at => @override.lock_at } - end - - it "should include the base lock date in the list of lock dates" do - _, as_instructor = @assignment.lock_ats_for(@teacher) - as_instructor.should include({ :base => true, :lock_at => @assignment.lock_at }) - end - - it "should include visible lock date overrides in the list of lock dates" do - _, as_instructor = @assignment.lock_ats_for(@teacher) - as_instructor.detect { |a| a[:override].present? }.should == { - :title => @course.default_section.name, - :lock_at => @override.lock_at, - :override => @override - } - end - - it "should exclude visible overrides that don't override lock_at from the list of lock dates" do - @override.clear_lock_at_override - @override.save! - - _, as_instructor = @assignment.lock_ats_for(@teacher) - as_instructor.size.should == 1 - as_instructor.first[:base].should be_true - end - - it "should exclude overrides that aren't visible from the list of lock dates" do - @enrollment = @teacher.enrollments.first - @enrollment.limit_privileges_to_course_section = true - @enrollment.save! - - @section2 = @course.course_sections.create! - @override.set = @section2 - @override.save! - - _, as_instructor = @assignment.lock_ats_for(@teacher) - as_instructor.size.should == 1 - as_instructor.first[:base].should be_true - end - end - context "concurrent inserts" do def concurrent_inserts assignment_model @@ -2679,6 +2328,78 @@ describe Assignment do @assignment.submitted_count.should == 50 end end + + describe "linking overrides with quizzes" do + let(:course) { course_model } + let(:assignment) { assignment_model(:course => course, :due_at => 5.days.from_now).reload } + let(:override) { assignment_override_model(:assignment => assignment) } + let(:override_student) { override.assignment_override_students.build } + + before do + override.override_due_at(7.days.from_now) + override.save! + + student_in_course(:course => course) + override_student.user = @student + override_student.save! + end + + context "before the assignment has a quiz" do + context "override" do + it "has a nil quiz" do + override.quiz.should be_nil + end + + it "has an assignment" do + override.assignment.should == assignment + end + end + + context "override student" do + it "has a nil quiz" do + override_student.quiz.should be_nil + end + + it "has an assignment" do + override_student.assignment.should == assignment + end + end + end + + context "once the assignment changes to a quiz submission" do + before do + assignment.submission_types = "online_quiz" + assignment.save + assignment.reload + override.reload + override_student.reload + end + + it "has a quiz" do + assignment.quiz.should be_present + end + + context "override" do + it "has an assignment" do + override.assignment.should == assignment + end + + it "has the assignment's quiz" do + override.quiz.should == assignment.quiz + end + end + + context "override student" do + it "has an assignment" do + override_student.assignment.should == assignment + end + + it "has the assignment's quiz" do + override_student.quiz.should == assignment.quiz + end + end + end + end end def setup_assignment_with_group diff --git a/spec/models/quiz_spec.rb b/spec/models/quiz_spec.rb index 47cb02bd6f8..71fb90aabce 100644 --- a/spec/models/quiz_spec.rb +++ b/spec/models/quiz_spec.rb @@ -969,4 +969,83 @@ describe Quiz do @quiz.has_student_submissions?.should be_true end end + + describe "linking overrides with assignments" do + let(:course) { course_model } + let(:quiz) { quiz_model(:course => course, :due_at => 5.days.from_now).reload } + let(:override) { assignment_override_model(:quiz => quiz) } + let(:override_student) { override.assignment_override_students.build } + + before do + override.override_due_at(7.days.from_now) + override.save! + + student_in_course(:course => course) + override_student.user = @student + override_student.save! + end + + context "before the quiz has an assignment" do + context "override" do + it "has a quiz" do + override.quiz.should == quiz + end + + it "has a nil assignment" do + override.assignment.should be_nil + end + end + + context "override student" do + it "has a quiz" do + override_student.quiz.should == quiz + end + + it "has a nil assignment" do + override_student.assignment.should be_nil + end + end + end + + context "once the quiz is published" do + before do + # publish the quiz + quiz.workflow_state = 'available' + quiz.save + override.reload + override_student.reload + end + + context "override" do + it "has a quiz" do + override.quiz.should == quiz + end + + it "has the quiz's assignment" do + override.assignment.should == quiz.assignment + end + end + + context "override student" do + it "has a quiz" do + override_student.quiz.should == quiz + end + + it "has the quiz's assignment" do + override_student.assignment.should == quiz.assignment + end + end + end + + context "when the assignment ID doesn't change" do + it "doesn't update overrides" do + quiz.expects(:link_assignment_overrides).once + # publish the quiz + quiz.workflow_state = 'available' + quiz.save + quiz.expects(:link_assignment_overrides).never + quiz.save + end + end + end end